Merge commit '39d8dd70184799d651d776eaae41ff7c980e78d6' into dev-release
diff --git a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriter.java b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriter.java
index 6107e67..c20de96 100644
--- a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriter.java
+++ b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriter.java
@@ -92,9 +92,9 @@
       return;
     }
     if (legacy == LEGACY
-        && (entry.getName().contains("java.nio.file")
+        && (entry.getName().contains("java/nio/file")
             || entry.getName().contains("ApiFlips")
-            || entry.getName().contains("java.adapter"))) {
+            || entry.getName().contains("java/adapter"))) {
       return;
     }
     final byte[] bytes = ByteStreams.toByteArray(input);
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
index 31e0dbd..f10cf07 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -38,9 +38,12 @@
  */
 @Keep
 public class ArchiveClassFileProvider implements ClassFileResourceProvider, Closeable {
+  private final Path archive;
   private final Origin origin;
-  private final ZipFile zipFile;
-  private final Set<String> descriptors = new HashSet<>();
+  private final Predicate<String> include;
+
+  private ZipFile lazyZipFile = null;
+  private Set<String> lazyDescriptors = null;
 
   /**
    * Creates a lazy class-file program-resource provider.
@@ -59,36 +62,23 @@
    */
   public ArchiveClassFileProvider(Path archive, Predicate<String> include) throws IOException {
     assert isArchive(archive);
+    this.archive = archive;
+    this.include = include;
     origin = new PathOrigin(archive);
-    try {
-      zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8);
-    } catch (IOException e) {
-      if (!Files.exists(archive)) {
-        throw new NoSuchFileException(archive.toString());
-      } else {
-        throw e;
-      }
-    }
-    final Enumeration<? extends ZipEntry> entries = zipFile.entries();
-    while (entries.hasMoreElements()) {
-      ZipEntry entry = entries.nextElement();
-      String name = entry.getName();
-      if (ZipUtils.isClassFile(name) && include.test(name)) {
-        descriptors.add(DescriptorUtils.guessTypeDescriptor(name));
-      }
-    }
+    ensureZipFile();
   }
 
   @Override
   public Set<String> getClassDescriptors() {
-    return Collections.unmodifiableSet(descriptors);
+    return ensureDescriptors();
   }
 
   @Override
   public ProgramResource getProgramResource(String descriptor) {
-    if (!descriptors.contains(descriptor)) {
+    if (!ensureDescriptors().contains(descriptor)) {
       return null;
     }
+    ZipFile zipFile = ensureZipFile();
     ZipEntry zipEntry = getZipEntryFromDescriptor(descriptor);
     try (InputStream inputStream = zipFile.getInputStream(zipEntry)) {
       return ProgramResource.fromBytes(
@@ -102,17 +92,60 @@
   }
 
   @Override
-  protected void finalize() throws Throwable {
+  public void finished(DiagnosticsHandler handler) throws IOException {
     close();
-    super.finalize();
   }
 
   @Override
   public void close() throws IOException {
-    zipFile.close();
+    if (lazyZipFile != null) {
+      lazyZipFile.close();
+    }
+    lazyZipFile = null;
+    lazyDescriptors = null;
+  }
+
+  private void reopenZipFile() throws IOException {
+    assert lazyZipFile == null;
+    assert lazyDescriptors == null;
+    try {
+      lazyZipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8);
+    } catch (IOException e) {
+      if (!Files.exists(archive)) {
+        throw new NoSuchFileException(archive.toString());
+      } else {
+        throw e;
+      }
+    }
+    lazyDescriptors = new HashSet<>();
+    final Enumeration<? extends ZipEntry> entries = lazyZipFile.entries();
+    while (entries.hasMoreElements()) {
+      ZipEntry entry = entries.nextElement();
+      String name = entry.getName();
+      if (ZipUtils.isClassFile(name) && include.test(name)) {
+        lazyDescriptors.add(DescriptorUtils.guessTypeDescriptor(name));
+      }
+    }
+  }
+
+  private ZipFile ensureZipFile() {
+    if (lazyZipFile == null) {
+      try {
+        reopenZipFile();
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return lazyZipFile;
+  }
+
+  private Set<String> ensureDescriptors() {
+    ensureZipFile();
+    return Collections.unmodifiableSet(lazyDescriptors);
   }
 
   private ZipEntry getZipEntryFromDescriptor(String descriptor) {
-    return zipFile.getEntry(descriptor.substring(1, descriptor.length() - 1) + CLASS_EXTENSION);
+    return ensureZipFile()
+        .getEntry(descriptor.substring(1, descriptor.length() - 1) + CLASS_EXTENSION);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 7e93145..08c7a84 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -245,7 +245,7 @@
     private List<AssertionsConfiguration> assertionsConfiguration = new ArrayList<>();
     private List<Consumer<Inspector>> outputInspections = new ArrayList<>();
     protected StringConsumer proguardMapConsumer = null;
-    private DumpInputFlags dumpInputFlags = DumpInputFlags.noDump();
+    private DumpInputFlags dumpInputFlags = DumpInputFlags.getDefault();
     private MapIdProvider mapIdProvider = null;
     private SourceFileProvider sourceFileProvider = null;
     private boolean isAndroidPlatformBuild = false;
@@ -664,6 +664,15 @@
       return isAndroidPlatformBuild;
     }
 
+    /**
+     * Allow to skip to dump into file and dump into directory instruction, this is primarily used
+     * for chained compilation in L8 so there are no duplicated dumps.
+     */
+    B skipDump() {
+      dumpInputFlags = DumpInputFlags.noDump();
+      return self();
+    }
+
     B dumpInputToFile(Path file) {
       dumpInputFlags = DumpInputFlags.dumpToFile(file);
       return self();
diff --git a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
index db0a9f8..ab6a79d 100644
--- a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import java.io.IOException;
 import java.util.Set;
 
 /**
@@ -33,4 +34,18 @@
    * calls from different threads.
    */
   ProgramResource getProgramResource(String descriptor);
+
+  /**
+   * Callback signifying that a given compilation unit is done using the resource provider.
+   *
+   * <p>This can be used to clean-up resources once it is guaranteed that the compiler will no
+   * longer request them. If a client shares a resource provider among multiple compilation units
+   * then the provider should be sure to either retain the resources or support reloading them on
+   * demand.
+   *
+   * <p>Providers should make sure finished can be safely called multiple times.
+   */
+  default void finished(DiagnosticsHandler handler) throws IOException {
+    // Do nothing by default.
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 9a340b8..34f4de6 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -208,6 +208,7 @@
     }
     Timing timing = Timing.create("D8", options);
     try {
+      timing.begin("Pre conversion");
       // Synthetic assertion to check that testing assertions works and can be enabled.
       assert forTesting(options, () -> !options.testing.testEnableTestAssertions);
 
@@ -246,9 +247,9 @@
       if (options.testing.enableD8ResourcesPassThrough) {
         appView.setAppServices(AppServices.builder(appView).build());
       }
-
+      timing.end();
       new IRConverter(appView, timing, printer).convert(appView, executor);
-
+      timing.begin("Post conversion");
       if (options.printCfg) {
         if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
           System.out.print(printer.toString());
@@ -286,9 +287,22 @@
       }
       Marker.checkCompatibleDesugaredLibrary(markers, options.reporter);
 
-      InspectorImpl.runInspections(options.outputInspections, appView.appInfo().classes());
-      appView.setNamingLens(PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView));
-      appView.setNamingLens(RecordRewritingNamingLens.createRecordRewritingNamingLens(appView));
+      timing.time(
+          "Run inspections",
+          () ->
+              InspectorImpl.runInspections(options.outputInspections, appView.appInfo().classes()));
+
+      timing.time(
+          "Create prefix rewriting lens",
+          () ->
+              appView.setNamingLens(
+                  PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView)));
+
+      timing.time(
+          "Create record rewriting lens",
+          () ->
+              appView.setNamingLens(
+                  RecordRewritingNamingLens.createRecordRewritingNamingLens(appView)));
 
       if (options.isGeneratingDex()
           && hasDexResources
@@ -313,20 +327,34 @@
       // Since tracing is not lens aware, this needs to be done prior to synthetic finalization
       // which will construct a graph lens.
       if (options.isGeneratingDex() && !options.mainDexKeepRules.isEmpty()) {
+        timing.begin("Generate main-dex list");
         appView.dexItemFactory().clearTypeElementsCache();
         MainDexInfo mainDexInfo =
             new GenerateMainDexList(options)
                 .traceMainDex(
                     executor, appView.appInfo().app(), appView.appInfo().getMainDexInfo());
         appView.setAppInfo(appView.appInfo().rebuildWithMainDexInfo(mainDexInfo));
+        timing.end();
       }
 
-      finalizeApplication(appView, executor);
+      timing.time("Finalize synthetics", () -> finalizeApplication(appView, executor, timing));
 
-      HorizontalClassMerger.createForD8ClassMerging(appView).runIfNecessary(executor, timing);
+      timing.time(
+          "Horizontal merger",
+          () ->
+              HorizontalClassMerger.createForD8ClassMerging(appView)
+                  .runIfNecessary(executor, timing));
 
-      new GenericSignatureRewriter(appView).runForD8(appView.appInfo().classes(), executor);
-      new KotlinMetadataRewriter(appView).runForD8(executor);
+      timing.time(
+          "Signature rewriter",
+          () ->
+              new GenericSignatureRewriter(appView)
+                  .runForD8(appView.appInfo().classes(), executor));
+
+      timing.time(
+          "Kotlin metadata rewriter", () -> new KotlinMetadataRewriter(appView).runForD8(executor));
+
+      timing.end(); // post-converter
 
       if (options.isGeneratingClassFiles()) {
         new CfApplicationWriter(appView, marker).write(options.getClassFileConsumer(), inputApp);
@@ -341,6 +369,7 @@
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } finally {
+      inputApp.signalFinishedToProviders(options.reporter);
       options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
@@ -365,9 +394,10 @@
     appView.setAssumeInfoCollection(assumeInfoCollectionBuilder.build());
   }
 
-  private static void finalizeApplication(AppView<AppInfo> appView, ExecutorService executorService)
+  private static void finalizeApplication(
+      AppView<AppInfo> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
-    SyntheticFinalization.finalize(appView, executorService);
+    SyntheticFinalization.finalize(appView, timing, executorService);
   }
 
   private static DexApplication rewriteNonDexInputs(
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 7ea383c..76c2966 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -90,7 +90,6 @@
     private String synthesizedClassPrefix = "";
     private boolean enableMainDexListCheck = true;
     private boolean minimalMainDex = false;
-    private boolean skipDump = false;
     private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
     private boolean enableMissingLibraryApiModeling = false;
 
@@ -265,15 +264,6 @@
       return self();
     }
 
-    /**
-     * Allow to skip to dump into file and dump into directory instruction, this is primarily used
-     * for chained compilation in L8 so there are no duplicated dumps.
-     */
-    Builder skipDump() {
-      skipDump = true;
-      return self();
-    }
-
     @Override
     Builder self() {
       return this;
@@ -427,7 +417,6 @@
           getAssertionsConfiguration(),
           getOutputInspections(),
           synthesizedClassPrefix,
-          skipDump,
           enableMainDexListCheck,
           minimalMainDex,
           mainDexKeepRules,
@@ -447,7 +436,6 @@
   private final StringConsumer desugaredLibraryKeepRuleConsumer;
   private final DesugaredLibrarySpecification desugaredLibrarySpecification;
   private final String synthesizedClassPrefix;
-  private final boolean skipDump;
   private final boolean enableMainDexListCheck;
   private final boolean minimalMainDex;
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
@@ -519,7 +507,6 @@
       List<AssertionsConfiguration> assertionsConfiguration,
       List<Consumer<Inspector>> outputInspections,
       String synthesizedClassPrefix,
-      boolean skipDump,
       boolean enableMainDexListCheck,
       boolean minimalMainDex,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
@@ -554,7 +541,6 @@
     this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
     this.synthesizedClassPrefix = synthesizedClassPrefix;
-    this.skipDump = skipDump;
     this.enableMainDexListCheck = enableMainDexListCheck;
     this.minimalMainDex = minimalMainDex;
     this.mainDexKeepRules = mainDexKeepRules;
@@ -571,7 +557,6 @@
     desugaredLibraryKeepRuleConsumer = null;
     desugaredLibrarySpecification = null;
     synthesizedClassPrefix = null;
-    skipDump = false;
     enableMainDexListCheck = true;
     minimalMainDex = false;
     mainDexKeepRules = null;
@@ -669,7 +654,7 @@
 
     internal.configureAndroidPlatformBuild(getAndroidPlatformBuild());
 
-    internal.setDumpInputFlags(getDumpInputFlags(), skipDump);
+    internal.setDumpInputFlags(getDumpInputFlags());
     internal.dumpOptions = dumpOptions();
 
     return internal;
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index b8163c7..ad3bf13 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -108,6 +108,7 @@
       } catch (ExecutionException e) {
         throw unwrapExecutionException(e);
       } finally {
+        inputApp.signalFinishedToProviders(options.reporter);
         options.signalFinishedToConsumers();
       }
     } finally {
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 1bb6dc5..1dddf3f 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -137,7 +137,7 @@
 
       new IRConverter(appView, timing).convert(appView, executor);
 
-      SyntheticFinalization.finalize(appView, executor);
+      SyntheticFinalization.finalize(appView, timing, executor);
 
       appView.setNamingLens(PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView));
       new GenericSignatureRewriter(appView).run(appView.appInfo().classes(), executor);
@@ -148,6 +148,7 @@
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } finally {
+      inputApp.signalFinishedToProviders(options.reporter);
       options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index dce9604..b9fe11b 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -217,7 +217,7 @@
     internal.apiModelingOptions().disableApiCallerIdentification();
     internal.apiModelingOptions().disableMissingApiModeling();
 
-    internal.setDumpInputFlags(getDumpInputFlags(), false);
+    internal.setDumpInputFlags(getDumpInputFlags());
     internal.dumpOptions = dumpOptions();
 
     return internal;
diff --git a/src/main/java/com/android/tools/r8/ProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
index d4b8a87..dd2bea8 100644
--- a/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import java.io.IOException;
 import java.util.Collection;
 
 /** Program resource provider. */
@@ -14,4 +15,18 @@
   default DataResourceProvider getDataResourceProvider() {
     return null;
   }
+
+  /**
+   * Callback signifying that a given compilation unit is done using the resource provider.
+   *
+   * <p>This can be used to clean-up resources once it is guaranteed that the compiler will no
+   * longer request them. If a client shares a resource provider among multiple compilation units
+   * then the provider should be sure to either retain the resources or support reloading them on
+   * demand.
+   *
+   * <p>Providers should make sure finished can be safely called multiple times.
+   */
+  default void finished(DiagnosticsHandler handler) throws IOException {
+    // Do nothing by default.
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 4cc8c25..b781d3a 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -67,7 +67,7 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
-import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
+import com.android.tools.r8.optimize.AccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
@@ -462,7 +462,7 @@
       if (options.getProguardConfiguration().isAccessModificationAllowed()) {
         SubtypingInfo subtypingInfo = appViewWithLiveness.appInfo().computeSubtypingInfo();
         GraphLens publicizedLens =
-            ClassAndMemberPublicizer.run(
+            AccessModifier.run(
                 executorService,
                 timing,
                 appViewWithLiveness.appInfo().app(),
@@ -718,9 +718,9 @@
       appView.setGraphLens(MemberRebindingIdentityLensFactory.create(appView, executorService));
 
       if (appView.appInfo().hasLiveness()) {
-        SyntheticFinalization.finalizeWithLiveness(appView.withLiveness(), executorService);
+        SyntheticFinalization.finalizeWithLiveness(appView.withLiveness(), executorService, timing);
       } else {
-        SyntheticFinalization.finalizeWithClassHierarchy(appView, executorService);
+        SyntheticFinalization.finalizeWithClassHierarchy(appView, executorService, timing);
       }
 
       // Read any -applymapping input to allow for repackaging to not relocate the classes.
@@ -845,6 +845,7 @@
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } finally {
+      inputApp.signalFinishedToProviders(options.reporter);
       options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 1496f40..7498045 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -114,11 +114,12 @@
     private InputDependencyGraphConsumer inputDependencyGraphConsumer = null;
     private final List<FeatureSplit> featureSplits = new ArrayList<>();
     private String synthesizedClassPrefix = "";
-    private boolean skipDump = false;
     private boolean enableMissingLibraryApiModeling = false;
 
     private final ProguardConfigurationParserOptions.Builder parserOptionsBuilder =
         ProguardConfigurationParserOptions.builder().readEnvironment();
+    private final boolean allowDexInArchive =
+        System.getProperty("com.android.tools.r8.allowDexInputToR8") != null;
 
     // TODO(zerny): Consider refactoring CompatProguardCommandBuilder to avoid subclassing.
     Builder() {
@@ -127,17 +128,17 @@
 
     Builder(DiagnosticsHandler diagnosticsHandler) {
       super(diagnosticsHandler);
-      setIgnoreDexInArchive(true);
+      setIgnoreDexInArchive(!allowDexInArchive);
     }
 
     private Builder(AndroidApp app) {
       super(app);
-      setIgnoreDexInArchive(true);
+      setIgnoreDexInArchive(!allowDexInArchive);
     }
 
     private Builder(AndroidApp app, DiagnosticsHandler diagnosticsHandler) {
       super(app, diagnosticsHandler);
-      setIgnoreDexInArchive(true);
+      setIgnoreDexInArchive(!allowDexInArchive);
     }
 
     // Internal
@@ -286,15 +287,6 @@
     }
 
     /**
-     * Allow to skip to dump into file and dump into directory instruction, this is primarily used
-     * for chained compilation in L8 so there are no duplicated dumps.
-     */
-    Builder skipDump() {
-      skipDump = true;
-      return self();
-    }
-
-    /**
      * Set a consumer for receiving the proguard usage information.
      *
      * <p>Note that any subsequent calls to this method will replace the previous setting.
@@ -632,7 +624,6 @@
               getAssertionsConfiguration(),
               getOutputInspections(),
               synthesizedClassPrefix,
-              skipDump,
               getThreadCount(),
               getDumpInputFlags(),
               getMapIdProvider(),
@@ -734,7 +725,6 @@
   private final DesugaredLibrarySpecification desugaredLibrarySpecification;
   private final FeatureSplitConfiguration featureSplitConfiguration;
   private final String synthesizedClassPrefix;
-  private final boolean skipDump;
   private final boolean enableMissingLibraryApiModeling;
 
   /** Get a new {@link R8Command.Builder}. */
@@ -820,7 +810,6 @@
       List<AssertionsConfiguration> assertionsConfiguration,
       List<Consumer<Inspector>> outputInspections,
       String synthesizedClassPrefix,
-      boolean skipDump,
       int threadCount,
       DumpInputFlags dumpInputFlags,
       MapIdProvider mapIdProvider,
@@ -865,7 +854,6 @@
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
     this.featureSplitConfiguration = featureSplitConfiguration;
     this.synthesizedClassPrefix = synthesizedClassPrefix;
-    this.skipDump = skipDump;
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
   }
 
@@ -889,7 +877,6 @@
     desugaredLibrarySpecification = null;
     featureSplitConfiguration = null;
     synthesizedClassPrefix = null;
-    skipDump = false;
     enableMissingLibraryApiModeling = false;
   }
 
@@ -1053,7 +1040,7 @@
       internal.threadCount = getThreadCount();
     }
 
-    internal.setDumpInputFlags(getDumpInputFlags(), skipDump);
+    internal.setDumpInputFlags(getDumpInputFlags());
     internal.dumpOptions = dumpOptions();
 
     return internal;
diff --git a/src/main/java/com/android/tools/r8/cf/CfVerifierTool.java b/src/main/java/com/android/tools/r8/cf/CfVerifierTool.java
index bd4a06f..5b1d74f 100644
--- a/src/main/java/com/android/tools/r8/cf/CfVerifierTool.java
+++ b/src/main/java/com/android/tools/r8/cf/CfVerifierTool.java
@@ -36,7 +36,12 @@
     appView.setAppServices(AppServices.builder(appView).build());
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       clazz.forEachProgramMethod(
-          method -> method.getDefinition().getCode().asCfCode().verifyFrames(method, appView));
+          method ->
+              method
+                  .getDefinition()
+                  .getCode()
+                  .asCfCode()
+                  .getOrComputeStackMapStatus(method, appView));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
index d52f83c..4bff728 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
@@ -14,10 +14,10 @@
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InitClassLens;
 import com.android.tools.r8.graph.JarApplicationReader;
@@ -34,10 +34,11 @@
 import com.android.tools.r8.optimize.interfaces.analysis.CfAnalysisConfig;
 import com.android.tools.r8.optimize.interfaces.analysis.CfFrameState;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.ListIterator;
 import org.objectweb.asm.ConstantDynamic;
 import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
 
 public class CfConstDynamic extends CfInstruction implements CfTypeInstruction {
 
@@ -47,12 +48,12 @@
       DexString name,
       DexType type,
       DexMethodHandle bootstrapMethod,
-      Object[] bootstrapMethodArguments) {
+      List<DexValue> bootstrapMethodArguments) {
     assert name != null;
     assert type != null;
     assert bootstrapMethod != null;
     assert bootstrapMethodArguments != null;
-    assert bootstrapMethodArguments.length == 0;
+    assert bootstrapMethodArguments.isEmpty();
 
     reference = new ConstantDynamicReference(name, type, bootstrapMethod, bootstrapMethodArguments);
   }
@@ -79,7 +80,7 @@
     return reference.getBootstrapMethod();
   }
 
-  public Object[] getBootstrapMethodArguments() {
+  public List<DexValue> getBootstrapMethodArguments() {
     return reference.getBootstrapMethodArguments();
   }
 
@@ -87,53 +88,20 @@
       ConstantDynamic insn, JarApplicationReader application, DexType clazz) {
     String constantName = insn.getName();
     String constantDescriptor = insn.getDescriptor();
-    // TODO(b/178172809): Handle bootstrap arguments.
-    if (insn.getBootstrapMethodArgumentCount() > 0) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (has arguments to bootstrap method)");
-    }
-    if (insn.getBootstrapMethod().getTag() != Opcodes.H_INVOKESTATIC) {
-      throw new CompilationError("Unsupported dynamic constant (not invoke static)");
-    }
-    if (insn.getBootstrapMethod().getOwner().equals("java/lang/invoke/ConstantBootstraps")) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (runtime provided bootstrap method)");
-    }
-    if (application.getTypeFromName(insn.getBootstrapMethod().getOwner()) != clazz) {
-      throw new CompilationError("Unsupported dynamic constant (different owner)");
-    }
-    // Resolve the bootstrap method.
     DexMethodHandle bootstrapMethodHandle =
         DexMethodHandle.fromAsmHandle(insn.getBootstrapMethod(), application, clazz);
-    if (!bootstrapMethodHandle.member.isDexMethod()) {
-      throw new CompilationError("Unsupported dynamic constant (invalid method handle)");
-    }
-    DexMethod bootstrapMethod = bootstrapMethodHandle.asMethod();
-    if (bootstrapMethod.getProto().returnType != application.getTypeFromDescriptor("[Z")
-        && bootstrapMethod.getProto().returnType
-            != application.getTypeFromDescriptor("Ljava/lang/Object;")) {
-      throw new CompilationError("Unsupported dynamic constant (unsupported constant type)");
-    }
-    if (bootstrapMethod.getProto().getParameters().size() != 3) {
-      throw new CompilationError("Unsupported dynamic constant (unsupported signature)");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(0) != application.getFactory().lookupType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of first argument to bootstrap method");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(1) != application.getFactory().stringType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of second argument to bootstrap method");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(2) != application.getFactory().classType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of third argument to bootstrap method");
+    int argumentCount = insn.getBootstrapMethodArgumentCount();
+    List<DexValue> bootstrapMethodArguments = new ArrayList<>(argumentCount);
+    for (int i = 0; i < argumentCount; i++) {
+      Object argument = insn.getBootstrapMethodArgument(i);
+      DexValue dexValue = DexValue.fromAsmBootstrapArgument(argument, application, clazz);
+      bootstrapMethodArguments.add(dexValue);
     }
     return new CfConstDynamic(
         application.getString(constantName),
         application.getTypeFromDescriptor(constantDescriptor),
         bootstrapMethodHandle,
-        new Object[] {});
+        bootstrapMethodArguments);
   }
 
   @Override
@@ -186,8 +154,30 @@
       NamingLens namingLens,
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
-    // TODO(b/198142625): Support CONSTANT_Dynamic for R8 cf to cf.
-    throw new CompilationError("Unsupported dynamic constant (not desugaring)");
+    DexMethodHandle rewrittenHandle =
+        rewriter.rewriteDexMethodHandle(
+            reference.getBootstrapMethod(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context);
+    List<DexValue> rewrittenArguments =
+        rewriter.rewriteBootstrapArguments(
+            reference.getBootstrapMethodArguments(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context);
+    Object[] bsmArgs = new Object[rewrittenArguments.size()];
+    for (int i = 0; i < rewrittenArguments.size(); i++) {
+      bsmArgs[i] = CfInvokeDynamic.decodeBootstrapArgument(rewrittenArguments.get(i), namingLens);
+    }
+    ConstantDynamic constantDynamic =
+        new ConstantDynamic(
+            reference.getName().toString(),
+            getConstantTypeDescriptor(graphLens, namingLens, dexItemFactory),
+            rewrittenHandle.toAsmHandle(namingLens),
+            bsmArgs);
+    visitor.visitLdcInsn(constantDynamic);
+  }
+
+  private String getConstantTypeDescriptor(
+      GraphLens graphLens, NamingLens namingLens, DexItemFactory factory) {
+    DexType rewrittenType = graphLens.lookupType(reference.getType());
+    DexType renamedType = namingLens.lookupType(rewrittenType, factory);
+    return renamedType.toDescriptorString();
   }
 
   @Override
@@ -212,7 +202,7 @@
     registry.registerTypeReference(reference.getType());
     registry.registerMethodHandle(
         reference.getBootstrapMethod(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
-    assert reference.getBootstrapMethodArguments().length == 0;
+    assert reference.getBootstrapMethodArguments().isEmpty();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
index 7866b2d..bdd6512 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
@@ -378,6 +378,10 @@
       return this == NOT_PRESENT;
     }
 
+    public boolean isNotVerified() {
+      return this == NOT_VERIFIED;
+    }
+
     public boolean isValid() {
       return this == VALID;
     }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index 45228d7..b880c47 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -98,7 +98,7 @@
     return 5;
   }
 
-  private Object decodeBootstrapArgument(DexValue value, NamingLens lens) {
+  public static Object decodeBootstrapArgument(DexValue value, NamingLens lens) {
     switch (value.getValueKind()) {
       case DOUBLE:
         return value.asDexValueDouble().getValue();
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index f00f7fb..e351e57 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringResource;
+import com.android.tools.r8.dump.DumpOptions;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.UnsupportedMainDexListUsageDiagnostic;
 import com.android.tools.r8.graph.ApplicationReaderMap;
@@ -41,6 +42,7 @@
 import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexVersion;
+import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexListParser;
@@ -49,9 +51,7 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import java.io.IOException;
-import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -113,7 +113,7 @@
         inputApp.getProguardMapInputData(),
         executorService,
         ProgramClassCollection.defaultConflictResolver(options.reporter),
-        false);
+        DumpInputFlags.noDump());
   }
 
   public final LazyLoadedDexApplication read(
@@ -131,19 +131,17 @@
       ExecutorService executorService,
       ProgramClassConflictResolver resolver)
       throws IOException {
-    return read(proguardMap, executorService, resolver, true);
+    return read(proguardMap, executorService, resolver, options.getDumpInputFlags());
   }
 
   public final LazyLoadedDexApplication read(
       StringResource proguardMap,
       ExecutorService executorService,
       ProgramClassConflictResolver resolver,
-      boolean shouldDump)
+      DumpInputFlags dumpInputFlags)
       throws IOException {
     assert verifyMainDexOptionsCompatible(inputApp, options);
-    if (shouldDump) {
-      dumpApplication();
-    }
+    dumpApplication(dumpInputFlags);
 
     if (options.testing.verifyInputs) {
       inputApp.validateInputs();
@@ -184,31 +182,20 @@
     return builder.build();
   }
 
-  private void dumpApplication() throws IOException {
-    Path dumpOutput = null;
-    boolean cleanDump = false;
-    if (options.dumpInputToFile != null) {
-      dumpOutput = Paths.get(options.dumpInputToFile);
-    } else if (options.dumpInputToDirectory != null) {
-      dumpOutput =
-          Paths.get(options.dumpInputToDirectory).resolve("dump" + System.nanoTime() + ".zip");
-    } else if (options.testing.dumpAll) {
-      cleanDump = true;
-      dumpOutput = Paths.get("/tmp").resolve("dump" + System.nanoTime() + ".zip");
+  private void dumpApplication(DumpInputFlags dumpInputFlags) {
+    DumpOptions dumpOptions = options.dumpOptions;
+    if (dumpOptions == null || !dumpInputFlags.shouldDump(dumpOptions)) {
+      return;
     }
-    if (dumpOutput != null) {
-      timing.begin("ApplicationReader.dump");
-      inputApp.dump(dumpOutput, options.dumpOptions, options.reporter, options.dexItemFactory());
-      if (cleanDump) {
-        Files.delete(dumpOutput);
-      }
-      timing.end();
-      Diagnostic message = new StringDiagnostic("Dumped compilation inputs to: " + dumpOutput);
-      if (options.dumpInputToFile != null) {
-        throw options.reporter.fatalError(message);
-      } else if (!cleanDump) {
-        options.reporter.info(message);
-      }
+    Path dumpOutput = dumpInputFlags.getDumpPath();
+    timing.begin("ApplicationReader.dump");
+    inputApp.dump(dumpOutput, dumpOptions, options.reporter, options.dexItemFactory());
+    timing.end();
+    Diagnostic message = new StringDiagnostic("Dumped compilation inputs to: " + dumpOutput);
+    if (dumpInputFlags.shouldFailCompilation()) {
+      throw options.reporter.fatalError(message);
+    } else {
+      options.reporter.info(message);
     }
   }
 
@@ -347,7 +334,7 @@
       return new DexApplicationReadFlags(
           hasReadProgramResourceFromDex,
           hasReadProgramResourceFromCf,
-          application.hasReadRecordReferenceFromProgramClass());
+          application.getRecordWitnesses());
     }
 
     private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 6167f9b..a6f6a52 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -7,10 +7,10 @@
 
 import com.android.tools.r8.ByteBufferProvider;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.DefaultInterfaceMethodDiagnostic;
-import com.android.tools.r8.errors.InvokeCustomDiagnostic;
-import com.android.tools.r8.errors.PrivateInterfaceMethodDiagnostic;
-import com.android.tools.r8.errors.StaticInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedDefaultInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedPrivateInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedStaticInterfaceMethodDiagnostic;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
@@ -290,9 +290,9 @@
       if (!options.canUseDefaultAndStaticInterfaceMethods()
           && !options.testing.allowStaticInterfaceMethodsForPreNApiLevel) {
         throw options.reporter.fatalError(
-            new StaticInterfaceMethodDiagnostic(holder.getOrigin(), MethodPosition.create(method)));
+            new UnsupportedStaticInterfaceMethodDiagnostic(
+                holder.getOrigin(), MethodPosition.create(method)));
       }
-
     } else {
       if (method.isInstanceInitializer()) {
         throw new CompilationError(
@@ -301,7 +301,7 @@
       if (!method.accessFlags.isAbstract() && !method.accessFlags.isPrivate() &&
           !options.canUseDefaultAndStaticInterfaceMethods()) {
         throw options.reporter.fatalError(
-            new DefaultInterfaceMethodDiagnostic(
+            new UnsupportedDefaultInterfaceMethodDiagnostic(
                 holder.getOrigin(), MethodPosition.create(method)));
       }
     }
@@ -311,7 +311,8 @@
         return;
       }
       throw options.reporter.fatalError(
-          new PrivateInterfaceMethodDiagnostic(holder.getOrigin(), MethodPosition.create(method)));
+          new UnsupportedPrivateInterfaceMethodDiagnostic(
+              holder.getOrigin(), MethodPosition.create(method)));
     }
 
     if (!method.accessFlags.isPublic()) {
@@ -1382,7 +1383,7 @@
   private void checkThatInvokeCustomIsAllowed() {
     if (!options.canUseInvokeCustom()) {
       throw options.reporter.fatalError(
-          new InvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN));
+          new UnsupportedInvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index cb15cb2..1056bda 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -78,7 +78,7 @@
   private final IndexedItemTransaction transaction;
   private final FeatureSplit featureSplit;
 
-  private final DexProgramClass primaryClass;
+  private final DexString primaryClassDescriptor;
   private DebugRepresentation debugRepresentation;
 
   VirtualFile(int id, AppView<?> appView) {
@@ -107,7 +107,10 @@
     this.id = id;
     this.indexedItems = new VirtualFileIndexedItemCollection(appView);
     this.transaction = new IndexedItemTransaction(indexedItems, appView);
-    this.primaryClass = primaryClass;
+    this.primaryClassDescriptor =
+        primaryClass == null
+            ? null
+            : appView.getNamingLens().lookupClassDescriptor(primaryClass.type);
     this.featureSplit = featureSplit;
   }
 
@@ -129,7 +132,7 @@
   }
 
   public String getPrimaryClassDescriptor() {
-    return primaryClass == null ? null : primaryClass.type.descriptor.toString();
+    return primaryClassDescriptor == null ? null : primaryClassDescriptor.toString();
   }
 
   public void setDebugRepresentation(DebugRepresentation debugRepresentation) {
diff --git a/src/main/java/com/android/tools/r8/dump/DumpOptions.java b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
index da85b0e..1d55987 100644
--- a/src/main/java/com/android/tools/r8/dump/DumpOptions.java
+++ b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
@@ -14,8 +14,10 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 
 @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@@ -109,34 +111,45 @@
     this.dumpInputToFile = dumpInputToFile;
   }
 
-  public String dumpOptions() {
+  public String getBuildPropertiesFileContent() {
     StringBuilder builder = new StringBuilder();
-    addDumpEntry(builder, TOOL_KEY, tool.name());
+    getBuildProperties()
+        .forEach((key, value) -> builder.append(key).append("=").append(value).append("\n"));
+    return builder.toString();
+  }
+
+  public Map<String, String> getBuildProperties() {
+    Map<String, String> buildProperties = new LinkedHashMap<>();
+    addDumpEntry(buildProperties, TOOL_KEY, tool.name());
     // We keep the following values for backward compatibility.
     addDumpEntry(
-        builder,
+        buildProperties,
         MODE_KEY,
         compilationMode == CompilationMode.DEBUG ? DEBUG_MODE_VALUE : RELEASE_MODE_VALUE);
-    addDumpEntry(builder, MIN_API_KEY, minApi);
-    addDumpEntry(builder, OPTIMIZE_MULTIDEX_FOR_LINEAR_ALLOC_KEY, optimizeMultidexForLinearAlloc);
+    addDumpEntry(buildProperties, MIN_API_KEY, minApi);
+    addDumpEntry(
+        buildProperties, OPTIMIZE_MULTIDEX_FOR_LINEAR_ALLOC_KEY, optimizeMultidexForLinearAlloc);
     if (threadCount != ThreadUtils.NOT_SPECIFIED) {
-      addDumpEntry(builder, THREAD_COUNT_KEY, threadCount);
+      addDumpEntry(buildProperties, THREAD_COUNT_KEY, threadCount);
     }
-    addDumpEntry(builder, DESUGAR_STATE_KEY, desugarState);
-    addDumpEntry(builder, ENABLE_MISSING_LIBRARY_API_MODELING, enableMissingLibraryApiModeling);
+    addDumpEntry(buildProperties, DESUGAR_STATE_KEY, desugarState);
+    addDumpEntry(
+        buildProperties, ENABLE_MISSING_LIBRARY_API_MODELING, enableMissingLibraryApiModeling);
     if (isAndroidPlatformBuild) {
-      addDumpEntry(builder, ANDROID_PLATFORM_BUILD, isAndroidPlatformBuild);
+      addDumpEntry(buildProperties, ANDROID_PLATFORM_BUILD, isAndroidPlatformBuild);
     }
-    addOptionalDumpEntry(builder, INTERMEDIATE_KEY, intermediate);
-    addOptionalDumpEntry(builder, INCLUDE_DATA_RESOURCES_KEY, includeDataResources);
-    addOptionalDumpEntry(builder, TREE_SHAKING_KEY, treeShaking);
-    addOptionalDumpEntry(builder, MINIFICATION_KEY, minification);
-    addOptionalDumpEntry(builder, FORCE_PROGUARD_COMPATIBILITY_KEY, forceProguardCompatibility);
+    addOptionalDumpEntry(buildProperties, INTERMEDIATE_KEY, intermediate);
+    addOptionalDumpEntry(buildProperties, INCLUDE_DATA_RESOURCES_KEY, includeDataResources);
+    addOptionalDumpEntry(buildProperties, TREE_SHAKING_KEY, treeShaking);
+    addOptionalDumpEntry(buildProperties, MINIFICATION_KEY, minification);
+    addOptionalDumpEntry(
+        buildProperties, FORCE_PROGUARD_COMPATIBILITY_KEY, forceProguardCompatibility);
     ArrayList<String> sortedKeys = new ArrayList<>(systemProperties.keySet());
     sortedKeys.sort(String::compareTo);
     sortedKeys.forEach(
-        key -> addDumpEntry(builder, SYSTEM_PROPERTY_PREFIX + key, systemProperties.get(key)));
-    return builder.toString();
+        key ->
+            addDumpEntry(buildProperties, SYSTEM_PROPERTY_PREFIX + key, systemProperties.get(key)));
+    return buildProperties;
   }
 
   public static void parse(String content, DumpOptions.Builder builder) {
@@ -219,12 +232,13 @@
     return minApi;
   }
 
-  private void addOptionalDumpEntry(StringBuilder builder, String key, Optional<?> optionalValue) {
-    optionalValue.ifPresent(bool -> addDumpEntry(builder, key, bool));
+  private void addOptionalDumpEntry(
+      Map<String, String> buildProperties, String key, Optional<?> optionalValue) {
+    optionalValue.ifPresent(bool -> addDumpEntry(buildProperties, key, bool));
   }
 
-  private void addDumpEntry(StringBuilder builder, String key, Object value) {
-    builder.append(key).append("=").append(value).append("\n");
+  private void addDumpEntry(Map<String, String> buildProperties, String key, Object value) {
+    buildProperties.put(key, Objects.toString(value));
   }
 
   private boolean hasDesugaredLibraryConfiguration() {
diff --git a/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java
deleted file mode 100644
index d77a5f5..0000000
--- a/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. 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.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class ConstMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public ConstMethodHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("const-method-handle", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Const-method-handle", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java
deleted file mode 100644
index 2a9eff5..0000000
--- a/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. 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.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class ConstMethodTypeDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public ConstMethodTypeDiagnostic(Origin origin, MethodPosition position) {
-    super("const-method-type", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Const-method-type", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java
new file mode 100644
index 0000000..d80b530
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+/** Common type for all diagnostics related to constant-dynamic desugaring. */
+@Keep
+public class ConstantDynamicDesugarDiagnostic implements DesugarDiagnostic {
+
+  private final Origin origin;
+  private final Position position;
+  private final String message;
+
+  public ConstantDynamicDesugarDiagnostic(Origin origin, Position position, String message) {
+    this.origin = origin;
+    this.position = position;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java
deleted file mode 100644
index 21b15b2..0000000
--- a/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class DefaultInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public DefaultInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("default-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Default interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.java
deleted file mode 100644
index 0f8c6ab..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.Position;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokeCustomDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokeCustomDiagnostic(Origin origin, Position position) {
-    super("invoke-custom", AndroidApiLevel.O, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(AndroidApiLevel.O, "Invoke-customs", null);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java
deleted file mode 100644
index 1efbb31..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. 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.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokePolymorphicMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokePolymorphicMethodHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("invoke-polymorphic-method-handle", AndroidApiLevel.O, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.O,
-        "MethodHandle.invoke and MethodHandle.invokeExact",
-        getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java
deleted file mode 100644
index 9758581..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. 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.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokePolymorphicVarHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokePolymorphicVarHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("invoke-polymorphic-var-handle", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Call to polymorphic signature of VarHandle", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java
deleted file mode 100644
index 9c85254..0000000
--- a/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class PrivateInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public PrivateInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("private-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Private interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java
deleted file mode 100644
index 9bc4f48..0000000
--- a/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class StaticInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public StaticInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("static-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Static interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java
new file mode 100644
index 0000000..b75f686
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.InternalOptions;
+
+@Keep
+public class UnsupportedConstDynamicDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-dynamic";
+
+  public UnsupportedConstDynamicDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, InternalOptions.constantDynamicApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        InternalOptions.constantDynamicApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java
new file mode 100644
index 0000000..375adf1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.constantMethodHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedConstMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-method-handle";
+
+  public UnsupportedConstMethodHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, constantMethodHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        constantMethodHandleApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java
new file mode 100644
index 0000000..5809011
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.constantMethodTypeApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedConstMethodTypeDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-method-type";
+
+  public UnsupportedConstMethodTypeDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, constantMethodTypeApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        constantMethodTypeApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..6905dcc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.defaultInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedDefaultInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "default-interface-method";
+
+  public UnsupportedDefaultInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, defaultInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        defaultInterfaceMethodsApiLevel(), "Default interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
index 74c3ad6..1ac0482 100644
--- a/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
@@ -15,12 +15,14 @@
   public static String makeMessage(
       AndroidApiLevel minApiLevel, String unsupportedFeatures, String sourceString) {
     String message =
-        unsupportedFeatures
-            + " are only supported starting with "
-            + minApiLevel.getName()
-            + " (--min-api "
-            + minApiLevel.getLevel()
-            + ")";
+        minApiLevel == null
+            ? (unsupportedFeatures + " are not supported at any API level known by the compiler")
+            : (unsupportedFeatures
+                + " are only supported starting with "
+                + minApiLevel.getName()
+                + " (--min-api "
+                + minApiLevel.getLevel()
+                + ")");
     message = (sourceString != null) ? message + ": " + sourceString : message;
     return message;
   }
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java
new file mode 100644
index 0000000..ca67569
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.invokeCustomApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokeCustomDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-custom";
+
+  public UnsupportedInvokeCustomDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokeCustomApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(invokeCustomApiLevel(), "Invoke-customs", null);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java
new file mode 100644
index 0000000..23718f3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.invokePolymorphicOnMethodHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokePolymorphicMethodHandleDiagnostic
+    extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-polymorphic-method-handle";
+
+  public UnsupportedInvokePolymorphicMethodHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokePolymorphicOnMethodHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        invokePolymorphicOnMethodHandleApiLevel(),
+        "MethodHandle.invoke and MethodHandle.invokeExact",
+        getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java
new file mode 100644
index 0000000..49f73a6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.invokePolymorphicOnVarHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokePolymorphicVarHandleDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-polymorphic-var-handle";
+
+  public UnsupportedInvokePolymorphicVarHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokePolymorphicOnVarHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        invokePolymorphicOnVarHandleApiLevel(),
+        "Call to polymorphic signature of VarHandle",
+        getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..0a52151
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.privateInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedPrivateInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "private-interface-method";
+
+  public UnsupportedPrivateInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, privateInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        privateInterfaceMethodsApiLevel(), "Private interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..3220744
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 static com.android.tools.r8.utils.InternalOptions.staticInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedStaticInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "static-interface-method";
+
+  public UnsupportedStaticInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, staticInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        staticInterfaceMethodsApiLevel(), "Static interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 0d4f9c9..9da9fd8 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -248,6 +248,10 @@
     return stackMapStatus;
   }
 
+  public void setStackMapStatus(StackMapStatus stackMapStatus) {
+    this.stackMapStatus = stackMapStatus;
+  }
+
   public com.android.tools.r8.position.Position getDiagnosticPosition() {
     return diagnosticPosition;
   }
@@ -407,7 +411,7 @@
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
     GraphLens graphLens = appView.graphLens();
-    assert verifyFrames(method, appView).isValidOrNotPresent()
+    assert getOrComputeStackMapStatus(method, appView).isValidOrNotPresent()
         : "Could not validate stack map frames";
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     InitClassLens initClassLens = appView.initClassLens();
@@ -418,11 +422,13 @@
       parameterLabel.write(
           appView, method, dexItemFactory, graphLens, initClassLens, namingLens, rewriter, visitor);
     }
+    boolean discardFrames =
+        classFileVersion.isLessThan(CfVersion.V1_6)
+            || (appView.enableWholeProgramOptimizations()
+                && classFileVersion.isEqualTo(CfVersion.V1_6)
+                && !options.shouldKeepStackMapTable());
     for (CfInstruction instruction : instructions) {
-      if (instruction instanceof CfFrame
-          && (classFileVersion.isLessThan(CfVersion.V1_6)
-              || (classFileVersion.isEqualTo(CfVersion.V1_6)
-                  && !options.shouldKeepStackMapTable()))) {
+      if (discardFrames && instruction instanceof CfFrame) {
         continue;
       }
       instruction.write(
@@ -545,7 +551,7 @@
   }
 
   private void verifyFramesOrRemove(ProgramMethod method, AppView<?> appView, GraphLens codeLens) {
-    stackMapStatus = verifyFrames(method, appView, codeLens);
+    stackMapStatus = getOrComputeStackMapStatus(method, appView, codeLens);
     if (!stackMapStatus.isValidOrNotPresent()) {
       ArrayList<CfInstruction> copy = new ArrayList<>(instructions);
       copy.removeIf(CfInstruction::isFrame);
@@ -884,11 +890,20 @@
         originalHolder, maxStack, maxLocals, newInstructions, tryCatchRanges, localVariables);
   }
 
-  public StackMapStatus verifyFrames(ProgramMethod method, AppView<?> appView) {
-    return verifyFrames(method, appView, getCodeLens(appView));
+  public StackMapStatus getOrComputeStackMapStatus(ProgramMethod method, AppView<?> appView) {
+    return getOrComputeStackMapStatus(method, appView, getCodeLens(appView));
   }
 
-  public StackMapStatus verifyFrames(ProgramMethod method, AppView<?> appView, GraphLens codeLens) {
+  public StackMapStatus getOrComputeStackMapStatus(
+      ProgramMethod method, AppView<?> appView, GraphLens codeLens) {
+    if (stackMapStatus.isNotVerified()) {
+      setStackMapStatus(computeStackMapStatus(method, appView, codeLens));
+    }
+    return stackMapStatus;
+  }
+
+  private StackMapStatus computeStackMapStatus(
+      ProgramMethod method, AppView<?> appView, GraphLens codeLens) {
     CfFrameVerifierEventConsumer eventConsumer =
         new CfFrameVerifierEventConsumer() {
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
index bb3bcf8..7bce5f6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
@@ -3,21 +3,23 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import java.util.Set;
+
 // Flags set based on the application when it was read.
 // Note that in r8, once classes are pruned, the flags may not reflect the application anymore.
 public class DexApplicationReadFlags {
 
   private final boolean hasReadProgramClassFromDex;
   private final boolean hasReadProgramClassFromCf;
-  private final boolean hasReadRecordReferenceFromProgramClass;
+  private final Set<DexType> recordWitnesses;
 
   public DexApplicationReadFlags(
       boolean hasReadProgramClassFromDex,
       boolean hasReadProgramClassFromCf,
-      boolean hasReadRecordReferenceFromProgramClass) {
+      Set<DexType> recordWitnesses) {
     this.hasReadProgramClassFromDex = hasReadProgramClassFromDex;
     this.hasReadProgramClassFromCf = hasReadProgramClassFromCf;
-    this.hasReadRecordReferenceFromProgramClass = hasReadRecordReferenceFromProgramClass;
+    this.recordWitnesses = recordWitnesses;
   }
 
   public boolean hasReadProgramClassFromCf() {
@@ -29,6 +31,10 @@
   }
 
   public boolean hasReadRecordReferenceFromProgramClass() {
-    return hasReadRecordReferenceFromProgramClass;
+    return !recordWitnesses.isEmpty();
+  }
+
+  public Set<DexType> getRecordWitnesses() {
+    return recordWitnesses;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 265fc65..3069a60 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.OptionalBool;
@@ -856,8 +857,8 @@
   public boolean isResolvable(AppView<?> appView) {
     if (isResolvable.isUnknown()) {
       boolean resolvable;
-      if (!isProgramClass()) {
-        resolvable = appView.dexItemFactory().libraryTypesAssumedToBePresent.contains(type);
+      if (isLibraryClass()) {
+        resolvable = AndroidApiLevelUtils.isApiSafeForReference(asLibraryClass(), appView);
       } else {
         resolvable = true;
         for (DexType supertype : allImmediateSupertypes()) {
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 51a4a4a..b785927 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -447,6 +447,10 @@
     return isInstanceInitializer() || isClassInitializer();
   }
 
+  public boolean isInitializer(boolean isStatic) {
+    return isStatic ? isClassInitializer() : isInstanceInitializer();
+  }
+
   public boolean isInstanceInitializer() {
     checkIfObsolete();
     return accessFlags.isConstructor() && !accessFlags.isStatic();
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 fbd4b5a..c0aaa95 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -666,6 +666,8 @@
 
   public final DexType metafactoryType =
       createStaticallyKnownType("Ljava/lang/invoke/LambdaMetafactory;");
+  public final DexType constantBootstrapsType =
+      createStaticallyKnownType("Ljava/lang/invoke/ConstantBootstraps;");
   public final DexType callSiteType = createStaticallyKnownType("Ljava/lang/invoke/CallSite;");
   public final DexType lookupType =
       createStaticallyKnownType("Ljava/lang/invoke/MethodHandles$Lookup;");
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 2cb6abd..c455396 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -312,7 +312,7 @@
 
   public abstract AbstractValue toAbstractValue(AbstractValueFactory factory);
 
-  static DexValue fromAsmBootstrapArgument(
+  public static DexValue fromAsmBootstrapArgument(
       Object value, JarApplicationReader application, DexType clazz) {
     if (value instanceof Integer) {
       return DexValue.DexValueInt.create((Integer) value);
diff --git a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
index ca751d0..d342780 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -7,7 +7,9 @@
 import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import org.objectweb.asm.Type;
 
@@ -25,6 +27,7 @@
   private final ConcurrentHashMap<String, Type> asmTypeCache = new ConcurrentHashMap<>();
   private final ConcurrentHashMap<String, DexString> stringCache = new ConcurrentHashMap<>();
   private final ApplicationReaderMap applicationReaderMap;
+  private final Set<DexType> recordWitnesses = Sets.newConcurrentHashSet();
 
   private boolean hasReadRecordReferenceFromProgramClass = false;
 
@@ -152,24 +155,26 @@
     return getAsmType(DescriptorUtils.getReturnTypeDescriptor(methodDescriptor));
   }
 
-  public void setHasReadRecordReferenceFromProgramClass() {
-    hasReadRecordReferenceFromProgramClass = true;
-  }
-
-  public boolean hasReadRecordReferenceFromProgramClass() {
-    return hasReadRecordReferenceFromProgramClass;
-  }
-
-  public void checkFieldForRecord(DexField dexField) {
-    if (options.shouldDesugarRecords() && RecordDesugaring.refersToRecord(dexField, getFactory())) {
-      setHasReadRecordReferenceFromProgramClass();
+  public void addRecordWitness(DexType witness, ClassKind<?> classKind) {
+    if (classKind == ClassKind.PROGRAM) {
+      recordWitnesses.add(witness);
     }
   }
 
-  public void checkMethodForRecord(DexMethod dexMethod) {
+  public Set<DexType> getRecordWitnesses() {
+    return recordWitnesses;
+  }
+
+  public void checkFieldForRecord(DexField dexField, ClassKind<?> classKind) {
+    if (options.shouldDesugarRecords() && RecordDesugaring.refersToRecord(dexField, getFactory())) {
+      addRecordWitness(dexField.getHolderType(), classKind);
+    }
+  }
+
+  public void checkMethodForRecord(DexMethod dexMethod, ClassKind<?> classKind) {
     if (options.shouldDesugarRecords()
         && RecordDesugaring.refersToRecord(dexMethod, getFactory())) {
-      setHasReadRecordReferenceFromProgramClass();
+      addRecordWitness(dexMethod.getHolderType(), classKind);
     }
   }
 }
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 8b8b9d8..73606cc 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -533,7 +533,7 @@
       if (!accessFlags.isRecord()) {
         return;
       }
-      application.setHasReadRecordReferenceFromProgramClass();
+      application.addRecordWitness(type, classKind);
       // TODO(b/169645628): Change this logic if we start stripping the record components.
       // Another approach would be to mark a bit in fields that are record components instead.
       String message = "Records are expected to have one record component per instance field.";
@@ -680,7 +680,7 @@
     public void visitEnd() {
       FieldAccessFlags flags = createFieldAccessFlags(access);
       DexField dexField = parent.application.getField(parent.type, name, desc);
-      parent.application.checkFieldForRecord(dexField);
+      parent.application.checkFieldForRecord(dexField, parent.classKind);
       Wrapper<DexField> signature = FieldSignatureEquivalence.get().wrap(dexField);
       if (parent.fieldSignatures.add(signature)) {
         DexAnnotationSet annotationSet =
@@ -898,7 +898,7 @@
     @Override
     public void visitEnd() {
       InternalOptions options = parent.application.options;
-      parent.application.checkMethodForRecord(method);
+      parent.application.checkMethodForRecord(method, parent.classKind);
       if (!flags.isAbstract() && !flags.isNative() && classRequiresCode()) {
         code = new LazyCfCode(parent.origin, parent.context, parent.application);
       }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMultiNewArray.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMultiNewArray.java
index e750ab3..7ac70e7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMultiNewArray.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMultiNewArray.java
@@ -140,12 +140,7 @@
 
     // Check if the type is guaranteed to be present.
     DexClass clazz = appView.definitionFor(baseType);
-    if (clazz == null) {
-      return true;
-    }
-
-    if (clazz.isLibraryClass()
-        && !appView.dexItemFactory().libraryTypesAssumedToBePresent.contains(baseType)) {
+    if (clazz == null || !clazz.isResolvable(appView)) {
       return true;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
index 9bc1728..5a3a1118 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
@@ -183,16 +183,10 @@
 
     // Check if the type is guaranteed to be present.
     DexClass clazz = appView.definitionFor(baseType);
-    if (clazz == null) {
+    if (clazz == null || !clazz.isResolvable(appView)) {
       return true;
     }
 
-    if (clazz.isLibraryClass()) {
-      if (!appView.dexItemFactory().libraryTypesAssumedToBePresent.contains(baseType)) {
-        return true;
-      }
-    }
-
     // Check if the type is guaranteed to be accessible.
     if (AccessControl.isClassAccessible(clazz, context, appViewWithClassHierarchy)
         .isPossiblyFalse()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index d6c938d..236bd0a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -170,12 +170,7 @@
     }
 
     DexClass definition = appView.definitionFor(clazz);
-    if (definition == null || definition.accessFlags.isAbstract()) {
-      return true;
-    }
-
-    if (definition.isLibraryClass()
-        && !dexItemFactory.libraryTypesAssumedToBePresent.contains(clazz)) {
+    if (definition == null || definition.isAbstract() || !definition.isResolvable(appView)) {
       return true;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index cb3bf73..a62fd2f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -193,7 +193,8 @@
     DexBuilder.removeRedundantDebugPositions(code);
     CfCode code = buildCfCode();
     assert verifyInvokeInterface(code, appView);
-    assert code.verifyFrames(method, appView, appView.graphLens()).isValidOrNotPresent();
+    assert code.getOrComputeStackMapStatus(method, appView, appView.graphLens())
+        .isValidOrNotPresent();
     return code;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 70fddd9..4a29cb6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -17,12 +17,8 @@
 
 import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.ConstMethodHandleDiagnostic;
-import com.android.tools.r8.errors.ConstMethodTypeDiagnostic;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
-import com.android.tools.r8.errors.InvokePolymorphicMethodHandleDiagnostic;
-import com.android.tools.r8.errors.InvokePolymorphicVarHandleDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
@@ -483,6 +479,14 @@
     this.basicBlockNumberGenerator = new NumberGenerator();
   }
 
+  private Origin getOrigin() {
+    return origin;
+  }
+
+  private MethodPosition getPosition() {
+    return MethodPosition.create(method);
+  }
+
   public DexItemFactory dexItemFactory() {
     return appView.dexItemFactory();
   }
@@ -1226,11 +1230,7 @@
   }
 
   public void addConstMethodHandle(int dest, DexMethodHandle methodHandle) {
-    if (!appView.options().canUseConstantMethodHandle()) {
-      throw appView
-          .reporter()
-          .fatalError(new ConstMethodHandleDiagnostic(origin, MethodPosition.create(method)));
-    }
+    assert appView.options().canUseConstantMethodHandle();
     TypeElement typeLattice =
         TypeElement.fromDexType(
             appView.dexItemFactory().methodHandleType, definitelyNotNull(), appView);
@@ -1240,11 +1240,7 @@
   }
 
   public void addConstMethodType(int dest, DexProto methodType) {
-    if (!appView.options().canUseConstantMethodType()) {
-      throw appView
-          .reporter()
-          .fatalError(new ConstMethodTypeDiagnostic(origin, MethodPosition.create(method)));
-    }
+    assert appView.options().canUseConstantMethodType();
     TypeElement typeLattice =
         TypeElement.fromDexType(
             appView.dexItemFactory().methodTypeType, definitelyNotNull(), appView);
@@ -1502,23 +1498,23 @@
     add(new RecordFieldValues(fields, out, arguments));
   }
 
+  private boolean verifyRepresentablePolymorphicInvoke(Type type, DexItem item) {
+    if (type != Type.POLYMORPHIC) {
+      return true;
+    }
+    assert item instanceof DexMethod;
+    if (((DexMethod) item).holder == appView.dexItemFactory().methodHandleType) {
+      assert appView.options().canUseInvokePolymorphicOnMethodHandle();
+    }
+    if (((DexMethod) item).holder == appView.dexItemFactory().varHandleType) {
+      assert appView.options().canUseInvokePolymorphicOnVarHandle();
+    }
+    return true;
+  }
+
   public void addInvoke(
       Type type, DexItem item, DexProto callSiteProto, List<Value> arguments, boolean itf) {
-    if (type == Type.POLYMORPHIC) {
-      assert item instanceof DexMethod;
-      if (!appView.options().canUseInvokePolymorphic()) {
-        throw appView
-            .reporter()
-            .fatalError(
-                new InvokePolymorphicMethodHandleDiagnostic(origin, MethodPosition.create(method)));
-      } else if (!appView.options().canUseInvokePolymorphicOnVarHandle()
-          && ((DexMethod) item).holder == appView.dexItemFactory().varHandleType) {
-        throw appView
-            .reporter()
-            .fatalError(
-                new InvokePolymorphicVarHandleDiagnostic(origin, MethodPosition.create(method)));
-      }
-    }
+    assert verifyRepresentablePolymorphicInvoke(type, item);
     add(Invoke.create(type, item, callSiteProto, null, arguments, itf));
   }
 
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 2a97c16..dfc82e0 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
@@ -81,7 +81,6 @@
 import com.android.tools.r8.ir.optimize.membervaluepropagation.R8MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.outliner.Outliner;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAppendOptimizer;
-import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer;
 import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.IdentifierNameStringMarker;
@@ -123,7 +122,6 @@
   private final FieldAccessAnalysis fieldAccessAnalysis;
   private final LibraryMethodOverrideAnalysis libraryMethodOverrideAnalysis;
   private final StringOptimizer stringOptimizer;
-  private final StringBuilderOptimizer stringBuilderOptimizer;
   private final IdempotentFunctionCallCanonicalizer idempotentFunctionCallCanonicalizer;
   private final ClassInliner classInliner;
   private final InternalOptions options;
@@ -182,7 +180,6 @@
     this.classInitializerDefaultsOptimization =
         new ClassInitializerDefaultsOptimization(appView, this);
     this.stringOptimizer = new StringOptimizer(appView);
-    this.stringBuilderOptimizer = new StringBuilderOptimizer(appView);
     this.deadCodeRemover = new DeadCodeRemover(appView, codeRewriter);
     this.assertionsRewriter = new AssertionsRewriter(appView);
     this.idempotentFunctionCallCanonicalizer = new IdempotentFunctionCallCanonicalizer(appView);
@@ -547,7 +544,8 @@
     if (options.testing.forceIRForCfToCfDesugar) {
       return true;
     }
-    return !options.isCfDesugaring();
+    assert method.getDefinition().getCode().isCfCode();
+    return !options.isGeneratingClassFiles();
   }
 
   private void checkPrefixMerging(ProgramMethod method) {
@@ -807,9 +805,6 @@
       if (stringOptimizer != null) {
         stringOptimizer.logResult();
       }
-      if (stringBuilderOptimizer != null) {
-        stringBuilderOptimizer.logResults();
-      }
     }
 
     // Assure that no more optimization feedback left after post processing.
@@ -1316,10 +1311,7 @@
     timing.begin("Rewrite move result");
     codeRewriter.rewriteMoveResult(code);
     timing.end();
-    // TODO(b/114002137): Also run for CF
-    if (options.enableStringConcatenationOptimization
-        && !isDebugMode
-        && options.isGeneratingDex()) {
+    if (options.enableStringConcatenationOptimization && !isDebugMode) {
       timing.begin("Rewrite string concat");
       StringBuilderAppendOptimizer.run(appView, code);
       timing.end();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
index 4adcf5f..eb12093 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
@@ -181,7 +181,7 @@
     return methodHandle;
   }
 
-  private List<DexValue> rewriteBootstrapArguments(
+  public List<DexValue> rewriteBootstrapArguments(
       List<DexValue> bootstrapArgs, MethodHandleUse use, ProgramMethod context) {
     List<DexValue> newBootstrapArgs = null;
     boolean changed = false;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index a0fd25e..3e41a70 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -137,6 +137,7 @@
     if (recordRewriter != null) {
       desugarings.add(recordRewriter);
     }
+    yieldingDesugarings.add(new UnrepresentableInDexInstructionRemover(appView));
   }
 
   static NonEmptyCfInstructionDesugaringCollection createForCfToCfNonDesugar(AppView<?> appView) {
@@ -157,6 +158,8 @@
         new NonEmptyCfInstructionDesugaringCollection(appView, noAndroidApiLevelCompute());
     desugaringCollection.desugarings.add(new InvokeSpecialToSelfDesugaring(appView));
     desugaringCollection.desugarings.add(new InvokeToPrivateRewriter());
+    desugaringCollection.yieldingDesugarings.add(
+        new UnrepresentableInDexInstructionRemover(appView));
     return desugaringCollection;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java b/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java
new file mode 100644
index 0000000..6d6dc0f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java
@@ -0,0 +1,397 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.desugar;
+
+import static com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.synthesizeThrowRuntimeExceptionWithMessageMethod;
+
+import com.android.tools.r8.cf.code.CfConstDynamic;
+import com.android.tools.r8.cf.code.CfConstMethodHandle;
+import com.android.tools.r8.cf.code.CfConstMethodType;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodTypeDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+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.ProgramMethod;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.MethodPosition;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Non desugared invoke-dynamic instructions as well as MethodHandle.invokeX instructions cannot be
+ * represented below O API level. Desugar them into throwing stubs to allow compilation to proceed
+ * under the assumption that the code is dead code.
+ */
+public class UnrepresentableInDexInstructionRemover implements CfInstructionDesugaring {
+
+  private abstract static class InstructionMatcher {
+    final AppView<?> appView;
+    final String descriptor;
+    final AndroidApiLevel supportedApiLevel;
+    // TODO(b/237250957): Using ConcurrentHashMap.newKeySet() causes failures on:
+    //  HelloWorldCompiledOnArtTest.testHelloCompiledWithX8Dex[Y, api:21, spec: JDK8, D8_L8DEBUG]
+    final Set<DexMethod> reported = Sets.newConcurrentHashSet();
+
+    InstructionMatcher(AppView<?> appView, String descriptor, AndroidApiLevel supportedApiLevel) {
+      this.appView = appView;
+      this.descriptor = descriptor;
+      this.supportedApiLevel = supportedApiLevel;
+    }
+
+    // Rewrite implementation for each instruction case.
+    abstract DesugarDescription compute(CfInstruction instruction);
+
+    abstract UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position);
+
+    // Helpers
+
+    void report(ProgramMethod context) {
+      if (reported.add(context.getReference())) {
+        UnsupportedFeatureDiagnostic diagnostic =
+            makeDiagnostic(context.getOrigin(), MethodPosition.create(context));
+        assert (diagnostic.getSupportedApiLevel() == -1 && supportedApiLevel == null)
+            || (diagnostic.getSupportedApiLevel() == supportedApiLevel.getLevel());
+        appView.reporter().error(diagnostic);
+      }
+    }
+
+    void invokeThrowingStub(
+        MethodProcessingContext methodProcessingContext,
+        CfInstructionDesugaringEventConsumer eventConsumer,
+        ProgramMethod context,
+        ImmutableList.Builder<CfInstruction> builder) {
+      UtilityMethodForCodeOptimizations throwUtility =
+          synthesizeThrowRuntimeExceptionWithMessageMethod(appView, methodProcessingContext);
+      ProgramMethod throwMethod = throwUtility.uncheckedGetMethod();
+      eventConsumer.acceptThrowMethod(throwMethod, context);
+      builder.add(
+          createMessageString(),
+          new CfInvoke(Opcodes.INVOKESTATIC, throwMethod.getReference(), false),
+          new CfStackInstruction(Opcode.Pop));
+    }
+
+    CfConstString createMessageString() {
+      return new CfConstString(
+          appView
+              .dexItemFactory()
+              .createString(
+                  "Instruction is unrepresentable in DEX "
+                      + appView.options().getMinApiLevel().getDexVersion()
+                      + ": "
+                      + descriptor));
+    }
+
+    static void pop(DexType type, Builder<CfInstruction> builder) {
+      assert !type.isVoidType();
+      builder.add(new CfStackInstruction(type.isWideType() ? Opcode.Pop2 : Opcode.Pop));
+    }
+
+    static void pop(Iterable<DexType> types, Builder<CfInstruction> builder) {
+      types.forEach(t -> pop(t, builder));
+    }
+
+    static Builder<CfInstruction> pushReturnValue(DexType type, Builder<CfInstruction> builder) {
+      if (!type.isVoidType()) {
+        builder.add(createDefaultValueForType(type));
+      }
+      return builder;
+    }
+
+    static CfInstruction createDefaultValueForType(DexType type) {
+      assert !type.isVoidType();
+      if (type.isPrimitiveType()) {
+        return new CfConstNumber(0, ValueType.fromDexType(type));
+      }
+      assert type.isReferenceType();
+      return new CfConstNull();
+    }
+  }
+
+  private static class InvokeDynamicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseInvokeCustom()) {
+        builder.add(new InvokeDynamicMatcher(appView));
+      }
+    }
+
+    InvokeDynamicMatcher(AppView<?> appView) {
+      super(appView, "invoke-dynamic", InternalOptions.invokeCustomApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedInvokeCustomDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      CfInvokeDynamic invokeDynamic = instruction.asInvokeDynamic();
+      if (invokeDynamic == null) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                DexCallSite callSite = invokeDynamic.getCallSite();
+                pop(callSite.getMethodProto().getParameters(), replacement);
+                localStackAllocator.allocateLocalStack(1);
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                pushReturnValue(callSite.getMethodProto().getReturnType(), replacement);
+                return replacement.build();
+              })
+          .build();
+    }
+  }
+
+  private static class InvokePolymorphicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseInvokePolymorphicOnMethodHandle()) {
+        builder.add(new InvokePolymorphicMatcher(appView));
+      }
+    }
+
+    InvokePolymorphicMatcher(AppView<?> appView) {
+      super(
+          appView, "invoke-polymorphic", InternalOptions.invokePolymorphicOnMethodHandleApiLevel());
+    }
+
+    boolean isPolymorphicInvoke(CfInvoke invoke) {
+      return appView.dexItemFactory().polymorphicMethods.isPolymorphicInvoke(invoke.getMethod());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedInvokePolymorphicMethodHandleDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      CfInvoke invoke = instruction.asInvoke();
+      if (invoke == null || !isPolymorphicInvoke(invoke)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                if (!invoke.isInvokeStatic()) {
+                  pop(dexItemFactory.objectType, replacement);
+                }
+                pop(invoke.getMethod().getParameters(), replacement);
+                localStackAllocator.allocateLocalStack(1);
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                pushReturnValue(invoke.getMethod().getReturnType(), replacement);
+                return replacement.build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstMethodHandleMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantMethodHandle()) {
+        builder.add(new ConstMethodHandleMatcher(appView));
+      }
+    }
+
+    ConstMethodHandleMatcher(AppView<?> appView) {
+      super(appView, "const-method-handle", InternalOptions.constantMethodHandleApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstMethodHandleDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      if (!(instruction instanceof CfConstMethodHandle)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return replacement.add(new CfConstNull()).build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstMethodTypeMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantMethodType()) {
+        builder.add(new ConstMethodTypeMatcher(appView));
+      }
+    }
+
+    ConstMethodTypeMatcher(AppView<?> appView) {
+      super(appView, "const-method-type", InternalOptions.constantMethodTypeApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstMethodTypeDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      if (!(instruction instanceof CfConstMethodType)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return replacement.add(new CfConstNull()).build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstDynamicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantDynamic()) {
+        builder.add(new ConstDynamicMatcher(appView));
+      }
+    }
+
+    ConstDynamicMatcher(AppView<?> appView) {
+      super(appView, "const-dynamic", InternalOptions.constantDynamicApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstDynamicDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      final CfConstDynamic constDynamic = instruction.asConstDynamic();
+      if (constDynamic == null) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return pushReturnValue(constDynamic.getType(), replacement).build();
+              })
+          .build();
+    }
+  }
+
+  private final List<InstructionMatcher> matchers;
+
+  public UnrepresentableInDexInstructionRemover(AppView<?> appView) {
+    Builder<InstructionMatcher> builder = ImmutableList.builder();
+    InvokeDynamicMatcher.addIfNeeded(appView, builder);
+    InvokePolymorphicMatcher.addIfNeeded(appView, builder);
+    ConstMethodHandleMatcher.addIfNeeded(appView, builder);
+    ConstMethodTypeMatcher.addIfNeeded(appView, builder);
+    ConstDynamicMatcher.addIfNeeded(appView, builder);
+    matchers = builder.build();
+  }
+
+  private DesugarDescription compute(CfInstruction instruction) {
+    for (InstructionMatcher matcher : matchers) {
+      DesugarDescription result = matcher.compute(instruction);
+      if (result != null) {
+        return result;
+      }
+    }
+    return DesugarDescription.nothing();
+  }
+
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      CfInstructionDesugaringCollection desugaringCollection,
+      DexItemFactory dexItemFactory) {
+    return compute(instruction)
+        .desugarInstruction(
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            dexItemFactory);
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    return compute(instruction).needsDesugaring();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
index 8853e8b..b9826d5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
@@ -7,16 +7,20 @@
 import com.android.tools.r8.cf.code.CfConstDynamic;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.utils.Box;
 import java.util.Collection;
 import java.util.HashMap;
@@ -33,9 +37,90 @@
     this.appView = appView;
   }
 
+  private DesugarDescription report(String message, ProgramMethod context) {
+    return DesugarDescription.builder()
+        .addScanEffect(
+            () ->
+                appView
+                    .reporter()
+                    .error(
+                        new ConstantDynamicDesugarDiagnostic(
+                            context.getOrigin(), MethodPosition.create(context), message)))
+        .build();
+  }
+
+  private DesugarDescription computeDesugaring(CfInstruction instruction, ProgramMethod context) {
+    if (!instruction.isConstDynamic()) {
+      return DesugarDescription.nothing();
+    }
+    CfConstDynamic constDynamic = instruction.asConstDynamic();
+    if (!constDynamic.getBootstrapMethodArguments().isEmpty()) {
+      // TODO(b/178172809): Handle bootstrap arguments.
+      return report("Unsupported dynamic constant (has arguments to bootstrap method)", context);
+    }
+    if (!constDynamic.getBootstrapMethod().type.isInvokeStatic()) {
+      return report("Unsupported dynamic constant (not invoke static)", context);
+    }
+    DexItemFactory factory = appView.dexItemFactory();
+    DexMethod bootstrapMethod = constDynamic.getBootstrapMethod().asMethod();
+    DexType holder = bootstrapMethod.getHolderType();
+    if (holder == factory.constantBootstrapsType) {
+      return report("Unsupported dynamic constant (runtime provided bootstrap method)", context);
+    }
+    if (holder != context.getHolderType()) {
+      return report("Unsupported dynamic constant (different owner)", context);
+    }
+    if (bootstrapMethod.getProto().returnType != factory.booleanArrayType
+        && bootstrapMethod.getProto().returnType != factory.objectType) {
+      return report("Unsupported dynamic constant (unsupported constant type)", context);
+    }
+    if (bootstrapMethod.getProto().getParameters().size() != 3) {
+      return report("Unsupported dynamic constant (unsupported signature)", context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(0) != factory.lookupType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of first argument to bootstrap method",
+          context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(1) != factory.stringType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of second argument to bootstrap method",
+          context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(2) != factory.classType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of third argument to bootstrap method",
+          context);
+    }
+    return DesugarDescription.builder()
+        .setDesugarRewrite(
+            (freshLocalProvider,
+                localStackAllocator,
+                eventConsumer,
+                context1,
+                methodProcessingContext,
+                dexItemFactory) ->
+                desugarConstDynamicInstruction(
+                    instruction.asConstDynamic(),
+                    freshLocalProvider,
+                    localStackAllocator,
+                    eventConsumer,
+                    context1,
+                    methodProcessingContext))
+        .build();
+  }
+
+  @Override
+  public void scan(ProgramMethod method, CfInstructionDesugaringEventConsumer eventConsumer) {
+    for (CfInstruction instruction :
+        method.getDefinition().getCode().asCfCode().getInstructions()) {
+      computeDesugaring(instruction, method).scan();
+    }
+  }
+
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
-    return instruction.isConstDynamic();
+    return computeDesugaring(instruction, context).needsDesugaring();
   }
 
   @Override
@@ -47,17 +132,15 @@
       ProgramMethod context,
       MethodProcessingContext methodProcessingContext,
       CfInstructionDesugaringCollection desugaringCollection,
-      DexItemFactory dexItemFactory) {
-    if (instruction.isConstDynamic()) {
-      return desugarConstDynamicInstruction(
-          instruction.asConstDynamic(),
-          freshLocalProvider,
-          localStackAllocator,
-          eventConsumer,
-          context,
-          methodProcessingContext);
-    }
-    return null;
+      DexItemFactory factory) {
+    return computeDesugaring(instruction, context)
+        .desugarInstruction(
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            factory);
   }
 
   private Collection<CfInstruction> desugarConstDynamicInstruction(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
index da8257c..43d76c7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
@@ -6,20 +6,22 @@
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import java.util.List;
 import java.util.Objects;
 
 public class ConstantDynamicReference {
   private final DexString name;
   private final DexType type;
   private final DexMethodHandle bootstrapMethod;
-  private final Object[] bootstrapMethodArguments;
+  private final List<DexValue> bootstrapMethodArguments;
 
   public ConstantDynamicReference(
       DexString name,
       DexType type,
       DexMethodHandle bootstrapMethod,
-      Object[] bootstrapMethodArguments) {
-    assert bootstrapMethodArguments.length == 0;
+      List<DexValue> bootstrapMethodArguments) {
+    assert bootstrapMethodArguments.isEmpty();
     this.name = name;
     this.type = type;
     this.bootstrapMethod = bootstrapMethod;
@@ -38,7 +40,7 @@
     return bootstrapMethod;
   }
 
-  public Object[] getBootstrapMethodArguments() {
+  public List<DexValue> getBootstrapMethodArguments() {
     return bootstrapMethodArguments;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
index 328bbf4..e17494f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
@@ -174,7 +174,8 @@
       throw reporter.fatalError(
           new StringDiagnostic(
               "Unsupported desugared library configuration version, please upgrade the D8/R8"
-                  + " compiler.",
+                  + " compiler."
+                  + " See https://developer.android.com/studio/build/library-desugaring-versions.",
               origin));
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
index 41941e1..f0ec7b2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexApplicationReadFlags;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -35,6 +36,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
@@ -125,7 +127,7 @@
       ProgramMethod programMethod, CfInstructionDesugaringEventConsumer eventConsumer) {
     CfCode cfCode = programMethod.getDefinition().getCode().asCfCode();
     for (CfInstruction instruction : cfCode.getInstructions()) {
-      scanInstruction(instruction, eventConsumer);
+      scanInstruction(instruction, eventConsumer, programMethod);
     }
   }
 
@@ -134,26 +136,28 @@
   // does not rewrite any instruction, and desugarInstruction is expected to rewrite at least one
   // instruction for assertions to be valid.
   private void scanInstruction(
-      CfInstruction instruction, CfInstructionDesugaringEventConsumer eventConsumer) {
+      CfInstruction instruction,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context) {
     assert !instruction.isInitClass();
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
       if (refersToRecord(cfInvoke.getMethod(), factory)) {
-        ensureRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer, context);
       }
       return;
     }
     if (instruction.isFieldInstruction()) {
       CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
       if (refersToRecord(fieldInstruction.getField(), factory)) {
-        ensureRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer, context);
       }
       return;
     }
     if (instruction.isTypeInstruction()) {
       CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
       if (refersToRecord(typeInstruction.getType(), factory)) {
-        ensureRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer, context);
       }
       return;
     }
@@ -369,15 +373,25 @@
     return false;
   }
 
-  private void ensureRecordClass(RecordDesugaringEventConsumer eventConsumer) {
+  /**
+   * If java.lang.Record is referenced from a class' supertype or a program method/field signature,
+   * then the global synthetic is generated upfront of the compilation to avoid confusing D8/R8.
+   *
+   * <p>However, if java.lang.Record is referenced only from an instruction, for example, the code
+   * contains "x instance of java.lang.Record" but no record type is present, then the global
+   * synthetic is generated during instruction desugaring scanning.
+   */
+  private void ensureRecordClass(
+      RecordDesugaringEventConsumer eventConsumer, Collection<ProgramDefinition> contexts) {
     DexItemFactory factory = appView.dexItemFactory();
     checkRecordTagNotPresent(factory);
     appView
         .getSyntheticItems()
-        .legacyEnsureGlobalClass(
+        .ensureGlobalClass(
             () -> new MissingGlobalSyntheticsConsumerDiagnostic("Record desugaring"),
             kinds -> kinds.RECORD_TAG,
             factory.recordType,
+            contexts,
             appView,
             builder -> {
               DexEncodedMethod init = synthesizeRecordInitMethod();
@@ -386,6 +400,11 @@
             eventConsumer::acceptRecordClass);
   }
 
+  private void ensureRecordClass(
+      RecordDesugaringEventConsumer eventConsumer, ProgramDefinition context) {
+    ensureRecordClass(eventConsumer, ImmutableList.of(context));
+  }
+
   private void checkRecordTagNotPresent(DexItemFactory factory) {
     DexClass r8RecordClass =
         appView.appInfo().definitionForWithoutExistenceAssert(factory.recordTagType);
@@ -483,8 +502,16 @@
   public void synthesizeClasses(
       ClassSynthesisDesugaringContext processingContext,
       CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
-    if (appView.appInfo().app().getFlags().hasReadRecordReferenceFromProgramClass()) {
-      ensureRecordClass(eventConsumer);
+    DexApplicationReadFlags flags = appView.appInfo().app().getFlags();
+    if (flags.hasReadRecordReferenceFromProgramClass()) {
+      List<ProgramDefinition> classes = new ArrayList<>();
+      for (DexType recordWitness : flags.getRecordWitnesses()) {
+        DexClass dexClass = appView.contextIndependentDefinitionFor(recordWitness);
+        assert dexClass != null;
+        assert dexClass.isProgramClass();
+        classes.add(dexClass.asProgramClass());
+      }
+      ensureRecordClass(eventConsumer, classes);
     }
   }
 
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 257fd7d..33e5159 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
@@ -172,6 +172,37 @@
         .CfUtilityMethodsForCodeOptimizationsTemplates_throwNoSuchMethodError(options, method);
   }
 
+  public static UtilityMethodForCodeOptimizations synthesizeThrowRuntimeExceptionWithMessageMethod(
+      AppView<?> appView, MethodProcessingContext methodProcessingContext) {
+    InternalOptions options = appView.options();
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexProto proto =
+        dexItemFactory.createProto(dexItemFactory.runtimeExceptionType, dexItemFactory.stringType);
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    ProgramMethod syntheticMethod =
+        syntheticItems.createMethod(
+            kinds -> kinds.THROW_RTE,
+            methodProcessingContext.createUniqueContext(),
+            appView,
+            builder ->
+                builder
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setClassFileVersion(CfVersion.V1_8)
+                    .setApiLevelForDefinition(appView.computedMinApiLevel())
+                    .setApiLevelForCode(appView.computedMinApiLevel())
+                    .setCode(
+                        method -> getThrowRuntimeExceptionWithMessageCodeTemplate(method, options))
+                    .setProto(proto));
+    return new UtilityMethodForCodeOptimizations(syntheticMethod);
+  }
+
+  private static CfCode getThrowRuntimeExceptionWithMessageCodeTemplate(
+      DexMethod method, InternalOptions options) {
+    return CfUtilityMethodsForCodeOptimizations
+        .CfUtilityMethodsForCodeOptimizationsTemplates_throwRuntimeExceptionWithMessage(
+            options, method);
+  }
+
   public static class UtilityMethodForCodeOptimizations {
 
     private final ProgramMethod method;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendFlowAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendFlowAnalysis.java
deleted file mode 100644
index bb75dc2..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendFlowAnalysis.java
+++ /dev/null
@@ -1,180 +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.string;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractState;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractTransferFunction;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.FailedTransferFunctionResult;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraproceduralDataflowAnalysis;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeVirtual;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.utils.SetUtils;
-import com.google.common.collect.Sets;
-import java.util.Set;
-
-/**
- * This defines a simple program analysis that determines if there is a path from a call to append()
- * on a StringBuilder back to itself in the control flow graph.
- *
- * <p>The analysis explicitly allows paths from a call to append() back to itself that go through a
- * call to toString() on the builder. This ensures that we can still optimize builders that are
- * fully enclosed in a loop: <code>
- *   while (true) {
- *     System.out.println(new StringBuilder().append("42").toString());
- *   }
- * </code>
- */
-class StringBuilderAppendFlowAnalysis {
-
-  /**
-   * Returns true if there is a call to {@code append()} on {@param builder}, which is inside a
-   * loop.
-   */
-  static boolean hasAppendInstructionInLoop(
-      AppView<?> appView,
-      IRCode code,
-      Value builder,
-      StringBuilderOptimizationConfiguration configuration) {
-    IntraproceduralDataflowAnalysis<AbstractStateImpl> analysis =
-        new IntraproceduralDataflowAnalysis<>(
-            appView,
-            AbstractStateImpl.bottom(),
-            code,
-            new TransferFunction(builder, configuration));
-    DataflowAnalysisResult result = analysis.run(builder.definition.getBlock());
-    return result.isFailedAnalysisResult();
-  }
-
-  /** This defines the state that we keep track of for each {@link BasicBlock}. */
-  private static class AbstractStateImpl extends AbstractState<AbstractStateImpl> {
-
-    private static final AbstractStateImpl BOTTOM = new AbstractStateImpl();
-
-    // The set of invoke instructions that call append(), which is on a path to the current program
-    // point.
-    private final Set<InvokeVirtual> liveAppendInstructions;
-
-    private AbstractStateImpl() {
-      this(Sets.newIdentityHashSet());
-    }
-
-    private AbstractStateImpl(Set<InvokeVirtual> liveAppendInstructions) {
-      this.liveAppendInstructions = liveAppendInstructions;
-    }
-
-    public static AbstractStateImpl bottom() {
-      return BOTTOM;
-    }
-
-    private AbstractStateImpl addLiveAppendInstruction(InvokeVirtual invoke) {
-      Set<InvokeVirtual> newLiveAppendInstructions =
-          SetUtils.newIdentityHashSet(liveAppendInstructions);
-      newLiveAppendInstructions.add(invoke);
-      return new AbstractStateImpl(newLiveAppendInstructions);
-    }
-
-    private boolean isAppendInstructionLive(InvokeVirtual invoke) {
-      return liveAppendInstructions.contains(invoke);
-    }
-
-    @Override
-    public AbstractStateImpl asAbstractState() {
-      return this;
-    }
-
-    @Override
-    public AbstractStateImpl join(AppView<?> appView, AbstractStateImpl state) {
-      if (liveAppendInstructions.isEmpty()) {
-        return state;
-      }
-      if (state.liveAppendInstructions.isEmpty()) {
-        return this;
-      }
-      Set<InvokeVirtual> newLiveAppendInstructions =
-          SetUtils.newIdentityHashSet(liveAppendInstructions, state.liveAppendInstructions);
-      return new AbstractStateImpl(newLiveAppendInstructions);
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other == null || getClass() != other.getClass()) {
-        return false;
-      }
-      AbstractStateImpl state = (AbstractStateImpl) other;
-      return liveAppendInstructions.equals(state.liveAppendInstructions);
-    }
-
-    @Override
-    public int hashCode() {
-      return liveAppendInstructions.hashCode();
-    }
-  }
-
-  /**
-   * This defines the transfer function for the analysis.
-   *
-   * <p>If a call to {@code append()} on the builder is seen, then that invoke instruction is added
-   * to the abstract state.
-   *
-   * <p>If a call to {@code toString()} on the builder i seen, then the abstract state is reset to
-   * bottom.
-   */
-  private static class TransferFunction
-      implements AbstractTransferFunction<BasicBlock, Instruction, AbstractStateImpl> {
-
-    private final Value builder;
-    private final StringBuilderOptimizationConfiguration configuration;
-
-    private TransferFunction(Value builder, StringBuilderOptimizationConfiguration configuration) {
-      this.builder = builder;
-      this.configuration = configuration;
-    }
-
-    @Override
-    public TransferFunctionResult<AbstractStateImpl> apply(
-        Instruction instruction, AbstractStateImpl state) {
-      if (instruction.isInvokeMethod()) {
-        return apply(state, instruction.asInvokeMethod());
-      }
-      return state;
-    }
-
-    private TransferFunctionResult<AbstractStateImpl> apply(
-        AbstractStateImpl state, InvokeMethod invoke) {
-      if (isAppendOnBuilder(invoke)) {
-        assert invoke.isInvokeVirtual();
-        InvokeVirtual appendInvoke = invoke.asInvokeVirtual();
-        if (state.isAppendInstructionLive(appendInvoke)) {
-          return new FailedTransferFunctionResult<>();
-        }
-        return state.addLiveAppendInstruction(appendInvoke);
-      }
-      if (isToStringOnBuilder(invoke)) {
-        return AbstractStateImpl.bottom();
-      }
-      return state;
-    }
-
-    private boolean isAppendOnBuilder(InvokeMethod invoke) {
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      return configuration.isAppendMethod(invokedMethod)
-          && invoke.getArgument(0).getAliasedValue() == builder;
-    }
-
-    private boolean isToStringOnBuilder(InvokeMethod invoke) {
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      return configuration.isToStringMethod(invokedMethod)
-          && invoke.getArgument(0).getAliasedValue() == builder;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index 830516e..2606826 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
@@ -262,7 +263,7 @@
               assert newInstanceValue != null;
               nodeConsumer.accept(
                   newInstanceValue, createNewInstanceNode(instruction.asNewInstance()));
-            } else {
+            } else if (instruction.isInvokeMethodWithReceiver()) {
               InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
               Value receiver = invoke.getReceiver();
               if (oracle.isInit(instruction)) {
@@ -316,6 +317,17 @@
                     escaped ->
                         nodeConsumer.accept(escaped, createOtherStringBuilderNode(instruction)));
               }
+            } else {
+              assert instruction.isInvokeStatic();
+              InvokeStatic invoke = instruction.asInvokeStatic();
+              assert invoke.getInvokedMethod()
+                  == appView.dexItemFactory().objectsMethods.toStringWithObject;
+              visitStringBuilderValues(
+                  invoke.getFirstOperand(),
+                  escapeState,
+                  actual ->
+                      nodeConsumer.accept(actual, createToStringNode(instruction.asInvokeMethod())),
+                  escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
             }
           }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
index bc92a0f..e2d46af 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
@@ -63,7 +63,8 @@
       }
     }
     if (isStringBuilderInstruction) {
-      if (instruction.isInvokeMethodWithReceiver()) {
+      if (instruction.isInvokeMethod()) {
+        assert !instruction.inValues().isEmpty();
         Value firstOperand = instruction.getFirstOperand();
         if (!builder.getLiveStringBuilders().contains(firstOperand)) {
           // We can have constant NULL being the first operand, which we have not marked as
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
index 22d5c70..c189299 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.google.common.collect.Sets;
@@ -426,9 +427,9 @@
    */
   static class ToStringNode extends StringBuilderNode implements StringBuilderInstruction {
 
-    private final InvokeVirtual instruction;
+    private final InvokeMethod instruction;
 
-    private ToStringNode(InvokeVirtual instruction) {
+    private ToStringNode(InvokeMethod instruction) {
       this.instruction = instruction;
     }
 
@@ -534,7 +535,7 @@
   }
 
   /**
-   * ImplicitToStringNode are placed a StringBuilder/StringBuffer is appended to another
+   * ImplicitToStringNode are placed when StringBuilder/StringBuffer is appended to another
    * StringBuilder/StringBuffer.
    */
   static class ImplicitToStringNode extends StringBuilderNode {
@@ -588,7 +589,7 @@
     return new AppendNode(instruction);
   }
 
-  static ToStringNode createToStringNode(InvokeVirtual instruction) {
+  static ToStringNode createToStringNode(InvokeMethod instruction) {
     return new ToStringNode(instruction);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java
deleted file mode 100644
index d1612c3..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.string;
-
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.code.InvokeMethod;
-
-interface StringBuilderOptimizationConfiguration {
-  boolean isBuilderType(DexType type);
-
-  boolean isBuilderInit(DexMethod method, DexType builderType);
-
-  boolean isBuilderInit(DexMethod method);
-
-  boolean isBuilderInitWithInitialValue(InvokeMethod invoke);
-
-  boolean isAppendMethod(DexMethod method);
-
-  boolean isSupportedAppendMethod(InvokeMethod invoke);
-
-  boolean isToStringMethod(DexMethod method);
-}
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
deleted file mode 100644
index 957ccfe..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
+++ /dev/null
@@ -1,1024 +0,0 @@
-// 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.string;
-
-import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-
-import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-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.ProgramMethod;
-import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
-import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.code.Assume;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.ConstNumber;
-import com.android.tools.r8.ir.code.ConstString;
-import com.android.tools.r8.ir.code.DominatorTree;
-import com.android.tools.r8.ir.code.DominatorTree.Assumption;
-import com.android.tools.r8.ir.code.Goto;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.If;
-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.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;
-import com.android.tools.r8.logging.Log;
-import com.android.tools.r8.utils.SetUtils;
-import com.android.tools.r8.utils.StringUtils;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
-import it.unimi.dsi.fastutil.objects.Object2IntMap;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-// This optimization attempts to replace all builder.toString() calls with a constant string.
-// TODO(b/114002137): for now, the analysis depends on rewriteMoveResult.
-// Consider the following example:
-//
-//   StringBuilder builder;
-//   if (...) {
-//     builder.append("X");
-//   } else {
-//     builder.append("Y");
-//   }
-//   builder.toString();
-//
-// Its corresponding IR looks like:
-//   block0:
-//     b <- new-instance StringBuilder
-//     if ... block2 // Otherwise, fallthrough
-//   block1:
-//     c1 <- "X"
-//     b1 <- invoke-virtual b, c1, ...append
-//     goto block3
-//   block2:
-//     c2 <- "Y"
-//     b2 <- invoke-virtual b, c2, ...append
-//     goto block3
-//   block3:
-//     invoke-virtual b, ...toString
-//
-// After rewriteMoveResult, aliased out values, b1 and b2, are gone. So the analysis can focus on
-// single SSA values, assuming it's flow-sensitive (which is not true in general).
-public class StringBuilderOptimizer {
-
-  private final AppView<?> appView;
-  private final DexItemFactory factory;
-  @VisibleForTesting
-  StringConcatenationAnalysis analysis;
-  final StringBuilderOptimizationConfiguration optimizationConfiguration;
-
-  private int numberOfBuildersWithMultipleToString = 0;
-  private int numberOfBuildersWithoutToString = 0;
-  private int numberOfBuildersThatEscape = 0;
-  private int numberOfBuildersWhoseResultIsInterned = 0;
-  private int numberOfBuildersWithNonTrivialStateChange = 0;
-  private int numberOfBuildersWithUnsupportedArg = 0;
-  private int numberOfBuildersWithMergingPoints = 0;
-  private int numberOfBuildersWithNonDeterministicArg = 0;
-  private int numberOfDeadBuilders = 0;
-  private int numberOfBuildersSimplified = 0;
-  private final Object2IntMap<Integer> histogramOfLengthOfAppendChains;
-  private final Object2IntMap<Integer> histogramOfLengthOfEndResult;
-  private final Object2IntMap<Integer> histogramOfLengthOfPartialAppendChains;
-  private final Object2IntMap<Integer> histogramOfLengthOfPartialResult;
-
-  public StringBuilderOptimizer(AppView<? extends AppInfo> appView) {
-    this.appView = appView;
-    this.factory = appView.dexItemFactory();
-    this.optimizationConfiguration = new DefaultStringBuilderOptimizationConfiguration();
-    if (Log.ENABLED && Log.isLoggingEnabledFor(StringBuilderOptimizer.class)) {
-      histogramOfLengthOfAppendChains = new Object2IntArrayMap<>();
-      histogramOfLengthOfEndResult = new Object2IntArrayMap<>();
-      histogramOfLengthOfPartialAppendChains = new Object2IntArrayMap<>();
-      histogramOfLengthOfPartialResult = new Object2IntArrayMap<>();
-    } else {
-      histogramOfLengthOfAppendChains = null;
-      histogramOfLengthOfEndResult = null;
-      histogramOfLengthOfPartialAppendChains = null;
-      histogramOfLengthOfPartialResult = null;
-    }
-  }
-
-  public void logResults() {
-    assert Log.ENABLED;
-    Log.info(getClass(),
-        "# builders w/ multiple toString(): %s", numberOfBuildersWithMultipleToString);
-    Log.info(getClass(),
-        "# builders w/o toString(): %s", numberOfBuildersWithoutToString);
-    Log.info(getClass(),
-        "# builders that escape: %s", numberOfBuildersThatEscape);
-    Log.info(getClass(),
-        "# builders whose result is interned: %s", numberOfBuildersWhoseResultIsInterned);
-    Log.info(getClass(),
-        "# builders w/ non-trivial state change: %s", numberOfBuildersWithNonTrivialStateChange);
-    Log.info(getClass(),
-        "# builders w/ unsupported arg: %s", numberOfBuildersWithUnsupportedArg);
-    Log.info(getClass(),
-        "# builders w/ merging points: %s", numberOfBuildersWithMergingPoints);
-    Log.info(getClass(),
-        "# builders w/ non-deterministic arg: %s", numberOfBuildersWithNonDeterministicArg);
-    Log.info(getClass(), "# dead builders : %s", numberOfDeadBuilders);
-    Log.info(getClass(), "# builders simplified: %s", numberOfBuildersSimplified);
-    if (histogramOfLengthOfAppendChains != null) {
-      Log.info(getClass(), "------ histogram of StringBuilder append chain lengths ------");
-      histogramOfLengthOfAppendChains.forEach((chainSize, count) -> {
-        Log.info(getClass(),
-            "%s: %s (%s)", chainSize, StringUtils.times("*", Math.min(count, 53)), count);
-      });
-    }
-    if (histogramOfLengthOfEndResult != null) {
-      Log.info(getClass(), "------ histogram of StringBuilder result lengths ------");
-      histogramOfLengthOfEndResult.forEach((length, count) -> {
-        Log.info(getClass(),
-            "%s: %s (%s)", length, StringUtils.times("*", Math.min(count, 53)), count);
-      });
-    }
-    if (histogramOfLengthOfPartialAppendChains != null) {
-      Log.info(getClass(),
-          "------ histogram of StringBuilder append chain lengths (partial) ------");
-      histogramOfLengthOfPartialAppendChains.forEach((chainSize, count) -> {
-        Log.info(getClass(),
-            "%s: %s (%s)", chainSize, StringUtils.times("*", Math.min(count, 53)), count);
-      });
-    }
-    if (histogramOfLengthOfPartialResult != null) {
-      Log.info(getClass(), "------ histogram of StringBuilder partial result lengths ------");
-      histogramOfLengthOfPartialResult.forEach(
-          (length, count) ->
-              Log.info(
-                  getClass(),
-                  "%s: %s (%s)",
-                  length,
-                  StringUtils.times("*", Math.min(count, 53)),
-                  count));
-    }
-  }
-
-  public void computeTrivialStringConcatenation(IRCode code) {
-    StringConcatenationAnalysis analysis = new StringConcatenationAnalysis(code);
-    // Only for testing purpose, where we ran the analysis for only one method.
-    // Using `this.analysis` is not thread-safe, of course.
-    this.analysis = analysis;
-    Set<Value> candidateBuilders =
-        analysis.findAllLocalBuilders()
-            .stream()
-            .filter(analysis::canBeOptimized)
-            .collect(Collectors.toSet());
-    if (candidateBuilders.isEmpty()) {
-      return;
-    }
-    analysis
-        .buildBuilderStateGraph(candidateBuilders)
-        .applyConcatenationResults(candidateBuilders)
-        .removeTrivialBuilders();
-  }
-
-  class StringConcatenationAnalysis {
-
-    // Inspired by {@link JumboStringTest}. Some code intentionally may have too many append(...).
-    private static final int CONCATENATION_THRESHOLD = 200;
-    private static final String ANY_STRING = "*";
-    private static final String DUMMY = "$dummy$";
-
-    private final IRCode code;
-
-    // A map from SSA Value of StringBuilder type to its toString() counts.
-    // Reused (e.g., concatenated, toString, concatenated more, toString) builders are out of scope.
-    // TODO(b/114002137): some of those toString could have constant string states.
-    final Object2IntMap<Value> builderToStringCounts = new Object2IntArrayMap<>();
-
-    StringConcatenationAnalysis(IRCode code) {
-      this.code = code;
-    }
-
-    // This optimization focuses on builders that are created and used locally.
-    // In the first step, we collect builders that are created in the current method.
-    // In the next step, we will filter out builders that cannot be optimized. To avoid multiple
-    // iterations per builder, we're collecting # of uses of those builders by iterating the code
-    // twice in this step.
-    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 (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);
-        }
-      }
-      if (builderToStringCounts.isEmpty()) {
-        return ImmutableSet.of();
-      }
-      int concatenationCount = 0;
-      // During the second iteration, count builders' usage.
-      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);
-            }
-          }
-        }
-      }
-      return builderToStringCounts.keySet();
-    }
-
-    private Set<Value> collectAllLinkedBuilders(Value builder) {
-      Set<Value> builders = Sets.newIdentityHashSet();
-      Set<Value> visited = Sets.newIdentityHashSet();
-      collectAllLinkedBuilders(builder, builders, visited);
-      return builders;
-    }
-
-    private void collectAllLinkedBuilders(Value builder, Set<Value> builders, Set<Value> visited) {
-      if (!visited.add(builder)) {
-        return;
-      }
-      if (builder.isPhi()) {
-        for (Value operand : builder.asPhi().getOperands()) {
-          collectAllLinkedBuilders(operand, builders, visited);
-        }
-      } else {
-        builders.add(builder);
-      }
-    }
-
-    private boolean canBeOptimized(Value builder) {
-      // If the builder is definitely null, it may be handled by other optimizations.
-      // E.g., any further operations, such as append, will raise NPE.
-      // But, as we collect local builders, it should never be null.
-      assert !builder.isAlwaysNull(appView);
-      // Before checking the builder usage, make sure we have its usage count.
-      assert builderToStringCounts.containsKey(builder);
-      // If a builder is reused, chances are the code is not trivial, e.g., building a prefix
-      // at some point; appending different suffices in different conditions; and building again.
-      if (builderToStringCounts.getInt(builder) > 1) {
-        numberOfBuildersWithMultipleToString++;
-        return false;
-      }
-      // If a builder is not used, i.e., never converted to string, it doesn't make sense to
-      // attempt to compute its compile-time constant string.
-      if (builderToStringCounts.getInt(builder) < 1) {
-        numberOfBuildersWithoutToString++;
-        return false;
-      }
-      // Make sure builder is neither phi nor coming from outside of the method.
-      assert !builder.isPhi() && builder.definition.isNewInstance();
-      assert builder.getType().isClassType();
-      DexType builderType = builder.getType().asClassType().getClassType();
-      assert optimizationConfiguration.isBuilderType(builderType);
-      EscapeAnalysis escapeAnalysis =
-          new EscapeAnalysis(
-              appView, new StringBuilderOptimizerEscapeAnalysisConfiguration(builder));
-      return !escapeAnalysis.isEscaping(code, builder);
-    }
-
-    // A map from SSA Value of StringBuilder type to its internal state per instruction.
-    final Map<Value, Map<Instruction, BuilderState>> builderStates = new HashMap<>();
-
-    // Create a builder state, only used when visiting new-instance instructions.
-    private Map<Instruction, BuilderState> createBuilderState(Value builder) {
-      // By using LinkedHashMap, we want to visit instructions in the order of their insertions.
-      return builderStates.computeIfAbsent(builder, ignore -> new LinkedHashMap<>());
-    }
-
-    // Get a builder state, used for all other cases except for new-instance instructions.
-    private Map<Instruction, BuilderState> getBuilderState(Value builder) {
-      return builderStates.get(builder);
-    }
-
-    // Suppose a simple, trivial chain:
-    //   new StringBuilder().append("a").append("b").toString();
-    //
-    // the resulting graph would be:
-    //   [s1, root, "", {s2}],
-    //   [s2, s1, "a", {s3}],
-    //   [s3, s2, "b", {}].
-    //
-    // For each meaningful IR (init, append, toString), the corresponding state will be bound:
-    // <init> -> s1, 1st append -> s2, 2nd append -> s3, toString -> s3.
-    //
-    // Suppose an example with a phi:
-    //   StringBuilder b = flag ? new StringBuilder("x") : new StringBuilder("y");
-    //   b.append("z").toString();
-    //
-    // the resulting graph would be:
-    //   [s1, root, "", {s2, s3, s4}],
-    //   [s2, s1, "x", {}],
-    //   [s3, s1, "y", {}],
-    //   [s4, s1, "z", {}].
-    //
-    // Note that neither s2 nor s3 can dominate s4, and thus all of s{2..4} are linked to s1.
-    // An alternative graph shape would be: [s4, {s2, s3}, "z", {}] (and proper successor update in
-    // s2 and s3, of course). But, from the point of the view of finding the trivial chain, there is
-    // no difference. The current graph construction relies on and resembles dominator tree.
-    private StringConcatenationAnalysis buildBuilderStateGraph(Set<Value> candidateBuilders) {
-      DominatorTree dominatorTree = new DominatorTree(code, Assumption.MAY_HAVE_UNREACHABLE_BLOCKS);
-      for (BasicBlock block : code.topologicallySortedBlocks()) {
-        for (Instruction instr : block.getInstructions()) {
-          if (instr.isNewInstance()
-              && optimizationConfiguration.isBuilderType(instr.asNewInstance().clazz)) {
-            Value builder = instr.asNewInstance().dest();
-            if (!candidateBuilders.contains(builder)) {
-              continue;
-            }
-            if (builder.hasPhiUsers()) {
-              candidateBuilders.remove(builder);
-              continue;
-            }
-            Map<Instruction, BuilderState> perInstrState = createBuilderState(builder);
-            perInstrState.put(instr, BuilderState.createRoot());
-            continue;
-          }
-
-          if (instr.isInvokeDirect()
-              && optimizationConfiguration.isBuilderInitWithInitialValue(instr.asInvokeDirect())) {
-            InvokeDirect invoke = instr.asInvokeDirect();
-            Value builder = invoke.getReceiver().getAliasedValue();
-            if (!candidateBuilders.contains(builder)) {
-              continue;
-            }
-            assert invoke.inValues().size() == 2;
-            Value arg = invoke.getFirstNonReceiverArgument().getAliasedValue();
-            DexMethod invokedMethod = invoke.getInvokedMethod();
-            DexType parameterType = invokedMethod.getParameter(0);
-            String addition = extractConstantArgument(arg, invokedMethod, parameterType);
-            Map<Instruction, BuilderState> perInstrState = getBuilderState(builder);
-            BuilderState dominantState = findDominantState(dominatorTree, perInstrState, instr);
-            if (dominantState != null) {
-              BuilderState currentState = dominantState.createChild(addition);
-              perInstrState.put(instr, currentState);
-            } else {
-              // TODO(b/114002137): if we want to utilize partial results, don't remove it here.
-              candidateBuilders.remove(builder);
-            }
-            continue;
-          }
-
-          if (!instr.isInvokeMethodWithReceiver()) {
-            continue;
-          }
-
-          InvokeMethodWithReceiver invoke = instr.asInvokeMethodWithReceiver();
-          if (optimizationConfiguration.isAppendMethod(invoke.getInvokedMethod())) {
-            Value builder = invoke.getReceiver().getAliasedValue();
-            // If `builder` is phi, itself and predecessors won't be tracked.
-            // Not being tracked means that they won't be optimized, which is intentional.
-            if (!candidateBuilders.contains(builder)) {
-              continue;
-            }
-            if (invoke.hasUsedOutValue()) {
-              candidateBuilders.remove(builder);
-              continue;
-            }
-            Value arg = invoke.getFirstNonReceiverArgument().getAliasedValue();
-            DexMethod invokedMethod = invoke.getInvokedMethod();
-            DexType parameterType = invokedMethod.getParameter(0);
-            String addition = extractConstantArgument(arg, invokedMethod, parameterType);
-            Map<Instruction, BuilderState> perInstrState = getBuilderState(builder);
-            BuilderState dominantState = findDominantState(dominatorTree, perInstrState, instr);
-            if (dominantState != null) {
-              BuilderState currentState = dominantState.createChild(addition);
-              perInstrState.put(instr, currentState);
-            } else {
-              // TODO(b/114002137): if we want to utilize partial results, don't remove it here.
-              candidateBuilders.remove(builder);
-            }
-          }
-          if (optimizationConfiguration.isToStringMethod(invoke.getInvokedMethod())) {
-            Value builder = invoke.getReceiver().getAliasedValue();
-            // If `builder` is phi, itself and predecessors won't be tracked.
-            // Not being tracked means that they won't be optimized, which is intentional.
-            if (!candidateBuilders.contains(builder)) {
-              continue;
-            }
-            Map<Instruction, BuilderState> perInstrState = getBuilderState(builder);
-            BuilderState dominantState = findDominantState(dominatorTree, perInstrState, instr);
-            if (dominantState != null) {
-              // Instead of using the dominant state directly, treat this retrieval point as a new
-              // state without an addition so that dominant state can account for dependent states.
-              BuilderState currentState = dominantState.createChild("");
-              perInstrState.put(instr, currentState);
-            } else {
-              // TODO(b/114002137): if we want to utilize partial results, don't remove it here.
-              candidateBuilders.remove(builder);
-            }
-          }
-        }
-      }
-
-      return this;
-    }
-
-    private String extractConstantArgument(Value arg, DexMethod method, DexType argType) {
-      String addition = ANY_STRING;
-      if (arg.isPhi()) {
-        return addition;
-      }
-      if (arg.definition.isConstString()) {
-        addition = arg.definition.asConstString().getValue().toString();
-      } else if (arg.definition.isConstNumber() || arg.definition.isNumberConversion()) {
-        Number number = extractConstantNumber(arg);
-        if (number == null) {
-          return addition;
-        }
-        if (arg.getType().isPrimitiveType()) {
-          if (argType == factory.booleanType) {
-            addition = String.valueOf(number.intValue() != 0);
-          } else if (argType == factory.byteType) {
-            addition = String.valueOf(number.byteValue());
-          } else if (argType == factory.shortType) {
-            addition = String.valueOf(number.shortValue());
-          } else if (argType == factory.charType) {
-            addition = String.valueOf((char) number.intValue());
-          } else if (argType == factory.intType) {
-            addition = String.valueOf(number.intValue());
-          } else if (argType == factory.longType) {
-            addition = String.valueOf(number.longValue());
-          } else if (argType == factory.floatType) {
-            addition = String.valueOf(number.floatValue());
-          } else if (argType == factory.doubleType) {
-            addition = String.valueOf(number.doubleValue());
-          }
-        } else if (arg.getType().isNullType()
-            && !method.isInstanceInitializer(factory)
-            && argType != factory.charArrayType) {
-          assert number.intValue() == 0;
-          addition = "null";
-        }
-      }
-      return addition;
-    }
-
-    private Number extractConstantNumber(Value arg) {
-      if (arg.isPhi()) {
-        return null;
-      }
-      if (arg.definition.isConstNumber()) {
-        ConstNumber cst = arg.definition.asConstNumber();
-        if (cst.outType() == ValueType.LONG) {
-          return cst.getLongValue();
-        } else if (cst.outType() == ValueType.FLOAT) {
-          return cst.getFloatValue();
-        } else if (cst.outType() == ValueType.DOUBLE) {
-          return cst.getDoubleValue();
-        } else {
-          assert cst.outType() == ValueType.INT || cst.outType() == ValueType.OBJECT;
-          return cst.getIntValue();
-        }
-      } else if (arg.definition.isNumberConversion()) {
-        NumberConversion conversion = arg.definition.asNumberConversion();
-        assert conversion.inValues().size() == 1;
-        Number temp = extractConstantNumber(conversion.inValues().get(0));
-        if (temp == null) {
-          return null;
-        }
-        DexType conversionType = conversion.to.toDexType(factory);
-        if (conversionType == factory.booleanType) {
-          return temp.intValue() != 0 ? 1 : 0;
-        } else if (conversionType == factory.byteType) {
-          return temp.byteValue();
-        } else if (conversionType == factory.shortType) {
-          return temp.shortValue();
-        } else if (conversionType == factory.charType) {
-          return temp.intValue();
-        } else if (conversionType == factory.intType) {
-          return temp.intValue();
-        } else if (conversionType == factory.longType) {
-          return temp.longValue();
-        } else if (conversionType == factory.floatType) {
-          return temp.floatValue();
-        } else if (conversionType == factory.doubleType) {
-          return temp.doubleValue();
-        }
-      }
-      return null;
-    }
-
-    private BuilderState findDominantState(
-        DominatorTree dominatorTree,
-        Map<Instruction, BuilderState> perInstrState,
-        Instruction current) {
-      BuilderState result = null;
-      BasicBlock lastDominantBlock = null;
-      for (Instruction instr : perInstrState.keySet()) {
-        BasicBlock block = instr.getBlock();
-        if (!dominatorTree.dominatedBy(current.getBlock(), block)) {
-          continue;
-        }
-        if (lastDominantBlock == null
-            || dominatorTree.dominatedBy(block, lastDominantBlock)) {
-          result = perInstrState.get(instr);
-          lastDominantBlock = block;
-        }
-      }
-      return result;
-    }
-
-    private void logHistogramOfChains(List<String> contents, boolean isPartial) {
-      if (!Log.ENABLED || !Log.isLoggingEnabledFor(StringOptimizer.class)) {
-        return;
-      }
-      if (contents == null || contents.isEmpty()) {
-        return;
-      }
-      String result = StringUtils.join("", contents);
-      Integer size = Integer.valueOf(contents.size());
-      Integer length = Integer.valueOf(result.length());
-      if (isPartial) {
-        assert histogramOfLengthOfPartialAppendChains != null;
-        synchronized (histogramOfLengthOfPartialAppendChains) {
-          int count = histogramOfLengthOfPartialAppendChains.getOrDefault(size, 0);
-          histogramOfLengthOfPartialAppendChains.put(size, count + 1);
-        }
-        assert histogramOfLengthOfPartialResult != null;
-        synchronized (histogramOfLengthOfPartialResult) {
-          int count = histogramOfLengthOfPartialResult.getOrDefault(length, 0);
-          histogramOfLengthOfPartialResult.put(length, count + 1);
-        }
-      } else {
-        assert histogramOfLengthOfAppendChains != null;
-        synchronized (histogramOfLengthOfAppendChains) {
-          int count = histogramOfLengthOfAppendChains.getOrDefault(size, 0);
-          histogramOfLengthOfAppendChains.put(size, count + 1);
-        }
-        assert histogramOfLengthOfEndResult != null;
-        synchronized (histogramOfLengthOfEndResult) {
-          int count = histogramOfLengthOfEndResult.getOrDefault(length, 0);
-          histogramOfLengthOfEndResult.put(length, count + 1);
-        }
-      }
-    }
-
-    // StringBuilders that are simplified by this analysis or simply dead (e.g., after applying
-    // -assumenosideeffects to logging calls, then logging messages built by builders are dead).
-    // Will be used to clean up uses of the builders, such as creation, <init>, and append calls.
-    final Set<Value> deadBuilders = Sets.newIdentityHashSet();
-    final Set<Value> simplifiedBuilders = Sets.newIdentityHashSet();
-
-    private StringConcatenationAnalysis applyConcatenationResults(Set<Value> candidateBuilders) {
-      Set<Value> affectedValues = Sets.newIdentityHashSet();
-      InstructionListIterator it = code.instructionListIterator();
-      while (it.hasNext()) {
-        Instruction instr = it.nextUntil(i -> isToStringOfInterest(candidateBuilders, i));
-        if (instr == null) {
-          break;
-        }
-        InvokeMethod invoke = instr.asInvokeMethod();
-        assert invoke.arguments().size() == 1;
-        Value builder = invoke.getArgument(0).getAliasedValue();
-        Value outValue = invoke.outValue();
-        if (outValue == null || outValue.isDead(appView, code)) {
-          // If the out value is still used but potentially dead, replace it with a dummy string.
-          if (outValue != null && outValue.isUsed()) {
-            Value dummy =
-                code.createValue(
-                    TypeElement.stringClassType(appView, definitelyNotNull()),
-                    invoke.getLocalInfo());
-            it.replaceCurrentInstruction(new ConstString(dummy, factory.createString(DUMMY)));
-          } else {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-          // Although this builder is not simplified by this analysis, add that to the set so that
-          // it can be removed at the final clean-up phase.
-          deadBuilders.add(builder);
-          numberOfDeadBuilders++;
-          continue;
-        }
-        Map<Instruction, BuilderState> perInstrState = builderStates.get(builder);
-        assert perInstrState != null;
-        BuilderState builderState = perInstrState.get(instr);
-        assert builderState != null;
-        String element = toCompileTimeString(builder, builderState);
-        assert element != null;
-        Value stringValue =
-            code.createValue(
-                TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        affectedValues.addAll(outValue.affectedValues());
-        it.replaceCurrentInstruction(new ConstString(stringValue, factory.createString(element)));
-        simplifiedBuilders.add(builder);
-        numberOfBuildersSimplified++;
-      }
-      // Concatenation results are not null, and thus propagate that information.
-      if (!affectedValues.isEmpty()) {
-        new TypeAnalysis(appView).narrowing(affectedValues);
-      }
-      return this;
-    }
-
-    private boolean isToStringOfInterest(Set<Value> candidateBuilders, Instruction instr) {
-      if (!instr.isInvokeMethod()) {
-        return false;
-      }
-      InvokeMethod invoke = instr.asInvokeMethod();
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (!optimizationConfiguration.isToStringMethod(invokedMethod)) {
-        return false;
-      }
-      assert invoke.arguments().size() == 1;
-      Value builder = invoke.getArgument(0).getAliasedValue();
-      if (!candidateBuilders.contains(builder)) {
-        return false;
-      }
-      // If the result of toString() is no longer used, computing the compile-time constant is
-      // even not necessary.
-      if (!invoke.hasOutValue() || invoke.outValue().isDead(appView, code)) {
-        return true;
-      }
-      Map<Instruction, BuilderState> perInstrState = builderStates.get(builder);
-      if (perInstrState == null) {
-        return false;
-      }
-      BuilderState builderState = perInstrState.get(instr);
-      if (builderState == null) {
-        return false;
-      }
-      String element = toCompileTimeString(builder, builderState);
-      if (element == null) {
-        return false;
-      }
-      return true;
-    }
-
-    // Find the trivial chain of builder-append*-toString.
-    // Note that we can't determine a compile-time constant string if there are any ambiguity.
-    @VisibleForTesting
-    String toCompileTimeString(Value builder, BuilderState state) {
-      boolean continuedForLogging = false;
-      LinkedList<String> contents = new LinkedList<>();
-      while (state != null) {
-        // Not a single chain if there are multiple successors.
-        if (state.nexts != null && state.nexts.size() > 1) {
-          numberOfBuildersWithMergingPoints++;
-          if (Log.ENABLED && Log.isLoggingEnabledFor(StringBuilderOptimizer.class)) {
-            logHistogramOfChains(contents, true);
-            continuedForLogging = true;
-            contents.clear();
-            state = state.previous;
-            continue;
-          }
-          return null;
-        }
-        // Reaching the root.
-        if (state.addition == null) {
-          break;
-        }
-        // A non-deterministic argument is appended.
-        // TODO(b/129200243): Even though it's ambiguous, if the chain length is 2, and the 1st one
-        //   is definitely non-null, we can convert it to String#concat.
-        if (state.addition.equals(ANY_STRING)) {
-          numberOfBuildersWithNonDeterministicArg++;
-          if (Log.ENABLED && Log.isLoggingEnabledFor(StringBuilderOptimizer.class)) {
-            logHistogramOfChains(contents, true);
-            continuedForLogging = true;
-            contents.clear();
-            state = state.previous;
-            continue;
-          }
-          return null;
-        }
-        contents.push(state.addition);
-        state = state.previous;
-      }
-      if (continuedForLogging) {
-        logHistogramOfChains(contents, true);
-        return null;
-      }
-      if (Log.ENABLED && Log.isLoggingEnabledFor(StringBuilderOptimizer.class)) {
-        logHistogramOfChains(contents, false);
-      }
-      if (contents.isEmpty()) {
-        return null;
-      }
-      if (StringBuilderAppendFlowAnalysis.hasAppendInstructionInLoop(
-          appView, code, builder, optimizationConfiguration)) {
-        return null;
-      }
-      return StringUtils.join("", contents);
-    }
-
-    void removeTrivialBuilders() {
-      if (deadBuilders.isEmpty() && simplifiedBuilders.isEmpty()) {
-        return;
-      }
-      Set<Value> affectedValues = Sets.newIdentityHashSet();
-      Set<Value> buildersToRemove = SetUtils.newIdentityHashSet(deadBuilders, simplifiedBuilders);
-      Set<Value> buildersUsedInIf = Sets.newIdentityHashSet();
-      // All instructions that refer to dead/simplified builders are dead.
-      // Here, we remove toString() calls, append(...) calls, <init>, and new-instance in order.
-      InstructionListIterator it = code.instructionListIterator();
-      boolean shouldRemoveUnreachableBlocks = false;
-      while (it.hasNext()) {
-        Instruction instr = it.next();
-        if (instr.isIf()) {
-          If theIf = instr.asIf();
-          Value lhs = theIf.lhs().getAliasedValue();
-          if (theIf.isZeroTest()) {
-            if (buildersToRemove.contains(lhs)) {
-              theIf.targetFromNullObject().unlinkSinglePredecessorSiblingsAllowed();
-              it.replaceCurrentInstruction(new Goto());
-              shouldRemoveUnreachableBlocks = true;
-            }
-          } else {
-            Value rhs = theIf.rhs().getAliasedValue();
-            if (buildersToRemove.contains(lhs)) {
-              buildersUsedInIf.add(lhs);
-            }
-            if (buildersToRemove.contains(rhs)) {
-              buildersUsedInIf.add(rhs);
-            }
-          }
-        } else if (instr.isInvokeMethod()) {
-          InvokeMethod invoke = instr.asInvokeMethod();
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (optimizationConfiguration.isToStringMethod(invokedMethod)
-              && buildersToRemove.contains(invoke.getArgument(0).getAliasedValue())) {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-        }
-      }
-      if (shouldRemoveUnreachableBlocks) {
-        affectedValues.addAll(code.removeUnreachableBlocks());
-      }
-      buildersToRemove.removeAll(buildersUsedInIf);
-      // append(...) and <init> don't have out values, so removing them won't bother each other.
-      it = code.instructionListIterator();
-      while (it.hasNext()) {
-        Instruction instr = it.next();
-        if (instr.isInvokeVirtual()) {
-          InvokeVirtual invoke = instr.asInvokeVirtual();
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (optimizationConfiguration.isAppendMethod(invokedMethod)
-              && buildersToRemove.contains(invoke.getReceiver().getAliasedValue())) {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-        }
-        if (instr.isInvokeDirect()) {
-          InvokeDirect invoke = instr.asInvokeDirect();
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (optimizationConfiguration.isBuilderInit(invokedMethod)
-              && buildersToRemove.contains(invoke.getReceiver().getAliasedValue())) {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-        }
-        // If there are aliasing instructions, they should be removed before new-instance.
-        if (instr.isAssume() && buildersToRemove.contains(instr.outValue().getAliasedValue())) {
-          Assume assumeInstruction = instr.asAssume();
-          Value src = assumeInstruction.src();
-          Value dest = assumeInstruction.outValue();
-          dest.replaceUsers(src);
-          it.remove();
-        }
-      }
-      // new-instance should be removed at last, since it will check the out value, builder, is not
-      // used anywhere, which we've removed so far.
-      it = code.instructionListIterator();
-      while (it.hasNext()) {
-        Instruction instr = it.next();
-        if (instr.isNewInstance()
-            && optimizationConfiguration.isBuilderType(instr.asNewInstance().clazz)
-            && instr.hasOutValue()
-            && buildersToRemove.contains(instr.outValue())) {
-          it.removeOrReplaceByDebugLocalRead();
-        }
-      }
-      if (!affectedValues.isEmpty()) {
-        new TypeAnalysis(appView).narrowing(affectedValues);
-      }
-      assert code.isConsistentSSA(appView);
-    }
-  }
-
-  class DefaultStringBuilderOptimizationConfiguration
-      implements StringBuilderOptimizationConfiguration {
-    @Override
-    public boolean isBuilderType(DexType type) {
-      return type == factory.stringBuilderType
-          || type == factory.stringBufferType;
-    }
-
-    @Override
-    public boolean isBuilderInit(DexMethod method, DexType builderType) {
-      return builderType == method.holder
-          && factory.isConstructor(method);
-    }
-
-    @Override
-    public boolean isBuilderInit(DexMethod method) {
-      return isBuilderType(method.holder)
-          && factory.isConstructor(method);
-    }
-
-    @Override
-    public boolean isBuilderInitWithInitialValue(InvokeMethod invoke) {
-      return isBuilderInit(invoke.getInvokedMethod())
-          && invoke.inValues().size() == 2
-          && !invoke.inValues().get(1).getType().isPrimitiveType();
-    }
-
-    @Override
-    public boolean isAppendMethod(DexMethod method) {
-      return factory.stringBuilderMethods.isAppendMethod(method)
-          || factory.stringBufferMethods.isAppendMethod(method);
-    }
-
-    @Override
-    public boolean isSupportedAppendMethod(InvokeMethod invoke) {
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      assert isAppendMethod(invokedMethod);
-      if (invoke.hasOutValue()) {
-        return false;
-      }
-      // Any methods other than append(arg) are not trivial since they may change the builder
-      // state not monotonically.
-      if (invoke.inValues().size() > 2) {
-        numberOfBuildersWithNonTrivialStateChange++;
-        return false;
-      }
-      assert invoke.inValues().size() == 2;
-      TypeElement argType = invoke.inValues().get(1).getType();
-      if (!argType.isPrimitiveType() && !argType.isClassType() && !argType.isNullType()) {
-        numberOfBuildersWithUnsupportedArg++;
-        return false;
-      }
-      if (argType.isClassType()) {
-        DexType argClassType = argType.asClassType().getClassType();
-        return canHandleArgumentType(argClassType);
-      }
-      return true;
-    }
-
-    @Override
-    public boolean isToStringMethod(DexMethod method) {
-      return method == factory.objectMembers.toString
-          || method == factory.stringBuilderMethods.toString
-          || method == factory.stringBufferMethods.toString
-          || method == factory.stringMembers.valueOf;
-    }
-
-    private boolean canHandleArgumentType(DexType argType) {
-      // TODO(b/113859361): passed to another builder should be an eligible case.
-      return argType == factory.stringType || argType == factory.charSequenceType;
-    }
-  }
-
-  class StringBuilderOptimizerEscapeAnalysisConfiguration implements EscapeAnalysisConfiguration {
-    final Value builder;
-    final DexType builderType;
-
-    private StringBuilderOptimizerEscapeAnalysisConfiguration(Value builder) {
-      this.builder = builder;
-      assert builder.getType().isClassType();
-      builderType = builder.getType().asClassType().getClassType();
-    }
-
-    private void logEscapingRoute(boolean legitimate) {
-      if (!legitimate) {
-        numberOfBuildersThatEscape++;
-      }
-    }
-
-    @Override
-    public boolean isLegitimateEscapeRoute(
-        AppView<?> appView,
-        EscapeAnalysis escapeAnalysis,
-        Instruction escapeRoute,
-        ProgramMethod context) {
-      if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) {
-        logEscapingRoute(false);
-        return false;
-      }
-      if (escapeRoute.isInvokeMethod()) {
-        // Program class may call String#intern(). Only allow library calls.
-        // TODO(b/114002137): For now, we allow only library calls to avoid a case like
-        //   identity(Builder.toString()).intern(); but it's too restrictive.
-        DexClass holderClass =
-            appView.definitionFor(escapeRoute.asInvokeMethod().getInvokedMethod().holder);
-        if (holderClass != null && !holderClass.isLibraryClass()) {
-          logEscapingRoute(false);
-          return false;
-        }
-
-        InvokeMethod invoke = escapeRoute.asInvokeMethod();
-        DexMethod invokedMethod = invoke.getInvokedMethod();
-
-        if (optimizationConfiguration.isToStringMethod(invokedMethod)) {
-          Value out = escapeRoute.outValue();
-          if (out != null) {
-            // If Builder#toString or String#valueOf is interned, it could be used for equality
-            // check. Replacing builder-based runtime result with a compile time constant may change
-            // the program's runtime behavior.
-            for (Instruction outUser : out.uniqueUsers()) {
-              if (outUser.isInvokeMethodWithReceiver()
-                  && outUser.asInvokeMethodWithReceiver().getInvokedMethod()
-                      == factory.stringMembers.intern) {
-                numberOfBuildersWhoseResultIsInterned++;
-                return false;
-              }
-            }
-          }
-          // Otherwise, use of Builder#toString and String#valueOf is legitimate.
-          return true;
-        }
-
-        // Make sure builder's uses are local, i.e., not escaping from the current method.
-        if (invokedMethod.holder != builderType) {
-          logEscapingRoute(false);
-          return false;
-        }
-
-        // <init> is legitimate.
-        if (optimizationConfiguration.isBuilderInit(invokedMethod, builderType)) {
-          return true;
-        }
-        // Even though all invocations belong to the builder type, there are some methods other
-        // than append/toString, e.g., setCharAt, setLength, subSequence, etc.
-        // Seeing any of them indicates that this code is not trivial.
-        if (!optimizationConfiguration.isAppendMethod(invokedMethod)) {
-          numberOfBuildersWithNonTrivialStateChange++;
-          return false;
-        }
-        if (!optimizationConfiguration.isSupportedAppendMethod(invoke)) {
-          return false;
-        }
-
-        // Reaching here means that this invocation is part of trivial patterns we're looking for.
-        return true;
-      }
-      if (escapeRoute.isArrayPut()) {
-        Value array = escapeRoute.asArrayPut().array().getAliasedValue();
-        boolean legitimate = !array.isPhi() && array.definition.isCreatingArray();
-        logEscapingRoute(legitimate);
-        return legitimate;
-      }
-      if (escapeRoute.isInstancePut()) {
-        Value instance = escapeRoute.asInstancePut().object().getAliasedValue();
-        boolean legitimate = !instance.isPhi() && instance.definition.isNewInstance();
-        logEscapingRoute(legitimate);
-        return legitimate;
-      }
-      // All other cases are not legitimate.
-      logEscapingRoute(false);
-      return false;
-    }
-  }
-
-  // A chain of builder's internal state changes.
-  static class BuilderState {
-    BuilderState previous;
-    String addition;
-    Set<BuilderState> nexts;
-
-    private BuilderState() {
-      previous = null;
-      addition = null;
-      nexts = null;
-    }
-
-    static BuilderState createRoot() {
-      return new BuilderState();
-    }
-
-    BuilderState createChild(String addition) {
-      BuilderState newState = new BuilderState();
-      newState.previous = this;
-      newState.addition = addition;
-      if (this.nexts == null) {
-        this.nexts = Sets.newIdentityHashSet();
-      }
-      this.nexts.add(newState);
-      return newState;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
index dd49ae4..d7d7f59 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
@@ -61,7 +61,8 @@
             || isStringBuildingMethod(factory.stringBufferMethods, invokedMethod)) {
           return true;
         }
-        return invokedMethod == factory.objectMembers.toString
+        return (invokedMethod == factory.objectMembers.toString
+                || invokedMethod == factory.objectsMethods.toStringWithObject)
             && isLiveStringBuilder.test(instruction.getFirstOperand());
       }
       return false;
@@ -87,17 +88,20 @@
 
     @Override
     public boolean isToString(Instruction instruction, Value value) {
-      if (!instruction.isInvokeVirtual()) {
+      if (!instruction.isInvokeMethod()) {
         return false;
       }
-      InvokeVirtual invoke = instruction.asInvokeVirtual();
-      if (invoke.getReceiver() != value) {
+      if (instruction.inValues().isEmpty()) {
         return false;
       }
-      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (instruction.getFirstOperand() != value) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
       return factory.stringBuilderMethods.toString == invokedMethod
           || factory.stringBufferMethods.toString == invokedMethod
-          || factory.objectMembers.toString == invokedMethod;
+          || factory.objectMembers.toString == invokedMethod
+          || factory.objectsMethods.toStringWithObject == invokedMethod;
     }
 
     @Override
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 0cb8e39..d512ff4 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
@@ -34,6 +34,7 @@
     factory.createSynthesizedType("Ljava/lang/IllegalAccessError;");
     factory.createSynthesizedType("Ljava/lang/IncompatibleClassChangeError;");
     factory.createSynthesizedType("Ljava/lang/NoSuchMethodError;");
+    factory.createSynthesizedType("Ljava/lang/RuntimeException;");
   }
 
   public static CfCode
@@ -145,6 +146,34 @@
         ImmutableList.of());
   }
 
+  public static CfCode
+      CfUtilityMethodsForCodeOptimizationsTemplates_throwRuntimeExceptionWithMessage(
+          InternalOptions options, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        3,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfNew(options.itemFactory.createType("Ljava/lang/RuntimeException;")),
+            new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                183,
+                options.itemFactory.createMethod(
+                    options.itemFactory.createType("Ljava/lang/RuntimeException;"),
+                    options.itemFactory.createProto(
+                        options.itemFactory.voidType, options.itemFactory.stringType),
+                    options.itemFactory.createString("<init>")),
+                false),
+            new CfThrow(),
+            label1),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
   public static CfCode CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(
       InternalOptions options, DexMethod method) {
     CfLabel label0 = new CfLabel();
diff --git a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
rename to src/main/java/com/android/tools/r8/optimize/AccessModifier.java
index ad17739..7661768 100644
--- a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
@@ -10,11 +10,12 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.MethodAccessFlags;
@@ -26,7 +27,8 @@
 import com.android.tools.r8.ir.optimize.MethodPoolCollection;
 import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.KeepInfoCollection;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Timing;
@@ -36,23 +38,23 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-public final class ClassAndMemberPublicizer {
+public final class AccessModifier {
 
   private final DexApplication application;
   private final AppView<AppInfoWithLiveness> appView;
-  private final KeepInfoCollection keepInfo;
+  private final InternalOptions options;
   private final SubtypingInfo subtypingInfo;
   private final MethodPoolCollection methodPoolCollection;
 
   private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
 
-  private ClassAndMemberPublicizer(
+  private AccessModifier(
       DexApplication application,
       AppView<AppInfoWithLiveness> appView,
       SubtypingInfo subtypingInfo) {
     this.application = application;
     this.appView = appView;
-    this.keepInfo = appView.appInfo().getKeepInfo();
+    this.options = appView.options();
     this.subtypingInfo = subtypingInfo;
     this.methodPoolCollection =
         // We will add private instance methods when we promote them.
@@ -73,8 +75,7 @@
       AppView<AppInfoWithLiveness> appView,
       SubtypingInfo subtypingInfo)
       throws ExecutionException {
-    return new ClassAndMemberPublicizer(application, appView, subtypingInfo)
-        .run(executorService, timing);
+    return new AccessModifier(application, appView, subtypingInfo).run(executorService, timing);
   }
 
   private GraphLens run(ExecutorService executorService, Timing timing) throws ExecutionException {
@@ -83,8 +84,8 @@
 
     // Phase 2: Visit classes and promote class/member to public if possible.
     timing.begin("Phase 2: promoteToPublic");
-    appView.appInfo().forEachReachableInterface(clazz -> publicizeType(clazz.getType()));
-    publicizeType(appView.dexItemFactory().objectType);
+    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
+    processType(appView.dexItemFactory().objectType);
     timing.end();
 
     return lensBuilder.build(appView);
@@ -94,21 +95,21 @@
     definition.getAccessFlags().promoteToPublic();
   }
 
-  private void publicizeType(DexType type) {
+  private void processType(DexType type) {
     DexProgramClass clazz = asProgramClassOrNull(application.definitionFor(type));
     if (clazz != null) {
-      publicizeClass(clazz);
+      processClass(clazz);
     }
-    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::publicizeType);
+    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
   }
 
-  private void publicizeClass(DexProgramClass clazz) {
+  private void processClass(DexProgramClass clazz) {
     if (appView.appInfo().isAccessModificationAllowed(clazz)) {
       doPublicize(clazz);
     }
 
     // Publicize fields.
-    clazz.forEachProgramField(this::publicizeField);
+    clazz.forEachProgramField(this::processField);
 
     // Publicize methods.
     Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
@@ -132,19 +133,43 @@
     }
   }
 
+  private void processField(ProgramField field) {
+    finalizeField(field);
+    publicizeField(field);
+  }
+
+  private void finalizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    FieldAccessInfo accessInfo =
+        appView.appInfo().getFieldAccessInfoCollection().get(field.getReference());
+    KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+    if (keepInfo.isAccessModificationAllowed(options)
+        && !keepInfo.isPinned(options)
+        && !accessInfo.hasReflectiveWrite()
+        && !accessInfo.isWrittenFromMethodHandle()
+        && accessInfo.isWrittenOnlyInMethodSatisfying(
+            method ->
+                method.getDefinition().isInitializer(flags.isStatic())
+                    && method.getHolder() == field.getHolder())
+        && !flags.isFinal()
+        && !flags.isVolatile()) {
+      flags.promoteToFinal();
+    }
+  }
+
   private void publicizeField(ProgramField field) {
-    DexEncodedField definition = field.getDefinition();
-    if (definition.isPublic()) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    if (flags.isPublic()) {
       return;
     }
     if (!appView.appInfo().isAccessModificationAllowed(field)) {
       // TODO(b/131130038): Also do not publicize package-private and protected fields that
       //  are kept.
-      if (definition.isPrivate()) {
+      if (flags.isPrivate()) {
         return;
       }
     }
-    doPublicize(field);
+    flags.promoteToPublic();
   }
 
   private boolean publicizeMethod(ProgramMethod method) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index df0f324..f968d56 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -1034,9 +1034,14 @@
       if (!appView.appInfo().isSubtype(newParameterType, staticType)) {
         return null;
       }
-      return AccessUtils.isAccessibleInSameContextsAs(newParameterType, staticType, appView)
-          ? newParameterType
-          : null;
+      if (!AccessUtils.isAccessibleInSameContextsAs(newParameterType, staticType, appView)) {
+        return null;
+      }
+      if (!AndroidApiLevelUtils.isApiSafeForTypeStrengthening(
+          newParameterType, staticType, appView)) {
+        return null;
+      }
+      return newParameterType;
     }
 
     private RewrittenPrototypeDescription computePrototypeChangesForMethod(
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
index bc44d01..7d65c1e 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
@@ -91,8 +91,12 @@
     CfAnalysisConfig config = createConfig(method, cfCode);
     CfOpenClosedInterfacesAnalysisHelper helper =
         new CfOpenClosedInterfacesAnalysisHelper(appView, method, unverifiableCodeDiagnostics);
-    if (runLinearScan(method, cfCode, config, helper).isNotPresent()) {
+    StackMapStatus stackMapStatus = runLinearScan(method, cfCode, config, helper);
+    if (stackMapStatus.isNotPresent()) {
       runFixpoint(method, cfCode, config, helper);
+      cfCode.setStackMapStatus(stackMapStatus);
+    } else if (stackMapStatus.isValid()) {
+      cfCode.setStackMapStatus(stackMapStatus);
     }
     return helper.getOpenInterfaces();
   }
diff --git a/src/main/java/com/android/tools/r8/relocator/Relocator.java b/src/main/java/com/android/tools/r8/relocator/Relocator.java
index 711610f..99c6471 100644
--- a/src/main/java/com/android/tools/r8/relocator/Relocator.java
+++ b/src/main/java/com/android/tools/r8/relocator/Relocator.java
@@ -93,6 +93,7 @@
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } finally {
+      inputApp.signalFinishedToProviders(options.reporter);
       options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
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 6214a5a..1df8107 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3436,6 +3436,20 @@
             markMethodAsTargeted(new ProgramMethod(clazz, method), reason);
           }
         });
+
+    // Disallow minification and optimization of types referenced from unresolvable methods. The
+    // graph lenses created by various optimizations only store mappings for method definitions,
+    // thus no lenses contain mappings for unresolvable methods. This can be problematic if an
+    // unresolvable method refers to a class that no longer exists as a result of an optimization.
+    for (DexType referencedType : symbolicMethod.getReferencedBaseTypes(appView.dexItemFactory())) {
+      if (referencedType.isClassType()) {
+        DexProgramClass clazz = asProgramClassOrNull(definitionFor(referencedType, context));
+        if (clazz != null) {
+          applyMinimumKeepInfoWhenLive(
+              clazz, KeepClassInfo.newEmptyJoiner().disallowMinification().disallowOptimization());
+        }
+      }
+    }
   }
 
   private DexMethod generatedEnumValuesMethod(DexClass enumClass) {
@@ -3576,7 +3590,7 @@
           .forEachRuleInstance(
               appView,
               (clazz, minimumKeepInfo) ->
-                  applyMinimumKeepInfoWhenLive(clazz, preconditionEvent, minimumKeepInfo),
+                  applyMinimumKeepInfoWhenLive(clazz, minimumKeepInfo, preconditionEvent),
               (field, minimumKeepInfo) ->
                   applyMinimumKeepInfoWhenLive(field, minimumKeepInfo, preconditionEvent),
               this::applyMinimumKeepInfoWhenLiveOrTargeted);
@@ -3646,8 +3660,14 @@
 
   private void applyMinimumKeepInfoWhenLive(
       DexProgramClass clazz,
-      EnqueuerEvent preconditionEvent,
       KeepClassInfo.Joiner minimumKeepInfo) {
+    applyMinimumKeepInfoWhenLive(clazz, minimumKeepInfo, EnqueuerEvent.unconditional());
+  }
+
+  private void applyMinimumKeepInfoWhenLive(
+      DexProgramClass clazz,
+      KeepClassInfo.Joiner minimumKeepInfo,
+      EnqueuerEvent preconditionEvent) {
     if (liveTypes.contains(clazz)) {
       keepInfo.joinClass(clazz, info -> info.merge(minimumKeepInfo));
     } else {
@@ -3674,7 +3694,7 @@
       DexProgramClass clazz,
       KeepClassInfo.Joiner minimumKeepInfo) {
     if (isPreconditionForMinimumKeepInfoSatisfied(preconditionEvent)) {
-      applyMinimumKeepInfoWhenLive(clazz, preconditionEvent, minimumKeepInfo);
+      applyMinimumKeepInfoWhenLive(clazz, minimumKeepInfo, preconditionEvent);
     } else {
       dependentMinimumKeepInfo
           .getOrCreateMinimumKeepInfoFor(preconditionEvent)
@@ -3803,7 +3823,7 @@
       minimumKeepClassInfoDependentOnPrecondition.forEach(
           appView,
           (clazz, minimumKeepInfoForClass) ->
-              applyMinimumKeepInfoWhenLive(clazz, preconditionEvent, minimumKeepInfoForClass),
+              applyMinimumKeepInfoWhenLive(clazz, minimumKeepInfoForClass, preconditionEvent),
           (field, minimumKeepInfoForField) ->
               applyMinimumKeepInfoWhenLive(field, minimumKeepInfoForField, preconditionEvent),
           (method, minimumKeepInfoForMethod) ->
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
index 94cf1b5..02979c5 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
@@ -300,9 +300,10 @@
         builder.addSyntheticInput(syntheticInput);
       }
     }
-    // Global synthetic contexts are only collected for per-file modes which should never
-    // prune items.
-    assert globalContexts.isEmpty();
+    // Global synthetic contexts are only collected for per-file modes which only prune synthetic
+    // items, not inputs.
+    assert globalContexts.isEmpty()
+        || prunedItems.getNoLongerSyntheticItems().size() == prunedItems.getRemovedClasses().size();
     return changed ? builder.build() : this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 822fa38..527cec3 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -36,6 +36,7 @@
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
@@ -63,6 +64,9 @@
 
 public class SyntheticFinalization {
 
+  // TODO(b/237413146): Implement a non-quadratic grouping algorithm.
+  private static final int GROUP_COUNT_THRESHOLD = 10;
+
   public static class Result {
     public final CommittedItems commit;
     public final NonIdentityGraphLens lens;
@@ -155,12 +159,13 @@
     this.committed = committed;
   }
 
-  public static void finalize(AppView<AppInfo> appView, ExecutorService executorService)
+  public static void finalize(
+      AppView<AppInfo> appView, Timing timing, ExecutorService executorService)
       throws ExecutionException {
     assert !appView.appInfo().hasClassHierarchy();
     assert !appView.appInfo().hasLiveness();
     appView.options().testing.checkDeterminism(appView);
-    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView);
+    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView, timing);
     appView.setAppInfo(new AppInfo(result.commit, result.mainDexInfo));
     if (result.lens != null) {
       appView.setAppInfo(
@@ -177,11 +182,11 @@
   }
 
   public static void finalizeWithClassHierarchy(
-      AppView<AppInfoWithClassHierarchy> appView, ExecutorService executorService)
+      AppView<AppInfoWithClassHierarchy> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
     assert !appView.appInfo().hasLiveness();
     appView.options().testing.checkDeterminism(appView);
-    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView);
+    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView, timing);
     appView.setAppInfo(appView.appInfo().rebuildWithClassHierarchy(result.commit));
     appView.setAppInfo(appView.appInfo().rebuildWithMainDexInfo(result.mainDexInfo));
     if (result.lens != null) {
@@ -199,10 +204,10 @@
   }
 
   public static void finalizeWithLiveness(
-      AppView<AppInfoWithLiveness> appView, ExecutorService executorService)
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
     appView.options().testing.checkDeterminism(appView);
-    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView);
+    Result result = appView.getSyntheticItems().computeFinalSynthetics(appView, timing);
     appView.setAppInfo(appView.appInfo().rebuildWithMainDexInfo(result.mainDexInfo));
     if (result.lens != null) {
       appView.rewriteWithLensAndApplication(result.lens, result.commit.getApplication().asDirect());
@@ -213,7 +218,7 @@
     appView.pruneItems(result.prunedItems, executorService);
   }
 
-  Result computeFinalSynthetics(AppView<?> appView) {
+  Result computeFinalSynthetics(AppView<?> appView, Timing timing) {
     assert verifyNoNestedSynthetics(appView.dexItemFactory());
     assert verifyOneSyntheticPerSyntheticClass();
     DexApplication application;
@@ -227,9 +232,18 @@
       Map<String, NumberGenerator> generators = new HashMap<>();
       application =
           buildLensAndProgram(
+              timing,
               appView,
-              computeEquivalences(appView, committed.getMethods(), generators, lensBuilder),
-              computeEquivalences(appView, committed.getClasses(), generators, lensBuilder),
+              timing.time(
+                  "Method equivalence",
+                  () ->
+                      computeEquivalences(
+                          appView, committed.getMethods(), generators, lensBuilder, timing)),
+              timing.time(
+                  "Class equivalence",
+                  () ->
+                      computeEquivalences(
+                          appView, committed.getClasses(), generators, lensBuilder, timing)),
               lensBuilder,
               (clazz, reference) ->
                   finalClassesBuilder.put(clazz.getType(), ImmutableList.of(reference)),
@@ -289,13 +303,15 @@
           AppView<?> appView,
           ImmutableMap<DexType, List<R>> references,
           Map<String, NumberGenerator> generators,
-          Builder lensBuilder) {
+          Builder lensBuilder,
+          Timing timing) {
     boolean intermediate = appView.options().intermediate;
     Map<DexType, D> definitions = lookupDefinitions(appView, references);
     ClassToFeatureSplitMap classToFeatureSplitMap =
         appView.appInfo().hasClassHierarchy()
             ? appView.appInfo().withClassHierarchy().getClassToFeatureSplitMap()
             : ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap();
+    timing.begin("Potential equivalences");
     Collection<List<D>> potentialEquivalences =
         computePotentialEquivalences(
             definitions,
@@ -304,13 +320,15 @@
             appView.graphLens(),
             classToFeatureSplitMap,
             synthetics);
+    timing.end();
     return computeActualEquivalences(
         potentialEquivalences,
         generators,
         appView,
         intermediate,
         classToFeatureSplitMap,
-        lensBuilder);
+        lensBuilder,
+        timing);
   }
 
   private boolean isNotSyntheticType(DexType type) {
@@ -361,6 +379,7 @@
   }
 
   private static DexApplication buildLensAndProgram(
+      Timing timing,
       AppView<?> appView,
       Map<DexType, EquivalenceGroup<SyntheticMethodDefinition>> syntheticMethodGroups,
       Map<DexType, EquivalenceGroup<SyntheticProgramClassDefinition>> syntheticClassGroups,
@@ -448,10 +467,12 @@
       assert verifyNonRepresentativesRemovedFromApplication(application, syntheticClassGroups);
       assert verifyNonRepresentativesRemovedFromApplication(application, syntheticMethodGroups);
 
+      timing.begin("Tree fixing");
       DexApplication.Builder<?> builder = application.builder();
       treeFixer.fixupClasses(deduplicatedClasses);
       builder.replaceProgramClasses(treeFixer.fixupClasses(application.classes()));
       application = builder.build();
+      timing.end();
     }
 
     DexString syntheticSourceFileName =
@@ -459,6 +480,7 @@
             ? appView.dexItemFactory().createString("R8$$SyntheticClass")
             : appView.dexItemFactory().createString("D8$$SyntheticClass");
 
+    timing.begin("Add final synthetics");
     // Add the synthesized from after repackaging which changed class definitions.
     final DexApplication appForLookup = application;
     syntheticClassGroups.forEach(
@@ -491,7 +513,9 @@
                   representative.getContext(),
                   syntheticMethodDefinition.getReference()));
         });
+    timing.end();
 
+    timing.begin("Finish lens");
     Iterables.<EquivalenceGroup<? extends SyntheticDefinition<?, ?, DexProgramClass>>>concat(
             syntheticClassGroups.values(), syntheticMethodGroups.values())
         .forEach(
@@ -511,6 +535,7 @@
                             lensBuilder.setRepresentative(rewrittenMethod, method);
                           }
                         }));
+    timing.end();
 
     for (DexType key : syntheticMethodGroups.keySet()) {
       assert application.definitionFor(key) != null;
@@ -557,9 +582,11 @@
           AppView<?> appView,
           boolean intermediate,
           ClassToFeatureSplitMap classToFeatureSplitMap,
-          Builder lensBuilder) {
+          Builder lensBuilder,
+          Timing timing) {
     Map<String, List<EquivalenceGroup<T>>> groupsPerPrefix = new HashMap<>();
     Map<DexType, EquivalenceGroup<T>> equivalences = new IdentityHashMap<>();
+    timing.begin("Groups");
     potentialEquivalences.forEach(
         members -> {
           List<EquivalenceGroup<T>> groups =
@@ -581,6 +608,8 @@
             }
           }
         });
+    timing.end();
+    timing.begin("External creation");
     groupsPerPrefix.forEach(
         (externalSyntheticTypePrefix, groups) -> {
           Comparator<EquivalenceGroup<T>> comparator = this::compareForFinalGroupSorting;
@@ -613,6 +642,7 @@
             equivalences.put(representativeType, group);
           }
         });
+    timing.end();
     equivalences.forEach(
         (representativeType, group) ->
             group.forEach(
@@ -639,6 +669,10 @@
     List<EquivalenceGroup<T>> groups = new ArrayList<>();
     // Each other member is in a shared group if it is actually equivalent to the first member.
     for (T synthetic : potentialEquivalence) {
+      if (groups.size() > GROUP_COUNT_THRESHOLD) {
+        return ListUtils.map(
+            potentialEquivalence, m -> new EquivalenceGroup<>(m, isPinned(appView, m)));
+      }
       boolean mustBeRepresentative = isPinned(appView, synthetic);
       EquivalenceGroup<T> equivalenceGroup = null;
       for (EquivalenceGroup<T> group : groups) {
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 08d9d27..044b77f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -43,6 +43,7 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -936,25 +937,6 @@
     return globalSynthetic;
   }
 
-  // TODO(b/230445931): Remove this once possible.
-  @Deprecated
-  public DexProgramClass legacyEnsureGlobalClass(
-      Supplier<MissingGlobalSyntheticsConsumerDiagnostic> diagnosticSupplier,
-      SyntheticKindSelector kindSelector,
-      DexType globalType,
-      AppView<?> appView,
-      Consumer<SyntheticProgramClassBuilder> fn,
-      Consumer<DexProgramClass> onCreationConsumer) {
-    SyntheticKind kind = kindSelector.select(naming);
-    assert kind.isGlobal();
-    if (appView.options().intermediate && !appView.options().hasGlobalSyntheticsConsumer()) {
-      appView.reporter().fatalError(diagnosticSupplier.get());
-    }
-    // A global type is its own context.
-    SynthesizingContext outerContext = SynthesizingContext.fromType(globalType);
-    return internalEnsureFixedProgramClass(kind, fn, onCreationConsumer, outerContext, appView);
-  }
-
   /** Create a single synthetic method item. */
   public ProgramMethod createMethod(
       SyntheticKindSelector kindSelector,
@@ -1086,9 +1068,9 @@
 
   // Finalization of synthetic items.
 
-  Result computeFinalSynthetics(AppView<?> appView) {
+  Result computeFinalSynthetics(AppView<?> appView, Timing timing) {
     assert !hasPendingSyntheticClasses();
     return new SyntheticFinalization(appView.options(), this, committed)
-        .computeFinalSynthetics(appView);
+        .computeFinalSynthetics(appView, timing);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 29ca8e4..631115f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -75,6 +75,7 @@
   public final SyntheticKind THROW_IAE = generator.forSingleMethod("ThrowIAE");
   public final SyntheticKind THROW_ICCE = generator.forSingleMethod("ThrowICCE");
   public final SyntheticKind THROW_NSME = generator.forSingleMethod("ThrowNSME");
+  public final SyntheticKind THROW_RTE = generator.forSingleMethod("ThrowRTE");
   public final SyntheticKind TWR_CLOSE_RESOURCE = generator.forSingleMethod("TwrCloseResource");
   public final SyntheticKind SERVICE_LOADER = generator.forSingleMethod("ServiceLoad");
   public final SyntheticKind OUTLINE = generator.forSingleMethod("Outline");
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 189d9f9..2c3b88b 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -106,7 +106,7 @@
     return apiLevelOfOriginal.max(apiLevel).isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
   }
 
-  private static boolean isApiSafeForReference(LibraryDefinition definition, AppView<?> appView) {
+  public static boolean isApiSafeForReference(LibraryDefinition definition, AppView<?> appView) {
     return isApiSafeForReference(definition, appView.apiLevelCompute(), appView.options());
   }
 
@@ -114,7 +114,9 @@
       LibraryDefinition definition,
       AndroidApiLevelCompute androidApiLevelCompute,
       InternalOptions options) {
-    assert options.apiModelingOptions().enableApiCallerIdentification;
+    if (!options.apiModelingOptions().enableApiCallerIdentification) {
+      return false;
+    }
     ComputedApiLevel apiLevel =
         androidApiLevelCompute.computeApiLevelForLibraryReference(
             definition.getReference(), ComputedApiLevel.unknown());
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 8060f36..fbf344b 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -467,9 +467,6 @@
   }
 
   public void dump(Path output, DumpOptions options, Reporter reporter, DexItemFactory factory) {
-    if (options == null) {
-      return;
-    }
     int nextDexIndex = 0;
     OpenOption[] openOptions =
         new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
@@ -477,7 +474,10 @@
       writeToZipStream(
           out, dumpVersionFileName, Version.getVersionString().getBytes(), ZipEntry.DEFLATED);
       writeToZipStream(
-          out, dumpBuildPropertiesFileName, options.dumpOptions().getBytes(), ZipEntry.DEFLATED);
+          out,
+          dumpBuildPropertiesFileName,
+          options.getBuildPropertiesFileContent().getBytes(),
+          ZipEntry.DEFLATED);
       if (options.getDesugaredLibraryJsonSource() != null) {
         writeToZipStream(
             out,
@@ -793,6 +793,18 @@
     }
   }
 
+  public void signalFinishedToProviders(Reporter reporter) throws IOException {
+    for (ProgramResourceProvider provider : programResourceProviders) {
+      provider.finished(reporter);
+    }
+    for (ClassFileResourceProvider provider : classpathResourceProviders) {
+      provider.finished(reporter);
+    }
+    for (ClassFileResourceProvider provider : libraryResourceProviders) {
+      provider.finished(reporter);
+    }
+  }
+
   /**
    * Builder interface for constructing an AndroidApp.
    */
diff --git a/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java b/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
index d37de80..368974b 100644
--- a/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
+++ b/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
@@ -3,53 +3,101 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.dump.DumpOptions;
+import com.android.tools.r8.errors.Unreachable;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
 
 public abstract class DumpInputFlags {
 
+  private static final String DUMP_INPUT_TO_FILE_PROPERTY = "com.android.tools.r8.dumpinputtofile";
+  private static final String DUMP_INPUT_TO_DIRECTORY_PROPERTY =
+      "com.android.tools.r8.dumpinputtodirectory";
+
+  public static DumpInputFlags getDefault() {
+    String dumpInputToFile = System.getProperty(DUMP_INPUT_TO_FILE_PROPERTY);
+    if (dumpInputToFile != null) {
+      return dumpToFile(Paths.get(dumpInputToFile));
+    }
+    String dumpInputToDirectory = System.getProperty(DUMP_INPUT_TO_DIRECTORY_PROPERTY);
+    if (dumpInputToDirectory != null) {
+      return dumpToDirectory(Paths.get(dumpInputToDirectory));
+    }
+    return noDump();
+  }
+
   public static DumpInputFlags noDump() {
     return new DumpInputFlags() {
+
       @Override
-      Path getDumpInputToFile() {
-        return null;
+      public Path getDumpPath() {
+        throw new Unreachable();
       }
 
       @Override
-      Path getDumpInputToDirectory() {
-        return null;
+      public boolean shouldDump(DumpOptions options) {
+        return false;
+      }
+
+      @Override
+      public boolean shouldFailCompilation() {
+        throw new Unreachable();
       }
     };
   }
 
   public static DumpInputFlags dumpToFile(Path file) {
-    return new DumpInputFlags() {
+    return new DumpInputToFileOrDirectoryFlags() {
+
       @Override
-      Path getDumpInputToFile() {
+      public Path getDumpPath() {
         return file;
       }
 
       @Override
-      Path getDumpInputToDirectory() {
-        return null;
+      public boolean shouldFailCompilation() {
+        return true;
       }
     };
   }
 
-  public static DumpInputFlags dumpToDirectory(Path file) {
-    return new DumpInputFlags() {
+  public static DumpInputFlags dumpToDirectory(Path directory) {
+    return new DumpInputToFileOrDirectoryFlags() {
+
       @Override
-      Path getDumpInputToFile() {
-        return null;
+      public Path getDumpPath() {
+        return directory.resolve("dump" + System.nanoTime() + ".zip");
       }
 
       @Override
-      Path getDumpInputToDirectory() {
-        return file;
+      public boolean shouldFailCompilation() {
+        return false;
       }
     };
   }
 
-  abstract Path getDumpInputToFile();
+  public abstract Path getDumpPath();
 
-  abstract Path getDumpInputToDirectory();
+  public abstract boolean shouldDump(DumpOptions options);
+
+  public abstract boolean shouldFailCompilation();
+
+  abstract static class DumpInputToFileOrDirectoryFlags extends DumpInputFlags {
+
+    @Override
+    public boolean shouldDump(DumpOptions options) {
+      Map<String, String> buildProperties = options.getBuildProperties();
+      for (Entry<String, String> entry : buildProperties.entrySet()) {
+        String valueRegExp =
+            System.getProperty("com.android.tools.r8.dump.filter.buildproperty." + entry.getKey());
+        if (valueRegExp != null && !Pattern.matches(valueRegExp, entry.getValue())) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
index e5557d9..5c2eec5 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 
 import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.errors.CompilationError;
@@ -113,9 +114,16 @@
   }
 
   @Override
+  public void finished(DiagnosticsHandler handler) throws IOException {
+    close();
+  }
+
+  @Override
   public void close() throws IOException {
-    openedZipFile.close();
-    openedZipFile = null;
+    if (openedZipFile != null) {
+      openedZipFile.close();
+      openedZipFile = null;
+    }
   }
 
   private ZipEntry getZipEntryFromDescriptor(String descriptor) throws IOException {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
index a1e7b70..1428459 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
@@ -209,14 +209,12 @@
         // is not applied to SyntheticItems in AppView.
         Set<DexType> contexts = globalsToContexts.get(globalType);
         // TODO(b/231598779): Contexts should never be null once fixed for records.
-        assert (contexts == null) == (globalType == appView.dexItemFactory().recordTagType);
-        if (contexts != null) {
-          assert !contexts.isEmpty();
-          for (DexType contextType : contexts) {
-            contextToGlobals
-                .computeIfAbsent(contextType, k -> SetUtils.newIdentityHashSet())
-                .add(globalType);
-          }
+        assert contexts != null;
+        assert !contexts.isEmpty();
+        for (DexType contextType : contexts) {
+          contextToGlobals
+              .computeIfAbsent(contextType, k -> SetUtils.newIdentityHashSet())
+              .add(globalType);
         }
       }
       contextToGlobals.forEach(
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 d338a11..7d6d982 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -278,10 +278,6 @@
   // To print memory one also have to enable printtimes.
   public boolean printMemory = System.getProperty("com.android.tools.r8.printmemory") != null;
 
-  public String dumpInputToFile = System.getProperty("com.android.tools.r8.dumpinputtofile");
-  public String dumpInputToDirectory =
-      System.getProperty("com.android.tools.r8.dumpinputtodirectory");
-
   // Flag to toggle if DEX code objects should pass-through without IR processing.
   public boolean passthroughDexCode = false;
 
@@ -414,6 +410,8 @@
   public boolean emitPermittedSubclassesAnnotationsInDex =
       System.getProperty("com.android.tools.r8.emitPermittedSubclassesAnnotationsInDex") != null;
 
+  private DumpInputFlags dumpInputFlags = DumpInputFlags.getDefault();
+
   // Contain the contents of the build properties file from the compiler command.
   public DumpOptions dumpOptions;
 
@@ -462,19 +460,8 @@
     return marker;
   }
 
-  public void setDumpInputFlags(DumpInputFlags dumpInputFlags, boolean skipDump) {
-    if (skipDump) {
-      dumpInputToDirectory = null;
-      dumpInputToFile = null;
-      return;
-    }
-
-    if (dumpInputFlags.getDumpInputToFile() != null) {
-      dumpInputToFile = dumpInputFlags.getDumpInputToFile().toString();
-    }
-    if (dumpInputFlags.getDumpInputToDirectory() != null) {
-      dumpInputToDirectory = dumpInputFlags.getDumpInputToDirectory().toString();
-    }
+  public void setDumpInputFlags(DumpInputFlags dumpInputFlags) {
+    this.dumpInputFlags = dumpInputFlags;
   }
 
   public boolean hasConsumer() {
@@ -522,10 +509,8 @@
   }
 
   public boolean shouldKeepStackMapTable() {
-    assert isCfDesugaring() || isRelocatorCompilation() || getProguardConfiguration() != null;
-    return isCfDesugaring()
-        || isRelocatorCompilation()
-        || getProguardConfiguration().getKeepAttributes().stackMapTable;
+    assert isRelocatorCompilation() || getProguardConfiguration() != null;
+    return isRelocatorCompilation() || getProguardConfiguration().getKeepAttributes().stackMapTable;
   }
 
   public boolean shouldRerunEnqueuer() {
@@ -872,6 +857,10 @@
     return cfCodeAnalysisOptions;
   }
 
+  public DumpInputFlags getDumpInputFlags() {
+    return dumpInputFlags;
+  }
+
   public OpenClosedInterfacesOptions getOpenClosedInterfacesOptions() {
     return openClosedInterfacesOptions;
   }
@@ -1953,10 +1942,6 @@
 
     public boolean disableShortenLiveRanges = false;
 
-    // Force each call of application read to dump its inputs to a file, which is subsequently
-    // deleted. Useful to check that our dump functionality does not cause compilation failure.
-    public boolean dumpAll = false;
-
     // Option for testing outlining with interface array arguments, see b/132420510.
     public boolean allowOutlinerInterfaceArrayArguments = false;
 
@@ -2083,28 +2068,76 @@
     return CfVersion.V1_5;
   }
 
-  public boolean canUseInvokePolymorphicOnVarHandle() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+  public static AndroidApiLevel invokePolymorphicOnMethodHandleApiLevel() {
+    return AndroidApiLevel.O;
   }
 
-  public boolean canUseInvokePolymorphic() {
-    return hasFeaturePresentFrom(AndroidApiLevel.O);
+  public boolean canUseInvokePolymorphicOnMethodHandle() {
+    return hasFeaturePresentFrom(invokePolymorphicOnMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel invokePolymorphicOnVarHandleApiLevel() {
+    return AndroidApiLevel.P;
+  }
+
+  public boolean canUseInvokePolymorphicOnVarHandle() {
+    return hasFeaturePresentFrom(invokePolymorphicOnMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel constantMethodHandleApiLevel() {
+    return AndroidApiLevel.P;
   }
 
   public boolean canUseConstantMethodHandle() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+    return hasFeaturePresentFrom(constantMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel constantMethodTypeApiLevel() {
+    return AndroidApiLevel.P;
   }
 
   public boolean canUseConstantMethodType() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+    return hasFeaturePresentFrom(constantMethodTypeApiLevel());
+  }
+
+  public static AndroidApiLevel invokeCustomApiLevel() {
+    return AndroidApiLevel.O;
   }
 
   public boolean canUseInvokeCustom() {
-    return hasFeaturePresentFrom(AndroidApiLevel.O);
+    return hasFeaturePresentFrom(invokeCustomApiLevel());
+  }
+
+  public static AndroidApiLevel constantDynamicApiLevel() {
+    return null;
+  }
+
+  public boolean canUseConstantDynamic() {
+    return hasFeaturePresentFrom(constantDynamicApiLevel());
+  }
+
+  public static AndroidApiLevel defaultAndStaticInterfaceMethodsApiLevel() {
+    return AndroidApiLevel.N;
+  }
+
+  public static AndroidApiLevel defaultInterfaceMethodsApiLevel() {
+    return defaultAndStaticInterfaceMethodsApiLevel();
+  }
+
+  public static AndroidApiLevel staticInterfaceMethodsApiLevel() {
+    return defaultAndStaticInterfaceMethodsApiLevel();
   }
 
   public boolean canUseDefaultAndStaticInterfaceMethods() {
-    return hasFeaturePresentFrom(AndroidApiLevel.N);
+    return hasFeaturePresentFrom(defaultInterfaceMethodsApiLevel());
+  }
+
+  public static AndroidApiLevel privateInterfaceMethodsApiLevel() {
+    return AndroidApiLevel.N;
+  }
+
+  public boolean canUsePrivateInterfaceMethods() {
+    return hasFeaturePresentFrom(privateInterfaceMethodsApiLevel());
   }
 
   public boolean canUseNestBasedAccess() {
@@ -2150,10 +2183,6 @@
     throw new Unreachable();
   }
 
-  public boolean canUsePrivateInterfaceMethods() {
-    return hasFeaturePresentFrom(AndroidApiLevel.N);
-  }
-
   // Debug entries may be dropped only if the source file content allows being omitted from
   // stack traces, or if the VM will report the source file even with a null valued debug info.
   public boolean allowDiscardingResidualDebugInfo() {
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 66e5fc8..dbfd4c6 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -106,10 +106,10 @@
       command.add(outJar.toString());
       command.add("-printmapping");
       command.add(mapFile.toString());
-      if (!enableTreeShaking) {
+      if (enableTreeShaking.isFalse()) {
         command.add("-dontshrink");
       }
-      if (!enableMinification) {
+      if (enableMinification.isFalse()) {
         command.add("-dontobfuscate");
       }
       ProcessBuilder pbuilder = new ProcessBuilder(command);
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 10c5358..f3c3dc5 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -498,37 +498,23 @@
   static {
     ImmutableMap.Builder<DexVm.Version, List<String>> builder = ImmutableMap.builder();
     builder
-        .put(
-            DexVm.Version.V13_0_0,
-            ImmutableList.of("454-get-vreg", "457-regs", "543-env-long-ref", "518-null-array-get"))
-        .put(
-            DexVm.Version.V12_0_0,
-            ImmutableList.of("454-get-vreg", "457-regs", "543-env-long-ref", "518-null-array-get"))
+        .put(DexVm.Version.V13_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
+        .put(DexVm.Version.V12_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
         .put(
             DexVm.Version.V10_0_0,
             ImmutableList.of(
                 // TODO(b/144975341): Triage, Verif error.
                 "518-null-array-get",
                 // TODO(b/144975341): Triage, Linking error.
-                "457-regs",
-                "543-env-long-ref",
-                "454-get-vreg"))
+                "543-env-long-ref"))
         .put(
             DexVm.Version.V9_0_0,
             ImmutableList.of(
-                // TODO(120400625): Triage.
-                "454-get-vreg",
-                // TODO(120402198): Triage.
-                "457-regs",
                 // TODO(120401674): Triage.
                 "543-env-long-ref",
                 // TODO(120261858) Triage.
                 "518-null-array-get"))
-        .put(
-            DexVm.Version.V8_1_0,
-            ImmutableList.of(
-                // TODO(119938529): Triage.
-                "709-checker-varhandles", "454-get-vreg", "457-regs"))
+        .put(DexVm.Version.V8_1_0, ImmutableList.of())
         .put(
             DexVm.Version.V7_0_0,
             ImmutableList.of(
@@ -872,7 +858,12 @@
                       DexVm.Version.V4_4_4,
                       DexVm.Version.V5_1_1,
                       DexVm.Version.V6_0_1,
-                      DexVm.Version.V7_0_0)))
+                      DexVm.Version.V7_0_0,
+                      DexVm.Version.V8_1_0,
+                      DexVm.Version.V9_0_0,
+                      DexVm.Version.V10_0_0,
+                      DexVm.Version.V12_0_0,
+                      DexVm.Version.V13_0_0)))
           .put("454-get-vreg", TestCondition.match(TestCondition.R8DEX_COMPILER))
           // Fails: regs_jni.cc:42] Check failed: GetVReg(m, 0, kIntVReg, &value)
           // The R8/D8 code does not put values in the same registers as the tests expects.
@@ -884,7 +875,12 @@
                       DexVm.Version.V4_4_4,
                       DexVm.Version.V5_1_1,
                       DexVm.Version.V6_0_1,
-                      DexVm.Version.V7_0_0)))
+                      DexVm.Version.V7_0_0,
+                      DexVm.Version.V8_1_0,
+                      DexVm.Version.V9_0_0,
+                      DexVm.Version.V10_0_0,
+                      DexVm.Version.V12_0_0,
+                      DexVm.Version.V13_0_0)))
           .put("457-regs", TestCondition.match(TestCondition.R8DEX_COMPILER))
           // Class not found.
           .put("529-checker-unresolved", TestCondition.any())
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
index 7c3a605..cd5e1ca 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
@@ -56,16 +56,4 @@
   R8TestRunner test(String testName, String packageName, String mainClass) {
     return new R8TestRunner(testName, packageName, mainClass);
   }
-
-  @Test
-  public void varHandle() throws Throwable {
-    test("varhandle", "varhandle", "VarHandleTests")
-        .withBuilderTransformation(
-            builder ->
-                builder.addProguardConfiguration(
-                    ImmutableList.of("-dontwarn java.lang.invoke.VarHandle"), Origin.unknown()))
-        .withMinApiLevel(AndroidApiLevel.P.getLevel())
-        .withKeepAll()
-        .run();
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index ebe8352..3bdfa4b 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -82,8 +82,12 @@
       builder.addProguardConfiguration(keepRules, Origin.unknown());
     }
     builder.addMainDexRulesFiles(mainDexRulesFiles);
-    builder.setDisableTreeShaking(!enableTreeShaking);
-    builder.setDisableMinification(!enableMinification);
+    if (enableTreeShaking.isFalse()) {
+      builder.setDisableTreeShaking(true);
+    }
+    if (enableMinification.isFalse()) {
+      builder.setDisableMinification(true);
+    }
     StringBuilder proguardMapBuilder = new StringBuilder();
     if (createDefaultProguardMapConsumer) {
       builder.setProguardMapConsumer(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index ed69ecf..caed567 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -228,14 +228,6 @@
         .run();
   }
 
-  @Test
-  public void invokeCustomErrorDueToMinSdk() throws Throwable {
-    test("invokecustom-error-due-to-min-sdk", "invokecustom", "InvokeCustom")
-        .withMinApiLevel(AndroidApiLevel.O.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
   abstract RunExamplesAndroidPTest<B>.TestRunner<?> test(String testName, String packageName,
       String mainClass);
 
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index a20964a..485ac54 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -120,42 +120,17 @@
     abstract void build(Path inputFile, Path out) throws Throwable;
   }
 
-  private static List<String> minSdkErrorExpected =
-      ImmutableList.of("varhandle-error-due-to-min-sdk");
+  private static List<String> minSdkErrorExpected = ImmutableList.of();
 
   private static Map<DexVm.Version, List<String>> failsOn;
 
   static {
     ImmutableMap.Builder<DexVm.Version, List<String>> builder = ImmutableMap.builder();
     builder
-        .put(DexVm.Version.V4_0_4, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V4_4_4, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V5_1_1, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V6_0_1, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V7_0_0, ImmutableList.of(
-            // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V8_1_0, ImmutableList.of(
-            // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.DEFAULT, ImmutableList.of(
-            // TODO(b/72536415): Update runtime when the support will be ready
-            "varhandle"
-        ));
+        .put(DexVm.Version.V4_0_4, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V4_4_4, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V5_1_1, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V6_0_1, ImmutableList.of("native-private-interface-methods"));
     failsOn = builder.build();
   }
 
@@ -168,20 +143,9 @@
               + "1\n2\n3\n4\n5\n6\n7\n8\n99\n"
               + "i1\ni2\ni3\ni4\ni5\ni6\ni7\ni8\ni99\n",
           "native-private-interface-methods",
-          "0: s>i>a\n"
-              + "1: d>i>s>i>a\n"
-              + "2: l>i>s>i>a\n"
-              + "3: x>s\n"
-              + "4: c>d>i>s>i>a\n",
+          "0: s>i>a\n" + "1: d>i>s>i>a\n" + "2: l>i>s>i>a\n" + "3: x>s\n" + "4: c>d>i>s>i>a\n",
           "desugared-private-interface-methods",
-          "0: s>i>a\n"
-              + "1: d>i>s>i>a\n"
-              + "2: l>i>s>i>a\n"
-              + "3: x>s\n"
-              + "4: c>d>i>s>i>a\n",
-          "varhandle",
-          "true\nfalse\n"
-      );
+          "0: s>i>a\n" + "1: d>i>s>i>a\n" + "2: l>i>s>i>a\n" + "3: x>s\n" + "4: c>d>i>s>i>a\n");
 
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@@ -241,22 +205,6 @@
   }
 
   @Test
-  public void varHandle() throws Throwable {
-    test("varhandle", "varhandle", "VarHandleTests")
-        .withMinApiLevel(AndroidApiLevel.P.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
-  @Test
-  public void varHandleErrorDueToMinSdk() throws Throwable {
-    test("varhandle-error-due-to-min-sdk", "varhandle", "VarHandleTests")
-        .withMinApiLevel(AndroidApiLevel.O.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
-  @Test
   public void testTwrCloseResourceMethod() throws Throwable {
     TestRunner<?> test = test("twr-close-resource", "twrcloseresource", "TwrCloseResourceTest");
     test
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 4383be5..98f1627 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1818,6 +1818,10 @@
     return AndroidApiLevel.O;
   }
 
+  public static AndroidApiLevel apiLevelWithInvokePolymorphicSupport() {
+    return AndroidApiLevel.O;
+  }
+
   public static AndroidApiLevel apiLevelWithConstMethodHandleSupport() {
     return AndroidApiLevel.P;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 52d84fc..2b29085 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -250,6 +251,12 @@
     return self();
   }
 
+  public T mapUnsupportedFeaturesToWarnings() {
+    return setDiagnosticsLevelModifier(
+        (level, diagnostic) ->
+            diagnostic instanceof UnsupportedFeatureDiagnostic ? DiagnosticsLevel.WARNING : level);
+  }
+
   public T allowStdoutMessages() {
     // Default ignored.
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 8b4c482..5d9d7f5 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -75,6 +75,8 @@
   private ProgramConsumer programConsumer;
   private MainDexClassesCollector mainDexClassesCollector;
   private StringConsumer mainDexListConsumer;
+  // TODO(b/186010707): This could become implicit once min always be set when fixed.
+  private boolean noMinApiLevel = false;
   private int minApiLevel = -1;
   private boolean optimizeMultidexForLinearAlloc = false;
   private Consumer<InternalOptions> optionsConsumer = DEFAULT_OPTIONS;
@@ -219,7 +221,7 @@
               .addIfNotNull(mainDexListConsumer)
               .build());
     }
-    if (backend.isDex() || !isTestShrinkerBuilder()) {
+    if (!noMinApiLevel && (backend.isDex() || !isTestShrinkerBuilder())) {
       assert !builder.isMinApiLevelSet()
           : "Don't set the API level directly through BaseCompilerCommand.Builder in tests";
       // TODO(b/186010707): This will always be set when fixed.
@@ -364,6 +366,12 @@
     return self();
   }
 
+  public T setNoMinApi() {
+    this.minApiLevel = -1;
+    this.noMinApiLevel = true;
+    return self();
+  }
+
   /** @deprecated use {@link #setMinApi(AndroidApiLevel)} instead. */
   @Deprecated
   public T setMinApi(TestRuntime runtime) {
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index bf47b6e..9072aa3 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Sets;
 import java.io.IOException;
@@ -29,8 +30,8 @@
         T extends TestShrinkerBuilder<C, B, CR, RR, T>>
     extends TestCompilerBuilder<C, B, CR, RR, T> {
 
-  protected boolean enableTreeShaking = true;
-  protected boolean enableMinification = true;
+  protected OptionalBool enableTreeShaking = OptionalBool.UNKNOWN;
+  protected OptionalBool enableMinification = OptionalBool.UNKNOWN;
 
   private final Set<Class<? extends Annotation>> addedTestingAnnotations =
       Sets.newIdentityHashSet();
@@ -67,7 +68,7 @@
   }
 
   public T treeShaking(boolean enable) {
-    enableTreeShaking = enable;
+    enableTreeShaking = OptionalBool.of(enable);
     return self();
   }
 
@@ -76,7 +77,7 @@
   }
 
   public T minification(boolean enable) {
-    enableMinification = enable;
+    enableMinification = OptionalBool.of(enable);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
new file mode 100644
index 0000000..b1c5fc8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EffectiveFinalFieldMarkedFinalTest extends TestBase {
+
+  @Parameter(0)
+  public boolean allowAccessModification;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, allowaccessmodification: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowAccessModification(allowAccessModification)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+              assertThat(
+                  mainClassSubject.uniqueFieldWithName("instanceField"),
+                  allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
+              assertThat(
+                  mainClassSubject.uniqueFieldWithName("staticField"),
+                  allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    static String staticField = System.currentTimeMillis() > 0 ? "Hello" : null;
+
+    String instanceField = System.currentTimeMillis() > 0 ? ", world!" : null;
+
+    public static void main(String[] args) {
+      System.out.print(staticField);
+      System.out.println(new Main().instanceField);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java b/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
deleted file mode 100644
index f400169..0000000
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.cf;
-
-import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.ClassWriter;
-import org.objectweb.asm.Handle;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.Type;
-
-// The method MethodHandleDump.transform() translates methods in MethodHandleTest that look like
-//     MethodType.methodType(TYPES)
-// and
-//     MethodHandles.lookup().findKIND(FOO.class, "METHOD", TYPE)
-// into LDC instructions.
-// This is necessary since there is no Java syntax that compiles to
-// LDC of a constant method handle or constant method type.
-//
-// The method dumpD() dumps a class equivalent to MethodHandleTest.D
-// that uses an LDC instruction instead of MethodHandles.lookup().findSpecial().
-// The LDC instruction loads an InvokeSpecial constant method handle to a C method,
-// so this LDC instruction must be in a subclass of C, and not directly on MethodHandleTest.
-public class MethodHandleDump implements Opcodes {
-
-  private static final String cDesc = "com/android/tools/r8/cf/MethodHandleTest$C";
-  private static final String eDesc = "com/android/tools/r8/cf/MethodHandleTest$E";
-  private static final String fDesc = "com/android/tools/r8/cf/MethodHandleTest$F";
-  private static final String iDesc = "com/android/tools/r8/cf/MethodHandleTest$I";
-  private static final Type viType = Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE);
-  private static final Type jiType = Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE);
-  private static final Type vicType =
-      Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
-  private static final Type jicType =
-      Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
-  private static final Type veType = Type.getMethodType(Type.VOID_TYPE, Type.getObjectType(eDesc));
-  private static final Type fType = Type.getMethodType(Type.getObjectType(fDesc));
-  private static final String viDesc = viType.getDescriptor();
-  private static final String jiDesc = jiType.getDescriptor();
-  private static final String vicDesc = vicType.getDescriptor();
-  private static final String jicDesc = jicType.getDescriptor();
-  private static final String intDesc = Type.INT_TYPE.getDescriptor();
-
-  public static byte[] transform(byte[] input) throws Exception {
-    ImmutableMap.Builder<String, Type> typesBuilder = ImmutableMap.builder();
-    ImmutableMap<String, Type> types =
-        typesBuilder
-            .put("viType", viType)
-            .put("jiType", jiType)
-            .put("vicType", vicType)
-            .put("jicType", jicType)
-            .put("veType", veType)
-            .put("fType", fType)
-            .build();
-
-    Builder<String, Handle> methodsBuilder = ImmutableMap.builder();
-    methodsBuilder
-        .put("scviMethod", new Handle(H_INVOKESTATIC, cDesc, "svi", viDesc, false))
-        .put("scjiMethod", new Handle(H_INVOKESTATIC, cDesc, "sji", jiDesc, false))
-        .put("scvicMethod", new Handle(H_INVOKESTATIC, cDesc, "svic", vicDesc, false))
-        .put("scjicMethod", new Handle(H_INVOKESTATIC, cDesc, "sjic", jicDesc, false))
-        .put("vcviMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvi", viDesc, false))
-        .put("vcjiMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vji", jiDesc, false))
-        .put("vcvicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvic", vicDesc, false))
-        .put("vcjicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vjic", jicDesc, false))
-        .put("siviMethod", new Handle(H_INVOKESTATIC, iDesc, "svi", viDesc, true))
-        .put("sijiMethod", new Handle(H_INVOKESTATIC, iDesc, "sji", jiDesc, true))
-        .put("sivicMethod", new Handle(H_INVOKESTATIC, iDesc, "svic", vicDesc, true))
-        .put("sijicMethod", new Handle(H_INVOKESTATIC, iDesc, "sjic", jicDesc, true))
-        .put("diviMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvi", viDesc, true))
-        .put("dijiMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dji", jiDesc, true))
-        .put("divicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvic", vicDesc, true))
-        .put("dijicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "djic", jicDesc, true))
-        .put("vciSetField", new Handle(H_PUTFIELD, cDesc, "vi", intDesc, false))
-        .put("sciSetField", new Handle(H_PUTSTATIC, cDesc, "si", intDesc, false))
-        .put("vciGetField", new Handle(H_GETFIELD, cDesc, "vi", intDesc, false))
-        .put("sciGetField", new Handle(H_GETSTATIC, cDesc, "si", intDesc, false))
-        .put("iiSetField", new Handle(H_PUTSTATIC, iDesc, "ii", intDesc, true))
-        .put("iiGetField", new Handle(H_GETSTATIC, iDesc, "ii", intDesc, true))
-        .put("constructorMethod", new Handle(H_NEWINVOKESPECIAL, cDesc, "<init>", viDesc, false));
-    ImmutableMap<String, Handle> methods = methodsBuilder.build();
-    ClassReader cr = new ClassReader(input);
-    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
-    cr.accept(
-        new ClassVisitor(InternalOptions.ASM_VERSION, cw) {
-
-          @Override
-          public MethodVisitor visitMethod(
-              int access, String name, String desc, String signature, String[] exceptions) {
-            switch (desc) {
-              case "()Ljava/lang/invoke/MethodType;":
-                {
-                  Type type = types.get(name);
-                  assert type != null : name;
-                  assert access == ACC_PUBLIC + ACC_STATIC;
-                  assert signature == null;
-                  assert exceptions == null;
-                  MethodVisitor mv = cw.visitMethod(access, name, desc, null, null);
-                  mv.visitCode();
-                  mv.visitLdcInsn(type);
-                  mv.visitInsn(ARETURN);
-                  mv.visitMaxs(-1, -1);
-                  mv.visitEnd();
-                  return null;
-                }
-              case "()Ljava/lang/invoke/MethodHandle;":
-                {
-                  Handle method = methods.get(name);
-                  assert access == ACC_PUBLIC + ACC_STATIC;
-                  assert method != null : name;
-                  assert signature == null;
-                  assert exceptions == null;
-                  MethodVisitor mv = cw.visitMethod(access, name, desc, null, null);
-                  mv.visitCode();
-                  mv.visitLdcInsn(method);
-                  mv.visitInsn(ARETURN);
-                  mv.visitMaxs(-1, -1);
-                  mv.visitEnd();
-                  return null;
-                }
-              default:
-                return super.visitMethod(access, name, desc, signature, exceptions);
-            }
-          }
-        },
-        0);
-    return cw.toByteArray();
-  }
-
-  public static byte[] dumpD() throws Exception {
-
-    ClassWriter cw = new ClassWriter(0);
-    MethodVisitor mv;
-
-    cw.visit(
-        V1_8,
-        ACC_PUBLIC + ACC_SUPER,
-        "com/android/tools/r8/cf/MethodHandleTest$D",
-        null,
-        "com/android/tools/r8/cf/MethodHandleTest$C",
-        null);
-
-    cw.visitInnerClass(
-        "com/android/tools/r8/cf/MethodHandleTest$D",
-        "com/android/tools/r8/cf/MethodHandleTest",
-        "D",
-        ACC_PUBLIC + ACC_STATIC);
-
-    cw.visitInnerClass(
-        "com/android/tools/r8/cf/MethodHandleTest$C",
-        "com/android/tools/r8/cf/MethodHandleTest",
-        "C",
-        ACC_PUBLIC + ACC_STATIC);
-
-    cw.visitInnerClass(
-        "java/lang/invoke/MethodHandles$Lookup",
-        "java/lang/invoke/MethodHandles",
-        "Lookup",
-        ACC_PUBLIC + ACC_FINAL + ACC_STATIC);
-
-    {
-      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
-      mv.visitCode();
-      mv.visitVarInsn(ALOAD, 0);
-      mv.visitMethodInsn(
-          INVOKESPECIAL, "com/android/tools/r8/cf/MethodHandleTest$C", "<init>", "()V", false);
-      mv.visitInsn(RETURN);
-      mv.visitMaxs(1, 1);
-      mv.visitEnd();
-    }
-    {
-      mv =
-          cw.visitMethod(
-              ACC_PUBLIC + ACC_STATIC,
-              "vcviSpecialMethod",
-              "()Ljava/lang/invoke/MethodHandle;",
-              null,
-              null);
-      mv.visitCode();
-      mv.visitLdcInsn(new Handle(H_INVOKESPECIAL, cDesc, "vvi", viDesc, false));
-      mv.visitInsn(ARETURN);
-      mv.visitMaxs(-1, -1);
-      mv.visitEnd();
-    }
-    {
-      mv = cw.visitMethod(ACC_PUBLIC, "vvi", "(I)V", null, null);
-      mv.visitCode();
-      mv.visitInsn(RETURN);
-      mv.visitMaxs(0, 2);
-      mv.visitEnd();
-    }
-    cw.visitEnd();
-
-    return cw.toByteArray();
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
deleted file mode 100644
index cc9b61a..0000000
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.cf;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.ClassFileConsumer;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.NoVerticalClassMerging;
-import com.android.tools.r8.ProgramConsumer;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.cf.MethodHandleTest.C;
-import com.android.tools.r8.cf.MethodHandleTest.I;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-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 MethodHandleTestRunner extends TestBase {
-  static final Class<?> CLASS = MethodHandleTest.class;
-
-  enum LookupType {
-    DYNAMIC,
-    CONSTANT,
-  }
-
-  enum MinifyMode {
-    NONE,
-    MINIFY,
-  }
-
-  private CompilationMode compilationMode;
-  private LookupType lookupType;
-  private ProcessResult runInput;
-  private MinifyMode minifyMode;
-
-  @Parameters(name = "{0}_{1}_{2}")
-  public static List<String[]> data() {
-    List<String[]> res = new ArrayList<>();
-    for (LookupType lookupType : LookupType.values()) {
-      for (MinifyMode minifyMode : MinifyMode.values()) {
-        if (lookupType == LookupType.DYNAMIC && minifyMode == MinifyMode.MINIFY) {
-          // Skip because we don't keep the members looked up dynamically.
-          continue;
-        }
-        for (CompilationMode compilationMode : CompilationMode.values()) {
-          res.add(new String[] {lookupType.name(), minifyMode.name(), compilationMode.name()});
-        }
-      }
-    }
-    return res;
-  }
-
-  public MethodHandleTestRunner(String lookupType, String minifyMode, String compilationMode) {
-    this.lookupType = LookupType.valueOf(lookupType);
-    this.minifyMode = MinifyMode.valueOf(minifyMode);
-    this.compilationMode = CompilationMode.valueOf(compilationMode);
-  }
-
-  @Test
-  public void test() throws Exception {
-    runInput();
-    runCf();
-    // TODO(mathiasr): Once we include a P runtime, change this to "P and above".
-    if (ToolHelper.getDexVm() == DexVm.ART_DEFAULT && ToolHelper.artSupported()) {
-      runDex();
-    }
-  }
-
-  private final Class<?>[] inputClasses = {
-    MethodHandleTest.class,
-    MethodHandleTest.C.class,
-    MethodHandleTest.I.class,
-    MethodHandleTest.Impl.class,
-    MethodHandleTest.D.class,
-    MethodHandleTest.E.class,
-    MethodHandleTest.F.class,
-    NoVerticalClassMerging.class
-  };
-
-  private void runInput() throws Exception {
-    Path out = temp.getRoot().toPath().resolve("input.jar");
-    ClassFileConsumer.ArchiveConsumer archiveConsumer = new ClassFileConsumer.ArchiveConsumer(out);
-    for (Class<?> c : inputClasses) {
-      archiveConsumer.accept(
-          ByteDataView.of(getClassAsBytes(c)),
-          DescriptorUtils.javaTypeToDescriptor(c.getName()),
-          null);
-    }
-    archiveConsumer.finished(null);
-    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
-    runInput = ToolHelper.runJava(out, CLASS.getName(), expected);
-    if (runInput.exitCode != 0) {
-      System.out.println(runInput);
-    }
-    assertEquals(0, runInput.exitCode);
-  }
-
-  private void runCf() throws Exception {
-    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
-    build(new ClassFileConsumer.ArchiveConsumer(outCf));
-    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
-    ProcessResult runCf = ToolHelper.runJava(outCf, CLASS.getCanonicalName(), expected);
-    assertEquals(runCf.stderr, 0, runCf.exitCode);
-    assertEquals(runInput.toString(), runCf.toString());
-    // Ensure that we did not inline the const method handle
-    ensureConstHandleNotInlined(outCf);
-  }
-
-  private void ensureConstHandleNotInlined(Path file) throws IOException, ExecutionException {
-    CodeInspector inspector = new CodeInspector(file);
-    MethodSubject subject = inspector.clazz(MethodHandleTest.D.class).method(
-        "java.lang.MethodHandle", "vcviSpecialMethod");
-    assertTrue(inspector.clazz(MethodHandleTest.D.class)
-        .method("java.lang.invoke.MethodHandle", "vcviSpecialMethod").isPresent());
-  }
-
-  private void runDex() throws Exception {
-    Path outDex = temp.getRoot().toPath().resolve("dex.zip");
-    build(new DexIndexedConsumer.ArchiveConsumer(outDex));
-    String expected = lookupType == LookupType.CONSTANT ? "pass" : "exception";
-    ProcessResult runDex =
-        ToolHelper.runArtRaw(
-            outDex.toString(),
-            CLASS.getCanonicalName(),
-            cmd -> cmd.appendProgramArgument(expected));
-    // Only compare stdout and exitCode since dex2oat prints to stderr.
-    if (runInput.exitCode != runDex.exitCode) {
-      System.out.println(runDex.stderr);
-    }
-    assertEquals(runInput.exitCode, runDex.exitCode);
-    assertEquals(runInput.stdout, runDex.stdout);
-  }
-
-  private void build(ProgramConsumer programConsumer) throws Exception {
-    // MethodHandle.invoke() only supported from Android O
-    // ConstMethodHandle only supported from Android P
-    Builder builder =
-        R8Command.builder()
-            .setMode(compilationMode)
-            .setProgramConsumer(programConsumer)
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true);
-    if (programConsumer instanceof ClassFileConsumer) {
-      builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
-    } else {
-      AndroidApiLevel apiLevel = AndroidApiLevel.P;
-      builder
-          .setMinApiLevel(apiLevel.getLevel())
-          .addLibraryFiles(ToolHelper.getAndroidJar(apiLevel));
-    }
-    for (Class<?> c : inputClasses) {
-      byte[] classAsBytes = getClassAsBytes(c);
-      builder.addClassProgramData(classAsBytes, Origin.unknown());
-    }
-    if (minifyMode == MinifyMode.MINIFY) {
-      ToolHelper.allowTestProguardOptions(builder);
-      builder.addProguardConfiguration(
-          Arrays.asList(
-              keepMainProguardConfiguration(MethodHandleTest.class),
-              noVerticalClassMergingRule(),
-              // Prevent the second argument of C.svic(), C.sjic(), I.sjic() and I.svic() from
-              // being removed although they are never used unused. This is needed since these
-              // methods are accessed reflectively.
-              "-keep,allowobfuscation public class " + C.class.getTypeName() + " {",
-              "  static void svic(int, char);",
-              "  static long sjic(int, char);",
-              "}",
-              "-keep,allowobfuscation public interface " + I.class.getTypeName() + " {",
-              "  static long sjic(int, char);",
-              "  static void svic(int, char);",
-              "}"),
-          Origin.unknown());
-    }
-    ToolHelper.runR8(builder.build());
-  }
-
-  private byte[] getClassAsBytes(Class<?> clazz) throws Exception {
-    if (lookupType == LookupType.CONSTANT) {
-      if (clazz == MethodHandleTest.D.class) {
-        return MethodHandleDump.dumpD();
-      } else if (clazz == MethodHandleTest.class) {
-        return MethodHandleDump.transform(ToolHelper.getClassAsBytes(clazz));
-      }
-    }
-    return ToolHelper.getClassAsBytes(clazz);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java
new file mode 100644
index 0000000..889fd91
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.references.Reference.classFromClass;
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Test that unrepresentable MethodHandle invokes are replaced by throwing instructions. See
+ * b/174733673.
+ */
+@RunWith(Parameterized.class)
+public class InvokeMethodHandleRuntimeErrorTest extends TestBase {
+
+  private static final String EXPECTED = StringUtils.lines("I.target");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InvokeMethodHandleRuntimeErrorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasCompileSupport() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(Main.class, I.class, Super.class)
+        .addProgramClassFileData(getInvokeCustomTransform())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8()
+        .addProgramClasses(Main.class, I.class, Super.class)
+        .addProgramClassFileData(getInvokeCustomTransform())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (hasCompileSupport()) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            hasCompileSupport(),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("const-method-handle")));
+  }
+
+  private static byte[] getInvokeCustomTransform() throws Throwable {
+    ClassReference symbolicHolder = classFromClass(InvokeCustom.class);
+    MethodReference method = methodFromMethod(InvokeCustom.class.getMethod("target"));
+    return transformer(InvokeCustom.class)
+        .transformMethodInsnInMethod(
+            "test",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              // Replace null argument by a const method handle.
+              visitor.visitInsn(Opcodes.POP);
+              visitor.visitLdcInsn(
+                  new Handle(
+                      Opcodes.H_INVOKEVIRTUAL,
+                      symbolicHolder.getBinaryName(),
+                      method.getMethodName(),
+                      method.getMethodDescriptor(),
+                      false));
+              visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+            })
+        .transform();
+  }
+
+  interface I {
+    default void target() {
+      System.out.println("I.target");
+    }
+  }
+
+  static class Super implements I {}
+
+  static class InvokeCustom extends Super {
+
+    public static void doInvoke(MethodHandle handle) throws Throwable {
+      handle.invoke(new InvokeCustom());
+    }
+
+    public static void test() throws Throwable {
+      doInvoke(null /* will be const method handle */);
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) throws Throwable {
+      InvokeCustom.test();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java
new file mode 100644
index 0000000..4881866
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf.methodhandles;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.google.common.collect.ImmutableMap;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// The method MethodHandleDump.transform() translates methods in MethodHandleTest that look like
+//     MethodType.methodType(TYPES)
+// and
+//     MethodHandles.lookup().findKIND(FOO.class, "METHOD", TYPE)
+// into LDC instructions.
+// This is necessary since there is no Java syntax that compiles to
+// LDC of a constant method handle or constant method type.
+//
+// The method dumpD() dumps a class equivalent to MethodHandleTest.D
+// that uses an LDC instruction instead of MethodHandles.lookup().findSpecial().
+// The LDC instruction loads an InvokeSpecial constant method handle to a C method,
+// so this LDC instruction must be in a subclass of C, and not directly on MethodHandleTest.
+public class MethodHandleDump implements Opcodes {
+
+  private static final String cDesc = TestBase.binaryName(MethodHandleTest.C.class);
+  private static final String eDesc = TestBase.binaryName(MethodHandleTest.E.class);
+  private static final String fDesc = TestBase.binaryName(MethodHandleTest.F.class);
+  private static final String iDesc = TestBase.binaryName(MethodHandleTest.I.class);
+  private static final Type viType = Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE);
+  private static final Type jiType = Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE);
+  private static final Type vicType =
+      Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
+  private static final Type jicType =
+      Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
+  private static final Type veType = Type.getMethodType(Type.VOID_TYPE, Type.getObjectType(eDesc));
+  private static final Type fType = Type.getMethodType(Type.getObjectType(fDesc));
+  private static final String viDesc = viType.getDescriptor();
+  private static final String jiDesc = jiType.getDescriptor();
+  private static final String vicDesc = vicType.getDescriptor();
+  private static final String jicDesc = jicType.getDescriptor();
+
+  public static byte[] getTransformedClass() throws Exception {
+    ImmutableMap<String, Type> types =
+        ImmutableMap.<String, Type>builder()
+            .put("viType", viType)
+            .put("jiType", jiType)
+            .put("vicType", vicType)
+            .put("jicType", jicType)
+            .put("veType", veType)
+            .put("fType", fType)
+            .build();
+
+    ImmutableMap<String, Handle> methods =
+        ImmutableMap.<String, Handle>builder()
+            .put("scviMethod", new Handle(H_INVOKESTATIC, cDesc, "svi", viDesc, false))
+            .put("scjiMethod", new Handle(H_INVOKESTATIC, cDesc, "sji", jiDesc, false))
+            .put("scvicMethod", new Handle(H_INVOKESTATIC, cDesc, "svic", vicDesc, false))
+            .put("scjicMethod", new Handle(H_INVOKESTATIC, cDesc, "sjic", jicDesc, false))
+            .put("vcviMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvi", viDesc, false))
+            .put("vcjiMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vji", jiDesc, false))
+            .put("vcvicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvic", vicDesc, false))
+            .put("vcjicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vjic", jicDesc, false))
+            .put("siviMethod", new Handle(H_INVOKESTATIC, iDesc, "svi", viDesc, true))
+            .put("sijiMethod", new Handle(H_INVOKESTATIC, iDesc, "sji", jiDesc, true))
+            .put("sivicMethod", new Handle(H_INVOKESTATIC, iDesc, "svic", vicDesc, true))
+            .put("sijicMethod", new Handle(H_INVOKESTATIC, iDesc, "sjic", jicDesc, true))
+            .put("diviMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvi", viDesc, true))
+            .put("dijiMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dji", jiDesc, true))
+            .put("divicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvic", vicDesc, true))
+            .put("dijicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "djic", jicDesc, true))
+            .put(
+                "constructorMethod", new Handle(H_NEWINVOKESPECIAL, cDesc, "<init>", viDesc, false))
+            .build();
+
+    return ClassFileTransformer.create(MethodHandleTest.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access, String name, String desc, String signature, String[] exceptions) {
+                switch (desc) {
+                  case "()Ljava/lang/invoke/MethodType;":
+                    {
+                      Type type = types.get(name);
+                      assert type != null : name;
+                      assert access == ACC_PUBLIC + ACC_STATIC;
+                      assert signature == null;
+                      assert exceptions == null;
+                      MethodVisitor mv = super.visitMethod(access, name, desc, null, null);
+                      mv.visitCode();
+                      mv.visitLdcInsn(type);
+                      mv.visitInsn(ARETURN);
+                      mv.visitMaxs(-1, -1);
+                      mv.visitEnd();
+                      return null;
+                    }
+                  case "()Ljava/lang/invoke/MethodHandle;":
+                    {
+                      Handle method = methods.get(name);
+                      assert access == ACC_PUBLIC + ACC_STATIC;
+                      assert method != null : name;
+                      assert signature == null;
+                      assert exceptions == null;
+                      MethodVisitor mv = super.visitMethod(access, name, desc, null, null);
+                      mv.visitCode();
+                      mv.visitLdcInsn(method);
+                      mv.visitInsn(ARETURN);
+                      mv.visitMaxs(-1, -1);
+                      mv.visitEnd();
+                      return null;
+                    }
+                  default:
+                    return super.visitMethod(access, name, desc, signature, exceptions);
+                }
+              }
+            })
+        .transform();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
similarity index 72%
rename from src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
rename to src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
index 690dd6b..497e19f 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.cf;
+package com.android.tools.r8.cf.methodhandles;
 
 import com.android.tools.r8.NoVerticalClassMerging;
 import java.lang.invoke.MethodHandle;
@@ -16,13 +16,6 @@
       System.out.println("C " + i);
     }
 
-    public C() {
-      System.out.println("C");
-    }
-
-    public int vi;
-    public static int si;
-
     public static void svi(int i) {
       System.out.println("svi " + i);
     }
@@ -60,20 +53,6 @@
     }
   }
 
-  public static class D extends C {
-    public static MethodHandle vcviSpecialMethod() {
-      try {
-        return MethodHandles.lookup().findSpecial(C.class, "vvi", viType(), D.class);
-      } catch (Exception e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    public void vvi(int i) {
-      // Overridden to output nothing.
-    }
-  }
-
   public static class E {
     // Class that is only mentioned in parameter list of LDC(MethodType)-instruction.
   }
@@ -84,7 +63,6 @@
 
   @NoVerticalClassMerging
   public interface I {
-    int ii = 42;
 
     static void svi(int i) {
       System.out.println("svi " + i);
@@ -128,7 +106,6 @@
   public static void main(String[] args) {
     // When MethodHandleTestRunner invokes this program with the JVM, "fail" is passed as arg.
     // When invoked with Art, no arg is passed since interface fields may be modified on Art.
-    String expectedResult = args[0];
     C c = new C(42);
     I i = new Impl();
     try {
@@ -148,33 +125,6 @@
       assertEquals(42L, (long) dijiMethod().invoke(i, 14));
       divicMethod().invoke(i, 15, 'x');
       assertEquals(42L, (long) dijicMethod().invoke(i, 16, 'x'));
-      vciSetField().invoke(c, 17);
-      assertEquals(17, (int) vciGetField().invoke(c));
-      sciSetField().invoke(18);
-      assertEquals(18, (int) sciGetField().invoke());
-      String interfaceSetResult;
-      try {
-        iiSetField().invoke(19);
-        interfaceSetResult = "pass";
-      } catch (RuntimeException e) {
-        if (e.getCause() instanceof IllegalAccessException) {
-          interfaceSetResult = "exception";
-        } else {
-          throw e;
-        }
-      } catch (IllegalAccessError e) {
-        interfaceSetResult = "error";
-      }
-      if (!interfaceSetResult.equals(expectedResult)) {
-        throw new RuntimeException(
-            "Wrong outcome of iiSetField().invoke(): Expected "
-                + expectedResult
-                + " but got "
-                + interfaceSetResult);
-      }
-      assertEquals(interfaceSetResult.equals("pass") ? 19 : 42, (int) iiGetField().invoke());
-      MethodHandle methodHandle = D.vcviSpecialMethod();
-      methodHandle.invoke(new D(), 20);
       constructorMethod().invoke(21);
       System.out.println(veType().parameterType(0).getName().lastIndexOf('.'));
       System.out.println(fType().returnType().getName().lastIndexOf('.'));
@@ -189,12 +139,6 @@
     }
   }
 
-  private static void assertEquals(int l, int x) {
-    if (l != x) {
-      throw new AssertionError("Not equal: " + l + " != " + x);
-    }
-  }
-
   public static MethodType viType() {
     return MethodType.methodType(void.class, int.class);
   }
@@ -347,54 +291,6 @@
     }
   }
 
-  public static MethodHandle vciSetField() {
-    try {
-      return MethodHandles.lookup().findSetter(C.class, "vi", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static MethodHandle sciSetField() {
-    try {
-      return MethodHandles.lookup().findStaticSetter(C.class, "si", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static MethodHandle vciGetField() {
-    try {
-      return MethodHandles.lookup().findGetter(C.class, "vi", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static MethodHandle sciGetField() {
-    try {
-      return MethodHandles.lookup().findStaticGetter(C.class, "si", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static MethodHandle iiSetField() {
-    try {
-      return MethodHandles.lookup().findStaticSetter(I.class, "ii", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static MethodHandle iiGetField() {
-    try {
-      return MethodHandles.lookup().findStaticGetter(I.class, "ii", int.class);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   public static MethodHandle constructorMethod() {
     try {
       return MethodHandles.lookup().findConstructor(C.class, viType());
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java
new file mode 100644
index 0000000..1812854
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java
@@ -0,0 +1,207 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+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.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.C;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.E;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.F;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.I;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.Impl;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import java.util.ArrayList;
+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 MethodHandleTestRunner extends TestBase {
+  static final Class<?> CLASS = MethodHandleTest.class;
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  enum MinifyMode {
+    NONE,
+    MINIFY,
+  }
+
+  private String getExpected() {
+    return StringUtils.lines(
+        "C 42", "svi 1", "sji 2", "svic 3", "sjic 4", "vvi 5", "vji 6", "vvic 7", "vjic 8", "svi 9",
+        "sji 10", "svic 11", "sjic 12", "dvi 13", "dji 14", "dvic 15", "djic 16", "C 21", "37",
+        "37");
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+  private final MinifyMode minifyMode;
+
+  @Parameters(name = "{0}, lookup:{1}, minify:{2}")
+  public static List<Object[]> data() {
+    List<Object[]> res = new ArrayList<>();
+    for (TestParameters params :
+        TestParameters.builder()
+            .withCfRuntimes()
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            // .withApiLevelsStartingAtIncluding(AndroidApiLevel.P)
+            .withAllApiLevels()
+            .build()) {
+      for (LookupType lookupType : LookupType.values()) {
+        for (MinifyMode minifyMode : MinifyMode.values()) {
+          if (lookupType == LookupType.DYNAMIC && minifyMode == MinifyMode.MINIFY) {
+            // Skip because we don't keep the members looked up dynamically.
+            continue;
+          }
+          res.add(new Object[] {params, lookupType.name(), minifyMode.name()});
+        }
+      }
+    }
+    return res;
+  }
+
+  public MethodHandleTestRunner(TestParameters parameters, String lookupType, String minifyMode) {
+    this.parameters = parameters;
+    this.lookupType = LookupType.valueOf(lookupType);
+    this.minifyMode = MinifyMode.valueOf(minifyMode);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(getInputClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), CLASS.getName())
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() && minifyMode == MinifyMode.NONE);
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClasses(getInputClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), CLASS.getName())
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestBuilder<?> builder =
+        testForR8(parameters.getBackend())
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClasses(getInputClasses())
+            .addProgramClassFileData(getTransformedClasses())
+            .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+            .addNoVerticalClassMergingAnnotations();
+    if (minifyMode == MinifyMode.MINIFY) {
+      builder
+          .enableProguardTestOptions()
+          .addKeepMainRule(MethodHandleTest.class)
+          .addKeepRules(
+              // Prevent the second argument of C.svic(), C.sjic(), I.sjic() and I.svic() from
+              // being removed although they are never used unused. This is needed since these
+              // methods are accessed reflectively.
+              "-keep,allowobfuscation public class " + typeName(C.class) + " {",
+              "  static void svic(int, char);",
+              "  static long sjic(int, char);",
+              "}",
+              "-keep,allowobfuscation public interface " + typeName(I.class) + " {",
+              "  static long sjic(int, char);",
+              "  static void svic(int, char);",
+              "}");
+      // TODO(b/235810300): The compiler fails with assertion in AppInfoWithLiveness.
+      if (lookupType == LookupType.CONSTANT && hasConstMethodCompileSupport()) {
+        builder.allowDiagnosticMessages();
+        assertThrows(CompilationFailedException.class, builder::compile);
+        return;
+      }
+    } else {
+      builder.noTreeShaking();
+      builder.noMinification();
+    }
+    builder
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), CLASS.getCanonicalName())
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport())) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport()) {
+      result
+          .assertFailureWithErrorThatThrows(RuntimeException.class)
+          .assertStderrMatches(containsString("invoke-polymorphic"));
+      return;
+    }
+    if (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      result
+          .assertFailureWithErrorThatThrows(RuntimeException.class)
+          .assertStderrMatches(containsString("const-method-handle"));
+      return;
+    }
+    result.assertSuccessWithOutput(getExpected());
+  }
+
+  private List<Class<?>> getInputClasses() {
+    Builder<Class<?>> builder =
+        ImmutableList.<Class<?>>builder().add(C.class, I.class, Impl.class, E.class, F.class);
+    if (lookupType == LookupType.DYNAMIC) {
+      builder.add(MethodHandleTest.class);
+    }
+    return builder.build();
+  }
+
+  private List<byte[]> getTransformedClasses() throws Exception {
+    if (lookupType == LookupType.DYNAMIC) {
+      return ImmutableList.of();
+    }
+    return ImmutableList.of(MethodHandleDump.getTransformedClass());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
new file mode 100644
index 0000000..ccb901d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
@@ -0,0 +1,137 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.examples.JavaExampleClassProxy;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Test for VarHandle (these are a refactoring of the old example test setup.) */
+@RunWith(Parameterized.class)
+public class VarHandleTest extends TestBase {
+
+  private static final String PKG = "varhandle";
+  private static final String EXAMPLE = "examplesJava9/" + PKG;
+  private final JavaExampleClassProxy MAIN =
+      new JavaExampleClassProxy(EXAMPLE, PKG + ".VarHandleTests");
+
+  private static final String EXPECTED = StringUtils.lines("true", "false");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimesStartingFromIncluding(CfVm.JDK9)
+        .withDexRuntimes()
+        .withAllApiLevels()
+        .build();
+  }
+
+  public VarHandleTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private boolean hasMethodHandlesClass() {
+    return parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O);
+  }
+
+  private boolean hasFindStaticVarHandleMethod() {
+    // API docs list this as present from T(33), but it was included from 28
+    return parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.P);
+  }
+
+  private boolean hasVarHandleInLibrary() {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.T);
+  }
+
+  public List<Path> getProgramInputs() {
+    return ImmutableList.of(JavaExampleClassProxy.examplesJar(EXAMPLE));
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(getProgramInputs())
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    assumeTrue(parameters.isDexRuntime());
+    assumeFalse(
+        "TODO(b/204855476): The default VM throws unsupported. Ignore it and reconsider for 8.0.0",
+        parameters.isDexRuntimeVersion(Version.DEFAULT));
+    testForD8()
+        .addProgramFiles(getProgramInputs())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (hasInvokePolymorphicCompileSupport()) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(
+                        diagnosticType(UnsupportedInvokePolymorphicMethodHandleDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .applyIf(
+            !hasMethodHandlesClass(),
+            r ->
+                r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class)
+                    .assertStderrMatches(containsString("java.lang.invoke.MethodHandles")),
+            !hasFindStaticVarHandleMethod(),
+            r ->
+                r.assertFailureWithErrorThatThrows(NoSuchMethodError.class)
+                    .assertStderrMatches(containsString("findStaticVarHandle")),
+            !hasInvokePolymorphicCompileSupport(),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("invoke-polymorphic")),
+            r -> r.assertSuccessWithOutput(EXPECTED));
+  }
+
+  @Test
+  public void testR8() throws Throwable {
+    // This just tests R8 on the targets where the program is fully supported.
+    assumeTrue(hasInvokePolymorphicCompileSupport() && hasFindStaticVarHandleMethod());
+    testForR8(parameters.getBackend())
+        .addProgramFiles(getProgramInputs())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(MAIN.typeName())
+        .applyIf(!hasVarHandleInLibrary(), b -> b.addDontWarn("java.lang.invoke.VarHandle"))
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java
new file mode 100644
index 0000000..0ccb1e8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.methodhandles.fields;
+
+// This is a top-level class.
+// The use of handles will check generics on C and fail if it cannot find the outer class.
+public class C {
+
+  public int vi;
+  public static int si;
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java
new file mode 100644
index 0000000..062f22e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java
@@ -0,0 +1,229 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.methodhandles.fields;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_GETFIELD;
+import static org.objectweb.asm.Opcodes.H_GETSTATIC;
+import static org.objectweb.asm.Opcodes.H_PUTFIELD;
+import static org.objectweb.asm.Opcodes.H_PUTSTATIC;
+
+import com.android.tools.r8.DiagnosticsMatcher;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableMap;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class ClassFieldMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder().withAllRuntimesAndApiLevels().build(), LookupType.values());
+  }
+
+  public ClassFieldMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(C.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private boolean hasMethodHandlesRuntimeSupport() {
+    return parameters.isCfRuntime()
+        || parameters
+            .asDexRuntime()
+            .maxSupportedApiLevel()
+            .isGreaterThanOrEqualTo(AndroidApiLevel.O);
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      diagnostics
+          .assertAllWarningsMatch(
+              DiagnosticsMatcher.diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (hasConstMethodCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (lookupType == LookupType.DYNAMIC && !hasMethodHandlesRuntimeSupport()) {
+      result.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    return StringUtils.lines("AOK");
+  }
+
+  byte[] getTransformedMain() throws Exception {
+    return transformer(Main.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.endsWith("Field")) {
+                  String fieldName = name.startsWith("sci") ? "si" : "vi";
+                  int type =
+                      ImmutableMap.<String, Integer>builder()
+                          .put("sciSetField", H_PUTSTATIC)
+                          .put("sciGetField", H_GETSTATIC)
+                          .put("vciSetField", H_PUTFIELD)
+                          .put("vciGetField", H_GETFIELD)
+                          .build()
+                          .get(name);
+                  mv.visitCode();
+                  mv.visitLdcInsn(new Handle(type, binaryName(C.class), fieldName, "I", false));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public static class Main {
+
+    public static MethodHandle vciSetField() {
+      try {
+        return MethodHandles.lookup().findSetter(C.class, "vi", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static MethodHandle vciGetField() {
+      try {
+        return MethodHandles.lookup().findGetter(C.class, "vi", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static MethodHandle sciSetField() {
+      try {
+        return MethodHandles.lookup().findStaticSetter(C.class, "si", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static MethodHandle sciGetField() {
+      try {
+        return MethodHandles.lookup().findStaticGetter(C.class, "si", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static void assertEquals(int x, int y) {
+      if (x != y) {
+        throw new AssertionError("failed!");
+      }
+    }
+
+    public static void main(String[] args) throws Throwable {
+      C c = new C();
+      vciSetField().invoke(c, 17);
+      assertEquals(17, (int) vciGetField().invoke(c));
+      sciSetField().invoke(18);
+      assertEquals(18, (int) sciGetField().invoke());
+      System.out.println("AOK");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java
new file mode 100644
index 0000000..3d62b40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java
@@ -0,0 +1,217 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.methodhandles.fields;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_GETSTATIC;
+import static org.objectweb.asm.Opcodes.H_PUTSTATIC;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class InterfaceFieldMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder()
+            // Runtimes without Handle APIs fail in various ways. Start testing beyond that point.
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            .withAllApiLevels()
+            .withCfRuntimes()
+            .build(),
+        LookupType.values());
+  }
+
+  public InterfaceFieldMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(I.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (parameters.isDexRuntimeVersion(Version.V13_0_0)
+        && lookupType == LookupType.CONSTANT
+        && hasConstMethodCompileSupport()) {
+      // TODO(b/235576668): VM 13 throws an escaping IAE outside the guarded range.
+      result
+          .assertFailureWithErrorThatThrows(IllegalAccessError.class)
+          .assertStderrMatches(containsString("Main.main"));
+      return;
+    }
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (hasConstMethodCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    if (lookupType == LookupType.CONSTANT && parameters.isDexRuntimeVersion(Version.V9_0_0)) {
+      // VM 9 will assign the value in the setter in contrast to RI.
+      return StringUtils.lines("42", "pass", "19");
+    }
+    return StringUtils.lines("42", lookupType == LookupType.DYNAMIC ? "exception" : "error", "42");
+  }
+
+  byte[] getTransformedMain() throws Exception {
+    return transformer(Main.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.endsWith("Field")) {
+                  int type = name.equals("iiSetField") ? H_PUTSTATIC : H_GETSTATIC;
+                  mv.visitCode();
+                  mv.visitLdcInsn(new Handle(type, binaryName(I.class), "ii", "I", true));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public interface I {
+    int ii = 42;
+  }
+
+  public static class Main {
+
+    public static MethodHandle iiSetField() {
+      try {
+        return MethodHandles.lookup().findStaticSetter(I.class, "ii", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static MethodHandle iiGetField() {
+      try {
+        return MethodHandles.lookup().findStaticGetter(I.class, "ii", int.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static void read() throws Throwable {
+      System.out.println(iiGetField().invoke());
+    }
+
+    public static void main(String[] args) throws Throwable {
+      read();
+      // Note: having the try-catch inlined here hits ART issue b/235576668.
+      try {
+        iiSetField().invoke(19);
+        System.out.println("pass");
+      } catch (IllegalAccessError e) {
+        System.out.println("error");
+      } catch (RuntimeException e) {
+        System.out.println("exception");
+      }
+      read();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
new file mode 100644
index 0000000..459ce08
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
@@ -0,0 +1,206 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.methodhandles.invokespecial;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_INVOKESPECIAL;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class InvokeSpecialMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder()
+            // Runtimes without Handle APIs fail in various ways. Start testing beyond that point.
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            .withAllApiLevels()
+            .withCfRuntimes()
+            .build(),
+        LookupType.values());
+  }
+
+  public InvokeSpecialMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(C.class, D.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport())) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (lookupType == LookupType.CONSTANT && hasConstMethodCompileSupport()) {
+      if (parameters.isDexRuntimeVersion(Version.V9_0_0)) {
+        // VM 9 incorrectly prints out the overridden method despite the direct target.
+        result.assertSuccessWithOutput("");
+      } else if (parameters.isDexRuntimeVersion(Version.V13_0_0)) {
+        // TODO(b/235807678): Subsequent ART VMs incorrectly throw IAE.
+        result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
+      } else if (parameters.isDexRuntime()
+          && parameters.asDexRuntime().getVersion().isNewerThan(Version.V9_0_0)) {
+        // VMs between 9 and 13 segfault.
+        result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+      } else {
+        result.assertSuccessWithOutput(getExpected());
+      }
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    return StringUtils.lines("vvi 20");
+  }
+
+  byte[] getTransformedD() throws Exception {
+    return transformer(D.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.equals("vcviSpecialMethod")) {
+                  mv.visitCode();
+                  mv.visitLdcInsn(
+                      new Handle(H_INVOKESPECIAL, binaryName(C.class), "vvi", "(I)V", false));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public static class C {
+
+    public void vvi(int i) {
+      System.out.println("vvi " + i);
+    }
+  }
+
+  public static class D extends C {
+
+    public void vvi(int i) {
+      // Overridden to output nothing.
+    }
+
+    public static MethodHandle vcviSpecialMethod() {
+      try {
+        return MethodHandles.lookup()
+            .findSpecial(C.class, "vvi", MethodType.methodType(void.class, int.class), D.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) throws Throwable {
+      MethodHandle methodHandle = D.vcviSpecialMethod();
+      methodHandle.invoke(new D(), 20);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/UnresolvableMethodWithClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/UnresolvableMethodWithClassMergingTest.java
new file mode 100644
index 0000000..c8a33ee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/UnresolvableMethodWithClassMergingTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+
+public class UnresolvableMethodWithClassMergingTest extends HorizontalClassMergingTestBase {
+
+  public UnresolvableMethodWithClassMergingTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, B.class)
+        .addKeepMainRule(Main.class)
+        .addDontWarn(Missing.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoOtherClassesMerged)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Missing.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B");
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      System.out.println(new A());
+      Missing.print(new B());
+    }
+  }
+
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+
+  static class B {
+
+    @Override
+    public String toString() {
+      return "B";
+    }
+  }
+
+  static class Missing {
+
+    static void print(B b) {
+      System.out.println(b);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java b/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java
new file mode 100644
index 0000000..cf34d5d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java
@@ -0,0 +1,198 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.code.invokedynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.CallSite;
+import java.lang.invoke.ConstantCallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Test that unrepresentable invoke-dynamic instructions are replaced by throwing instructions. See
+ * b/174733673.
+ */
+@RunWith(Parameterized.class)
+public class InvokeCustomRuntimeErrorTest extends TestBase {
+
+  private static final String EXPECTED = StringUtils.lines("A::foo");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InvokeCustomRuntimeErrorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasCompileSupport() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokeCustomSupport());
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8CfNoDesugaring() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    // Explicitly test that no-desugaring will maintain a passthrough of the CF code.
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setNoMinApi()
+        .disableDesugaring()
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8DexNoDesugaring() throws Throwable {
+    assumeTrue(parameters.isDexRuntime() && parameters.getApiLevel().equals(AndroidApiLevel.B));
+    // Explicitly test that no-desugaring will still strip instructions.
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setMinApi(parameters.getApiLevel())
+        .disableDesugaring()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedInvokeCustomDiagnostic.class))
+                    .assertOnlyWarnings())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertStderrMatches(containsString("invoke-dynamic"));
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    // For CF compilations we desugar to API level B, thus it should always fail.
+    AndroidApiLevel minApi =
+        parameters.isDexRuntime() ? parameters.getApiLevel() : AndroidApiLevel.B;
+    boolean expectedSuccess = parameters.isDexRuntime() && hasCompileSupport();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setMinApi(minApi)
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (expectedSuccess) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedInvokeCustomDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            expectedSuccess,
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("invoke-dynamic")));
+  }
+
+  private byte[] getTransformedTestClass() throws Exception {
+    ClassReference aClass = Reference.classFromClass(A.class);
+    MethodReference iFoo = Reference.methodFromMethod(I.class.getDeclaredMethod("foo"));
+    MethodReference bsm =
+        Reference.methodFromMethod(
+            TestClass.class.getDeclaredMethod(
+                "bsmCreateCallSite",
+                Lookup.class,
+                String.class,
+                MethodType.class,
+                MethodHandle.class));
+    return transformer(TestClass.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              if (name.equals("replaced")) {
+                visitor.visitInvokeDynamicInsn(
+                    iFoo.getMethodName(),
+                    "(" + aClass.getDescriptor() + ")V",
+                    new Handle(
+                        Opcodes.H_INVOKESTATIC,
+                        bsm.getHolderClass().getBinaryName(),
+                        bsm.getMethodName(),
+                        bsm.getMethodDescriptor(),
+                        false),
+                    new Handle(
+                        Opcodes.H_INVOKEVIRTUAL,
+                        aClass.getBinaryName(),
+                        iFoo.getMethodName(),
+                        iFoo.getMethodDescriptor(),
+                        false));
+              } else {
+                visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+              }
+            })
+        .transform();
+  }
+
+  public interface I {
+    void foo();
+  }
+
+  public static class A implements I {
+
+    @Override
+    public void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  static class TestClass {
+
+    public static CallSite bsmCreateCallSite(
+        MethodHandles.Lookup caller, String name, MethodType type, MethodHandle handle)
+        throws Throwable {
+      return new ConstantCallSite(handle);
+    }
+
+    public static void replaced(Object o) {
+      throw new RuntimeException("unreachable!");
+    }
+
+    public static void main(String[] args) {
+      replaced(new A());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
index d6eeb25..efcbe63 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
@@ -10,10 +10,19 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.compilerapi.CompilerApiTest;
 import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
-import com.android.tools.r8.errors.InvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodTypeDiagnostic;
+import com.android.tools.r8.errors.UnsupportedDefaultInterfaceMethodDiagnostic;
 import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicVarHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedPrivateInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedStaticInterfaceMethodDiagnostic;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import org.junit.Test;
 
@@ -30,12 +39,29 @@
 
   @Test
   public void test() throws Exception {
+    check(UnsupportedDefaultInterfaceMethodDiagnostic::new, "default-interface-method", 24);
+    check(UnsupportedStaticInterfaceMethodDiagnostic::new, "static-interface-method", 24);
+    check(UnsupportedPrivateInterfaceMethodDiagnostic::new, "private-interface-method", 24);
+    check(UnsupportedInvokeCustomDiagnostic::new, "invoke-custom", 26);
+    check(
+        UnsupportedInvokePolymorphicMethodHandleDiagnostic::new,
+        "invoke-polymorphic-method-handle",
+        26);
+    check(
+        UnsupportedInvokePolymorphicVarHandleDiagnostic::new, "invoke-polymorphic-var-handle", 28);
+    check(UnsupportedConstMethodHandleDiagnostic::new, "const-method-handle", 28);
+    check(UnsupportedConstMethodTypeDiagnostic::new, "const-method-type", 28);
+    check(UnsupportedConstDynamicDiagnostic::new, "const-dynamic", -1);
+  }
+
+  public void check(
+      BiFunction<Origin, Position, UnsupportedFeatureDiagnostic> makeFn,
+      String descriptor,
+      int level) {
     ApiTest test = new ApiTest(ApiTest.PARAMETERS);
     test.run(
-        new InvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN),
-        result -> {
-          assertEquals("invoke-custom @ 26", result);
-        });
+        makeFn.apply(Origin.unknown(), Position.UNKNOWN),
+        result -> assertEquals(descriptor + " @ " + level, result));
   }
 
   public static class ApiTest extends CompilerApiTest {
@@ -49,7 +75,16 @@
           new DiagnosticsHandler() {
             @Override
             public void warning(Diagnostic warning) {
-              if (warning instanceof UnsupportedFeatureDiagnostic) {
+              if (warning instanceof UnsupportedConstDynamicDiagnostic
+                  || warning instanceof UnsupportedConstMethodHandleDiagnostic
+                  || warning instanceof UnsupportedConstMethodTypeDiagnostic
+                  || warning instanceof UnsupportedDefaultInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedInvokeCustomDiagnostic
+                  || warning instanceof UnsupportedInvokePolymorphicMethodHandleDiagnostic
+                  || warning instanceof UnsupportedInvokePolymorphicVarHandleDiagnostic
+                  || warning instanceof UnsupportedPrivateInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedStaticInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedFeatureDiagnostic) {
                 UnsupportedFeatureDiagnostic unsupportedFeature =
                     (UnsupportedFeatureDiagnostic) warning;
                 String featureDescriptor = unsupportedFeature.getFeatureDescriptor();
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
index 48cbeaf..8c1d8f1 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
@@ -4,16 +4,23 @@
 
 package com.android.tools.r8.desugar.constantdynamic;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsLevel;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import org.junit.Test;
@@ -37,7 +44,7 @@
   @Test
   public void testReference() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
     assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForJvm()
@@ -46,33 +53,70 @@
         .assertSuccessWithOutputLines("null");
   }
 
-  @Test(expected = CompilationFailedException.class)
+  @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
 
     testForD8()
         .addProgramClassFileData(getTransformedMain())
         .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof UnsupportedFeatureDiagnostic
+                        || diagnostic instanceof ConstantDynamicDesugarDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
         .compileWithExpectedDiagnostics(
             diagnostics ->
-                diagnostics.assertErrorMessageThatMatches(
-                    containsString(
-                        "Unsupported dynamic constant (runtime provided bootstrap method)")));
+                diagnostics.assertWarningsMatch(
+                    diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                    allOf(
+                        diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                        diagnosticMessage(
+                            containsString(
+                                "Unsupported dynamic constant (runtime provided bootstrap"
+                                    + " method)")))))
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
+  // TODO(b/198142625): Support const-dynamic in IR CF/CF.
   @Test(expected = CompilationFailedException.class)
-  public void testR8() throws Exception {
-    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+  public void testR8Cf() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForR8(parameters.getBackend())
         .addProgramClassFileData(getTransformedMain())
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
+        .compile();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .allowDiagnosticWarningMessages()
+        .mapUnsupportedFeaturesToWarnings()
         .compileWithExpectedDiagnostics(
-            diagnostics ->
-                diagnostics.assertErrorMessageThatMatches(
-                    containsString(
-                        "Unsupported dynamic constant (runtime provided bootstrap method)")));
+            diagnostics -> {
+              if (parameters.isDexRuntime()) {
+                diagnostics.assertWarningsMatch(
+                    allOf(
+                        diagnosticType(UnsupportedFeatureDiagnostic.class),
+                        diagnosticMessage(containsString("const-dynamic"))));
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   private byte[] getTransformedMain() throws IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
index 1ae1ccf..87f3382 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
@@ -4,19 +4,23 @@
 package com.android.tools.r8.desugar.constantdynamic;
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
-import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
-import static com.android.tools.r8.OriginMatcher.hasParent;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
 import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.anyOf;
 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.DiagnosticsLevel;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -49,7 +53,7 @@
   @Test
   public void testReference() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
     assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForJvm()
@@ -59,65 +63,130 @@
   }
 
   @Test
+  public void testD8CfNoDesugar() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setNoMinApi()
+        .disableDesugaring()
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
   public void testD8Cf() throws Exception {
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForD8(Backend.CF)
-                .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      diagnostics.assertOnlyErrors();
-                      diagnostics.assertErrorsMatch(
-                          diagnosticMessage(
-                              containsString("Unsupported dynamic constant (different owner)")));
-                    }));
+    assumeTrue(parameters.isCfRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof ConstantDynamicDesugarDiagnostic
+                        || diagnostic instanceof UnsupportedFeatureDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics
+                    .assertAllWarningsMatch(
+                        anyOf(
+                            allOf(
+                                diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                                diagnosticMessage(containsString("const-dynamic"))),
+                            allOf(
+                                diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                                diagnosticMessage(
+                                    containsString(
+                                        "Unsupported dynamic constant (different owner)")))))
+                    .assertOnlyWarnings())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForD8(parameters.getBackend())
-                .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      diagnostics.assertOnlyErrors();
-                      diagnostics.assertErrorsMatch(
-                          allOf(
-                              diagnosticMessage(
-                                  containsString("Unsupported dynamic constant (different owner)")),
-                              diagnosticOrigin(hasParent(Origin.unknown()))));
-                    }));
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof ConstantDynamicDesugarDiagnostic
+                        || diagnostic instanceof UnsupportedFeatureDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics.assertOnlyWarnings();
+              diagnostics.assertAllWarningsMatch(
+                  anyOf(
+                      allOf(
+                          diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                          diagnosticMessage(containsString("const-dynamic"))),
+                      allOf(
+                          diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                          diagnosticMessage(
+                              containsString("Unsupported dynamic constant (different owner)")))));
+            })
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   @Test
-  public void testR8() throws Exception {
-    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
-
+  public void testR8Cf() {
+    assumeTrue(parameters.isCfRuntime() && parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
     assertThrows(
         CompilationFailedException.class,
         () ->
             testForR8(parameters.getBackend())
                 .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
                 .addKeepMainRule(MAIN_CLASS)
                 .compileWithExpectedDiagnostics(
                     diagnostics -> {
                       diagnostics.assertOnlyErrors();
                       diagnostics.assertErrorsMatch(
-                          allOf(
-                              diagnosticMessage(
-                                  containsString("Unsupported dynamic constant (different owner)")),
-                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                          diagnosticMessage(
+                              containsString("Unsupported dynamic constant (not desugaring)")));
                     }));
   }
 
+  @Test
+  public void testR8Dex() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics
+                  .assertAllWarningsMatch(
+                      anyOf(
+                          allOf(
+                              diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                              diagnosticMessage(containsString("const-dynamic"))),
+                          allOf(
+                              diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                              diagnosticMessage(
+                                  containsString(
+                                      "Unsupported dynamic constant (different owner)")))))
+                  .assertOnlyWarnings();
+            })
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
+  }
+
   private Collection<byte[]> getTransformedClasses() throws IOException {
     return ImmutableList.of(
         transformer(A.class)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDumpInputsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDumpInputsTest.java
index 6200ec5..1e475ce 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDumpInputsTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDumpInputsTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.ZipUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -58,7 +59,8 @@
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
         .addProgramClasses(TestClass.class)
         .addKeepMainRule(TestClass.class)
-        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .addOptionsModification(
+            options -> options.setDumpInputFlags(DumpInputFlags.dumpToDirectory(dumpDir)))
         .allowDiagnosticInfoMessages()
         .compile()
         .inspectDiagnosticMessages(
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordClasspathTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordClasspathTest.java
new file mode 100644
index 0000000..7c2ae96
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordClasspathTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * The test verifies records on classpath do not generate a record global synthetic on the program
+ * if the program does not refer to record.
+ */
+@RunWith(Parameterized.class)
+public class RecordClasspathTest extends TestBase {
+
+  private static final String RECORD_NAME_1 = "RecordWithMembers";
+  private static final byte[][] PROGRAM_DATA_1 = RecordTestUtils.getProgramData(RECORD_NAME_1);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Hello");
+
+  private final TestParameters parameters;
+
+  public RecordClasspathTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(CfVm.JDK17)
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClasses(TestClass.class)
+          .addClasspathClassFileData(PROGRAM_DATA_1)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addClasspathClassFileData(PROGRAM_DATA_1)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::assertNoRecord)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8DexPerFile() throws Exception {
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
+    Assume.assumeFalse(parameters.isCfRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addClasspathClassFileData(PROGRAM_DATA_1)
+        .setMinApi(parameters.getApiLevel())
+        .setIntermediate(true)
+        .setOutputMode(OutputMode.DexFilePerClassFile)
+        .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals))
+        .compile()
+        .inspect(this::assertNoRecord)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+    assertFalse(globals.hasGlobals());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(TestClass.class)
+            .addClasspathClassFileData(PROGRAM_DATA_1)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(TestClass.class);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(this::assertNoRecord)
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder.run(parameters.getRuntime(), TestClass.class).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private void assertNoRecord(CodeInspector inspector) {
+    // Verify that the record class was not added as part of the compilation.
+    assertEquals(1, inspector.allClasses().size());
+    assertTrue(inspector.clazz(TestClass.class).isPresent());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
index b77f5a7..0866830 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -10,10 +10,10 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -23,6 +23,7 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -73,15 +74,25 @@
 
   @Test
   public void testMergeDesugaredInputs() throws Exception {
-    // TODO(b/231598779): Records do not yet have contexts so per-file modes fail.
-    //  This test should be extended or duplicated to also test the pre-file-dex modes.
-    assumeTrue("b/230445931", parameters.isDexRuntime());
+    testMergeDesugaredInputsDexPerClass(false);
+  }
+
+  @Test
+  public void testMergeDesugaredInputsDexPerClass() throws Exception {
+    Assume.assumeTrue("CF is already run from the other test", parameters.isDexRuntime());
+    testMergeDesugaredInputsDexPerClass(true);
+  }
+
+  private void testMergeDesugaredInputsDexPerClass(boolean filePerClass) throws Exception {
     GlobalSyntheticsTestingConsumer globals1 = new GlobalSyntheticsTestingConsumer();
     Path output1 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_1)
             .setMinApi(parameters.getApiLevel())
             .setIntermediate(true)
+            .applyIf(
+                filePerClass && !parameters.isCfRuntime(),
+                b -> b.setOutputMode(OutputMode.DexFilePerClassFile))
             .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals1))
             .compile()
             .inspect(this::assertDoesNotHaveRecordTag)
@@ -93,6 +104,9 @@
             .addProgramClassFileData(PROGRAM_DATA_2)
             .setMinApi(parameters.getApiLevel())
             .setIntermediate(true)
+            .applyIf(
+                filePerClass && !parameters.isCfRuntime(),
+                b -> b.setOutputMode(OutputMode.DexFilePerClassFile))
             .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals2))
             .compile()
             .inspect(this::assertDoesNotHaveRecordTag)
@@ -107,8 +121,8 @@
             .apply(
                 b ->
                     b.getBuilder()
-                        .addGlobalSyntheticsResourceProviders(
-                            globals1.getIndexedModeProvider(), globals2.getIndexedModeProvider()))
+                        .addGlobalSyntheticsResourceProviders(globals1.getProviders())
+                        .addGlobalSyntheticsResourceProviders(globals2.getProviders()))
             .setMinApi(parameters.getApiLevel())
             .compile()
             .inspect(this::assertHasRecordTag);
diff --git a/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java b/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
index 05824cc..52aafa3 100644
--- a/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
+++ b/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
@@ -3,16 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dump;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.ZipUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -40,6 +45,28 @@
   }
 
   @Test
+  public void testDumpToFileOptionsModification() throws Exception {
+    Path dump = temp.newFolder().toPath().resolve("dump.zip");
+    try {
+      testForR8(parameters.getBackend())
+          .addProgramClasses(TestClass.class)
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+          .addKeepMainRule(TestClass.class)
+          .addOptionsModification(
+              options -> options.setDumpInputFlags(DumpInputFlags.dumpToFile(dump)))
+          .allowDiagnosticErrorMessages()
+          .compileWithExpectedDiagnostics(
+              diagnostics ->
+                  diagnostics.assertErrorsMatch(
+                      diagnosticMessage(containsString("Dumped compilation inputs to:"))));
+      fail("Expected compilation to fail");
+    } catch (CompilationFailedException e) {
+      // Expected.
+    }
+    verifyDump(dump, false, true);
+  }
+
+  @Test
   public void testDumpToFileSystemProperty() throws Exception {
     Path dump = temp.newFolder().toPath().resolve("dump.zip");
     try {
@@ -47,11 +74,35 @@
           .addJvmFlag("-Dcom.android.tools.r8.dumpinputtofile=" + dump)
           .addProgramClasses(TestClass.class)
           .compile();
+      fail("Expected external compilation to exit");
     } catch (AssertionError e) {
-      verifyDump(dump, false, true);
-      return;
+      // Expected.
     }
-    fail("Expected external compilation to exit");
+    verifyDump(dump, false, true);
+  }
+
+  @Test
+  public void testDumpToFileSystemPropertyWhenMinifying() throws Exception {
+    for (boolean minification : BooleanUtils.values()) {
+      Path dump = temp.newFolder().toPath().resolve("dump.zip");
+      try {
+        testForExternalR8(parameters.getBackend(), parameters.getRuntime())
+            .addJvmFlag("-Dcom.android.tools.r8.dumpinputtofile=" + dump)
+            .addJvmFlag("-Dcom.android.tools.r8.dump.filter.buildproperty.minification=^true$")
+            .addProgramClasses(TestClass.class)
+            .applyIf(!minification, builder -> builder.addKeepRules("-dontobfuscate"))
+            .compile();
+        // Without minification there should be no dump, and thus compilation should succeed.
+        assertFalse(minification);
+      } catch (AssertionError e) {
+        assertTrue(minification);
+      }
+      if (minification) {
+        verifyDump(dump, false, true);
+      } else {
+        assertFalse(Files.exists(dump));
+      }
+    }
   }
 
   @Test
@@ -70,7 +121,7 @@
   }
 
   @Test
-  public void testDumpToDirectorySystemProperty() throws Exception {
+  public void testDumpToDirectoryOptionsModification() throws Exception {
     Path dumpDir = temp.newFolder().toPath();
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class)
@@ -78,14 +129,28 @@
         // Ensure the compilation and run can actually succeed.
         .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
         .addKeepMainRule(TestClass.class)
-        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .addOptionsModification(
+            options -> options.setDumpInputFlags(DumpInputFlags.dumpToDirectory(dumpDir)))
         .allowDiagnosticInfoMessages()
         .compile()
         .assertAllInfoMessagesMatch(containsString("Dumped compilation inputs to:"))
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Hello, world");
+    verifyDumpDirectory(dumpDir, false, true);
+  }
 
-    verifyDumpDirectory(dumpDir, false, false);
+  @Test
+  public void testDumpToDirectorySystemProperty() throws Exception {
+    Path dumpDir = temp.newFolder().toPath();
+    testForExternalR8(parameters.getBackend(), parameters.getRuntime())
+        .addJvmFlag("-Dcom.android.tools.r8.dumpinputtodirectory=" + dumpDir.toString())
+        .addProgramClasses(TestClass.class)
+        // Setting a directory will allow compilation to continue.
+        // Ensure the compilation and run can actually succeed.
+        .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+        .addKeepMainRule(TestClass.class)
+        .compile();
+    verifyDumpDirectory(dumpDir, false, true);
   }
 
   @Test
@@ -95,7 +160,6 @@
         .dumpInputToDirectory(dumpDir.toString())
         .addProgramClasses(TestClass.class)
         .compile();
-
     verifyDumpDirectory(dumpDir, false, false);
   }
 
diff --git a/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java b/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java
index 1d01be7..f0372a8 100644
--- a/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java
+++ b/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
@@ -103,7 +104,8 @@
         .addMainDexListFiles(
             mainDexListForMainDexFile1AndMainDexFile2(), mainDexListForMainDexFile3())
         .addMainDexListClasses(MainDexClass1.class, MainDexClass2.class, TestClass.class)
-        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .addOptionsModification(
+            options -> options.setDumpInputFlags(DumpInputFlags.dumpToDirectory(dumpDir)))
         .compileWithExpectedDiagnostics(
             diagnostics ->
                 diagnostics
@@ -131,7 +133,8 @@
         .addInnerClasses(DumpMainDexInputsTest.class)
         .addMainDexRulesFiles(newMainDexRulesPath1(), newMainDexRulesPath2())
         .addMainDexKeepClassRules(MainDexClass1.class, MainDexClass2.class, TestClass.class)
-        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .addOptionsModification(
+            options -> options.setDumpInputFlags(DumpInputFlags.dumpToDirectory(dumpDir)))
         .compile()
         .assertAllInfoMessagesMatch(containsString("Dumped compilation inputs to:"))
         .run(parameters.getRuntime(), TestClass.class)
@@ -150,7 +153,8 @@
         .addInnerClasses(DumpMainDexInputsTest.class)
         .addMainDexRuleFiles(newMainDexRulesPath1(), newMainDexRulesPath2())
         .addMainDexKeepClassRules(MainDexClass1.class, MainDexClass2.class, TestClass.class)
-        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .addOptionsModification(
+            options -> options.setDumpInputFlags(DumpInputFlags.dumpToDirectory(dumpDir)))
         .addKeepAllClassesRule()
         .allowDiagnosticMessages()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FinalFieldWithMultipleWritesTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FinalFieldWithMultipleWritesTest.java
new file mode 100644
index 0000000..26fde45
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FinalFieldWithMultipleWritesTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.membervaluepropagation;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AccessFlags;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests the handling of final fields that are assigned more than once.
+ *
+ * <p>According to the JVM specification, fields that are declared final must never be assigned to
+ * after object construction, but the specification does not require that the field is assigned at
+ * most once.
+ */
+@RunWith(Parameterized.class)
+public class FinalFieldWithMultipleWritesTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(getTransformedClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "1", "2", "2");
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(getTransformedClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "1", "2", "2");
+  }
+
+  private static byte[] getTransformedClass() throws IOException, NoSuchFieldException {
+    return transformer(A.class)
+        .setAccessFlags(A.class.getDeclaredField("f"), AccessFlags::setFinal)
+        .transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A a = new A();
+      System.out.println(a.f);
+    }
+  }
+
+  static class A {
+
+    /* final */ int f;
+
+    A() {
+      System.out.println(this);
+      f = 1;
+      System.out.println(this);
+      f = 2;
+      System.out.println(this);
+    }
+
+    @Override
+    public String toString() {
+      return Integer.toString(f);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
index 8cc3b8c..0e8b0ad 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
@@ -46,7 +46,6 @@
   private void validateOutlining(CodeInspector inspector, Class<?> testClass, String argumentType) {
     boolean isStringBuilderOptimized =
         enableArgumentPropagation
-            && parameters.isDexRuntime()
             && (argumentType.equals("char") || argumentType.equals("boolean"));
     if (isStringBuilderOptimized) {
       assertEquals(1, inspector.allClasses().size());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderFullyInDoWhileLoopTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderFullyInDoWhileLoopTest.java
index 0074f01..5cc664d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderFullyInDoWhileLoopTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderFullyInDoWhileLoopTest.java
@@ -5,7 +5,7 @@
 
 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.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -45,11 +45,10 @@
   private void inspect(CodeInspector inspector) {
     MethodSubject mainMethodSubject = inspector.clazz(TestClass.class).mainMethod();
     assertThat(mainMethodSubject, isPresent());
-    assertEquals(
-        parameters.isCfRuntime(),
+    assertTrue(
         mainMethodSubject
             .streamInstructions()
-            .anyMatch(instruction -> instruction.isNewInstance("java.lang.StringBuilder")));
+            .noneMatch(instruction -> instruction.isNewInstance("java.lang.StringBuilder")));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
index 365ced3..e7c5301 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
@@ -37,7 +37,7 @@
   @Parameters(name = "{0}, configuration: {1}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withDexRuntimes().withAllApiLevels().build(), getTestExpectations());
+        getTestParameters().withAllRuntimesAndApiLevels().build(), getTestExpectations());
   }
 
   private static class StringBuilderResult {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
index 88f0cf8..92a4f9a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
@@ -42,7 +42,7 @@
                 inspector -> {
                   MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
                   assertEquals(
-                      parameters.isCfRuntime() ? 4 : 3,
+                      3,
                       mainMethodSubject
                           .streamInstructions()
                           .filter(isInvokeStringBuilderAppendWithString())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
index 81fa83b..14abc82 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
@@ -41,9 +41,8 @@
             .inspect(
                 inspector -> {
                   MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
-                  // TODO(b/114002137): Also run for CF
                   assertEquals(
-                      parameters.isCfRuntime() ? 4 : 3,
+                      3,
                       mainMethodSubject
                           .streamInstructions()
                           .filter(isInvokeStringBuilderAppendWithString())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithObjectsToStringTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithObjectsToStringTest.java
index 9b7594b..c89ae2e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithObjectsToStringTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithObjectsToStringTest.java
@@ -6,7 +6,7 @@
 
 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.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -42,12 +42,10 @@
             inspector -> {
               MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
               assertThat(mainMethodSubject, isPresent());
-              // TODO(b/219455761): Extend StringBuilder optimizer to Objects.toString().
-              assertEquals(
-                  canUseJavaUtilObjects(parameters),
+              assertTrue(
                   mainMethodSubject
                       .streamInstructions()
-                      .anyMatch(InstructionSubject::isNewInstance));
+                      .noneMatch(InstructionSubject::isNewInstance));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("foo");
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 b8ad8d9..2bc9233 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
@@ -29,4 +29,8 @@
   public static NoSuchMethodError throwNoSuchMethodError() {
     throw new NoSuchMethodError();
   }
+
+  public static RuntimeException throwRuntimeExceptionWithMessage(String message) {
+    throw new RuntimeException(message);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
index 5db275b..aecc306 100644
--- a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
+++ b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
@@ -579,12 +579,10 @@
 
   private void ensureSameOutput(JasminBuilder app) throws Exception {
     String javaOutput = runOnJava(app, MAIN_CLASS);
-    String dxOutput = runOnArtDx(app, MAIN_CLASS);
     String d8Output = runOnArtD8(app, MAIN_CLASS);
     String r8Output = runOnArtR8(app, MAIN_CLASS);
     String r8ShakenOutput = runOnArtR8(app, MAIN_CLASS, keepMainProguardConfiguration(MAIN_CLASS),
         null);
-    Assert.assertEquals(javaOutput, dxOutput);
     Assert.assertEquals(javaOutput, d8Output);
     Assert.assertEquals(javaOutput, r8Output);
     Assert.assertEquals(javaOutput, r8ShakenOutput);
@@ -603,7 +601,6 @@
 
   private void ensureAllFail(JasminBuilder app) throws Exception {
     ensureFails(app, MAIN_CLASS, this::runOnJavaRaw);
-    ensureFails(app, MAIN_CLASS, this::runOnArtDxRaw);
     ensureFails(app, MAIN_CLASS, this::runOnArtD8Raw);
     ensureFails(app, MAIN_CLASS, (a, m) -> runOnArtR8Raw(a, m, null));
     ensureFails(app, MAIN_CLASS,
@@ -623,7 +620,6 @@
           }
         };
     runtest.accept(() -> runOnJavaNoVerifyRaw(app, MAIN_CLASS), null);
-    runtest.accept(() -> runOnArtDxRaw(app, MAIN_CLASS), null);
     runtest.accept(() -> runOnArtD8Raw(app, MAIN_CLASS), CompilerUnderTest.D8);
     runtest.accept(() -> runOnArtR8Raw(app, MAIN_CLASS, null), CompilerUnderTest.R8);
     runtest.accept(() -> runOnArtR8Raw(app, MAIN_CLASS, keepMainProguardConfiguration(MAIN_CLASS),
@@ -644,8 +640,6 @@
 
   private void ensureRuntimeException(JasminBuilder app, Class exception) throws Exception {
     String name = exception.getSimpleName();
-    ProcessResult dxOutput = runOnArtDxRaw(app, MAIN_CLASS);
-    Assert.assertTrue(dxOutput.stderr.contains(name));
     ProcessResult d8Output = runOnArtD8Raw(app, MAIN_CLASS);
     Assert.assertTrue(d8Output.stderr.contains(name));
     ProcessResult r8Output = runOnArtR8Raw(app, MAIN_CLASS, null);
@@ -660,16 +654,14 @@
   private void ensureRuntimeException(JasminBuilder app, JasminBuilder library, Class exception)
       throws Exception {
     String name = exception.getSimpleName();
-    ProcessResult dxOutput = runOnArtDxRaw(app, library, MAIN_CLASS);
-    Assert.assertTrue(dxOutput.stderr.contains(name));
     ProcessResult d8Output = runOnArtD8Raw(app, library, MAIN_CLASS);
-    Assert.assertTrue(d8Output.stderr.contains(name));
+    Assert.assertTrue(d8Output.toString(), d8Output.stderr.contains(name));
     ProcessResult r8Output = runOnArtR8Raw(app, library, MAIN_CLASS,
         noShrinkingNoMinificationProguardConfiguration(), null);
-    Assert.assertTrue(r8Output.stderr.contains(name));
+    Assert.assertTrue(r8Output.toString(), r8Output.stderr.contains(name));
     ProcessResult r8ShakenOutput = runOnArtR8Raw(app, library, MAIN_CLASS,
         keepMainProguardConfiguration(MAIN_CLASS), null);
-    Assert.assertTrue(r8ShakenOutput.stderr.contains(name));
+    Assert.assertTrue(r8ShakenOutput.toString(), r8ShakenOutput.stderr.contains(name));
     ProcessResult javaOutput = runOnJavaNoVerifyRaw(app, library, MAIN_CLASS);
     Assert.assertTrue(javaOutput.stderr.contains(name));
   }
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 b7fe293..31740b7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -37,6 +37,7 @@
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -161,7 +162,7 @@
               } else {
                 assertThat(
                     inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateless$1"),
-                    notIf(isPresent(), testParameters.isDexRuntime()));
+                    CoreMatchers.not(isPresent()));
               }
 
               // TODO(b/173337498): MainKt$testStateful$1 should be class inlined.
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromForcefullyMovedMethodTest.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromForcefullyMovedMethodTest.java
new file mode 100644
index 0000000..f75e718
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromForcefullyMovedMethodTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. 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.missingclasses;
+
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.diagnostic.DefinitionContext;
+import com.android.tools.r8.diagnostic.internal.DefinitionClassContextImpl;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import org.junit.Test;
+
+public class MissingClassReferencedFromForcefullyMovedMethodTest extends MissingClassesTestBase {
+
+  private static final DefinitionContext referencedFrom =
+      DefinitionClassContextImpl.builder()
+          .setClassContext(Reference.classFromClass(I.class))
+          .setOrigin(getOrigin(I.class))
+          .build();
+
+  public MissingClassReferencedFromForcefullyMovedMethodTest(TestParameters parameters) {
+    super(parameters);
+    assumeFalse(parameters.canUseDefaultAndStaticInterfaceMethods());
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void testNoRules() throws Exception {
+    compileWithExpectedDiagnostics(
+        Main.class,
+        diagnostics -> inspectDiagnosticsWithNoRules(diagnostics, referencedFrom),
+        addInterface());
+  }
+
+  @Test
+  public void testDontWarnMainClass() throws Exception {
+    compileWithExpectedDiagnostics(
+        Main.class,
+        TestDiagnosticMessages::assertNoMessages,
+        addInterface().andThen(addDontWarn(I.class)));
+  }
+
+  @Test
+  public void testDontWarnMissingClass() throws Exception {
+    compileWithExpectedDiagnostics(
+        Main.class,
+        TestDiagnosticMessages::assertNoMessages,
+        addInterface().andThen(addDontWarn(MissingFunctionalInterface.class)));
+  }
+
+  @Test
+  public void testIgnoreWarnings() throws Exception {
+    compileWithExpectedDiagnostics(
+        Main.class,
+        diagnostics -> inspectDiagnosticsWithIgnoreWarnings(diagnostics, referencedFrom),
+        addInterface().andThen(addIgnoreWarnings()));
+  }
+
+  @Override
+  ClassReference getMissingClassReference() {
+    return Reference.classFromClass(MissingFunctionalInterface.class);
+  }
+
+  private ThrowableConsumer<R8FullTestBuilder> addInterface() {
+    return builder -> builder.addProgramClasses(I.class);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I.forcefullyMovedMethod();
+    }
+  }
+
+  interface I {
+
+    static void forcefullyMovedMethod() {
+      MissingFunctionalInterface i = () -> {};
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
index 2e6ffd2..73b6dfc 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
@@ -39,6 +39,11 @@
 
   interface MissingInterface {}
 
+  interface MissingFunctionalInterface {
+
+    void m();
+  }
+
   protected final TestParameters parameters;
 
   @Parameters(name = "{0}")
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageObjectOnProgramPathTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageObjectOnProgramPathTest.java
new file mode 100644
index 0000000..1e0b59e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageObjectOnProgramPathTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import static org.junit.Assert.assertThrows;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsMatcher;
+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.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassWriter;
+
+/** This is a regression test for b/237124748 */
+@RunWith(Parameterized.class)
+public class RepackageObjectOnProgramPathTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(dumpObject())
+              .addProgramClasses(A.class, Main.class)
+              .setMinApi(parameters.getApiLevel())
+              .enableInliningAnnotations()
+              .addKeepMainRule(Main.class)
+              .addDontWarn("*")
+              .compileWithExpectedDiagnostics(
+                  diagnostics ->
+                      // TODO(b/237124748): We should not throw an error.
+                      diagnostics.assertErrorThatMatches(
+                          DiagnosticsMatcher.diagnosticException(NullPointerException.class)));
+        });
+  }
+
+  public static byte[] dumpObject() {
+    ClassWriter classWriter = new ClassWriter(0);
+    classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "java/lang/Object", null, null, null);
+    classWriter.visitEnd();
+    return classWriter.toByteArray();
+  }
+
+  public static class A {
+
+    @NeverInline
+    public static void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
index 997bfd8..0cf3e9e 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
@@ -78,13 +78,8 @@
     if (enableOptimization) {
       assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("simple"), 1);
       assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("local"), 1);
-      // String concatenation optimization is enabled for DEX output.
       // Even replaced ordinal is concatenated (and gone).
-      if (parameters.isDexRuntime()) {
-        assertOrdinalReplacedAndGone(clazz.uniqueMethodWithName("multipleUsages"));
-      } else {
-        assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("multipleUsages"), 1);
-      }
+      assertOrdinalReplacedAndGone(clazz.uniqueMethodWithName("multipleUsages"));
       assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("inlined"), 1);
       assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("inSwitch"), 11);
       assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("differentTypeStaticField"), 1);
@@ -131,9 +126,7 @@
     if (enableOptimization) {
       assertNameReplacedWithConst(clazz.uniqueMethodWithName("simple"), "TWO");
       assertNameReplacedWithConst(clazz.uniqueMethodWithName("local"), "TWO");
-      // String concatenation optimization is enabled for DEX output.
-      String expectedConst = parameters.isDexRuntime() ? "1TWO" : "TWO";
-      assertNameReplacedWithConst(clazz.uniqueMethodWithName("multipleUsages"), expectedConst);
+      assertNameReplacedWithConst(clazz.uniqueMethodWithName("multipleUsages"), "1TWO");
       assertNameReplacedWithConst(clazz.uniqueMethodWithName("inlined"), "TWO");
       assertNameReplacedWithConst(clazz.uniqueMethodWithName("differentTypeStaticField"), "DOWN");
       assertNameReplacedWithConst(clazz.uniqueMethodWithName("nonStaticGet"), "TWO");
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index 6df7b0a..5a16cf6 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -10,6 +10,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8FullTestBuilder;
@@ -232,7 +233,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector);
+    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector, true);
 
     GraphInspector ifThenKeepClassesWithMembersInspector =
         testForR8(Backend.CF)
@@ -251,7 +252,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassesWithMembersInspector);
+    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassesWithMembersInspector, true);
 
     GraphInspector ifHasMemberThenKeepClassInspector =
         testForR8(Backend.CF)
@@ -272,7 +273,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector);
+    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector, true);
   }
 
   private void configureHorizontalClassMerging(R8FullTestBuilder testBuilder) {
@@ -292,6 +293,13 @@
 
   private void assertRetainedClassesEqual(
       GraphInspector referenceResult, GraphInspector conditionalResult) {
+    assertRetainedClassesEqual(referenceResult, conditionalResult, false);
+  }
+
+  private void assertRetainedClassesEqual(
+      GraphInspector referenceResult,
+      GraphInspector conditionalResult,
+      boolean expectConditionalIsLarger) {
     Set<String> referenceClasses =
         new TreeSet<>(
             referenceResult.codeInspector().allClasses().stream()
@@ -303,10 +311,14 @@
             .collect(Collectors.toSet());
     Set<String> notInReference =
         new TreeSet<>(Sets.difference(conditionalClasses, referenceClasses));
-    assertEquals(
-        "Classes in -if rule that are not in -keepclassmembers rule",
-        Collections.emptySet(),
-        notInReference);
+    if (expectConditionalIsLarger) {
+      assertFalse("Expected classes in -if rule to retain more.", notInReference.isEmpty());
+    } else {
+      assertEquals(
+          "Classes in -if rule that are not in -keepclassmembers rule",
+          Collections.emptySet(),
+          notInReference);
+    }
     Set<String> notInConditional =
         new TreeSet<>(Sets.difference(referenceClasses, conditionalClasses));
     assertEquals(
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index 56435ed..da3ef50 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -46,7 +46,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.Assert;
@@ -1384,14 +1383,8 @@
               opts.outline.minSize = 3;
               opts.outline.maxSize = 3;
 
-              // Do not allow dead code elimination of the new-instance instructions. This can be
-              // achieved
-              // by not assuming that StringBuilder is present.
-              DexItemFactory dexItemFactory = opts.itemFactory;
-              opts.itemFactory.libraryTypesAssumedToBePresent =
-                  new HashSet<>(dexItemFactory.libraryTypesAssumedToBePresent);
-              dexItemFactory.libraryTypesAssumedToBePresent.remove(
-                  dexItemFactory.stringBuilderType);
+              // Do not allow dead code elimination of the new-instance instructions.
+              opts.apiModelingOptions().enableApiCallerIdentification = false;
             });
 
     AndroidApp originalApplication = buildApplicationWithAndroidJar(builder);
diff --git a/tools/archive.py b/tools/archive.py
index d7bf1a9..91affd6 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -161,11 +161,13 @@
     create_maven_release.generate_desugar_configuration_maven_zip(
         utils.DESUGAR_CONFIGURATION_MAVEN_ZIP,
         utils.DESUGAR_CONFIGURATION,
-        utils.DESUGAR_IMPLEMENTATION)
+        utils.DESUGAR_IMPLEMENTATION,
+        utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP)
     create_maven_release.generate_desugar_configuration_maven_zip(
-        utils.DESUGAR_CONFIGURATION_LEGACY_JDK11_MAVEN_ZIP,
+        utils.DESUGAR_CONFIGURATION_JDK11_LEGACY_MAVEN_ZIP,
         utils.DESUGAR_CONFIGURATION_JDK11_LEGACY,
-        utils.DESUGAR_IMPLEMENTATION_JDK11)
+        utils.DESUGAR_IMPLEMENTATION_JDK11,
+        utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP)
 
     version = GetVersion()
     is_main = IsMain(version)
@@ -196,6 +198,8 @@
       utils.MAVEN_ZIP_LIB,
       utils.DESUGAR_CONFIGURATION,
       utils.DESUGAR_CONFIGURATION_MAVEN_ZIP,
+      utils.DESUGAR_CONFIGURATION_JDK11_LEGACY,
+      utils.DESUGAR_CONFIGURATION_JDK11_LEGACY_MAVEN_ZIP,
       utils.GENERATED_LICENSE,
     ]:
       file_name = os.path.basename(file)
@@ -266,6 +270,40 @@
             utils.upload_file_to_cloud_storage(
                 desugar_jdk_libs_configuration_jar, jar_destination)
 
+      # TODO(sgjesse): Refactor this to avoid the duplication of what is above.
+      # Upload desugar_jdk_libs JDK-11 legacyconfiguration to a maven compatible location.
+      if file == utils.DESUGAR_CONFIGURATION_JDK11_LEGACY:
+        jar_basename = 'desugar_jdk_libs_configuration.jar'
+        jar_version_name = 'desugar_jdk_libs_configuration-%s-jdk11-legacy.jar' % version
+        maven_dst = GetUploadDestination(
+            utils.get_maven_path('desugar_jdk_libs_configuration', version),
+                                 jar_version_name, is_main)
+
+        with utils.TempDir() as tmp_dir:
+          desugar_jdk_libs_configuration_jar = os.path.join(tmp_dir,
+                                                            jar_version_name)
+          create_maven_release.generate_jar_with_desugar_configuration(
+              utils.DESUGAR_CONFIGURATION_JDK11_LEGACY,
+              utils.DESUGAR_IMPLEMENTATION_JDK11,
+              utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP,
+              desugar_jdk_libs_configuration_jar)
+
+          if options.dry_run:
+            print('Dry run, not actually creating maven repo for '
+                + 'desugar configuration.')
+            if options.dry_run_output:
+              shutil.copyfile(
+                  desugar_jdk_libs_configuration_jar,
+                  os.path.join(options.dry_run_output, jar_version_name))
+          else:
+            utils.upload_file_to_cloud_storage(
+                desugar_jdk_libs_configuration_jar, maven_dst)
+            print('Maven repo root available at: %s' % GetMavenUrl(is_main))
+            # Also archive the jar as non maven destination for Google3
+            jar_destination = GetUploadDestination(
+                version, jar_basename, is_main)
+            utils.upload_file_to_cloud_storage(
+                desugar_jdk_libs_configuration_jar, jar_destination)
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/archive_desugar_jdk_libs.py b/tools/archive_desugar_jdk_libs.py
index 10b300a..5298435 100755
--- a/tools/archive_desugar_jdk_libs.py
+++ b/tools/archive_desugar_jdk_libs.py
@@ -174,7 +174,7 @@
     version = GetVersion(
       os.path.join(
         checkout_dir,
-        VERSION_FILE_JDK11 if variant == 'jdk11' else VERSION_FILE_JDK8))
+        VERSION_FILE_JDK11 if options.variant == 'jdk11' else VERSION_FILE_JDK8))
 
     destination = archive.GetVersionDestination(
         'gs://', LIBRARY_NAME + '/' + version, is_main)
diff --git a/tools/create_maven_release.py b/tools/create_maven_release.py
index 6250c75..de9f0ac 100755
--- a/tools/create_maven_release.py
+++ b/tools/create_maven_release.py
@@ -383,7 +383,8 @@
     move(destination + '.zip', destination)
 
 # Generate the maven zip for the configuration to desugar desugar_jdk_libs.
-def generate_desugar_configuration_maven_zip(out, configuration, implementation):
+def generate_desugar_configuration_maven_zip(
+    out, configuration, implementation, conversions):
   with utils.TempDir() as tmp_dir:
     version = utils.desugar_configuration_version(configuration)
     # Generate the pom file.
@@ -394,7 +395,7 @@
     generate_jar_with_desugar_configuration(
         configuration,
         implementation,
-        utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP,
+        conversions,
         jar_file)
     # Write the maven zip file.
     generate_maven_zip(
diff --git a/tools/git_sync_cl_chain.py b/tools/git_sync_cl_chain.py
index 7dc512e..0fd65c6 100755
--- a/tools/git_sync_cl_chain.py
+++ b/tools/git_sync_cl_chain.py
@@ -40,6 +40,9 @@
 
 def ParseOptions(argv):
   result = optparse.OptionParser()
+  result.add_option('--bypass-hooks',
+                    help='Bypass presubmit hooks',
+                    action='store_true')
   result.add_option('--delete', '-d',
                     help='Delete closed branches',
                     choices=['y', 'n', 'ask'],
@@ -103,7 +106,7 @@
 
       utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
 
-      status = get_status_for_current_branch()
+      status = get_status_for_current_branch(branch)
       print('Syncing %s (status: %s)' % (branch.name, status))
 
       pull_for_current_branch(branch, options)
@@ -135,8 +138,10 @@
               'Cannot upload branch %s since it comes after a local branch'
                   % branch.name)
         else:
-          utils.RunCmd(
-              ['git', 'cl', 'upload', '-m', options.message], quiet=True)
+          upload_cmd = ['git', 'cl', 'upload', '-m', options.message]
+          if options.bypass_hooks:
+            upload_cmd.append('--bypass-hooks')
+          utils.RunCmd(upload_cmd, quiet=True)
 
     if get_delete_branches_option(closed_branches, options):
       delete_branches(closed_branches)
@@ -169,11 +174,13 @@
   answer = sys.stdin.read(1)
   return answer.lower() == 'y'
 
-def get_status_for_current_branch():
+def get_status_for_current_branch(current_branch):
+  if current_branch.name == 'main':
+    return 'main'
   return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()
 
 def is_root_branch(branch, options):
-  return branch == options.from_branch or branch.upstream is None
+  return branch.name == options.from_branch or branch.upstream is None
 
 def pull_for_current_branch(branch, options):
   if branch.name == 'main' and options.skip_main:
diff --git a/tools/r8_release.py b/tools/r8_release.py
index eab96be..f19c99e 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -377,15 +377,24 @@
                     'desugar_jdk_libs_configuration.jar')
       download_file(options.version, 'r8retrace-exclude-deps.jar', 'retrace_lib.jar')
       g4_open('METADATA')
-      sed(r'[1-9]\.[0-9]{1,2}\.[0-9]{1,3}-dev',
-          options.version,
-          os.path.join(third_party_r8, 'METADATA'))
+      metadata_path = os.path.join(third_party_r8, 'METADATA')
+      match_count = 0
+      version_match_regexp = r'[1-9]\.[0-9]{1,2}\.[0-9]{1,3}-dev'
+      for line in open(metadata_path, 'r'):
+        result = re.search(version_match_regexp, line)
+        if result:
+          match_count = match_count + 1
+      if match_count != 3:
+        print(("Could not find the previous -dev release string to replace in " +
+            "METADATA. Expected to find is mentioned 3 times. Please update %s " +
+            "manually and run again with options --google " +
+            "--use-existing-work-branch.") % metadata_path)
+        sys.exit(1)
+      sed(version_match_regexp, options.version, metadata_path)
       sed(r'\{ year.*\}',
           ('{ year: %i month: %i day: %i }'
            % (today.year, today.month, today.day)),
-          os.path.join(third_party_r8, 'METADATA'))
-
-
+          metadata_path)
       subprocess.check_output('chmod u+w *', shell=True)
 
     with utils.ChangedWorkingDirectory(google3_base):
@@ -554,10 +563,14 @@
   if not args.use_existing_work_branch:
     clients = subprocess.check_output('g4 myclients', shell=True).decode('utf-8')
     if ':%s:' % client_name in clients:
-      print(("Remove the existing '%s' client before continuing " +
-             "(force delete: 'g4 citc -d -f %s'), " +
-             "or use option --use-existing-work-branch.") % (client_name, client_name))
-      sys.exit(1)
+      if args.delete_work_branch:
+        subprocess.check_call('g4 citc -d -f %s' % client_name, shell=True)
+      else:
+        print(("Remove the existing '%s' client before continuing " +
+               "(force delete: 'g4 citc -d -f %s'), " +
+               "or use either --use-existing-work-branch or " +
+               "--delete-work-branch.") % (client_name, client_name))
+        sys.exit(1)
 
 
 def extract_version_from_pom(pom_file):
@@ -823,6 +836,10 @@
                       default=False,
                       action='store_true',
                       help='Use existing work branch/CL in aosp/studio/google3')
+  result.add_argument('--delete-work-branch', '--delete_work_branch',
+                      default=False,
+                      action='store_true',
+                      help='Delete CL in google3')
   result.add_argument('--no-upload', '--no_upload',
                       default=False,
                       action='store_true',
diff --git a/tools/utils.py b/tools/utils.py
index cd41ad0..ecd172c 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -66,6 +66,7 @@
 R8LIB_TESTS_DEPS_JAR = R8_TESTS_DEPS_JAR
 MAVEN_ZIP = os.path.join(LIBS, 'r8.zip')
 MAVEN_ZIP_LIB = os.path.join(LIBS, 'r8lib.zip')
+LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP = os.path.join(LIBS, 'library_desugar_conversions_legacy.jar')
 LIBRARY_DESUGAR_CONVERSIONS_ZIP = os.path.join(LIBS, 'library_desugar_conversions.jar')
 
 DESUGAR_CONFIGURATION = os.path.join(
@@ -78,8 +79,8 @@
       'third_party', 'openjdk', 'desugar_jdk_libs_11', 'desugar_jdk_libs.jar')
 DESUGAR_CONFIGURATION_MAVEN_ZIP = os.path.join(
   LIBS, 'desugar_jdk_libs_configuration.zip')
-DESUGAR_CONFIGURATION_LEGACY_JDK11_MAVEN_ZIP = os.path.join(
-  LIBS, 'desugar_jdk_libs_configuration_legacy_jdk11.zip')
+DESUGAR_CONFIGURATION_JDK11_LEGACY_MAVEN_ZIP = os.path.join(
+  LIBS, 'desugar_jdk_libs_configuration_jdk11_legacy.zip')
 GENERATED_LICENSE = os.path.join(GENERATED_LICENSE_DIR, 'LICENSE')
 RT_JAR = os.path.join(REPO_ROOT, 'third_party/openjdk/openjdk-rt-1.8/rt.jar')
 R8LIB_KEEP_RULES = os.path.join(REPO_ROOT, 'src/main/keep.txt')
@@ -208,6 +209,8 @@
     self._quiet = quiet
 
   def log(self, text):
+    if len(text.strip()) == 0:
+      return
     if self._quiet:
       if self._has_printed:
         sys.stdout.write(ProgressLogger.UP + ProgressLogger.CLEAR_LINE)