Merge commit '6a9a086393127c12eeec489d216f434223af9910' into dev-release

Change-Id: Ide3635762e37d6296872028c9dee0c51ad51fb18
diff --git a/doc/compilerdump.md b/doc/compilerdump.md
index 7bffd27..c85a9ff 100644
--- a/doc/compilerdump.md
+++ b/doc/compilerdump.md
@@ -9,13 +9,16 @@
 The dump contains almost all of the inputs that are given to the compiler as
 part of compilation. In particular, it contains *all* class definitions in the
 form of Java classfiles (i.e., bytecode, not the Java source files).
+If resource shrinking is enabled, the dump also contains *all* resources (i.e.,
+the manifest, all res folder entries in proto and raw format, and any other
+file flowing into resource shrinking).
 In addition to the classfiles, the dump also includes Java resources, the
 compiler type, version, and flags, such as `--debug` or `--release`,
 main-dex lists or rules, and more. For R8 the dump also contains the full
 concatenation of all keep rules.
 
 The dump is a zip file containing the above. You should unzip it and review
-the content locally. The program, classpath and library content will be in
+the content locally. The program, classpath, library and resource content will be in
 nested zip files. The remaining content is in plain text files.
 
 
diff --git a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
index a8c4a65..29b468f 100644
--- a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -49,4 +49,10 @@
   default void finished(DiagnosticsHandler handler) throws IOException {
     // Do nothing by default.
   }
+
+  /** Experimental addition still incubating. */
+  @Deprecated
+  default DataResourceProvider getDataResourceProvider() {
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 55a8199..8fccc8d 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -99,7 +99,7 @@
         command.getReporter(),
         () -> {
           try {
-            run(app, options, executor);
+            runInternal(app, options, executor);
           } finally {
             executor.shutdown();
           }
@@ -119,7 +119,7 @@
     ExceptionUtils.withD8CompilationHandler(
         command.getReporter(),
         () -> {
-          run(app, options, executor);
+          runInternal(app, options, executor);
         });
   }
 
@@ -159,7 +159,7 @@
         options.reporter,
         () -> {
           try {
-            run(inputApp, options, executor);
+            runInternal(inputApp, options, executor);
           } finally {
             executor.shutdown();
           }
@@ -190,7 +190,7 @@
     return timing.time("Create app-view", () -> AppView.createForD8(appInfo, typeRewriter, timing));
   }
 
-  private static void run(AndroidApp inputApp, InternalOptions options, ExecutorService executor)
+  static void runInternal(AndroidApp inputApp, InternalOptions options, ExecutorService executor)
       throws IOException {
     if (options.printMemory) {
       // Run GC twice to remove objects with finalizers.
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6a8d346..24d8545 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -171,7 +171,7 @@
   private final Timing timing;
   private final InternalOptions options;
 
-  private R8(InternalOptions options) {
+  R8(InternalOptions options) {
     this.options = options;
     if (options.printMemory) {
       System.gc();
@@ -209,7 +209,7 @@
     ExceptionUtils.withR8CompilationHandler(
         command.getReporter(),
         () -> {
-          run(app, options, executor);
+          runInternal(app, options, executor);
         });
   }
 
@@ -238,16 +238,24 @@
         options.reporter,
         () -> {
           try {
-            run(app, options, executor);
+            runInternal(app, options, executor);
           } finally {
             executor.shutdown();
           }
         });
   }
 
-  private static void run(AndroidApp app, InternalOptions options, ExecutorService executor)
+  static void runInternal(AndroidApp app, InternalOptions options, ExecutorService executor)
       throws IOException {
-    new R8(options).run(app, executor);
+    if (options.r8PartialCompilationOptions.enabled) {
+      try {
+        new R8Partial(options).runInternal(app, executor);
+      } catch (ResourceException e) {
+        throw new RuntimeException(e);
+      }
+    } else {
+      new R8(options).runInternal(app, executor);
+    }
   }
 
   private static DirectMappedDexApplication getDirectApp(AppView<?> appView) {
@@ -255,7 +263,7 @@
   }
 
   @SuppressWarnings("DefaultCharset")
-  private void run(AndroidApp inputApp, ExecutorService executorService) throws IOException {
+  void runInternal(AndroidApp inputApp, ExecutorService executorService) throws IOException {
     timing.begin("Run prelude");
     assert options.programConsumer != null;
     if (options.quiet) {
@@ -1268,8 +1276,8 @@
     InternalOptions options = command.getInternalOptions();
     ExecutorService executorService = ThreadUtils.getExecutorService(options);
     try {
-      ExceptionUtils.withR8CompilationHandler(options.reporter, () ->
-          run(command.getInputApp(), options, executorService));
+      ExceptionUtils.withR8CompilationHandler(
+          options.reporter, () -> runInternal(command.getInputApp(), options, executorService));
     } finally {
       executorService.shutdown();
     }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 70220ac..1077122 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -141,6 +141,8 @@
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations =
         System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
+    private boolean readEmbeddedRulesFromClasspathAndLibrary =
+        System.getProperty("com.android.tools.r8.readEmbeddedRulesFromClasspathAndLibrary") != null;
     public boolean enableStartupLayoutOptimization = true;
     private SemanticVersion fakeCompilerVersion = null;
     private AndroidResourceProvider androidResourceProvider = null;
@@ -545,6 +547,13 @@
       return self();
     }
 
+    // Package private only for testing. Use system property
+    // com.android.tools.r8.readEmbeddedRulesFromClasspathAndLibrary to enable.
+    @Deprecated
+    void setReadEmbeddedRulesFromClasspathAndLibrary(boolean enable) {
+      this.readEmbeddedRulesFromClasspathAndLibrary = enable;
+    }
+
     @Override
     protected InternalProgramOutputPathConsumer createProgramOutputConsumer(
         Path path,
@@ -801,6 +810,33 @@
     private void amendWithRulesAndProvidersForInjarsAndMetaInf(
         Reporter reporter, ProguardConfigurationParser parser) {
 
+      Supplier<SemanticVersion> semanticVersionSupplier =
+          Suppliers.memoize(
+              () -> {
+                SemanticVersion compilerVersion =
+                    fakeCompilerVersion == null
+                        ? SemanticVersion.create(
+                            Version.getMajorVersion(),
+                            Version.getMinorVersion(),
+                            Version.getPatchVersion())
+                        : fakeCompilerVersion;
+                if (compilerVersion.getMajor() < 0) {
+                  compilerVersion =
+                      SemanticVersion.create(
+                          Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+                  reporter.warning(
+                      "Running R8 version "
+                          + Version.getVersionString()
+                          + ", which cannot be represented as a semantic version. Using"
+                          + " an artificial version newer than any known version for selecting"
+                          + " Proguard configurations embedded under META-INF/. This means that"
+                          + " all rules with a '-upto-' qualifier will be excluded and all rules"
+                          + " with a -from- qualifier will be included.");
+                }
+                return compilerVersion;
+              });
+      // TODO(b/370665838): Remove the fixpoint, as -injars are not supported in embedded keep
+      // rules. The comment below is not true.
       // Since -injars can itself reference archives with rules and that in turn have -injars the
       // completion of amending rules and providers must run in a fixed point. The fixed point is
       // reached once the injars set is stable.
@@ -817,47 +853,41 @@
           }
         }
         if (providers.isEmpty()) {
-          return;
+          break;
         }
 
-        Supplier<SemanticVersion> semanticVersionSupplier =
-            Suppliers.memoize(
-                () -> {
-                  SemanticVersion compilerVersion =
-                      fakeCompilerVersion == null
-                          ? SemanticVersion.create(
-                              Version.getMajorVersion(),
-                              Version.getMinorVersion(),
-                              Version.getPatchVersion())
-                          : fakeCompilerVersion;
-                  if (compilerVersion.getMajor() < 0) {
-                    compilerVersion =
-                        SemanticVersion.create(
-                            Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
-                    reporter.warning(
-                        "Running R8 version "
-                            + Version.getVersionString()
-                            + ", which cannot be represented as a semantic version. Using"
-                            + " an artificial version newer than any known version for selecting"
-                            + " Proguard configurations embedded under META-INF/. This means that"
-                            + " all rules with a '-upto-' qualifier will be excluded and all rules"
-                            + " with a -from- qualifier will be included.");
-                  }
-                  return compilerVersion;
-                });
-
         while (!providers.isEmpty()) {
           DataResourceProvider dataResourceProvider = providers.pop().getDataResourceProvider();
-          if (dataResourceProvider != null) {
-            try {
-              ExtractEmbeddedRules embeddedProguardConfigurationVisitor =
-                  new ExtractEmbeddedRules(reporter, semanticVersionSupplier);
-              dataResourceProvider.accept(embeddedProguardConfigurationVisitor);
-              embeddedProguardConfigurationVisitor.parseRelevantRules(parser);
-            } catch (ResourceException e) {
-              reporter.error(new ExceptionDiagnostic(e));
-            }
-          }
+          parseEmbeddedRules(reporter, parser, semanticVersionSupplier, dataResourceProvider);
+        }
+        // TODO(b/370665838): Remove the fixpoint. For now assert that it is not needed.
+        assert seenInjars.containsAll(parser.getConfigurationBuilder().getInjars());
+      }
+      if (readEmbeddedRulesFromClasspathAndLibrary) {
+        for (ClassFileResourceProvider provider : getAppBuilder().getClasspathResourceProviders()) {
+          parseEmbeddedRules(
+              reporter, parser, semanticVersionSupplier, provider.getDataResourceProvider());
+        }
+        for (ClassFileResourceProvider provider : getAppBuilder().getLibraryResourceProviders()) {
+          parseEmbeddedRules(
+              reporter, parser, semanticVersionSupplier, provider.getDataResourceProvider());
+        }
+      }
+    }
+
+    private static void parseEmbeddedRules(
+        Reporter reporter,
+        ProguardConfigurationParser parser,
+        Supplier<SemanticVersion> semanticVersionSupplier,
+        DataResourceProvider dataResourceProvider) {
+      if (dataResourceProvider != null) {
+        try {
+          ExtractEmbeddedRules embeddedProguardConfigurationVisitor =
+              new ExtractEmbeddedRules(reporter, semanticVersionSupplier);
+          dataResourceProvider.accept(embeddedProguardConfigurationVisitor);
+          embeddedProguardConfigurationVisitor.parseRelevantRules(parser);
+        } catch (ResourceException e) {
+          reporter.error(new ExceptionDiagnostic(e));
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index c5fc86b..898f4eb 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -312,6 +312,8 @@
         builder.setAndroidResourceProvider(new ArchiveProtoAndroidResourceProvider(inputPath));
         builder.setAndroidResourceConsumer(
             new ArchiveProtoAndroidResourceConsumer(outputPath, inputPath));
+        // In the CLI we default to optimized resource shrinking.
+        builder.setResourceShrinkerConfiguration(b -> b.enableOptimizedShrinkingWithR8().build());
       } else if (arg.equals("--feature")) {
         featureSplitConfigCollector.addInputOutput(nextArg, nextNextArg);
       } else if (arg.equals(ISOLATED_SPLITS_FLAG)) {
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
new file mode 100644
index 0000000..e9cb9b8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -0,0 +1,232 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dump.CompilerDump;
+import com.android.tools.r8.dump.DumpOptions;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.naming.MapConsumer;
+import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
+import com.android.tools.r8.tracereferences.TraceReferencesBridge;
+import com.android.tools.r8.tracereferences.TraceReferencesCommand;
+import com.android.tools.r8.tracereferences.TraceReferencesKeepRules;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DumpInputFlags;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+class R8Partial {
+
+  private final InternalOptions options;
+
+  R8Partial(InternalOptions options) {
+    this.options = options;
+  }
+
+  void runInternal(AndroidApp app, ExecutorService executor) throws IOException, ResourceException {
+    Timing timing = Timing.create("R8 partial " + Version.LABEL, options);
+
+    ProgramConsumer originalProgramConsumer = options.programConsumer;
+    MapConsumer originalMapConsumer = options.mapConsumer;
+
+    Path tmp = options.r8PartialCompilationOptions.getTemp();
+    Path dumpFile = options.r8PartialCompilationOptions.getDumpFile();
+
+    // Create a dump of the compiler input.
+    // TODO(b/309743298): Do not use compiler dump to handle splitting the compilation. This should
+    // be all in memory.
+    ApplicationReader applicationReader = new ApplicationReader(app, options, timing);
+    applicationReader.dump(
+        new DumpInputFlags() {
+
+          @Override
+          public Path getDumpPath() {
+            return dumpFile;
+          }
+
+          @Override
+          public boolean shouldDump(DumpOptions options) {
+            return true;
+          }
+
+          @Override
+          public boolean shouldFailCompilation() {
+            return false;
+          }
+
+          @Override
+          public boolean shouldLogDumpInfoMessage() {
+            return false;
+          }
+        });
+    CompilerDump dump = CompilerDump.fromArchive(dumpFile, tmp);
+    if (dump.getBuildProperties().hasMainDexKeepRules()
+        || dump.getBuildProperties().hasArtProfileProviders()
+        || dump.getBuildProperties().hasStartupProfileProviders()) {
+      throw options.reporter.fatalError(
+          "Split compilation does not support legacy multi-dex, baseline or startup profiles");
+    }
+
+    DexApplication dapp = applicationReader.read().toDirect();
+    AppInfoWithClassHierarchy appInfo =
+        AppInfoWithClassHierarchy.createForDesugaring(
+            AppInfo.createInitialAppInfo(dapp, GlobalSyntheticsStrategy.forNonSynthesizing()));
+
+    Predicate<String> isR8 = options.r8PartialCompilationOptions.isR8;
+    Set<String> d8classes = new HashSet<>();
+    appInfo
+        .classes()
+        .forEach(
+            clazz -> {
+              String key = clazz.toSourceString();
+              if (!d8classes.contains(key) && !isR8.test(key)) {
+                d8classes.add(key);
+                // TODO(b/309743298): Improve this to only visit each class once and stop at
+                //  library boundary.
+                appInfo.forEachSuperType(
+                    clazz,
+                    (superType, subclass, ignored) -> {
+                      DexProgramClass superClass =
+                          asProgramClassOrNull(appInfo.definitionFor(superType));
+                      if (superClass != null) {
+                        d8classes.add(superClass.toSourceString());
+                      }
+                    });
+              }
+            });
+
+    // Filter the program input into the D8 and R8 parts.
+    Set<String> d8ZipEntries =
+        d8classes.stream()
+            .map(name -> name.replace('.', '/') + ".class")
+            .collect(Collectors.toSet());
+    ZipBuilder d8ProgramBuilder = ZipBuilder.builder(tmp.resolve("d8-program.jar"));
+    ZipBuilder r8ProgramBuilder = ZipBuilder.builder(tmp.resolve("r8-program.jar"));
+    ZipUtils.iter(
+        dump.getProgramArchive(),
+        (entry, input) -> {
+          if (d8ZipEntries.contains(entry.getName())) {
+            d8ProgramBuilder.addBytes(entry.getName(), ByteStreams.toByteArray(input));
+          } else {
+            r8ProgramBuilder.addBytes(entry.getName(), ByteStreams.toByteArray(input));
+          }
+        });
+    Path d8Program = d8ProgramBuilder.build();
+    Path r8Program = r8ProgramBuilder.build();
+
+    // Compile D8 input with D8.
+    Path d8Output = tmp.resolve("d8-output.zip");
+    D8Command.Builder d8Builder =
+        D8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addClasspathFiles(r8Program)
+            .addProgramFiles(d8Program)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setOutput(d8Output, OutputMode.DexIndexed);
+    if (dump.hasDesugaredLibrary()) {
+      d8Builder.addDesugaredLibraryConfiguration(
+          Files.readString(dump.getDesugaredLibraryFile(), UTF_8));
+    }
+    d8Builder.validate();
+    D8Command d8command = d8Builder.makeCommand();
+    AndroidApp d8App = d8command.getInputApp();
+    InternalOptions d8Options = d8command.getInternalOptions();
+    assert d8Options.getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N)
+        : "Default interface methods not yet supported";
+    D8.runInternal(d8App, d8Options, executor);
+
+    // Run trace references to produce keep rules for the D8 compiled part.
+    // TODO(b/309743298): Do not emit keep rules into a file.
+    Path traceReferencesRules = tmp.resolve("tr.rules");
+    TraceReferencesKeepRules keepRulesConsumer =
+        TraceReferencesKeepRules.builder()
+            .setOutputConsumer(new FileConsumer(traceReferencesRules))
+            .build();
+    TraceReferencesCommand.Builder trBuilder =
+        TraceReferencesCommand.builder()
+            .setConsumer(keepRulesConsumer)
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addTargetFiles(r8Program)
+            .addSourceFiles(d8Program);
+    TraceReferencesCommand tr = TraceReferencesBridge.makeCommand(trBuilder);
+    TraceReferencesBridge.runInternal(tr);
+
+    // Compile R8 input with R8 using the keep rules from trace references.
+    Path r8Output = tmp.resolve("r8-output.zip");
+    R8Command.Builder r8Builder =
+        R8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addClasspathFiles(d8Program)
+            .addProgramFiles(r8Program)
+            .addProguardConfigurationFiles(dump.getProguardConfigFile(), traceReferencesRules)
+            .setEnableEmptyMemberRulesToDefaultInitRuleConversion(true)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setOutput(r8Output, OutputMode.DexIndexed);
+    if (dump.hasDesugaredLibrary()) {
+      r8Builder.addDesugaredLibraryConfiguration(
+          Files.readString(dump.getDesugaredLibraryFile(), UTF_8));
+    }
+    R8Command r8Command = r8Builder.makeCommand();
+    AndroidApp r8App = r8Command.getInputApp();
+    InternalOptions r8Options = r8Command.getInternalOptions();
+    r8Options.mapConsumer = originalMapConsumer;
+    r8Options.quiet = true; // Don't write the R8 version.
+    R8.runInternal(r8App, r8Options, executor);
+
+    // Emit resources and merged DEX to the output consumer.
+    // TODO(b/309743298): Consider passing the DataResourceConsumer to the R8 invocation above.
+    DataResourceConsumer dataResourceConsumer = originalProgramConsumer.getDataResourceConsumer();
+    if (dataResourceConsumer != null) {
+      ZipUtils.iterWithZipFile(
+          r8Output,
+          (zip, entry) -> {
+            if (entry.getName().endsWith(FileUtils.DEX_EXTENSION)) {
+              return;
+            }
+            dataResourceConsumer.accept(
+                DataEntryResource.fromZip(zip, entry), new DiagnosticsHandler() {});
+          });
+    }
+    // TODO(b/309743298): Handle jumbo string rewriting with PCs in mapping file.
+    D8Command.Builder mergerBuilder =
+        D8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addProgramFiles(d8Output, r8Output)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setProgramConsumer(originalProgramConsumer);
+    mergerBuilder.validate();
+    D8Command mergeCommand = mergerBuilder.makeCommand();
+    AndroidApp mergeApp = mergeCommand.getInputApp();
+    InternalOptions mergeOptions = mergeCommand.getInternalOptions();
+    D8.runInternal(mergeApp, mergeOptions, executor);
+    timing.end();
+  }
+}
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 5e31c47..8890de3 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -157,6 +157,11 @@
     return builder.build();
   }
 
+  public final void dump(DumpInputFlags dumpInputFlags) {
+    assert verifyMainDexOptionsCompatible(inputApp, options);
+    dumpApplication(dumpInputFlags);
+  }
+
   private void dumpApplication(DumpInputFlags dumpInputFlags) {
     DumpOptions dumpOptions = options.dumpOptions;
     if (dumpOptions == null || !dumpInputFlags.shouldDump(dumpOptions)) {
diff --git a/src/test/testbase/java/com/android/tools/r8/dump/CompilerDump.java b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
similarity index 100%
rename from src/test/testbase/java/com/android/tools/r8/dump/CompilerDump.java
rename to src/main/java/com/android/tools/r8/dump/CompilerDump.java
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 85215e5..2a21f45 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -330,6 +330,18 @@
       return null;
     }
 
+    // Package-private service types are allowed only when the load() call is made from the same
+    // package. Not worth modelling.
+    DexClass serviceTypeResolved = appView.definitionFor(serviceType);
+    if (serviceTypeResolved == null) {
+      report(code.context(), serviceType, "Service type could not be resolved");
+      return null;
+    }
+    if (!serviceTypeResolved.isPublic()) {
+      report(code.context(), serviceType, "Service type must be public");
+      return null;
+    }
+
     // Check that ClassLoader used is the ClassLoader defined for the service configuration
     // that we are instantiating or NULL.
     Value classLoaderValue = invokeInstr.getLastArgument().getAliasedValue();
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
index a2d0f00..649b64f 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
@@ -161,6 +161,9 @@
     //  `original.holder` on all API levels, in which case it is not OK to rebind to the resolved
     //  method.
     return resolvedMethod.isLibraryMethod()
+        // Only rebind to protected methods in library when generating DEX. Dalvik/ART does not
+        // implement the full instruction verification.
+        && (options.isGeneratingDex() || !resolvedMethod.getAccessFlags().isProtected())
         && isAccessibleInAllContexts(resolvedMethod, resolutionResult, contexts)
         && !isInvokeSuperToInterfaceMethod(resolvedMethod, invokeType)
         && !isInvokeSuperToAbstractMethod(resolvedMethod, invokeType)
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java
index 28d6b5c..6d28cbe 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java
@@ -197,8 +197,8 @@
                           info.addOverride(existing);
                           return existing;
                         }
+                        info.addSibling(existing);
                         if (existing.getMethod().getHolder().isInterface() && !info.isAbstract()) {
-                          info.addSibling(existing);
                           existing.addOverride(info);
                           return info;
                         }
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 401cd13..20db4fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -5506,6 +5506,18 @@
 
   private void handleServiceInstantiation(
       DexType serviceType, ProgramMethod context, KeepReason reason) {
+    DexProgramClass serviceClass = getProgramClassOrNullFromReflectiveAccess(serviceType, context);
+    if (serviceClass != null && !serviceClass.isPublic()) {
+      // Package-private service types are allowed only when the load() call is made from the same
+      // package.
+      applyMinimumKeepInfoWhenLive(
+          serviceClass,
+          KeepClassInfo.newEmptyJoiner()
+              .disallowHorizontalClassMerging()
+              .disallowVerticalClassMerging()
+              .disallowAccessModification());
+    }
+
     List<DexType> serviceImplementationTypes =
         appView.appServices().serviceImplementationsFor(serviceType);
     for (DexType serviceImplementationType : serviceImplementationTypes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
index d1e1d5b..8cea4ff 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.shaking;
 
+import static com.android.tools.r8.shaking.KeepInfoCollectionExported.KeepAnnotationCollectionInfoExported.createExported;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexType;
@@ -310,6 +312,19 @@
       }
       return true;
     }
+
+    @Override
+    public String toString() {
+      StringBuilder stringBuilder = new StringBuilder();
+      stringBuilder
+          .append("IntermediateKeepAnnotationCollection{ ")
+          .append(anyTypeInfo.toString())
+          .append("; ");
+      specificTypeInfo.forEach(
+          (t, info) -> stringBuilder.append(t).append(": ").append(info.toString()));
+      stringBuilder.append("}");
+      return stringBuilder.toString();
+    }
   }
 
   public static Builder builder() {
@@ -328,6 +343,10 @@
     return false;
   }
 
+  public boolean isTopOrBottom() {
+    return isTop() || isBottom();
+  }
+
   IntermediateKeepAnnotationCollectionInfo asIntermediate() {
     throw new Unreachable();
   }
@@ -368,6 +387,7 @@
 
     // Info applicable to only specific types. Null if no type specific info is present.
     private Map<DexType, KeepAnnotationInfo> specificTypeInfo = null;
+    private boolean export = false;
 
     private Builder(KeepAnnotationInfo anyTypeInfo) {
       assert anyTypeInfo != null;
@@ -375,6 +395,10 @@
       this.anyTypeInfo = anyTypeInfo;
     }
 
+    void setExport() {
+      this.export = true;
+    }
+
     private static Builder createFrom(KeepAnnotationCollectionInfo original) {
       if (original.isTop()) {
         return createTop();
@@ -506,7 +530,9 @@
       if (isBottom()) {
         return BottomKeepAnnotationCollectionInfo.getInstance();
       }
-      return new IntermediateKeepAnnotationCollectionInfo(anyTypeInfo, specificTypeInfo);
+      IntermediateKeepAnnotationCollectionInfo intermediate =
+          new IntermediateKeepAnnotationCollectionInfo(anyTypeInfo, specificTypeInfo);
+      return export ? createExported(intermediate) : intermediate;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
index 985559d..308e29c 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
 import java.util.function.Function;
 
 /** Immutable keep requirements for a class. */
@@ -214,6 +215,20 @@
     return hash;
   }
 
+  @Override
+  public List<String> lines() {
+    List<String> lines = super.lines();
+    lines.add("allowClassInlining: " + allowClassInlining);
+    lines.add("allowHorizontalClassMerging: " + allowHorizontalClassMerging);
+    lines.add("allowPermittedSubclassesRemoval: " + allowPermittedSubclassesRemoval);
+    lines.add("allowRepackaging: " + allowRepackaging);
+    lines.add("allowSyntheticSharing: " + allowSyntheticSharing);
+    lines.add("allowUnusedInterfaceRemoval: " + allowUnusedInterfaceRemoval);
+    lines.add("allowVerticalClassMerging: " + allowVerticalClassMerging);
+    lines.add("checkEnumUnboxed: " + checkEnumUnboxed);
+    return lines;
+  }
+
   public static class Builder extends KeepInfo.Builder<Builder, KeepClassInfo> {
 
     private boolean allowClassInlining;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index 1c6cfa5..b31df54 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
+import java.util.List;
+
 /** Immutable keep requirements for a field. */
 public final class KeepFieldInfo extends KeepMemberInfo<KeepFieldInfo.Builder, KeepFieldInfo> {
 
@@ -88,6 +90,14 @@
     return hash;
   }
 
+  @Override
+  public List<String> lines() {
+    List<String> lines = super.lines();
+    lines.add("allowFieldTypeStrengthening: " + allowFieldTypeStrengthening);
+    lines.add("allowRedundantFieldLoadElimination: " + allowRedundantFieldLoadElimination);
+    return lines;
+  }
+
   public static class Builder extends KeepMemberInfo.Builder<Builder, KeepFieldInfo> {
 
     private boolean allowFieldTypeStrengthening;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
index a085f21..50c76a6 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -13,7 +13,9 @@
 import com.android.tools.r8.shaking.KeepReason.ReflectiveUseFrom;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
 
@@ -293,6 +295,8 @@
   }
 
   public boolean equalsNoAnnotations(K other) {
+    assert annotationsInfo.isTopOrBottom();
+    assert typeAnnotationsInfo.isTopOrBottom();
     return getClass() == other.getClass()
         && (allowAccessModification == other.internalIsAccessModificationAllowed())
         && (allowAccessModificationForTesting
@@ -301,10 +305,14 @@
         && (allowOptimization == other.internalIsOptimizationAllowed())
         && (allowShrinking == other.internalIsShrinkingAllowed())
         && (allowSignatureRemoval == other.internalIsSignatureRemovalAllowed())
-        && (checkDiscarded == other.internalIsCheckDiscardedEnabled());
+        && (checkDiscarded == other.internalIsCheckDiscardedEnabled())
+        && (annotationsInfo == other.internalAnnotationsInfo())
+        && (typeAnnotationsInfo == other.internalTypeAnnotationsInfo());
   }
 
   public int hashCodeNoAnnotations() {
+    assert annotationsInfo.isTopOrBottom();
+    assert typeAnnotationsInfo.isTopOrBottom();
     int hash = 0;
     int index = 0;
     hash += bit(allowAccessModification, index++);
@@ -313,12 +321,28 @@
     hash += bit(allowOptimization, index++);
     hash += bit(allowShrinking, index++);
     hash += bit(allowSignatureRemoval, index++);
-    hash += bit(checkDiscarded, index);
+    hash += bit(checkDiscarded, index++);
+    hash += bit(annotationsInfo.isTop(), index++);
+    hash += bit(typeAnnotationsInfo.isTop(), index);
     return hash;
   }
 
+  public List<String> lines() {
+    List<String> lines = new ArrayList<>();
+    lines.add("allowAccessModification: " + allowAccessModification);
+    lines.add("allowAccessModificationForTesting: " + allowAccessModificationForTesting);
+    lines.add("allowMinification: " + allowMinification);
+    lines.add("allowOptimization: " + allowOptimization);
+    lines.add("allowShrinking: " + allowShrinking);
+    lines.add("allowSignatureRemoval: " + allowSignatureRemoval);
+    lines.add("checkDiscarded: " + checkDiscarded);
+    lines.add("annotationsInfo: " + annotationsInfo);
+    lines.add("typeAnnotationsInfo: " + typeAnnotationsInfo);
+    return lines;
+  }
+
   protected int numberOfBooleans() {
-    return 7;
+    return 9;
   }
 
   protected int bit(boolean bool, int index) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java
index ada990b..356ef8c 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java
@@ -43,13 +43,14 @@
     private final Map<Equivalence.Wrapper<KeepFieldInfo>, Equivalence.Wrapper<KeepFieldInfo>>
         keepFieldInfos = new LRUCache<>(CACHE_SIZE);
 
-    private boolean hasKeepInfoAnnotationInfo(KeepInfo<?, ?> info) {
-      return !info.internalAnnotationsInfo().isTop() || !info.internalTypeAnnotationsInfo().isTop();
+    private boolean hasNonTrivialKeepInfoAnnotationInfo(KeepInfo<?, ?> info) {
+      return !info.internalAnnotationsInfo().isTopOrBottom()
+          || !info.internalTypeAnnotationsInfo().isTopOrBottom();
     }
 
     @Override
     public KeepClassInfo canonicalizeKeepClassInfo(KeepClassInfo classInfo) {
-      if (hasKeepInfoAnnotationInfo(classInfo)) {
+      if (hasNonTrivialKeepInfoAnnotationInfo(classInfo)) {
         return classInfo;
       }
       return keepClassInfos.computeIfAbsent(classEquivalence.wrap(classInfo), w -> w).get();
@@ -57,8 +58,8 @@
 
     @Override
     public KeepMethodInfo canonicalizeKeepMethodInfo(KeepMethodInfo methodInfo) {
-      if (hasKeepInfoAnnotationInfo(methodInfo)
-          || !methodInfo.internalParameterAnnotationsInfo().isTop()) {
+      if (hasNonTrivialKeepInfoAnnotationInfo(methodInfo)
+          || !methodInfo.internalParameterAnnotationsInfo().isTopOrBottom()) {
         return methodInfo;
       }
       return keepMethodInfos.computeIfAbsent(methodEquivalence.wrap(methodInfo), w -> w).get();
@@ -66,7 +67,7 @@
 
     @Override
     public KeepFieldInfo canonicalizeKeepFieldInfo(KeepFieldInfo fieldInfo) {
-      if (hasKeepInfoAnnotationInfo(fieldInfo)) {
+      if (hasNonTrivialKeepInfoAnnotationInfo(fieldInfo)) {
         return fieldInfo;
       }
       return keepFieldInfos.computeIfAbsent(fieldEquivalence.wrap(fieldInfo), w -> w).get();
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index d89da04..3a4b7e4 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -264,6 +264,12 @@
       InternalOptions options,
       Timing timing);
 
+  public abstract KeepInfoCollectionExported exportToCollection();
+
+  public void exportToDirectory(Path directory) throws IOException {
+    exportToCollection().exportToDirectory(directory);
+  }
+
   public abstract KeepInfoCollection mutate(Consumer<MutableKeepInfoCollection> mutator);
 
   public abstract void writeToDirectory(Path directory) throws IOException;
@@ -398,6 +404,11 @@
       return result;
     }
 
+    @Override
+    public KeepInfoCollectionExported exportToCollection() {
+      return new KeepInfoCollectionExported(keepClassInfo, keepMethodInfo, keepFieldInfo);
+    }
+
     private Map<DexType, KeepClassInfo> rewriteClassInfo(
         NonIdentityGraphLens lens, InternalOptions options, Timing timing) {
       timing.begin("Rewrite class info");
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
new file mode 100644
index 0000000..92aaad2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
@@ -0,0 +1,203 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class KeepInfoCollectionExported {
+
+  private final Map<TypeReference, ExportedClassInfo> classInfos;
+
+  static class ExportedClassInfo {
+
+    private final KeepClassInfo keepClassInfo;
+    private final Map<FieldReference, KeepFieldInfo> fieldInfos;
+    private final Map<MethodReference, KeepMethodInfo> methodInfos;
+
+    ExportedClassInfo(
+        KeepClassInfo keepClassInfo,
+        Map<FieldReference, KeepFieldInfo> fieldInfos,
+        Map<MethodReference, KeepMethodInfo> methodInfos) {
+      this.keepClassInfo = keepClassInfo;
+      this.fieldInfos = fieldInfos;
+      this.methodInfos = methodInfos;
+    }
+
+    public List<String> lines() {
+      List<String> lines = new ArrayList<>();
+      if (keepClassInfo != null) {
+        lines.add("class info");
+        lines.addAll(keepClassInfo.lines());
+      }
+      List<FieldReference> fieldRefs = new ArrayList<>(fieldInfos.keySet());
+      fieldRefs.sort(Comparator.comparing(FieldReference::toSourceString));
+      for (FieldReference fieldRef : fieldRefs) {
+        lines.add("field " + fieldRef.toSourceString());
+        lines.addAll(fieldInfos.get(fieldRef).lines());
+      }
+      List<MethodReference> methodRefs = new ArrayList<>(methodInfos.keySet());
+      methodRefs.sort(Comparator.comparing(MethodReference::toSourceString));
+      for (MethodReference methodRef : methodRefs) {
+        lines.add("method " + methodRef.toSourceString());
+        lines.addAll(methodInfos.get(methodRef).lines());
+      }
+      return lines;
+    }
+
+    static Builder builder() {
+      return new Builder();
+    }
+
+    static class Builder {
+
+      private KeepClassInfo keepClassInfo;
+      private final Map<FieldReference, KeepFieldInfo> fieldInfos = new HashMap<>();
+      private final Map<MethodReference, KeepMethodInfo> methodInfos = new HashMap<>();
+
+      public void setKeepClassInfo(KeepClassInfo keepClassInfo) {
+        this.keepClassInfo = keepClassInfo;
+      }
+
+      public void putFieldInfo(FieldReference fieldReference, KeepFieldInfo keepFieldInfo) {
+        fieldInfos.put(fieldReference, keepFieldInfo);
+      }
+
+      public void putMethodInfo(MethodReference methodReference, KeepMethodInfo keepMethodInfo) {
+        methodInfos.put(methodReference, keepMethodInfo);
+      }
+
+      public ExportedClassInfo build() {
+        return new ExportedClassInfo(
+            keepClassInfo, ImmutableMap.copyOf(fieldInfos), ImmutableMap.copyOf(methodInfos));
+      }
+    }
+  }
+
+  public KeepInfoCollectionExported(
+      Map<DexType, KeepClassInfo> keepClassInfo,
+      Map<DexMethod, KeepMethodInfo> keepMethodInfo,
+      Map<DexField, KeepFieldInfo> keepFieldInfo) {
+    Map<TypeReference, ExportedClassInfo.Builder> classInfosBuilder = new HashMap<>();
+    keepClassInfo.forEach(
+        (type, info) -> {
+          ExportedClassInfo.Builder builder = getBuilder(classInfosBuilder, type.asTypeReference());
+          builder.setKeepClassInfo(export(info));
+        });
+    keepMethodInfo.forEach(
+        (method, info) -> {
+          ExportedClassInfo.Builder builder =
+              getBuilder(classInfosBuilder, method.getHolderType().asTypeReference());
+          builder.putMethodInfo(method.asMethodReference(), export(info));
+        });
+    keepFieldInfo.forEach(
+        (field, info) -> {
+          ExportedClassInfo.Builder builder =
+              getBuilder(classInfosBuilder, field.getHolderType().asTypeReference());
+          builder.putFieldInfo(field.asFieldReference(), export(info));
+        });
+    ImmutableMap.Builder<TypeReference, ExportedClassInfo> mapBuilder = ImmutableMap.builder();
+    classInfosBuilder.forEach((typeRef, builder) -> mapBuilder.put(typeRef, builder.build()));
+    classInfos = mapBuilder.build();
+  }
+
+  private ExportedClassInfo.Builder getBuilder(
+      Map<TypeReference, ExportedClassInfo.Builder> classInfosBuilder,
+      TypeReference typeReference) {
+    return classInfosBuilder.computeIfAbsent(typeReference, t -> ExportedClassInfo.builder());
+  }
+
+  public void exportToDirectory(Path directory) throws IOException {
+    for (Entry<TypeReference, ExportedClassInfo> entry : classInfos.entrySet()) {
+      TypeReference typeReference = entry.getKey();
+      ExportedClassInfo exportedClassInfo = entry.getValue();
+      String binaryName =
+          DescriptorUtils.getClassBinaryNameFromDescriptor(typeReference.getDescriptor());
+      Files.createDirectories(directory.resolve(binaryName).getParent());
+      Files.write(
+          directory.resolve(binaryName), exportedClassInfo.lines(), StandardOpenOption.CREATE);
+    }
+  }
+
+  static boolean noAnnotation(KeepInfo<?, ?> info) {
+    return info.internalAnnotationsInfo().isTopOrBottom()
+        && info.internalTypeAnnotationsInfo().isTopOrBottom();
+  }
+
+  private KeepClassInfo export(KeepClassInfo classInfo) {
+    if (noAnnotation(classInfo)) {
+      return classInfo;
+    }
+    KeepClassInfo.Builder builder = classInfo.builder();
+    builder.getAnnotationsInfo().setExport();
+    builder.getTypeAnnotationsInfo().setExport();
+    return builder.build();
+  }
+
+  private KeepFieldInfo export(KeepFieldInfo fieldInfo) {
+    if (noAnnotation(fieldInfo)) {
+      return fieldInfo;
+    }
+    KeepFieldInfo.Builder builder = fieldInfo.builder();
+    builder.getAnnotationsInfo().setExport();
+    builder.getTypeAnnotationsInfo().setExport();
+    return builder.build();
+  }
+
+  private KeepMethodInfo export(KeepMethodInfo methodInfo) {
+    if (noAnnotation(methodInfo) && methodInfo.internalParameterAnnotationsInfo().isTopOrBottom()) {
+      return methodInfo;
+    }
+    KeepMethodInfo.Builder builder = methodInfo.builder();
+    builder.getAnnotationsInfo().setExport();
+    builder.getTypeAnnotationsInfo().setExport();
+    builder.getParameterAnnotationsInfo().setExport();
+    return builder.build();
+  }
+
+  public static class KeepAnnotationCollectionInfoExported extends KeepAnnotationCollectionInfo {
+
+    static KeepAnnotationCollectionInfo createExported(KeepAnnotationCollectionInfo from) {
+      if (from.isTopOrBottom()) {
+        return from;
+      }
+      return new KeepAnnotationCollectionInfoExported(from.toString());
+    }
+
+    private final String export;
+
+    KeepAnnotationCollectionInfoExported(String export) {
+      this.export = export;
+    }
+
+    @Override
+    public String toString() {
+      return export;
+    }
+
+    @Override
+    public boolean isRemovalAllowed(DexAnnotation annotation) {
+      throw new Unreachable();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
index 887dff9..5e2f8d4 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ProgramMember;
 import com.android.tools.r8.shaking.KeepMemberInfo.Builder;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
 
 /** Immutable keep requirements for a member. */
 @SuppressWarnings("BadImport")
@@ -105,6 +106,13 @@
   }
 
   @Override
+  public List<String> lines() {
+    List<String> lines = super.lines();
+    lines.add("allowValuePropagation: " + allowValuePropagation);
+    return lines;
+  }
+
+  @Override
   protected int numberOfBooleans() {
     return super.numberOfBooleans() + 1;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
index 0711d8a..f121490 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.KeepAnnotationCollectionInfo.RetentionInfo;
+import java.util.List;
 
 /** Immutable keep requirements for a method. */
 public class KeepMethodInfo extends KeepMemberInfo<KeepMethodInfo.Builder, KeepMethodInfo> {
@@ -282,6 +283,7 @@
 
   @Override
   public boolean equalsNoAnnotations(KeepMethodInfo other) {
+    assert parameterAnnotationsInfo.isTopOrBottom();
     return super.equalsNoAnnotations(other)
         && allowThrowsRemoval == other.internalIsThrowsRemovalAllowed()
         && allowClassInlining == other.internalIsClassInliningAllowed()
@@ -300,11 +302,13 @@
         && allowUnusedArgumentOptimization == other.internalIsUnusedArgumentOptimizationAllowed()
         && allowUnusedReturnValueOptimization
             == other.internalIsUnusedReturnValueOptimizationAllowed()
-        && allowParameterNamesRemoval == other.internalIsParameterNamesRemovalAllowed();
+        && allowParameterNamesRemoval == other.internalIsParameterNamesRemovalAllowed()
+        && parameterAnnotationsInfo == other.internalParameterAnnotationsInfo();
   }
 
   @Override
   public int hashCodeNoAnnotations() {
+    assert parameterAnnotationsInfo.isTopOrBottom();
     int hash = super.hashCodeNoAnnotations();
     int index = super.numberOfBooleans();
     hash += bit(allowThrowsRemoval, index++);
@@ -322,10 +326,34 @@
     hash += bit(allowSingleCallerInlining, index++);
     hash += bit(allowUnusedArgumentOptimization, index++);
     hash += bit(allowUnusedReturnValueOptimization, index++);
-    hash += bit(allowParameterNamesRemoval, index);
+    hash += bit(allowParameterNamesRemoval, index++);
+    hash += bit(parameterAnnotationsInfo.isTop(), index);
     return hash;
   }
 
+  @Override
+  public List<String> lines() {
+    List<String> lines = super.lines();
+    lines.add("allowThrowsRemoval: " + allowThrowsRemoval);
+    lines.add("allowClassInlining: " + allowClassInlining);
+    lines.add("allowClosedWorldReasoning: " + allowClosedWorldReasoning);
+    lines.add("allowCodeReplacement: " + allowCodeReplacement);
+    lines.add("allowConstantArgumentOptimization: " + allowConstantArgumentOptimization);
+    lines.add("allowInlining: " + allowInlining);
+    lines.add("allowMethodStaticizing: " + allowMethodStaticizing);
+    lines.add("allowParameterRemoval: " + allowParameterRemoval);
+    lines.add("allowParameterReordering: " + allowParameterReordering);
+    lines.add("allowParameterTypeStrengthening: " + allowParameterTypeStrengthening);
+    lines.add("allowReprocessing: " + allowReprocessing);
+    lines.add("allowReturnTypeStrengthening: " + allowReturnTypeStrengthening);
+    lines.add("allowSingleCallerInlining: " + allowSingleCallerInlining);
+    lines.add("allowUnusedArgumentOptimization: " + allowUnusedArgumentOptimization);
+    lines.add("allowUnusedReturnValueOptimization: " + allowUnusedReturnValueOptimization);
+    lines.add("allowParameterNamesRemoval: " + allowParameterNamesRemoval);
+    lines.add("parameterAnnotationsInfo: " + parameterAnnotationsInfo);
+    return lines;
+  }
+
   public static class Builder extends KeepMemberInfo.Builder<Builder, KeepMethodInfo> {
 
     private boolean allowThrowsRemoval;
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
index 50a016c..c186bb9 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
@@ -67,7 +67,7 @@
         command.getReporter(), () -> runInternal(command, options));
   }
 
-  private static void runInternal(TraceReferencesCommand command, InternalOptions options)
+  static void runInternal(TraceReferencesCommand command, InternalOptions options)
       throws IOException, ResourceException {
     AndroidApp.Builder builder = AndroidApp.builder();
     command.getLibrary().forEach(builder::addLibraryResourceProvider);
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
new file mode 100644
index 0000000..5c0b302
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.tracereferences;
+
+import com.android.tools.r8.ResourceException;
+import java.io.IOException;
+
+// Provide access to some package private APIs.
+public class TraceReferencesBridge {
+
+  public static TraceReferencesCommand makeCommand(TraceReferencesCommand.Builder builder) {
+    return builder.makeCommand();
+  }
+
+  public static void runInternal(TraceReferencesCommand command)
+      throws IOException, ResourceException {
+    TraceReferences.runInternal(command, command.getInternalOptions());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
index 28deeb3..429b912 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
@@ -337,7 +337,7 @@
       return this;
     }
 
-    private TraceReferencesCommand makeCommand() {
+    TraceReferencesCommand makeCommand() {
       if (isPrintHelp() || isPrintVersion()) {
         return new TraceReferencesCommand(isPrintHelp(), isPrintVersion());
       }
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 9ce000f..6672f02 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -806,7 +806,13 @@
       ZipOutputStream dexArchiveOutputStream,
       ProgramResource programResource)
       throws ResourceException, IOException {
-    byte[] bytes = StreamUtils.streamToByteArrayClose(programResource.getByteStream());
+    byte[] bytes;
+    if (programResource instanceof OneShotByteResource) {
+      OneShotByteResource oneShotByteResource = (OneShotByteResource) programResource;
+      bytes = oneShotByteResource.getBytesForDumpInput();
+    } else {
+      bytes = StreamUtils.streamToByteArrayClose(programResource.getByteStream());
+    }
     if (programResource.getKind() == Kind.CF) {
       Set<String> classDescriptors = programResource.getClassDescriptors();
       String classDescriptor =
@@ -1424,6 +1430,14 @@
       }
     }
 
+    public List<ClassFileResourceProvider> getLibraryResourceProviders() {
+      return libraryResourceProviders;
+    }
+
+    public List<ClassFileResourceProvider> getClasspathResourceProviders() {
+      return classpathResourceProviders;
+    }
+
     public List<ProgramResourceProvider> getProgramResourceProviders() {
       ensureAllResourcesAreInProviders();
       return programResourceProviders;
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java b/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
index f6c7269..a5aadef 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
@@ -34,6 +34,22 @@
         .accept(new Object[] {isolatedSplits});
   }
 
+  static void setupResourceShrinking(
+      Path androidResourcesInput, Path androidResourcesOutput, Object builder) {
+    try {
+      Class<?> androidResourceProvider =
+          Class.forName("com.android.tools.r8.AndroidResourceProvider");
+      Class<?> androidResourceConsumer =
+          Class.forName("com.android.tools.r8.AndroidResourceConsumer");
+      getReflectiveBuilderMethod(builder, "setAndroidResourceProvider", androidResourceProvider)
+          .accept(new Object[] {createAndroidResourceProvider(androidResourcesInput)});
+      getReflectiveBuilderMethod(builder, "setAndroidResourceConsumer", androidResourceConsumer)
+          .accept(new Object[] {createAndroidResourceConsumer(androidResourcesOutput)});
+    } catch (ClassNotFoundException e) {
+      // Ignore
+    }
+  }
+
   static void addArtProfilesForRewriting(Object builder, Map<Path, Path> artProfileFiles) {
     try {
       Class<?> artProfileProviderClass =
@@ -62,38 +78,44 @@
         .accept(new Object[] {createStartupProfileProviders(startupProfileFiles)});
   }
 
-  static Object createArtProfileProvider(Path artProfile) {
-    Object[] artProfileProvider = new Object[1];
+  static Object callReflectiveDumpUtilsMethodWithPath(Path path, String method) {
+    Object[] returnObject = new Object[1];
     boolean found =
         callReflectiveUtilsMethod(
-            "createArtProfileProviderFromDumpFile",
+            method,
             new Class<?>[] {Path.class},
-            fn -> artProfileProvider[0] = fn.apply(new Object[] {artProfile}));
+            fn -> returnObject[0] = fn.apply(new Object[] {path}));
     if (!found) {
       System.out.println(
-          "Unable to add art profiles as input. "
-              + "Method createArtProfileProviderFromDumpFile() was not found.");
+          "Unable to call invoke method on path "
+              + path
+              + ". "
+              + "Method "
+              + method
+              + "() was not found.");
       return null;
     }
-    System.out.println(artProfileProvider[0]);
-    return artProfileProvider[0];
+    return returnObject[0];
+  }
+
+  static Object createAndroidResourceProvider(Path resourceInput) {
+    return callReflectiveDumpUtilsMethodWithPath(
+        resourceInput, "createAndroidResourceProviderFromDumpFile");
+  }
+
+  static Object createAndroidResourceConsumer(Path resourceOutput) {
+    return callReflectiveDumpUtilsMethodWithPath(
+        resourceOutput, "createAndroidResourceConsumerFromDumpFile");
+  }
+
+  static Object createArtProfileProvider(Path artProfile) {
+    return callReflectiveDumpUtilsMethodWithPath(
+        artProfile, "createArtProfileProviderFromDumpFile");
   }
 
   static Object createResidualArtProfileConsumer(Path residualArtProfile) {
-    Object[] residualArtProfileConsumer = new Object[1];
-    boolean found =
-        callReflectiveUtilsMethod(
-            "createResidualArtProfileConsumerFromDumpFile",
-            new Class<?>[] {Path.class},
-            fn -> residualArtProfileConsumer[0] = fn.apply(new Object[] {residualArtProfile}));
-    if (!found) {
-      System.out.println(
-          "Unable to add art profiles as input. "
-              + "Method createResidualArtProfileConsumerFromDumpFile() was not found.");
-      return null;
-    }
-    System.out.println(residualArtProfileConsumer[0]);
-    return residualArtProfileConsumer[0];
+    return callReflectiveDumpUtilsMethodWithPath(
+        residualArtProfile, "createResidualArtProfileConsumerFromDumpFile");
   }
 
   static Collection<Object> createStartupProfileProviders(List<Path> startupProfileFiles) {
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
index cd01544..9112b77 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
@@ -64,7 +64,7 @@
           "--startup-profile");
 
   private static final List<String> VALID_OPTIONS_WITH_TWO_OPERANDS =
-      Arrays.asList("--art-profile", "--feature-jar");
+      Arrays.asList("--art-profile", "--feature-jar", "--android-resources");
 
   private static boolean FileUtils_isArchive(Path path) {
     String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
@@ -91,6 +91,8 @@
     List<Path> mainDexRulesFiles = new ArrayList<>();
     Map<Path, Path> artProfileFiles = new LinkedHashMap<>();
     List<Path> startupProfileFiles = new ArrayList<>();
+    Path androidResourcesInput = null;
+    Path androidResourcesOutput = null;
     int minApi = 1;
     int threads = -1;
     boolean enableMissingLibraryApiModeling = false;
@@ -202,6 +204,12 @@
               artProfileFiles.put(Paths.get(firstOperand), Paths.get(secondOperand));
               break;
             }
+          case "--android-resources":
+            {
+              androidResourcesInput = Paths.get(firstOperand);
+              androidResourcesOutput = Paths.get(secondOperand);
+              break;
+            }
           case "--feature-jar":
             {
               Path featureIn = Paths.get(firstOperand);
@@ -237,6 +245,9 @@
     if (desugaredLibJson != null) {
       commandBuilder.addDesugaredLibraryConfiguration(readAllBytesJava7(desugaredLibJson));
     }
+    if (androidResourcesInput != null) {
+      setupResourceShrinking(androidResourcesInput, androidResourcesOutput, commandBuilder);
+    }
     if (desugaredLibKeepRuleConsumer != null) {
       commandBuilder.setDesugaredLibraryKeepRuleConsumer(desugaredLibKeepRuleConsumer);
     }
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java b/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java
index 4356671..ef013f6 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java
@@ -4,6 +4,10 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.AndroidResourceConsumer;
+import com.android.tools.r8.AndroidResourceProvider;
+import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
+import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
 import com.android.tools.r8.KeepMethodForCompileDump;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
@@ -34,6 +38,16 @@
   }
 
   @KeepMethodForCompileDump
+  static AndroidResourceProvider createAndroidResourceProviderFromDumpFile(Path resourceInput) {
+    return new ArchiveProtoAndroidResourceProvider(resourceInput);
+  }
+
+  @KeepMethodForCompileDump
+  static AndroidResourceConsumer createAndroidResourceConsumerFromDumpFile(Path resourceOutput) {
+    return new ArchiveProtoAndroidResourceConsumer(resourceOutput);
+  }
+
+  @KeepMethodForCompileDump
   static StartupProfileProvider createStartupProfileProviderFromDumpFile(Path path) {
     return new StartupProfileProvider() {
 
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 5c2eec5..7685a95 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
@@ -7,9 +7,13 @@
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 
 import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.DataDirectoryResource;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.Origin;
@@ -37,7 +41,8 @@
  * the zip-file descriptor throughout compilation and close at the end of reading. It must also be
  * safe to reopen it as currently our own tests reuse AndroidApp structures.
  */
-class InternalArchiveClassFileProvider implements ClassFileResourceProvider, AutoCloseable {
+class InternalArchiveClassFileProvider
+    implements ClassFileResourceProvider, DataResourceProvider, AutoCloseable {
   private final Path path;
   private final Origin origin;
   private final Set<String> descriptors = new HashSet<>();
@@ -130,4 +135,30 @@
     return getOpenZipFile()
         .getEntry(descriptor.substring(1, descriptor.length() - 1) + CLASS_EXTENSION);
   }
+
+  @Override
+  public DataResourceProvider getDataResourceProvider() {
+    return this;
+  }
+
+  @Override
+  public void accept(Visitor resourceBrowser) throws ResourceException {
+    try {
+      ZipUtils.iterWithZipFile(
+          path,
+          (zipFile, entry) -> {
+            if (!ZipUtils.isClassFile(entry.getName())) {
+              if (entry.isDirectory()) {
+                resourceBrowser.visit(DataDirectoryResource.fromZip(zipFile, entry));
+              } else {
+                resourceBrowser.visit(DataEntryResource.fromZip(zipFile, entry));
+              }
+            }
+          });
+    } catch (IOException e) {
+      throw new ResourceException(
+          origin,
+          new CompilationError("I/O exception while reading '" + path + "': " + e.getMessage(), e));
+    }
+  }
 }
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 da0da31..7d9439b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -114,11 +114,14 @@
 import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 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;
@@ -1019,6 +1022,9 @@
   private final ArtProfileOptions artProfileOptions = new ArtProfileOptions(this);
   private final StartupOptions startupOptions = new StartupOptions();
   private final InstrumentationOptions instrumentationOptions;
+  public final R8PartialCompilationOptions r8PartialCompilationOptions =
+      new R8PartialCompilationOptions(
+          System.getProperty("com.android.tools.r8.r8PartialCompilation"));
   public final TestingOptions testing = new TestingOptions();
 
   public List<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
@@ -2276,6 +2282,39 @@
     }
   }
 
+  public static class R8PartialCompilationOptions {
+    public boolean enabled;
+    public Path tempDir = null;
+    public Predicate<String> isR8 = null;
+
+    R8PartialCompilationOptions(String partialR8) {
+      this.enabled = partialR8 != null;
+      if (this.enabled) {
+        final List<String> prefixes = Splitter.on(",").splitToList(partialR8);
+        this.isR8 =
+            name -> {
+              for (int i = 0; i < prefixes.size(); i++) {
+                if (name.startsWith(prefixes.get(i))) {
+                  return true;
+                }
+              }
+              return false;
+            };
+      }
+    }
+
+    public synchronized Path getTemp() throws IOException {
+      if (tempDir == null) {
+        tempDir = Files.createTempDirectory("r8PartialCompilation");
+      }
+      return tempDir;
+    }
+
+    public Path getDumpFile() throws IOException {
+      return getTemp().resolve("dump.zip");
+    }
+  }
+
   public static class TestingOptions {
 
     public boolean enableEmbeddedKeepAnnotations =
diff --git a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
index 494d500..76be7da 100644
--- a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
+++ b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
@@ -54,6 +54,11 @@
     return result;
   }
 
+  public byte[] getBytesForDumpInput() {
+    assert bytes != null;
+    return bytes;
+  }
+
   @Override
   public Set<String> getClassDescriptors() {
     return classDescriptors;
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 2b11a9f..fb33e96 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -89,18 +89,42 @@
     iter(Paths.get(zipFileStr), handler);
   }
 
-  public static void iter(Path zipFilePath, OnEntryHandler handler) throws IOException {
+  public static void iter(Path zipFile, OnEntryHandler handler) throws IOException {
+    iterWithZipFileAndInputStream(zipFile, (zip, entry, input) -> handler.onEntry(entry, input));
+  }
+
+  public interface OnEntryHandlerWithZipFile {
+    void onEntry(ZipFile zip, ZipEntry entry) throws IOException;
+  }
+
+  public interface OnEntryHandlerWithZipFileAndInputStream {
+    void onEntry(ZipFile zip, ZipEntry entry, InputStream input) throws IOException;
+  }
+
+  public static void iterWithZipFileAndInputStream(
+      Path zipFilePath, OnEntryHandlerWithZipFileAndInputStream handler) throws IOException {
     try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
         try (InputStream entryStream = zipFile.getInputStream(entry)) {
-          handler.onEntry(entry, entryStream);
+          handler.onEntry(zipFile, entry, entryStream);
         }
       }
     }
   }
 
+  public static void iterWithZipFile(Path zipFilePath, OnEntryHandlerWithZipFile handler)
+      throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        handler.onEntry(zipFile, entry);
+      }
+    }
+  }
+
   public static void iter(Path zipFilePath, Consumer<ZipEntry> entryConsumer) throws IOException {
     try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 875288c..fccc50d 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -57,7 +57,7 @@
 
   private final List<Supplier<InputStream>> manifestProviders = new ArrayList<>();
   private final Map<String, Supplier<InputStream>> resfileProviders = new HashMap<>();
-  private final Map<ResourceTable, FeatureSplit> resourceTables = new HashMap<>();
+  private final Map<FeatureSplit, ResourceTable> resourceTables = new HashMap<>();
   private ClassReferenceCallback enqueuerCallback;
   private Map<Integer, List<String>> resourceIdToXmlFiles;
   private Set<String> packageNames;
@@ -138,7 +138,7 @@
     // feature.
     if (packageNames == null) {
       packageNames = new HashSet<>();
-      for (ResourceTable resourceTable : resourceTables.keySet()) {
+      for (ResourceTable resourceTable : resourceTables.values()) {
         for (Package aPackage : resourceTable.getPackageList()) {
           packageNames.add(aPackage.getPackageName());
         }
@@ -165,7 +165,7 @@
 
   public void addResourceTable(InputStream inputStream, FeatureSplit featureSplit) {
     this.resourceTables.put(
-        r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream, true), featureSplit);
+        featureSplit, r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream, true));
   }
 
   public R8ResourceShrinkerModel getR8ResourceShrinkerModel() {
@@ -191,7 +191,7 @@
   }
 
   public void setupReferences() {
-    for (ResourceTable resourceTable : resourceTables.keySet()) {
+    for (ResourceTable resourceTable : resourceTables.values()) {
       new ProtoResourcesGraphBuilder(this::getXmlOrResFileBytes, unused -> resourceTable)
           .buildGraph(r8ResourceShrinkerModel);
     }
@@ -207,12 +207,11 @@
 
     Map<FeatureSplit, ResourceTable> shrunkenTables = new IdentityHashMap<>();
     resourceTables.forEach(
-        (resourceTable, featureSplit) ->
-            shrunkenTables.put(
-                featureSplit,
-                ResourceTableUtilKt.nullOutEntriesWithIds(
-                    resourceTable, resourceIdsToRemove, true)));
-
+        (featureSplit, resourceTable) -> {
+          shrunkenTables.put(
+              featureSplit,
+              ResourceTableUtilKt.nullOutEntriesWithIds(resourceTable, resourceIdsToRemove, true));
+        });
     return new ShrinkerResult(resEntriesToKeep, shrunkenTables);
   }
 
@@ -311,7 +310,7 @@
   public Map<Integer, List<String>> getResourceIdToXmlFiles() {
     if (resourceIdToXmlFiles == null) {
       resourceIdToXmlFiles = new HashMap<>();
-      for (ResourceTable resourceTable : resourceTables.keySet()) {
+      for (ResourceTable resourceTable : resourceTables.values()) {
         for (Package packageEntry : resourceTable.getPackageList()) {
           for (Resources.Type type : packageEntry.getTypeList()) {
             for (Entry entry : type.getEntryList()) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
index ac11021..1a2a5db 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
@@ -30,7 +30,7 @@
     test(testForR8Compat(parameters.getBackend()));
   }
 
-  private <T extends R8TestBuilder<T>> void test(T testBuilder) throws Exception {
+  private <B extends R8TestBuilder<?, ?, ?>> void test(B testBuilder) throws Exception {
     testBuilder
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
index b11215a..09c5556 100644
--- a/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
+++ b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
@@ -63,7 +63,7 @@
         .compile();
   }
 
-  private void configure(R8TestBuilder<?> testBuilder) {
+  private void configure(R8TestBuilder<?, ?, ?> testBuilder) {
     testBuilder
         .addClasspathFiles(outDirectory.resolve("classpath.jar"))
         .addLibraryFiles(outDirectory.resolve("library.jar"))
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java
index a9dd2b2..3a12b3a 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java
@@ -24,7 +24,7 @@
     this.syntheticVersionNumber = syntheticVersionNumber;
   }
 
-  public void addRuntime(R8TestBuilder<?> testBuilder) {
+  public void addRuntime(R8TestBuilder<?, ?, ?> testBuilder) {
     Path runtimeDir = Paths.get(ToolHelper.PROTO_RUNTIME_DIR, runtimeName);
     testBuilder
         .addProgramFiles(runtimeDir.resolve("libprotobuf_lite.jar"))
@@ -58,7 +58,7 @@
   }
 
   // The class com.google.protobuf.ProtoMessage is not present in newer proto lite runtimes.
-  public void workaroundProtoMessageRemoval(R8TestBuilder<?> testBuilder) {
+  public void workaroundProtoMessageRemoval(R8TestBuilder<?, ?, ?> testBuilder) {
     if (isNewerThanOrEqualTo(ProtoRuntime.EDITION2023)) {
       testBuilder.addDontWarn("com.google.protobuf.ProtoMessage");
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B369670481.java b/src/test/java/com/android/tools/r8/ir/optimize/B369670481.java
new file mode 100644
index 0000000..9eb062c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B369670481.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2024, 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;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+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 B369670481 extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(ConcurrentModificationException.class);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isCfRuntime(),
+            rr -> rr.assertFailureWithErrorThatThrows(ConcurrentModificationException.class),
+            rr -> rr.assertSuccessWithOutputLines("2"));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isCfRuntime(),
+            rr -> rr.assertFailureWithErrorThatThrows(ConcurrentModificationException.class),
+            rr -> rr.assertSuccessWithOutputLines("2"));
+  }
+
+  static class Main {
+
+    public static void main(String[] strArr) {
+      ArrayList<Integer> numbers = new ArrayList<>();
+      numbers.add(10);
+      numbers.add(20);
+      numbers.add(40);
+      Iterator<Integer> iterator = numbers.iterator();
+      while (iterator.hasNext()) {
+        if (iterator.next() == 40) {
+          numbers.remove(2);
+        }
+      }
+      System.out.println(numbers.size());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B370217724Test.java b/src/test/java/com/android/tools/r8/ir/optimize/B370217724Test.java
new file mode 100644
index 0000000..2e862cb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B370217724Test.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2024, 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;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
+
+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.utils.StringUtils;
+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 B370217724Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String OUTPUT_JVM8 = StringUtils.lines("8989.358669383371");
+  private static final String OUTPUT_FROM_JVM9 = StringUtils.lines("3695.708516962155");
+  // Depending on the ART host run environment these results are seen.
+  private static final String OUTPUT_ART_1 = StringUtils.lines("5753.491198916323");
+  private static final String OUTPUT_ART_2 = StringUtils.lines("10192.673136265881");
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK9),
+            r -> r.assertSuccessWithOutput(OUTPUT_FROM_JVM9),
+            r -> r.assertSuccessWithOutput(OUTPUT_JVM8));
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputThatMatches(anyOf(is(OUTPUT_ART_1), is(OUTPUT_ART_2)));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isCfRuntime(),
+            r ->
+                r.assertSuccessWithOutput(
+                    parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK9)
+                        ? OUTPUT_FROM_JVM9
+                        : OUTPUT_JVM8),
+            r -> r.assertSuccessWithOutputThatMatches(anyOf(is(OUTPUT_ART_1), is(OUTPUT_ART_2))));
+  }
+
+  static class TestClass {
+    public static void main(String[] strArr) {
+      double d = 8.65068;
+      for (int i = 1; i < 240; ++i) {
+        d = 6372.8 * Math.acos(Math.cos(d));
+      }
+      System.out.println(d);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B370303498Test.java b/src/test/java/com/android/tools/r8/ir/optimize/B370303498Test.java
new file mode 100644
index 0000000..9578b71
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B370303498Test.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2024, 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;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import 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 B370303498Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        // TODO(b/370303498): Should throw ArrayIndexOutOfBoundsException.
+        .assertSuccess();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isCfRuntime(),
+            r -> r.assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class),
+            // TODO(b/370303498): Should throw ArrayIndexOutOfBoundsException.
+            SingleTestRunResult::assertSuccess);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      java.lang.reflect.Array.set(new int[2], 2, new Object());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineMappingOnSameLineTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineMappingOnSameLineTest.java
index d26643f..f4a55f5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineMappingOnSameLineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineMappingOnSameLineTest.java
@@ -47,7 +47,7 @@
   }
 
   @Override
-  public void configure(R8TestBuilder<?> builder) {
+  public void configure(R8TestBuilder<?, ?, ?> builder) {
     builder.enableInliningAnnotations();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/B369739224Test.java b/src/test/java/com/android/tools/r8/ir/optimize/string/B369739224Test.java
new file mode 100644
index 0000000..615ba60
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/B369739224Test.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.string;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import 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 B369739224Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(IndexOutOfBoundsException.class);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(IndexOutOfBoundsException.class);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        // TODO(b/369739224): Should throw IndexOutOfBoundsException.
+        .assertSuccessWithOutputLines("46");
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      String f = "";
+      int c = '.';
+      new StringBuilder().append(f, 0, c);
+      System.out.println(c);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/B370217723Test.java b/src/test/java/com/android/tools/r8/ir/optimize/string/B370217723Test.java
new file mode 100644
index 0000000..fb42612
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/B370217723Test.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.string;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+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 B370217723Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        // TODO(b/370217723): Should throw NullPointerException.
+        .assertSuccess();
+  }
+
+  static class TestClass {
+    public static void main(String[] strArr) {
+      StringBuilder b = null;
+      b.append("0900");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java b/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
index feb4819..135fc1f 100644
--- a/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
@@ -60,7 +60,7 @@
   @Test
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
-    R8TestBuilder<?> builder =
+    R8TestBuilder<?, ?, ?> builder =
         testForR8(parameters.getBackend())
             .addProgramFiles(JAR)
             .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index cf7edc4..5a548bc 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -108,7 +108,8 @@
     return this;
   }
 
-  public KeepAnnoTestBuilder applyIfR8Current(ThrowableConsumer<R8TestBuilder<?>> builderConsumer) {
+  public KeepAnnoTestBuilder applyIfR8Current(
+      ThrowableConsumer<R8TestBuilder<?, ?, ?>> builderConsumer) {
     return this;
   }
 
@@ -229,7 +230,7 @@
 
     @Override
     public KeepAnnoTestBuilder applyIfR8Current(
-        ThrowableConsumer<R8TestBuilder<?>> builderConsumer) {
+        ThrowableConsumer<R8TestBuilder<?, ?, ?>> builderConsumer) {
       builderConsumer.acceptWithRuntimeException(builder);
       return this;
     }
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 4014e78..41c2ec4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -9,7 +9,7 @@
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_6_0;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_0_20;
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLIN_DEV;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_1_0_BETA1;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -223,7 +223,7 @@
                                     kotlinParameters
                                         .getCompiler()
                                         .getCompilerVersion()
-                                        .isEqualTo(KOTLIN_DEV),
+                                        .isGreaterThanOrEqualTo(KOTLINC_2_1_0_BETA1),
                                     i -> {
                                       ClassReference sequencesKt =
                                           Reference.classFromTypeName(
diff --git a/src/test/java/com/android/tools/r8/kotlin/access/B369418242Test.java b/src/test/java/com/android/tools/r8/kotlin/access/B369418242Test.java
new file mode 100644
index 0000000..0d39931
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/access/B369418242Test.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin.access;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.KotlinTestBase;
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.kotlin.access.b369418242.pkg.B;
+import com.android.tools.r8.kotlin.access.b369418242.pkg.Helper;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class B369418242Test extends KotlinTestBase {
+
+  private static final String PKG = B369418242Test.class.getPackage().getName() + ".b369418242";
+  private static final String KOTLIN_FILE = "main";
+  private static final String MAIN_CLASS = PKG + ".MainKt";
+
+  private static KotlinCompileMemoizer compiledJars;
+
+  private final TestParameters parameters;
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    Path classpath = getStaticTemp().newFolder().toPath().resolve("out.jar");
+    ZipBuilder.builder(classpath)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(),
+            ToolHelper.getClassFileForTestClass(Helper.getClassA()),
+            ToolHelper.getClassFileForTestClass(B.class))
+        .build();
+    compiledJars =
+        getCompileMemoizer(
+                Paths.get(
+                    ToolHelper.TESTS_DIR,
+                    "java",
+                    DescriptorUtils.getBinaryNameFromJavaType(PKG),
+                    KOTLIN_FILE + FileUtils.KT_EXTENSION))
+            .configure(x -> x.addClasspathFiles(classpath));
+  }
+
+  @Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        getKotlinTestParameters()
+            .withAllCompilers()
+            .withAllTargetVersions()
+            .withLambdaGenerationInvokeDynamic()
+            .build());
+  }
+
+  public B369418242Test(TestParameters parameters, KotlinTestParameters kotlinParameters) {
+    super(kotlinParameters);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testKotlinExampleOnJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinParameters))
+        .addProgramFiles(kotlinc.getKotlinStdlibJar())
+        .addProgramClasses(Helper.getClassA(), B.class)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testKotlinExampleWithD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinParameters))
+        .addProgramFiles(kotlinc.getKotlinStdlibJar())
+        .addProgramClasses(Helper.getClassA(), B.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testKotlinExampleWithR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinParameters))
+        .addProgramFiles(kotlinc.getKotlinStdlibJar())
+        .addProgramClasses(Helper.getClassA(), B.class)
+        .addKeepMainRule(MAIN_CLASS)
+        .addDontWarn("org.jetbrains.annotations.NotNull")
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        // TODO(b/369418242): R8 should in principle preserve the IAE.
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testJavaExampleOnJVM() throws Exception {
+    parameters.assumeJvmTestParameters();
+    assumeTrue(kotlinParameters.isFirst());
+    testForJvm(parameters)
+        .addProgramClasses(Main.class, Helper.getClassA(), B.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B child = new B();
+      child.doSomething(new B());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/access/b369418242/main.kt b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/main.kt
new file mode 100644
index 0000000..cc3ea76
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/main.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+ * for details. All rights reserved. Use of this source code is governed by a
+ * BSD-style license that can be found in the LICENSE file.
+ */
+package com.android.tools.r8.kotlin.access.b369418242
+
+import com.android.tools.r8.kotlin.access.b369418242.pkg.B
+
+fun main(args: Array<String>) {
+  // Kotlinc generates bytecode for `child.doSomething((A) B())`
+  val child = B()
+  child.doSomething(B())
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/A.java b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/A.java
new file mode 100644
index 0000000..d9a39ea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/A.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin.access.b369418242.pkg;
+
+class A {
+
+  public void doSomething(A other) {}
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/B.java b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/B.java
new file mode 100644
index 0000000..ffa5079
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/B.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin.access.b369418242.pkg;
+
+public class B extends A {}
diff --git a/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/Helper.java b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/Helper.java
new file mode 100644
index 0000000..727b410
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/access/b369418242/pkg/Helper.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin.access.b369418242.pkg;
+
+public class Helper {
+
+  public static Class<?> getClassA() {
+    return A.class;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/libcore/B369739225Test.java b/src/test/java/com/android/tools/r8/libcore/B369739225Test.java
new file mode 100644
index 0000000..ee5fa8a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/libcore/B369739225Test.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2024, 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.libcore;
+
+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.utils.StringUtils;
+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 B369739225Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT_DALVIK_ART_JDK_UNTIL_11 =
+      StringUtils.lines("5", "5", "5", "5", "5");
+
+  private static final String EXPECTED_OUTPUT_JDK_FROM_17 =
+      StringUtils.lines("5", "10", "10", "10", "10");
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT_JDK_FROM_17),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT_DALVIK_ART_JDK_UNTIL_11));
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT_DALVIK_ART_JDK_UNTIL_11);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT_JDK_FROM_17),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT_DALVIK_ART_JDK_UNTIL_11));
+  }
+
+  static class TestClass {
+    long a;
+    long b;
+
+    void m() {
+      int k = 5;
+      k <<= a;
+      try {
+        String j = "sss";
+        java.io.LineNumberReader d =
+            new java.io.LineNumberReader(new java.io.CharArrayReader(j.toCharArray()));
+        d.skip(Long.MAX_VALUE);
+        a = d.getLineNumber();
+      } catch (Exception e) {
+      }
+      System.out.println(k);
+    }
+
+    public static void main(String[] n) {
+      TestClass test = new TestClass();
+      for (int i = 0; i < 5; i++) test.m();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInLibraryHierarchyTest.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInLibraryHierarchyTest.java
new file mode 100644
index 0000000..de3d6bb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInLibraryHierarchyTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
+import java.util.LinkedHashMap;
+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;
+
+// Test for B/367915233.
+@RunWith(Parameterized.class)
+public class MemberRebindingProtectedInLibraryHierarchyTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector ->
+                assertTrue(
+                    inspector
+                        .clazz(TestClass.class)
+                        .mainMethod()
+                        .streamInstructions()
+                        .filter(InstructionSubject::isInvokeVirtual)
+                        .map(
+                            instruction -> ((InvokeInstructionSubject) instruction).invokedMethod())
+                        .anyMatch(
+                            method ->
+                                (parameters.isCfRuntime()
+                                        ? method
+                                            .getHolderType()
+                                            .toSourceString()
+                                            .equals("java.util.HashMap")
+                                        : method
+                                            .getHolderType()
+                                            .toSourceString()
+                                            .equals("java.lang.Object"))
+                                    && method.getName().toString().equals("clone"))));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new LinkedHashMap<>().clone();
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyInvalidInputTest.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyInvalidInputTest.java
new file mode 100644
index 0000000..886da04
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyInvalidInputTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.memberrebinding.protectedaccess.p1.ClassWithProtectedField;
+import com.android.tools.r8.memberrebinding.protectedaccess.p2.SubClassOfClassWithProtectedField;
+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 MemberRebindingProtectedInProgramHierachyInvalidInputTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addProgramClasses(SubClassOfClassWithProtectedField.class, TestClass.class)
+        .addProgramClassFileData(getTransformedClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.getCfRuntime().isNewerThanOrEqual(CfVm.JDK21),
+            r -> r.assertFailureWithErrorThatThrows(VerifyError.class),
+            parameters.getCfRuntime().getVm() == CfVm.JDK11,
+            SingleTestRunResult::assertFailure, // JDK-11 either crash or report VerifyError.
+            r -> r.assertFailureWithOutputThatMatches(containsString("SIGSEGV")));
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(SubClassOfClassWithProtectedField.class, TestClass.class)
+        .addProgramClassFileData(getTransformedClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        // The code runs fine on Dalvik/ART.
+        .assertSuccessWithOutputLines("1");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(SubClassOfClassWithProtectedField.class, TestClass.class)
+        .addProgramClassFileData(getTransformedClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        // The code runs fine after R8 processing on both JDK and Dalvik/ART.
+        .assertSuccessWithOutputLines("1");
+  }
+
+  private byte[] getTransformedClass() throws Exception {
+    return transformer(ClassWithProtectedField.class)
+        .setAccessFlags(
+            ClassWithProtectedField.class.getDeclaredField("f"),
+            accessFlags -> {
+              accessFlags.unsetPublic();
+              accessFlags.setProtected();
+            })
+        .transform();
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      new SubClassOfClassWithProtectedField(2).m(new ClassWithProtectedField(1));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyTest.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyTest.java
new file mode 100644
index 0000000..962517f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/MemberRebindingProtectedInProgramHierachyTest.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithHolderAndName;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.memberrebinding.protectedaccess.util.A;
+import com.android.tools.r8.memberrebinding.protectedaccess.util.B;
+import com.android.tools.r8.memberrebinding.protectedaccess.util.Base;
+import com.android.tools.r8.utils.StringUtils;
+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 MemberRebindingProtectedInProgramHierachyTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Base.class, A.class, B.class, TestClass.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassAndMembersRules(Base.class, A.class, B.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector ->
+                assertThat(
+                    inspector.clazz(TestClass.class).mainMethod(),
+                    invokesMethodWithHolderAndName(A.class.getTypeName(), "m")));
+  }
+
+  static class TestClass extends Base {
+    public static void main(String[] args) {
+      // This will be rebound to A.m. If member rebinding is changed to rebind to higher in the
+      // hierarchy then this still cannot be rebound to Base.m (at least not for class file output).
+      new B().m();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p1/ClassWithProtectedField.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p1/ClassWithProtectedField.java
new file mode 100644
index 0000000..bc143c5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p1/ClassWithProtectedField.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess.p1;
+
+public class ClassWithProtectedField {
+  public int f; // protected with transformer.
+
+  public ClassWithProtectedField(int f) {
+    this.f = f;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p2/SubClassOfClassWithProtectedField.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p2/SubClassOfClassWithProtectedField.java
new file mode 100644
index 0000000..da144a0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/p2/SubClassOfClassWithProtectedField.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess.p2;
+
+import com.android.tools.r8.memberrebinding.protectedaccess.p1.ClassWithProtectedField;
+
+public class SubClassOfClassWithProtectedField extends ClassWithProtectedField {
+  public SubClassOfClassWithProtectedField(int v) {
+    super(v);
+  }
+
+  public void m(ClassWithProtectedField instance) {
+    System.out.println(instance.f);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/A.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/A.java
new file mode 100644
index 0000000..7af2b36
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/A.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess.util;
+
+public class A extends Base {
+  public void m() {
+    super.m();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/B.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/B.java
new file mode 100644
index 0000000..9468783
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/B.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess.util;
+
+public class B extends A {}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/Base.java b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/Base.java
new file mode 100644
index 0000000..9774c7e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/protectedaccess/util/Base.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2024, 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.memberrebinding.protectedaccess.util;
+
+public class Base {
+  protected void m() {
+    System.out.println("Hello, world!");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java b/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
index b307545..6142fb7 100644
--- a/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
@@ -12,6 +12,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.graph.DexClass;
@@ -62,7 +63,7 @@
   @Test
   public void testR8()
       throws ExecutionException, CompilationFailedException, IOException, NoSuchMethodException {
-    R8TestBuilder<? extends R8TestBuilder<?>> r8TestBuilder =
+    R8TestBuilder<?, R8TestRunResult, ?> r8TestBuilder =
         isCompat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend());
     if (keepSourceFile) {
       r8TestBuilder.addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE);
@@ -85,7 +86,7 @@
   @Test
   public void testRenameSourceFileR8()
       throws ExecutionException, CompilationFailedException, IOException, NoSuchMethodException {
-    R8TestBuilder<? extends R8TestBuilder<?>> r8TestBuilder =
+    R8TestBuilder<?, R8TestRunResult, ?> r8TestBuilder =
         isCompat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend());
     if (keepSourceFile) {
       r8TestBuilder.addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE);
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodDirectRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodDirectRetraceTest.java
index 2439b3b..758fd3e 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodDirectRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodDirectRetraceTest.java
@@ -46,7 +46,7 @@
   }
 
   @Override
-  public void configure(R8TestBuilder<?> builder) {
+  public void configure(R8TestBuilder<?, ?, ?> builder) {
     builder.enableInliningAnnotations();
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
index 21ea804..b82ab15 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
@@ -37,7 +37,7 @@
                   .assertFailure()
                   .map(StackTrace::extractFromJvm));
 
-  public void configure(R8TestBuilder<?> builder) {}
+  public void configure(R8TestBuilder<?, ?, ?> builder) {}
 
   public void inspect(CodeInspector inspector) {}
 
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
index ebf25d9..8bd3581 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -48,7 +48,7 @@
   }
 
   @Override
-  public void configure(R8TestBuilder<?> builder) {
+  public void configure(R8TestBuilder<?, ?, ?> builder) {
     builder
         .addOptionsModification(
             options ->
diff --git a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
index f08c178..6a32a4c 100644
--- a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
@@ -81,7 +81,7 @@
                     internalOptions.testing.assertConsistentRenamingOfSignature = true));
   }
 
-  private void test(R8TestBuilder<?> builder) throws Exception {
+  private void test(R8TestBuilder<?, ?, ?> builder) throws Exception {
     builder
         .addKeepRules("-dontoptimize")
         .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationMultipleInterfacesSameMethodTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationMultipleInterfacesSameMethodTest.java
index b38b518..20c556c 100644
--- a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationMultipleInterfacesSameMethodTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationMultipleInterfacesSameMethodTest.java
@@ -40,8 +40,7 @@
                 inspector
                     .assertHasPolymorphicMethodState(
                         Reference.methodFromMethod(I.class.getDeclaredMethod("m")))
-                    // TODO(b/363492038): J.m is not monomorphic.
-                    .assertHasMonomorphicMethodState(
+                    .assertHasPolymorphicMethodState(
                         Reference.methodFromMethod(J.class.getDeclaredMethod("m")))
                     .apply(ignore -> inspected.set()))
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderDesugaredLibraryTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderDesugaredLibraryTest.java
index a3bc5b4..22389b4 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderDesugaredLibraryTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderDesugaredLibraryTest.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.google.common.collect.ImmutableList;
@@ -79,7 +78,7 @@
   private static final String servicesFile =
       StringUtils.lines(SimpleChronology.class.getTypeName());
 
-  private void configureR8(R8TestBuilder<?> builder) {
+  private void configureR8(R8TestBuilder<?, ?, ?> builder) {
     // When testing R8 add the META-INF/services to the input to apply rewriting.
     builder
         .addDataEntryResources(
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
index 18f4a79..75d51ed 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.ToolHelper.DexVm.Version.V7_0_0;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverInline;
@@ -31,12 +30,16 @@
   private final String EXPECTED_OUTPUT =
       StringUtils.lines("Hello World!", "Hello World!", "Hello World!");
 
+  interface NonPublicService {
+    void print();
+  }
+
   public interface Service {
 
     void print();
   }
 
-  public static class ServiceImpl implements Service {
+  public static class ServiceImpl implements Service, NonPublicService {
 
     @Override
     public void print() {
@@ -150,6 +153,13 @@
     }
   }
 
+  public static class MainWithNonPublicService {
+
+    public static void main(String[] args) {
+      ServiceLoader.load(NonPublicService.class, null).iterator().next().print();
+    }
+  }
+
   @Parameterized.Parameters(name = "{0}, enableRewriting: {1}")
   public static List<Object[]> data() {
     return buildParameters(
@@ -169,8 +179,11 @@
     }
   }
 
-  private boolean isDexV7() {
-    // Runtime uses boot classloader rather than system classloader on this version.
+  private boolean isAndroid7() {
+    // Runtime uses boot classloader rather than system classloader on this version. See b/130164528
+    // for more details.
+    // The CL that changed behaviour after Nougat is:
+    // https://android-review.googlesource.com/c/platform/libcore/+/273135
     return parameters.isDexRuntime() && parameters.getDexRuntimeVersion() == V7_0_0;
   }
 
@@ -192,7 +205,7 @@
         .compile()
         .run(parameters.getRuntime(), MainRunner.class)
         .applyIf(
-            isDexV7() && !enableRewriting,
+            isAndroid7() && !enableRewriting,
             runResult ->
                 runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
             runResult ->
@@ -212,7 +225,7 @@
         .compile()
         .run(parameters.getRuntime(), MainRunner.class)
         .applyIf(
-            isDexV7() && !enableRewriting,
+            isAndroid7() && !enableRewriting,
             runResult ->
                 runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
             runResult ->
@@ -288,7 +301,7 @@
         .compileWithExpectedDiagnostics(expectedDiagnostics)
         .run(parameters.getRuntime(), MainRunner.class)
         .applyIf(
-            !isDexV7(),
+            !isAndroid7(),
             runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT),
             runResult ->
                 runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class));
@@ -332,22 +345,39 @@
   @Test
   public void testKeepAsOriginal()
       throws IOException, CompilationFailedException, ExecutionException {
-    // The CL that changed behaviour after Nougat is:
-    // https://android-review.googlesource.com/c/platform/libcore/+/273135
-    assumeTrue(
-        parameters.getRuntime().isCf()
-            || !parameters.getRuntime().asDex().getVm().getVersion().equals(V7_0_0));
     serviceLoaderTest(Service.class, ServiceImpl.class)
         .addKeepMainRule(MainRunner.class)
         .addKeepClassRules(Service.class)
         .allowDiagnosticInfoMessages(enableRewriting)
         .compileWithExpectedDiagnostics(expectedDiagnostics)
-        .run(parameters.getRuntime(), MainRunner.class)
-        .assertSuccessWithOutput(EXPECTED_OUTPUT)
         .inspect(
             inspector -> {
               assertEquals(3, getServiceLoaderLoads(inspector));
               verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
-            });
+            })
+        .run(parameters.getRuntime(), MainRunner.class)
+        .applyIf(
+            isAndroid7(),
+            r -> r.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  @Test
+  public void testNonPublicService()
+      throws IOException, CompilationFailedException, ExecutionException {
+    serviceLoaderTest(NonPublicService.class, ServiceImpl.class)
+        .addKeepMainRule(MainWithNonPublicService.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .inspect(
+            inspector -> {
+              assertEquals(1, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(inspector, NonPublicService.class, ServiceImpl.class);
+            })
+        .run(parameters.getRuntime(), MainWithNonPublicService.class)
+        .applyIf(
+            isAndroid7(),
+            r -> r.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
+            r -> r.assertSuccessWithOutputLines("Hello World!"));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
new file mode 100644
index 0000000..db45859
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.function.Predicate;
+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 ClassHierarchyInterleavedD8AndR8Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24 where default interface methods are supported instead of using
+  // dump.getBuildProperties().getMinApi(). Tivi has min API 23 and there are currently trace
+  // references issues with CC classes.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  private void runTest(
+      Predicate<String> isR8, ThrowingConsumer<CodeInspector, RuntimeException> inspector)
+      throws Exception {
+    // Path tempDir = temp.newFolder().toPath();
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, C.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .setR8PartialConfigurationPredicate(isR8)
+        .compile()
+        .inspect(inspector)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testD8Top() throws Exception {
+    runTest(
+        name -> !name.equals(A.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isAbsent()); // Merged into C.
+          assertThat(inspector.clazz(C.class), isPresentAndRenamed());
+        });
+  }
+
+  @Test
+  public void testD8Middle() throws Exception {
+    runTest(
+        name -> !name.equals(B.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(C.class), isPresentAndRenamed());
+        });
+  }
+
+  @Test
+  public void testD8Bottom() throws Exception {
+    runTest(
+        name -> !name.equals(C.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(C.class), isPresentAndNotRenamed());
+        });
+  }
+
+  public static class A {}
+
+  public static class B extends A {}
+
+  public static class C extends B {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new C();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
new file mode 100644
index 0000000..662aa21
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+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 PartialCompilationBasicTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  @Test
+  public void runTestClassAIsCompiledWithD8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(A.class).build())
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(A.class), isPresent());
+              assertThat(inspector.clazz(B.class), isAbsent());
+            })
+        .run(parameters.getRuntime(), Main.class, getClass().getTypeName())
+        .assertSuccessWithOutputLines("Instantiated", "Not instantiated");
+  }
+
+  @Test
+  public void runTestClassBIsCompiledWithD8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(B.class).build())
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(A.class), isAbsent());
+              assertThat(inspector.clazz(B.class), isPresent());
+            })
+        .run(parameters.getRuntime(), Main.class, getClass().getTypeName())
+        .assertSuccessWithOutputLines("Not instantiated", "Instantiated");
+  }
+
+  public static class A {}
+
+  public static class B {}
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      // Instantiate class A.
+      try {
+        Class.forName(new String(args[0] + "$" + new String(new byte[] {65})));
+        System.out.println("Instantiated");
+      } catch (ClassNotFoundException e) {
+        System.out.println("Not instantiated");
+      }
+      // Instantiate class B.
+      try {
+        Class.forName(new String(args[0] + "$" + new String(new byte[] {66})));
+        System.out.println("Instantiated");
+      } catch (ClassNotFoundException e) {
+        System.out.println("Not instantiated");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
new file mode 100644
index 0000000..0ccd8ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
@@ -0,0 +1,309 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.ToolHelper.DESUGARED_JDK_11_LIB_JAR;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.L8;
+import com.android.tools.r8.L8Command;
+import com.android.tools.r8.LibraryDesugaringTestConfiguration;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.desugar.desugaredlibrary.jdk11.DesugaredLibraryJDK11Undesugarer;
+import com.android.tools.r8.dump.CompilerDump;
+import com.android.tools.r8.tracereferences.TraceReferences;
+import com.android.tools.r8.tracereferences.TraceReferencesCommand;
+import com.android.tools.r8.tracereferences.TraceReferencesKeepRules;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Predicate;
+import org.junit.Ignore;
+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;
+
+// This test is currently for demonstrating R8 partial compilation, and for now all tests should
+// be @Ignore(d) and only enabled for local experiments.
+@RunWith(Parameterized.class)
+public class PartialCompilationDemoTest extends TestBase {
+
+  private static final Path TIVI_DUMP_PATH =
+      Paths.get(ToolHelper.THIRD_PARTY_DIR, "opensource-apps", "tivi", "dump_app.zip");
+  private static final Path NOWINANDROID_DUMP_PATH =
+      Paths.get(
+          ToolHelper.THIRD_PARTY_DIR, "opensource-apps", "android", "nowinandroid", "dump_app.zip");
+  // When using with the desugar_jdk_libs.jar in third_party (DESUGARED_JDK_11_LIB_JAR) for L8
+  // compilation then the configuration from the dump cannot be used for L8, as the configuration
+  // in the dump is the "machine specification" which only works with the specific version it was
+  // built for.
+  private static final boolean useDesugaredLibraryConfigurationFromDump = false;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24 where default interface methods are supported instead fo using
+  // dump.getBuildProperties().getMinApi(). Tivi has min API 23 and there are currently trace
+  // references issues with CC classes for default interface methods.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  private void configureDesugaredLibrary(
+      TestCompilerBuilder<?, ?, ?, ?, ?> builder, CompilerDump dump) {
+    if (useDesugaredLibraryConfigurationFromDump) {
+      builder.enableCoreLibraryDesugaring(
+          LibraryDesugaringTestConfiguration.forSpecification(dump.getDesugaredLibraryFile()));
+    } else {
+      builder.enableCoreLibraryDesugaring(
+          LibraryDesugaringTestConfiguration.forSpecification(
+              Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR, "jdk11", "desugar_jdk_libs.json")));
+    }
+  }
+
+  private void configureDesugaredLibrary(L8Command.Builder builder, CompilerDump dump) {
+    if (useDesugaredLibraryConfigurationFromDump) {
+      builder.addDesugaredLibraryConfiguration(
+          StringResource.fromFile(dump.getDesugaredLibraryFile()));
+    } else {
+      builder.addDesugaredLibraryConfiguration(
+          StringResource.fromFile(
+              Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR, "jdk11", "desugar_jdk_libs.json")));
+    }
+  }
+
+  @Test
+  @Ignore("Will be removed, only present to easily compare with partial compilation")
+  public void testD8() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+
+    CompilerDump dump = CompilerDump.fromArchive(TIVI_DUMP_PATH, temp.newFolder().toPath());
+    Path output = tempDir.resolve("tivid8.zip");
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .setMode(CompilationMode.RELEASE)
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .compile()
+        .writeToZip(output);
+
+    Path l8Output = tempDir.resolve("tivid8l8.zip");
+    runL8(tempDir, dump, output, l8Output);
+  }
+
+  @Test
+  @Ignore("Will be removed, only present to easily compare with partial compilation")
+  public void testR8() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+
+    CompilerDump dump = CompilerDump.fromArchive(TIVI_DUMP_PATH, temp.newFolder().toPath());
+    Path output = tempDir.resolve("tivir8.zip");
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .addKeepRuleFiles(dump.getProguardConfigFile())
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .setMode(CompilationMode.RELEASE)
+        .addOptionsModification(
+            options -> options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces())
+        .allowDiagnosticMessages()
+        .allowUnnecessaryDontWarnWildcards()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules()
+        .compile()
+        .writeToZip(output);
+
+    Path l8Output = tempDir.resolve("tivir8l8.zip");
+    runL8(tempDir, dump, output, l8Output);
+  }
+
+  private void testDump(CompilerDump dump, String appNamespace) throws Exception {
+    assert !appNamespace.endsWith(".");
+    Path tempDir = temp.newFolder().toPath();
+
+    // Different sets of namespaces to shrink.
+    ImmutableMap<String, Predicate<String>> splits =
+        ImmutableMap.of(
+            "androidx", name -> name.startsWith("androidx."),
+            "androidx_kotlin_and_kotlinx",
+                name ->
+                    name.startsWith("androidx.")
+                        || name.startsWith("kotlin.")
+                        || name.startsWith("kotlinx."),
+            "more_libraries",
+                name ->
+                    name.startsWith("androidx.")
+                        || name.startsWith("kotlin.")
+                        || name.startsWith("kotlinx.")
+                        || name.startsWith("android.support.")
+                        || name.startsWith("io.ktor.")
+                        || name.startsWith("com.google.android.gms.")
+                        || name.startsWith("com.google.firebase."),
+            "all_but_app namespace", name -> !name.startsWith(appNamespace + "."));
+
+    // Compile with each set of namespaces to shrink and collect DEX size.
+    Map<String, Pair<Long, Long>> dexSize = new LinkedHashMap<>();
+    for (Entry<String, Predicate<String>> entry : splits.entrySet()) {
+      long size = runR8PartialAndL8(tempDir, dump, entry.getKey(), entry.getValue());
+      dexSize.put(entry.getKey(), new Pair<>(size, 0L));
+    }
+    dexSize.forEach(
+        (name, size) ->
+            System.out.println(name + ": " + size.getFirst() + ", " + size.getSecond()));
+
+    // Check that sizes for increased namespaces to shrink does not increase size.
+    Pair<Long, Long> previousSize = null;
+    for (Entry<String, Pair<Long, Long>> entry : dexSize.entrySet()) {
+      if (previousSize != null) {
+        assertTrue(entry.getKey(), entry.getValue().getFirst() <= previousSize.getFirst());
+        assertTrue(entry.getKey(), entry.getValue().getSecond() <= previousSize.getSecond());
+      }
+      previousSize = entry.getValue();
+    }
+  }
+
+  @Test
+  @Ignore("Still experimental, do not run this test by default")
+  public void testTivi() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+    Path dumpDir = tempDir.resolve("dump");
+    testDump(CompilerDump.fromArchive(TIVI_DUMP_PATH, dumpDir), "app.tivi");
+  }
+
+  @Test
+  @Ignore("Still experimental, do not run this test by default")
+  public void testNowinandroid() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+    Path dumpDir = tempDir.resolve("dump");
+    testDump(
+        CompilerDump.fromArchive(NOWINANDROID_DUMP_PATH, dumpDir),
+        "com.google.samples.apps.nowinandroid");
+  }
+
+  private long runR8PartialAndL8(
+      Path tempDir, CompilerDump dump, String name, Predicate<String> isR8) throws Exception {
+    Path tmp = tempDir.resolve(name);
+    Files.createDirectory(tmp);
+    Path output = tmp.resolve("tivix8.zip");
+    runR8Partial(tempDir, dump, output, isR8);
+    Path l8Output = tmp.resolve("tivix8l8.zip");
+    runL8(tmp, dump, output, l8Output);
+    Box<Long> size = new Box<>(0L);
+    for (Path zipWithDex : new Path[] {output, l8Output}) {
+      ZipUtils.iter(
+          zipWithDex,
+          (zipEntry, input) ->
+              size.set(
+                  size.get()
+                      + (zipEntry.getName().endsWith(FileUtils.DEX_EXTENSION)
+                          ? zipEntry.getSize()
+                          : 0L)));
+    }
+    return size.get();
+  }
+
+  private void runR8Partial(Path tempDir, CompilerDump dump, Path output, Predicate<String> isR8)
+      throws IOException, CompilationFailedException {
+    testForR8Partial(parameters.getBackend())
+        .setR8PartialConfigurationPredicate(isR8)
+        .addOptionsModification(
+            options -> {
+              options.r8PartialCompilationOptions.tempDir = tempDir;
+
+              // For compiling nowonandroid.
+              options.testing.allowUnnecessaryDontWarnWildcards = true;
+              options.testing.allowUnusedDontWarnRules = true;
+              options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces();
+            })
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .addKeepRuleFiles(dump.getProguardConfigFile())
+        // Add this keep rules as trace references does not trace annotations. The consumer rules
+        // explicitly leaves out the annotation class Navigator.Name:
+        //
+        // # A -keep rule for the Navigator.Name annotation class is not required
+        // # since the annotation is referenced from the code.
+        //
+        // As this code reference is not in the D8 part of the code Navigator.Name is removed.
+        .addKeepRules("-keep class androidx.navigation.Navigator$Name { *; }")
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .setMode(CompilationMode.RELEASE)
+        .allowStdoutMessages()
+        .allowStderrMessages()
+        .allowUnusedProguardConfigurationRules() // nowonandroid
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(true) // avoid warnings
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .writeToZip(output);
+  }
+
+  private void runL8(Path tempDir, CompilerDump dump, Path appDexCode, Path output)
+      throws Exception {
+    Path desugaredLibraryRules = tempDir.resolve("desugared_library.rules");
+    TraceReferencesKeepRules keepRulesConsumer =
+        TraceReferencesKeepRules.builder()
+            .setOutputConsumer(new FileConsumer(desugaredLibraryRules))
+            .build();
+
+    AndroidApiLevel apiLevel = AndroidApiLevel.N;
+    Path path = tempDir.resolve("desugared_library.jar");
+    Path dd =
+        DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(tempDir, DESUGARED_JDK_11_LIB_JAR);
+    L8Command.Builder commandBuilder =
+        L8Command.builder()
+            .setMinApiLevel(apiLevel.getLevel())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addProgramFiles(dd)
+            .setOutput(path, OutputMode.ClassFile);
+    configureDesugaredLibrary(commandBuilder, dump);
+    L8.run(commandBuilder.build());
+
+    TraceReferencesCommand.Builder traceReferencesCommandBuilder =
+        TraceReferencesCommand.builder()
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addSourceFiles(appDexCode)
+            .addTargetFiles(path)
+            .setConsumer(keepRulesConsumer);
+    TraceReferences.run(traceReferencesCommandBuilder.build());
+
+    testForR8(Backend.DEX)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addProgramFiles(path)
+        .addKeepRuleFiles(desugaredLibraryRules)
+        .compile()
+        .writeToZip(output);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageDontObfuscateTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageDontObfuscateTest.java
index 677fe21..030966f 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageDontObfuscateTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageDontObfuscateTest.java
@@ -99,7 +99,8 @@
         .assertSuccessWithOutputLines(EXPECTED);
   }
 
-  private R8TestCompileResult setup(R8TestBuilder<?> r8TestBuilder) throws Exception {
+  private R8TestCompileResult setup(R8TestBuilder<R8TestCompileResult, ?, ?> r8TestBuilder)
+      throws Exception {
     return r8TestBuilder
         .addInnerClasses(getClass())
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
index a026793..003b2e5 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
@@ -39,7 +39,8 @@
     test(testForR8Compat(parameters.getBackend()).addProgramClasses(TestClass.class), false);
   }
 
-  private void test(R8TestBuilder<?> testBuilder, boolean eligibleForRepackaging) throws Exception {
+  private void test(R8TestBuilder<?, ?, ?> testBuilder, boolean eligibleForRepackaging)
+      throws Exception {
     testBuilder
         .addProgramClasses(Outer.class, Inner.class)
         .addKeepMainRule(TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateInnerClassTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateInnerClassTest.java
index 70bd738..6234266 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateInnerClassTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateInnerClassTest.java
@@ -39,7 +39,7 @@
     test(testForR8(parameters.getBackend()).addKeepClassRules(NonPublicKeptClass.class), true);
   }
 
-  private void test(R8TestBuilder<?> builder, boolean expectRepackaged) throws Exception {
+  private void test(R8TestBuilder<?, ?, ?> builder, boolean expectRepackaged) throws Exception {
     builder
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/resolution/B367915233Test.java b/src/test/java/com/android/tools/r8/resolution/B367915233Test.java
deleted file mode 100644
index c01e188..0000000
--- a/src/test/java/com/android/tools/r8/resolution/B367915233Test.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2024, 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.resolution;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.StringUtils;
-import java.util.LinkedHashMap;
-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 B367915233Test extends TestBase {
-
-  @Parameter(0)
-  public TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .addInnerClasses(getClass())
-        .addKeepMainRule(TestClass.class)
-        .setMinApi(parameters)
-        .run(parameters.getRuntime(), TestClass.class)
-        .applyIf(
-            parameters.isCfRuntime(),
-            r -> r.assertFailureWithErrorThatThrows(VerifyError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
-  }
-
-  static class TestClass {
-
-    public static void main(String[] args) {
-      new LinkedHashMap<>().clone();
-      System.out.println("Hello, world!");
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerAssertionInClinitOnlyTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerAssertionInClinitOnlyTest.java
index f2de8f4..160d344 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerAssertionInClinitOnlyTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerAssertionInClinitOnlyTest.java
@@ -38,7 +38,7 @@
   }
 
   @Override
-  protected void configure(R8TestBuilder<?> builder) {
+  protected void configure(R8TestBuilder<?, ?, ?> builder) {
     builder.allowUnusedProguardConfigurationRules();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerTestBase.java b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerTestBase.java
index c3a4eeb..004e756 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerTestBase.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationAssertionHandlerTestBase.java
@@ -40,7 +40,7 @@
     return ImmutableList.of(AssertionHandlers.class);
   }
 
-  protected void configure(R8TestBuilder<?> builder) {}
+  protected void configure(R8TestBuilder<?, ?, ?> builder) {}
 
   protected void inspect(CodeInspector inspector) {}
 
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 b7c0d9a..e523554 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -343,7 +343,8 @@
     }
   }
 
-  private void suppressZipFileAssignmentsToJavaLangAutoCloseable(R8TestBuilder<?> testBuilder) {
+  private void suppressZipFileAssignmentsToJavaLangAutoCloseable(
+      R8TestBuilder<?, ?, ?> testBuilder) {
     testBuilder.addOptionsModification(
         options ->
             options
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryClassExtendingProgramClassSuperTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryClassExtendingProgramClassSuperTest.java
index e856027..f3db1be 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryClassExtendingProgramClassSuperTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryClassExtendingProgramClassSuperTest.java
@@ -59,7 +59,7 @@
 
   @Test
   public void testR8() throws Exception {
-    R8TestBuilder<? extends R8TestBuilder<?>> r8TestBuilder =
+    R8TestBuilder<?, ?, ?> r8TestBuilder =
         (proguardCompatibility
                 ? testForR8Compat(parameters.getBackend(), true)
                 : testForR8(parameters.getBackend()))
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java
new file mode 100644
index 0000000..d9631f1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java
@@ -0,0 +1,266 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasPart;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+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 LibraryProvidedProguardRulesFromClasspathOrLibraryTest
+    extends LibraryProvidedProguardRulesTestBase {
+
+  @interface Keep {}
+
+  public interface Interface {}
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public LibraryType libraryType;
+
+  @Parameter(2)
+  public boolean isClasspath;
+
+  @Parameters(name = "{0}, libraryType: {1}, isClasspath {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build(),
+        LibraryType.values(),
+        BooleanUtils.values());
+  }
+
+  private Path buildLibrary(List<String> rules) throws Exception {
+    return buildLibrary(libraryType, ImmutableList.of(Interface.class, Keep.class), rules);
+  }
+
+  private CodeInspector runTest(List<String> rules) throws Exception {
+    Path library = buildLibrary(rules);
+    return testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class)
+        .applyIf(
+            isClasspath,
+            b -> b.addClasspathFiles(library),
+            b ->
+                b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+                    .addLibraryFiles(library))
+        .setMinApi(parameters)
+        .apply(b -> ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(b.getBuilder(), true))
+        .compile()
+        .inspector();
+  }
+
+  private CodeInspector runTest(String rules) throws Exception {
+    return runTest(ImmutableList.of(rules));
+  }
+
+  @Test
+  public void providedKeepRuleImplements() throws Exception {
+    CodeInspector inspector =
+        runTest("-keep class * implements " + Interface.class.getTypeName() + " { *; }");
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assertThat(inspector.clazz(A.class), notIf(isPresent(), libraryType.isAar()));
+    assertThat(inspector.clazz(B.class), not(isPresent()));
+  }
+
+  @Test
+  public void providedKeepRuleAnnotated() throws Exception {
+    CodeInspector inspector = runTest("-keep @" + Keep.class.getTypeName() + " class * { *; }");
+    assertThat(inspector.clazz(A.class), not(isPresent()));
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assertThat(inspector.clazz(B.class), notIf(isPresent(), libraryType.isAar()));
+  }
+
+  @Test
+  public void providedKeepRuleImplementsOrAnnotated() throws Exception {
+    CodeInspector inspector =
+        runTest(
+            ImmutableList.of(
+                "-keep class * implements " + Interface.class.getTypeName() + " { *; }",
+                "-keep @" + Keep.class.getTypeName() + " class * { *; }"));
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assertThat(inspector.clazz(A.class), notIf(isPresent(), libraryType.isAar()));
+    assertThat(inspector.clazz(B.class), notIf(isPresent(), libraryType.isAar()));
+  }
+
+  @Test
+  public void providedKeepRuleSyntaxError() {
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assumeTrue(!libraryType.isAar());
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramFiles(buildLibrary(ImmutableList.of("error")))
+                .setMinApi(parameters)
+                .apply(
+                    b ->
+                        ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(
+                            b.getBuilder(), true))
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics.assertErrorThatMatches(
+                            allOf(
+                                diagnosticMessage(containsString("Expected char '-'")),
+                                diagnosticOrigin(hasPart("META-INF/proguard/jar.rules")),
+                                diagnosticOrigin(instanceOf(ArchiveEntryOrigin.class))))));
+  }
+
+  @Test
+  public void providedKeepRuleInjarsError() {
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assumeTrue(!libraryType.isAar());
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          Path library = buildLibrary(ImmutableList.of("-injars some.jar"));
+          testForR8(parameters.getBackend())
+              .applyIf(
+                  isClasspath,
+                  b -> b.addClasspathFiles(library),
+                  b ->
+                      b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+                          .addLibraryFiles(library))
+              .setMinApi(parameters)
+              .apply(
+                  b -> ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(b.getBuilder(), true))
+              .compileWithExpectedDiagnostics(
+                  diagnostics ->
+                      diagnostics.assertErrorThatMatches(
+                          diagnosticMessage(
+                              containsString("Options with file names are not supported"))));
+        });
+  }
+
+  @Test
+  public void providedKeepRuleIncludeError() {
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assumeTrue(!libraryType.isAar());
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          Path library = buildLibrary(ImmutableList.of("-include other.rules"));
+          testForR8(parameters.getBackend())
+              .applyIf(
+                  isClasspath,
+                  b -> b.addClasspathFiles(library),
+                  b ->
+                      b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+                          .addLibraryFiles(library))
+              .setMinApi(parameters)
+              .apply(
+                  b -> ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(b.getBuilder(), true))
+              .compileWithExpectedDiagnostics(
+                  diagnostics ->
+                      diagnostics.assertErrorThatMatches(
+                          diagnosticMessage(
+                              containsString("Options with file names are not supported"))));
+        });
+  }
+
+  static class TestProvider implements ClassFileResourceProvider, DataResourceProvider {
+
+    @Override
+    public ProgramResource getProgramResource(String descriptor) {
+      byte[] bytes;
+      try {
+        bytes = ByteStreams.toByteArray(Keep.class.getResourceAsStream("K.class"));
+      } catch (IOException e) {
+        return null;
+      }
+      return ProgramResource.fromBytes(
+          Origin.unknown(),
+          Kind.CF,
+          bytes,
+          Collections.singleton(DescriptorUtils.javaTypeToDescriptor(Keep.class.getTypeName())));
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return null;
+    }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return this;
+    }
+
+    @Override
+    public void accept(Visitor visitor) throws ResourceException {
+      throw new ResourceException(Origin.unknown(), "Cannot provide data resources after all");
+    }
+  }
+
+  @Test
+  public void throwingDataResourceProvider() {
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assumeTrue(!libraryType.isAar());
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .applyIf(
+                    isClasspath,
+                    b -> b.addClasspathResourceProviders(new TestProvider()),
+                    b ->
+                        b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+                            .addLibraryResourceProviders(new TestProvider()))
+                .setMinApi(parameters)
+                .apply(
+                    b ->
+                        ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(
+                            b.getBuilder(), true))
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics.assertErrorThatMatches(
+                            allOf(
+                                diagnosticMessage(
+                                    containsString("Cannot provide data resources after all")),
+                                diagnosticOrigin(is(Origin.unknown()))))));
+  }
+
+  static class A implements Interface {}
+
+  @Keep
+  static class B {}
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesR8SpecificTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesR8SpecificTest.java
index ccb56de..ecc00c4 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesR8SpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesR8SpecificTest.java
@@ -37,7 +37,7 @@
   @Parameter(2)
   public ProviderType providerType;
 
-  @Parameters(name = "{0}, AAR: {1}, {2}")
+  @Parameters(name = "{0}, libraryType: {1}, providerType: {2}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withNoneRuntime().build(),
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
index 72b1cb7..dfd61c5 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
@@ -29,8 +29,6 @@
 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.StringUtils;
-import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
@@ -74,7 +72,7 @@
   @Parameter(2)
   public ProviderType providerType;
 
-  @Parameters(name = "{0}, AAR: {1}, {2}")
+  @Parameters(name = "{0}, libraryType: {1}, providerType: {2}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build(),
@@ -83,27 +81,7 @@
   }
 
   private Path buildLibrary(List<String> rules) throws Exception {
-    ZipBuilder jarBuilder =
-        ZipBuilder.builder(temp.newFile(libraryType.isAar() ? "classes.jar" : "test.jar").toPath());
-    addTestClassesToZip(jarBuilder.getOutputStream(), ImmutableList.of(A.class, B.class));
-    if (libraryType.hasRulesInJar()) {
-      for (int i = 0; i < rules.size(); i++) {
-        String name = "META-INF/proguard/jar" + (i == 0 ? "" : i) + ".rules";
-        jarBuilder.addText(name, rules.get(i));
-      }
-    }
-    if (libraryType.isAar()) {
-      Path jar = jarBuilder.build();
-      String allRules = StringUtils.lines(rules);
-      ZipBuilder aarBuilder = ZipBuilder.builder(temp.newFile("test.aar").toPath());
-      aarBuilder.addFilesRelative(jar.getParent(), jar);
-      if (libraryType.hasRulesInAar()) {
-        aarBuilder.addText("proguard.txt", allRules);
-      }
-      return aarBuilder.build();
-    } else {
-      return jarBuilder.build();
-    }
+    return buildLibrary(libraryType, ImmutableList.of(A.class, B.class), rules);
   }
 
   private CodeInspector runTest(List<String> rules) throws Exception {
@@ -153,7 +131,7 @@
   }
 
   @Test
-  public void syntaxError() {
+  public void providedKeepRuleSyntaxError() {
     // TODO(b/228319861): Read Proguard rules from AAR's.
     assumeTrue(!libraryType.isAar());
     assertThrows(
@@ -172,7 +150,24 @@
   }
 
   @Test
-  public void includeError() {
+  public void providedKeepRuleInjarsError() {
+    // TODO(b/228319861): Read Proguard rules from AAR's.
+    assumeTrue(!libraryType.isAar());
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramFiles(buildLibrary(ImmutableList.of("-injars some.jar")))
+                .setMinApi(parameters)
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics.assertErrorThatMatches(
+                            diagnosticMessage(
+                                containsString("Options with file names are not supported")))));
+  }
+
+  @Test
+  public void providedKeepRuleIncludeError() {
     // TODO(b/228319861): Read Proguard rules from AAR's.
     assumeTrue(!libraryType.isAar());
     assertThrows(
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTestBase.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTestBase.java
index 4bca489..3bdedc2 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTestBase.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTestBase.java
@@ -5,6 +5,10 @@
 package com.android.tools.r8.shaking;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.file.Path;
+import java.util.List;
 
 public class LibraryProvidedProguardRulesTestBase extends TestBase {
 
@@ -31,4 +35,29 @@
     API,
     INJARS
   }
+
+  protected Path buildLibrary(LibraryType libraryType, List<Class<?>> classes, List<String> rules)
+      throws Exception {
+    ZipBuilder jarBuilder =
+        ZipBuilder.builder(temp.newFile(libraryType.isAar() ? "classes.jar" : "test.jar").toPath());
+    addTestClassesToZip(jarBuilder.getOutputStream(), classes);
+    if (libraryType.hasRulesInJar()) {
+      for (int i = 0; i < rules.size(); i++) {
+        String name = "META-INF/proguard/jar" + (i == 0 ? "" : i) + ".rules";
+        jarBuilder.addText(name, rules.get(i));
+      }
+    }
+    if (libraryType.isAar()) {
+      Path jar = jarBuilder.build();
+      String allRules = StringUtils.lines(rules);
+      ZipBuilder aarBuilder = ZipBuilder.builder(temp.newFile("test.aar").toPath());
+      aarBuilder.addFilesRelative(jar.getParent(), jar);
+      if (libraryType.hasRulesInAar()) {
+        aarBuilder.addText("proguard.txt", allRules);
+      }
+      return aarBuilder.build();
+    } else {
+      return jarBuilder.build();
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepEnclosingMethodForKeptMethodTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepEnclosingMethodForKeptMethodTest.java
index 9a66258..f6d7ea4 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepEnclosingMethodForKeptMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepEnclosingMethodForKeptMethodTest.java
@@ -61,7 +61,8 @@
     runTest(testForR8Compat(parameters.getBackend())).assertSuccessWithOutputLines(EXPECTED);
   }
 
-  private R8TestRunResult runTest(R8TestBuilder<?> testBuilder) throws Exception {
+  private R8TestRunResult runTest(R8TestBuilder<?, R8TestRunResult, ?> testBuilder)
+      throws Exception {
     return testBuilder
         .addInnerClasses(KeptClass.class)
         .addProgramClassFileData(
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
index 56e7b82..57a904f 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
@@ -66,7 +66,8 @@
     runTest(testForR8Compat(parameters.getBackend()), true);
   }
 
-  private void runTest(R8TestBuilder<?> testBuilder, boolean keptForNotKept) throws Exception {
+  private void runTest(R8TestBuilder<?, ?, ?> testBuilder, boolean keptForNotKept)
+      throws Exception {
     testBuilder
         .addProgramClassFileData(transformer(KeptClass.class).removeInnerClasses().transform())
         .addProgramClassFileData(transformer(NotKeptClass.class).removeInnerClasses().transform())
diff --git a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
index 41c6730..2670bee 100644
--- a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
@@ -194,7 +194,7 @@
     }
   }
 
-  public static <B extends R8TestBuilder<B>> ThrowableConsumer<B> addStartupProfile(
+  public static <B extends R8TestBuilder<?, ?, ?>> ThrowableConsumer<B> addStartupProfile(
       Collection<ExternalStartupItem> startupItems) {
     return testBuilder -> addStartupProfile(testBuilder, startupItems);
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
index eb7eb2d..1c0c6a8 100644
--- a/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -78,6 +78,7 @@
     KOTLINC_1_8_0("kotlin-compiler-1.8.0", KotlinLambdaGeneration.CLASS),
     KOTLINC_1_9_21("kotlin-compiler-1.9.21", KotlinLambdaGeneration.CLASS),
     KOTLINC_2_0_20("kotlin-compiler-2.0.20", KotlinLambdaGeneration.INVOKE_DYNAMIC),
+    KOTLINC_2_1_0_BETA1("kotlin-compiler-2.1.0-Beta1", KotlinLambdaGeneration.INVOKE_DYNAMIC),
     KOTLIN_DEV("kotlin-compiler-dev", KotlinLambdaGeneration.INVOKE_DYNAMIC);
 
     public static final KotlinCompilerVersion MIN_SUPPORTED_VERSION = KOTLINC_2_0_20;
diff --git a/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
index 8074250..bc904c7 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
@@ -5,8 +5,17 @@
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
-public class R8CompatTestBuilder extends R8TestBuilder<R8CompatTestBuilder> {
+public class R8CompatTestBuilder
+    extends R8TestBuilder<R8TestCompileResult, R8TestRunResult, R8CompatTestBuilder> {
 
   private R8CompatTestBuilder(TestState state, Builder builder, Backend backend) {
     super(state, builder, backend);
@@ -28,4 +37,31 @@
   R8CompatTestBuilder self() {
     return this;
   }
+
+  R8TestCompileResult internalCompileR8(
+      Builder builder,
+      Consumer<InternalOptions> optionsConsumer,
+      Supplier<AndroidApp> app,
+      BenchmarkResults benchmarkResults,
+      StringBuilder pgConfOutput,
+      Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer,
+      StringBuilder proguardMapBuilder)
+      throws CompilationFailedException {
+    ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
+    return new R8TestCompileResult(
+        getState(),
+        getOutputMode(),
+        libraryDesugaringTestConfiguration,
+        app.get(),
+        pgConfOutput.toString(),
+        syntheticProguardRulesConsumer.get(),
+        proguardMapBuilder.toString(),
+        graphConsumer,
+        getMinApiLevel(),
+        features,
+        residualArtProfiles,
+        resourceShrinkerOutput,
+        resourceShrinkerOutputForFeatures,
+        buildMetadata != null ? buildMetadata.get() : null);
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
index a8f6d60..3caab28 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
@@ -5,9 +5,17 @@
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
-public class R8FullTestBuilder extends R8TestBuilder<R8FullTestBuilder> {
+public class R8FullTestBuilder
+    extends R8TestBuilder<R8TestCompileResult, R8TestRunResult, R8FullTestBuilder> {
 
   private R8FullTestBuilder(TestState state, Builder builder, Backend backend) {
     super(state, builder, backend);
@@ -32,4 +40,31 @@
   R8FullTestBuilder self() {
     return this;
   }
+
+  R8TestCompileResult internalCompileR8(
+      Builder builder,
+      Consumer<InternalOptions> optionsConsumer,
+      Supplier<AndroidApp> app,
+      BenchmarkResults benchmarkResults,
+      StringBuilder pgConfOutput,
+      Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer,
+      StringBuilder proguardMapBuilder)
+      throws CompilationFailedException {
+    ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
+    return new R8TestCompileResult(
+        getState(),
+        getOutputMode(),
+        libraryDesugaringTestConfiguration,
+        app.get(),
+        pgConfOutput.toString(),
+        syntheticProguardRulesConsumer.get(),
+        proguardMapBuilder.toString(),
+        graphConsumer,
+        getMinApiLevel(),
+        features,
+        residualArtProfiles,
+        resourceShrinkerOutput,
+        resourceShrinkerOutputForFeatures,
+        buildMetadata != null ? buildMetadata.get() : null);
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
new file mode 100644
index 0000000..319b633
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
@@ -0,0 +1,191 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class R8PartialTestBuilder
+    extends R8TestBuilder<R8PartialTestCompileResult, R8TestRunResult, R8PartialTestBuilder> {
+
+  private R8PartialConfiguration r8PartialConfiguration =
+      R8PartialConfiguration.defaultConfiguration();
+
+  private R8PartialTestBuilder(TestState state, Builder builder, Backend backend) {
+    super(state, builder, backend);
+  }
+
+  public static R8PartialTestBuilder create(TestState state, Backend backend) {
+    Builder builder = R8Command.builder(state.getDiagnosticsHandler());
+    return new R8PartialTestBuilder(state, builder, backend);
+  }
+
+  public static R8PartialTestBuilder create(
+      TestState state, AndroidApp.Builder appBuilder, Backend backend) {
+    return new R8PartialTestBuilder(state, R8Command.builder(appBuilder.build()), backend);
+  }
+
+  @Override
+  public boolean isR8TestBuilder() {
+    return true;
+  }
+
+  @Override
+  R8PartialTestBuilder self() {
+    return this;
+  }
+
+  public static class R8PartialConfiguration implements Predicate<String> {
+    private static final R8PartialConfiguration defaultConfiguration =
+        new R8PartialConfiguration(ImmutableList.of(), ImmutableList.of());
+    private final List<Predicate<String>> includePredicates;
+    private final List<Predicate<String>> excludePredicates;
+
+    public R8PartialConfiguration(
+        List<Predicate<String>> includePredicates, List<Predicate<String>> excludePredicates) {
+      this.includePredicates = includePredicates;
+      this.excludePredicates = excludePredicates;
+    }
+
+    private static R8PartialConfiguration defaultConfiguration() {
+      return defaultConfiguration;
+    }
+
+    public static Builder builder() {
+      return new Builder();
+    }
+
+    public boolean test(String name) {
+      for (Predicate<String> isR8ClassPredicate : includePredicates) {
+        if (isR8ClassPredicate.test(name)) {
+          for (Predicate<String> isD8ClassPredicate : excludePredicates) {
+            if (isD8ClassPredicate.test(name)) {
+              return false;
+            }
+          }
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public static class Builder {
+      private final List<Predicate<String>> includePredicates = new ArrayList<>();
+      private final List<Predicate<String>> excludePredicates = new ArrayList<>();
+
+      public R8PartialConfiguration build() {
+        return new R8PartialConfiguration(includePredicates, excludePredicates);
+      }
+
+      public Builder includeAll() {
+        includePredicates.add(Predicates.alwaysTrue());
+        return this;
+      }
+
+      public Builder includeClasses(Class<?>... classes) {
+        return includeClasses(Arrays.asList(classes));
+      }
+
+      public Builder includeClasses(Collection<Class<?>> classes) {
+        Collection<String> typeNames =
+            classes.stream().map(Class::getTypeName).collect(Collectors.toList());
+        includePredicates.add(typeNames::contains);
+        return this;
+      }
+
+      public Builder include(Predicate<String> include) {
+        includePredicates.add(include);
+        return this;
+      }
+
+      public Builder excludeClasses(Class<?>... classes) {
+        return excludeClasses(Arrays.asList(classes));
+      }
+
+      public Builder excludeClasses(Collection<Class<?>> classes) {
+        Collection<String> typeNames =
+            classes.stream().map(Class::getTypeName).collect(Collectors.toList());
+        excludePredicates.add(typeNames::contains);
+        return this;
+      }
+
+      public Builder exclude(Predicate<String> exclude) {
+        excludePredicates.add(exclude);
+        return this;
+      }
+    }
+  }
+
+  public R8PartialTestBuilder setR8PartialConfigurationPredicate(Predicate<String> include) {
+    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+        : "Overwriting configuration...?";
+    r8PartialConfiguration = R8PartialConfiguration.builder().include(include).build();
+    return self();
+  }
+
+  public R8PartialTestBuilder setR8PartialConfiguration(R8PartialConfiguration configuration) {
+    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+        : "Overwriting configuration...?";
+    r8PartialConfiguration = configuration;
+    return self();
+  }
+
+  public R8PartialTestBuilder setR8PartialConfiguration(
+      Function<R8PartialConfiguration.Builder, R8PartialConfiguration> fn) {
+    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+        : "Overwriting configuration...?";
+    r8PartialConfiguration = fn.apply(R8PartialConfiguration.builder());
+    return self();
+  }
+
+  @Override
+  R8PartialTestCompileResult internalCompileR8(
+      Builder builder,
+      Consumer<InternalOptions> optionsConsumer,
+      Supplier<AndroidApp> app,
+      BenchmarkResults benchmarkResults,
+      StringBuilder pgConfOutput,
+      Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer,
+      StringBuilder proguardMapBuilder)
+      throws CompilationFailedException {
+    Consumer<InternalOptions> configureR8PartialCompilation =
+        options -> {
+          options.r8PartialCompilationOptions.enabled = true;
+          options.r8PartialCompilationOptions.isR8 = r8PartialConfiguration;
+        };
+    ToolHelper.runAndBenchmarkR8WithoutResult(
+        builder, configureR8PartialCompilation.andThen(optionsConsumer), benchmarkResults);
+    return new R8PartialTestCompileResult(
+        getState(),
+        getOutputMode(),
+        libraryDesugaringTestConfiguration,
+        app.get(),
+        pgConfOutput.toString(),
+        syntheticProguardRulesConsumer.get(),
+        proguardMapBuilder.toString(),
+        graphConsumer,
+        getMinApiLevel(),
+        features,
+        residualArtProfiles,
+        resourceShrinkerOutput,
+        resourceShrinkerOutputForFeatures,
+        buildMetadata != null ? buildMetadata.get() : null);
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java
new file mode 100644
index 0000000..18f75fb
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.metadata.R8BuildMetadata;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.shaking.CollectingGraphConsumer;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.AndroidApp;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+
+public class R8PartialTestCompileResult
+    extends R8TestCompileResultBase<R8PartialTestCompileResult> {
+
+  R8PartialTestCompileResult(
+      TestState state,
+      OutputMode outputMode,
+      LibraryDesugaringTestConfiguration libraryDesugaringTestConfiguration,
+      AndroidApp app,
+      String proguardConfiguration,
+      List<ProguardConfigurationRule> syntheticProguardRules,
+      String proguardMap,
+      CollectingGraphConsumer graphConsumer,
+      int minApiLevel,
+      List<Path> features,
+      List<ExternalArtProfile> residualArtProfiles,
+      Path resourceShrinkerOutput,
+      HashMap<String, Path> resourceShrinkerOutputForFeatures,
+      R8BuildMetadata buildMetadata) {
+    super(
+        state,
+        outputMode,
+        libraryDesugaringTestConfiguration,
+        app,
+        proguardConfiguration,
+        syntheticProguardRules,
+        proguardMap,
+        graphConsumer,
+        minApiLevel,
+        features,
+        residualArtProfiles,
+        resourceShrinkerOutput,
+        resourceShrinkerOutputForFeatures,
+        buildMetadata);
+  }
+
+  @Override
+  public R8PartialTestCompileResult self() {
+    return this;
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 066e1ff..72fd0ae 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -65,8 +65,11 @@
 import java.util.stream.Collectors;
 import org.hamcrest.core.IsAnything;
 
-public abstract class R8TestBuilder<T extends R8TestBuilder<T>>
-    extends TestShrinkerBuilder<R8Command, Builder, R8TestCompileResult, R8TestRunResult, T> {
+public abstract class R8TestBuilder<
+        CR extends TestCompileResult<CR, RR>,
+        RR extends TestRunResult<RR>,
+        T extends R8TestBuilder<CR, RR, T>>
+    extends TestShrinkerBuilder<R8Command, Builder, CR, RR, T> {
 
   enum AllowedDiagnosticMessages {
     ALL,
@@ -86,15 +89,15 @@
   private boolean enableIsolatedSplits = false;
   private boolean enableMissingLibraryApiModeling = true;
   private boolean enableStartupLayoutOptimization = true;
-  private CollectingGraphConsumer graphConsumer = null;
-  private final List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
+  CollectingGraphConsumer graphConsumer = null;
+  final List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
   private final List<String> keepRules = new ArrayList<>();
   private final List<Path> mainDexRulesFiles = new ArrayList<>();
   private final List<String> applyMappingMaps = new ArrayList<>();
-  private final List<Path> features = new ArrayList<>();
-  private Path resourceShrinkerOutput = null;
-  private HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
-  private Box<R8BuildMetadata> buildMetadata;
+  final List<Path> features = new ArrayList<>();
+  Path resourceShrinkerOutput = null;
+  HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
+  Box<R8BuildMetadata> buildMetadata;
   private boolean androidPlatformBuild = false;
 
   @Override
@@ -103,12 +106,22 @@
   }
 
   @Override
-  public R8TestBuilder<?> asR8TestBuilder() {
+  public R8TestBuilder<?, ?, ?> asR8TestBuilder() {
     return this;
   }
 
+  abstract CR internalCompileR8(
+      Builder builder,
+      Consumer<InternalOptions> optionsConsumer,
+      Supplier<AndroidApp> app,
+      BenchmarkResults benchmarkResults,
+      StringBuilder pgConfOutput,
+      Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer,
+      StringBuilder proguardMapBuilder)
+      throws CompilationFailedException;
+
   @Override
-  R8TestCompileResult internalCompile(
+  CR internalCompile(
       Builder builder,
       Consumer<InternalOptions> optionsConsumer,
       Supplier<AndroidApp> app,
@@ -134,13 +147,9 @@
       }
     }
 
-    class Box {
-
-      private List<ProguardConfigurationRule> syntheticProguardRules;
-    }
-    Box box = new Box();
+    Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer = new Box<>();
     ToolHelper.addSyntheticProguardRulesConsumerForTesting(
-        builder, rules -> box.syntheticProguardRules = rules);
+        builder, syntheticProguardRulesConsumer::set);
     libraryDesugaringTestConfiguration.configure(builder);
     builder.setAndroidPlatformBuild(androidPlatformBuild);
     if (!enableEmptyMemberRulesToDefaultInitRuleConversion.isUnknown()) {
@@ -154,23 +163,15 @@
       builder.setBuildMetadataConsumer(buildMetadata::set);
     }
     StringBuilder pgConfOutput = wrapProguardConfigConsumer(builder);
-    ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
-    R8TestCompileResult compileResult =
-        new R8TestCompileResult(
-            getState(),
-            getOutputMode(),
-            libraryDesugaringTestConfiguration,
-            app.get(),
-            pgConfOutput.toString(),
-            box.syntheticProguardRules,
-            proguardMapBuilder.toString(),
-            graphConsumer,
-            getMinApiLevel(),
-            features,
-            residualArtProfiles,
-            resourceShrinkerOutput,
-            resourceShrinkerOutputForFeatures,
-            buildMetadata != null ? buildMetadata.get() : null);
+    CR compileResult =
+        internalCompileR8(
+            builder,
+            optionsConsumer,
+            app,
+            benchmarkResults,
+            pgConfOutput,
+            syntheticProguardRulesConsumer,
+            proguardMapBuilder);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -238,21 +239,42 @@
     return pgConfOutput;
   }
 
+  private static StringBuilder wrapProguardMapConsumer(Builder builder, StringBuilder pgMapOutput) {
+    StringConsumer pgMapConsumer = builder.getProguardMapConsumer();
+    builder.setProguardMapConsumer(
+        new StringConsumer.ForwardingConsumer(pgMapConsumer) {
+          @Override
+          public void accept(String string, DiagnosticsHandler handler) {
+            super.accept(string, handler);
+            pgMapOutput.append(string);
+          }
+
+          @Override
+          public void finished(DiagnosticsHandler handler) {
+            super.finished(handler);
+          }
+        });
+    return pgMapOutput;
+  }
+
+  private static StringBuilder wrapProguardConfigConsumer(
+      Builder builder, StringBuilder pgConfOutput) {
+    StringConsumer pgConfConsumer = builder.getProguardConfigurationConsumer();
+    builder.setProguardConfigurationConsumer(
+        new StringConsumer.ForwardingConsumer(pgConfConsumer) {
+          @Override
+          public void accept(String string, DiagnosticsHandler handler) {
+            super.accept(string, handler);
+            pgConfOutput.append(string);
+          }
+        });
+    return pgConfOutput;
+  }
+
   public Builder getBuilder() {
     return builder;
   }
 
-  public T addProgramResourceProviders(Collection<ProgramResourceProvider> providers) {
-    for (ProgramResourceProvider provider : providers) {
-      builder.addProgramResourceProvider(provider);
-    }
-    return self();
-  }
-
-  public T addProgramResourceProviders(ProgramResourceProvider... providers) {
-    return addProgramResourceProviders(Arrays.asList(providers));
-  }
-
   @Override
   public T addClasspathClasses(Collection<Class<?>> classes) {
     builder.addClasspathResourceProvider(ClassFileResourceProviderFromClasses(classes));
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
index e9956ad..9354c62 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
@@ -3,61 +3,16 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import com.android.tools.r8.DexSegments.SegmentInfo;
-import com.android.tools.r8.TestRuntime.DexRuntime;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.androidresources.AndroidResourceTestingUtils;
-import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
-import com.android.tools.r8.benchmarks.BenchmarkResults;
-import com.android.tools.r8.benchmarks.InstructionCodeSizeResult;
-import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
-import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.ThrowingBiConsumer;
-import com.android.tools.r8.utils.ThrowingConsumer;
-import com.android.tools.r8.utils.ZipUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import com.android.tools.r8.utils.graphinspector.GraphInspector;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Consumer;
 
-public class R8TestCompileResult extends TestCompileResult<R8TestCompileResult, R8TestRunResult> {
-
-  private final String proguardConfiguration;
-  private final List<ProguardConfigurationRule> syntheticProguardRules;
-  private final String proguardMap;
-  private final CollectingGraphConsumer graphConsumer;
-  private final List<Path> features;
-  private final List<ExternalArtProfile> residualArtProfiles;
-  private final Path resourceShrinkerOutput;
-  private final Map<String, Path> resourceShrinkerOutputForFeatures;
-  private final R8BuildMetadata buildMetadata;
+public class R8TestCompileResult extends R8TestCompileResultBase<R8TestCompileResult> {
 
   R8TestCompileResult(
       TestState state,
@@ -74,318 +29,25 @@
       Path resourceShrinkerOutput,
       HashMap<String, Path> resourceShrinkerOutputForFeatures,
       R8BuildMetadata buildMetadata) {
-    super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
-    this.proguardConfiguration = proguardConfiguration;
-    this.syntheticProguardRules = syntheticProguardRules;
-    this.proguardMap = proguardMap;
-    this.graphConsumer = graphConsumer;
-    this.features = features;
-    this.residualArtProfiles = residualArtProfiles;
-    this.resourceShrinkerOutput = resourceShrinkerOutput;
-    this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
-    this.buildMetadata = buildMetadata;
-  }
-
-  public R8TestCompileResult benchmarkResourceSize(BenchmarkResults results) throws IOException {
-    results.addResourceSizeResult(Files.size(resourceShrinkerOutput));
-    return self();
+    super(
+        state,
+        outputMode,
+        libraryDesugaringTestConfiguration,
+        app,
+        proguardConfiguration,
+        syntheticProguardRules,
+        proguardMap,
+        graphConsumer,
+        minApiLevel,
+        features,
+        residualArtProfiles,
+        resourceShrinkerOutput,
+        resourceShrinkerOutputForFeatures,
+        buildMetadata);
   }
 
   @Override
   public R8TestCompileResult self() {
     return this;
   }
-
-  public R8BuildMetadata getBuildMetadata() {
-    assert buildMetadata != null;
-    return buildMetadata;
-  }
-
-  @Override
-  public TestDiagnosticMessages getDiagnosticMessages() {
-    return state.getDiagnosticsMessages();
-  }
-
-  @Override
-  public R8TestCompileResult inspectDiagnosticMessages(Consumer<TestDiagnosticMessages> consumer) {
-    consumer.accept(state.getDiagnosticsMessages());
-    return self();
-  }
-
-  public Path getFeature(int index) {
-    return features.get(index);
-  }
-
-  public List<Path> getFeatures() {
-    return features;
-  }
-
-  @Override
-  public Set<String> getMainDexClasses() {
-    return state.getMainDexClasses();
-  }
-
-  @Override
-  public String getStdout() {
-    return state.getStdout();
-  }
-
-  @Override
-  public String getStderr() {
-    return state.getStderr();
-  }
-
-  @Override
-  public CodeInspector inspector() throws IOException {
-    return inspector(null);
-  }
-
-  public CodeInspector inspector(Consumer<InternalOptions> debugOptionsConsumer)
-      throws IOException {
-    return new CodeInspector(app, proguardMap, debugOptionsConsumer);
-  }
-
-  private CodeInspector featureInspector(Path feature) throws IOException {
-    return new CodeInspector(
-        AndroidApp.builder().addProgramFile(feature).setProguardMapOutputData(proguardMap).build());
-  }
-
-  public CodeInspector featureInspector() throws IOException {
-    assert features.size() == 1;
-    return featureInspector(features.get(0));
-  }
-
-  @SafeVarargs
-  public final <E extends Throwable> R8TestCompileResult inspect(
-      ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
-    assertEquals(1 + features.size(), consumers.length);
-    consumers[0].accept(inspector());
-    for (int i = 0; i < features.size(); i++) {
-      consumers[i + 1].accept(featureInspector(features.get(i)));
-    }
-    return self();
-  }
-
-  @SafeVarargs
-  @Override
-  public final <E extends Throwable> R8TestCompileResult inspectMultiDex(
-      ThrowingConsumer<CodeInspector, E>... consumers) throws E {
-    try {
-      return inspectMultiDex(writeProguardMap(), consumers);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  public final <E extends Throwable> R8TestCompileResult inspectGraph(
-      ThrowingConsumer<GraphInspector, E> consumer) throws IOException, E {
-    consumer.accept(graphInspector());
-    return self();
-  }
-
-  public <E extends Throwable> R8TestCompileResult inspectResidualArtProfile(
-      ThrowingConsumer<ArtProfileInspector, E> consumer) throws E, IOException {
-    return inspectResidualArtProfile(
-        (rewrittenArtProfile, inspector) -> consumer.accept(rewrittenArtProfile));
-  }
-
-  public <E extends Throwable> R8TestCompileResult inspectResidualArtProfile(
-      ThrowingBiConsumer<ArtProfileInspector, CodeInspector, E> consumer) throws E, IOException {
-    assertEquals(1, residualArtProfiles.size());
-    consumer.accept(new ArtProfileInspector(residualArtProfiles.iterator().next()), inspector());
-    return self();
-  }
-
-  public <E extends Throwable> R8TestCompileResult inspectShrunkenResources(
-      Consumer<ResourceTableInspector> consumer) throws IOException {
-    assertNotNull(resourceShrinkerOutput);
-    consumer.accept(
-        new ResourceTableInspector(
-            ZipUtils.readSingleEntry(resourceShrinkerOutput, "resources.pb")));
-    return self();
-  }
-
-  public <E extends Throwable> R8TestCompileResult assertResourceFile(String name, boolean present)
-      throws IOException {
-    assertNotNull(resourceShrinkerOutput);
-    assertEquals(ZipUtils.containsEntry(resourceShrinkerOutput, name), present);
-    return self();
-  }
-
-  public <E extends Throwable> R8TestCompileResult assertFeatureResourceFile(
-      String name, boolean present, String featureName) throws IOException {
-    Path path = resourceShrinkerOutputForFeatures.get(featureName);
-    assertEquals(ZipUtils.containsEntry(path, name), present);
-    return self();
-  }
-
-  public String dumpResources() throws IOException {
-    ProcessResult processResult = AndroidResourceTestingUtils.dumpWithAapt2(resourceShrinkerOutput);
-    assert processResult.exitCode == 0;
-    return processResult.stdout;
-  }
-
-  public <E extends Throwable> R8TestCompileResult inspectShrunkenResourcesForFeature(
-      Consumer<ResourceTableInspector> consumer, String featureName) throws IOException {
-    Path path = resourceShrinkerOutputForFeatures.get(featureName);
-    assertNotNull(path);
-    consumer.accept(new ResourceTableInspector(ZipUtils.readSingleEntry(path, "resources.pb")));
-    return self();
-  }
-
-  public GraphInspector graphInspector() throws IOException {
-    assert graphConsumer != null;
-    return new GraphInspector(graphConsumer, inspector());
-  }
-
-  public String getProguardConfiguration() {
-    return proguardConfiguration;
-  }
-
-  public R8TestCompileResult inspectProguardConfiguration(Consumer<String> consumer) {
-    consumer.accept(getProguardConfiguration());
-    return self();
-  }
-
-  public List<ProguardConfigurationRule> getSyntheticProguardRules() {
-    return syntheticProguardRules;
-  }
-
-  public R8TestCompileResult inspectSyntheticProguardRules(
-      Consumer<List<ProguardConfigurationRule>> consumer) {
-    consumer.accept(getSyntheticProguardRules());
-    return self();
-  }
-
-  @Override
-  public R8TestRunResult createRunResult(TestRuntime runtime, ProcessResult result) {
-    return new R8TestRunResult(app, runtime, result, proguardMap, this::graphInspector, state);
-  }
-
-  public R8TestCompileResult addFeatureSplitsToRunClasspathFiles() {
-    return addRunClasspathFiles(features);
-  }
-
-  public R8TestRunResult runFeature(TestRuntime runtime, Class<?> mainFeatureClass)
-      throws IOException {
-    return runFeature(runtime, mainFeatureClass, features.get(0));
-  }
-
-  public R8TestRunResult runFeature(
-      TestRuntime runtime, Class<?> mainFeatureClass, Path feature, Path... featureDependencies)
-      throws IOException {
-    assert getBackend() == runtime.getBackend();
-    ClassSubject mainClassSubject = inspector().clazz(SplitRunner.class);
-    assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
-    assertThat(
-        "Did you forget a keep rule for the main method?",
-        mainClassSubject.mainMethod(),
-        isPresent());
-    ClassSubject mainFeatureClassSubject = featureInspector(feature).clazz(mainFeatureClass);
-    assertThat(
-        "Did you forget a keep rule for the run method?", mainFeatureClassSubject, isPresent());
-    assertThat(
-        "Did you forget a keep rule for the run method?",
-        mainFeatureClassSubject.uniqueMethodWithOriginalName("run"),
-        isPresent());
-    String[] args = new String[2 + featureDependencies.length];
-    args[0] = mainFeatureClassSubject.getFinalName();
-    args[1] = feature.toString();
-    for (int i = 2; i < args.length; i++) {
-      args[i] = featureDependencies[i - 2].toString();
-    }
-    return runArt(runtime, mainClassSubject.getFinalName(), args);
-  }
-
-  public String getProguardMap() {
-    return proguardMap;
-  }
-
-  public R8TestCompileResult inspectProguardMap(ThrowableConsumer<String> consumer)
-      throws Throwable {
-    consumer.accept(getProguardMap());
-    return this;
-  }
-
-  public Path writeProguardMap() throws IOException {
-    Path file = state.getNewTempFolder().resolve("out.zip");
-    writeProguardMap(file);
-    return file;
-  }
-
-  public R8TestCompileResult writeProguardMap(Path path) throws IOException {
-    FileUtils.writeTextFile(path, getProguardMap());
-    return self();
-  }
-
-  @Override
-  public R8TestCompileResult benchmarkCodeSize(BenchmarkResults results)
-      throws IOException, ResourceException {
-    if (results.isBenchmarkingCodeSize()) {
-      int applicationSizeWithFeatures =
-          AndroidApp.builder(app).addProgramFiles(features).build().applicationSize();
-      results.addCodeSizeResult(applicationSizeWithFeatures);
-    }
-    return self();
-  }
-
-  @Override
-  public R8TestCompileResult benchmarkInstructionCodeSize(BenchmarkResults results)
-      throws IOException {
-    if (results.isBenchmarkingCodeSize()) {
-      InstructionCodeSizeResult result = getComposableCodeSize(inspector());
-      for (Path feature : features) {
-        result.add(getComposableCodeSize(featureInspector(feature)));
-      }
-      results.addInstructionCodeSizeResult(result.instructionCodeSize);
-      results.addComposableInstructionCodeSizeResult(result.composableInstructionCodeSize);
-    }
-    return self();
-  }
-
-  private InstructionCodeSizeResult getComposableCodeSize(CodeInspector inspector) {
-    DexType composableType =
-        inspector.getFactory().createType("Landroidx/compose/runtime/Composable;");
-    InstructionCodeSizeResult result = new InstructionCodeSizeResult();
-    for (FoundClassSubject classSubject : inspector.allClasses()) {
-      DexProgramClass clazz = classSubject.getDexProgramClass();
-      for (DexEncodedMethod method : clazz.methods(DexEncodedMethod::hasCode)) {
-        int instructionCodeSize = method.getCode().asDexCode().codeSizeInBytes();
-        result.instructionCodeSize += instructionCodeSize;
-        if (method.annotations().hasAnnotation(composableType)) {
-          result.composableInstructionCodeSize += instructionCodeSize;
-        }
-      }
-    }
-    return result;
-  }
-
-  @Override
-  public R8TestCompileResult benchmarkDexSegmentsCodeSize(BenchmarkResults results)
-      throws IOException, ResourceException {
-    if (results.isBenchmarkingCodeSize()) {
-      AndroidApp appWithFeatures =
-          features.isEmpty() ? app : AndroidApp.builder(app).addProgramFiles(features).build();
-      results.addDexSegmentsSizeResult(runDexSegments(appWithFeatures));
-    }
-    return self();
-  }
-
-  private Int2ReferenceMap<SegmentInfo> runDexSegments(AndroidApp app)
-      throws IOException, ResourceException {
-    Map<Integer, SegmentInfo> result = DexSegments.runForTesting(app);
-    Int2ReferenceMap<SegmentInfo> rewrittenResult = new Int2ReferenceLinkedOpenHashMap<>();
-    rewrittenResult.putAll(result);
-    return rewrittenResult;
-  }
-
-  @Override
-  public R8TestCompileResult benchmarkDex2OatCodeSize(BenchmarkResults results) throws IOException {
-    if (results.isBenchmarkingCodeSize()) {
-      Dex2OatTestRunResult dex2OatTestRunResult =
-          runDex2Oat(new DexRuntime(DexVm.Version.LATEST_DEX2OAT));
-      results.addDex2OatSizeResult(dex2OatTestRunResult.getOatSizeOrDefault(0));
-    }
-    return self();
-  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java
new file mode 100644
index 0000000..b38c63c
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java
@@ -0,0 +1,403 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static 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.assertNotNull;
+
+import com.android.tools.r8.DexSegments.SegmentInfo;
+import com.android.tools.r8.TestRuntime.DexRuntime;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
+import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.benchmarks.InstructionCodeSizeResult;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.metadata.R8BuildMetadata;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.shaking.CollectingGraphConsumer;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public abstract class R8TestCompileResultBase<CR extends R8TestCompileResultBase<CR>>
+    extends TestCompileResult<CR, R8TestRunResult> {
+
+  private final String proguardConfiguration;
+  private final List<ProguardConfigurationRule> syntheticProguardRules;
+  private final String proguardMap;
+  private final CollectingGraphConsumer graphConsumer;
+  private final List<Path> features;
+  private final List<ExternalArtProfile> residualArtProfiles;
+  private final Path resourceShrinkerOutput;
+  private final Map<String, Path> resourceShrinkerOutputForFeatures;
+  private final R8BuildMetadata buildMetadata;
+
+  R8TestCompileResultBase(
+      TestState state,
+      OutputMode outputMode,
+      LibraryDesugaringTestConfiguration libraryDesugaringTestConfiguration,
+      AndroidApp app,
+      String proguardConfiguration,
+      List<ProguardConfigurationRule> syntheticProguardRules,
+      String proguardMap,
+      CollectingGraphConsumer graphConsumer,
+      int minApiLevel,
+      List<Path> features,
+      List<ExternalArtProfile> residualArtProfiles,
+      Path resourceShrinkerOutput,
+      HashMap<String, Path> resourceShrinkerOutputForFeatures,
+      R8BuildMetadata buildMetadata) {
+    super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
+    this.proguardConfiguration = proguardConfiguration;
+    this.syntheticProguardRules = syntheticProguardRules;
+    this.proguardMap = proguardMap;
+    this.graphConsumer = graphConsumer;
+    this.features = features;
+    this.residualArtProfiles = residualArtProfiles;
+    this.resourceShrinkerOutput = resourceShrinkerOutput;
+    this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
+    this.buildMetadata = buildMetadata;
+  }
+
+  public CR benchmarkResourceSize(BenchmarkResults results) throws IOException {
+    results.addResourceSizeResult(Files.size(resourceShrinkerOutput));
+    return self();
+  }
+
+  public R8BuildMetadata getBuildMetadata() {
+    assert buildMetadata != null;
+    return buildMetadata;
+  }
+
+  @Override
+  public TestDiagnosticMessages getDiagnosticMessages() {
+    return state.getDiagnosticsMessages();
+  }
+
+  @Override
+  public CR inspectDiagnosticMessages(Consumer<TestDiagnosticMessages> consumer) {
+    consumer.accept(state.getDiagnosticsMessages());
+    return self();
+  }
+
+  public Path getFeature(int index) {
+    return features.get(index);
+  }
+
+  public List<Path> getFeatures() {
+    return features;
+  }
+
+  @Override
+  public Set<String> getMainDexClasses() {
+    return state.getMainDexClasses();
+  }
+
+  @Override
+  public String getStdout() {
+    return state.getStdout();
+  }
+
+  @Override
+  public String getStderr() {
+    return state.getStderr();
+  }
+
+  @Override
+  public CodeInspector inspector() throws IOException {
+    return inspector(null);
+  }
+
+  public CodeInspector inspector(Consumer<InternalOptions> debugOptionsConsumer)
+      throws IOException {
+    return new CodeInspector(app, proguardMap, debugOptionsConsumer);
+  }
+
+  private CodeInspector featureInspector(Path feature) throws IOException {
+    return new CodeInspector(
+        AndroidApp.builder().addProgramFile(feature).setProguardMapOutputData(proguardMap).build());
+  }
+
+  public CodeInspector featureInspector() throws IOException {
+    assert features.size() == 1;
+    return featureInspector(features.get(0));
+  }
+
+  @SafeVarargs
+  public final <E extends Throwable> CR inspect(ThrowingConsumer<CodeInspector, E>... consumers)
+      throws IOException, E {
+    assertEquals(1 + features.size(), consumers.length);
+    consumers[0].accept(inspector());
+    for (int i = 0; i < features.size(); i++) {
+      consumers[i + 1].accept(featureInspector(features.get(i)));
+    }
+    return self();
+  }
+
+  @SafeVarargs
+  @Override
+  public final <E extends Throwable> CR inspectMultiDex(
+      ThrowingConsumer<CodeInspector, E>... consumers) throws E {
+    try {
+      return inspectMultiDex(writeProguardMap(), consumers);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  public final <E extends Throwable> CR inspectGraph(ThrowingConsumer<GraphInspector, E> consumer)
+      throws IOException, E {
+    consumer.accept(graphInspector());
+    return self();
+  }
+
+  public <E extends Throwable> CR inspectResidualArtProfile(
+      ThrowingConsumer<ArtProfileInspector, E> consumer) throws E, IOException {
+    return inspectResidualArtProfile(
+        (rewrittenArtProfile, inspector) -> consumer.accept(rewrittenArtProfile));
+  }
+
+  public <E extends Throwable> CR inspectResidualArtProfile(
+      ThrowingBiConsumer<ArtProfileInspector, CodeInspector, E> consumer) throws E, IOException {
+    assertEquals(1, residualArtProfiles.size());
+    consumer.accept(new ArtProfileInspector(residualArtProfiles.iterator().next()), inspector());
+    return self();
+  }
+
+  public <E extends Throwable> CR inspectShrunkenResources(
+      Consumer<ResourceTableInspector> consumer) throws IOException {
+    assertNotNull(resourceShrinkerOutput);
+    consumer.accept(
+        new ResourceTableInspector(
+            ZipUtils.readSingleEntry(resourceShrinkerOutput, "resources.pb")));
+    return self();
+  }
+
+  public <E extends Throwable> CR assertResourceFile(String name, boolean present)
+      throws IOException {
+    assertNotNull(resourceShrinkerOutput);
+    assertEquals(ZipUtils.containsEntry(resourceShrinkerOutput, name), present);
+    return self();
+  }
+
+  public <E extends Throwable> CR assertFeatureResourceFile(
+      String name, boolean present, String featureName) throws IOException {
+    Path path = resourceShrinkerOutputForFeatures.get(featureName);
+    assertEquals(ZipUtils.containsEntry(path, name), present);
+    return self();
+  }
+
+  public String dumpResources() throws IOException {
+    ProcessResult processResult = AndroidResourceTestingUtils.dumpWithAapt2(resourceShrinkerOutput);
+    assert processResult.exitCode == 0;
+    return processResult.stdout;
+  }
+
+  public <E extends Throwable> CR inspectShrunkenResourcesForFeature(
+      Consumer<ResourceTableInspector> consumer, String featureName) throws IOException {
+    Path path = resourceShrinkerOutputForFeatures.get(featureName);
+    assertNotNull(path);
+    consumer.accept(new ResourceTableInspector(ZipUtils.readSingleEntry(path, "resources.pb")));
+    return self();
+  }
+
+  public GraphInspector graphInspector() throws IOException {
+    assert graphConsumer != null;
+    return new GraphInspector(graphConsumer, inspector());
+  }
+
+  public String getProguardConfiguration() {
+    return proguardConfiguration;
+  }
+
+  public CR inspectProguardConfiguration(Consumer<String> consumer) {
+    consumer.accept(getProguardConfiguration());
+    return self();
+  }
+
+  public List<ProguardConfigurationRule> getSyntheticProguardRules() {
+    return syntheticProguardRules;
+  }
+
+  public CR inspectSyntheticProguardRules(Consumer<List<ProguardConfigurationRule>> consumer) {
+    consumer.accept(getSyntheticProguardRules());
+    return self();
+  }
+
+  @Override
+  public R8TestRunResult createRunResult(TestRuntime runtime, ProcessResult result) {
+    return new R8TestRunResult(app, runtime, result, proguardMap, this::graphInspector, state);
+  }
+
+  public CR addFeatureSplitsToRunClasspathFiles() {
+    return addRunClasspathFiles(features);
+  }
+
+  public R8TestRunResult runFeature(TestRuntime runtime, Class<?> mainFeatureClass)
+      throws IOException {
+    return runFeature(runtime, mainFeatureClass, features.get(0));
+  }
+
+  public R8TestRunResult runFeature(
+      TestRuntime runtime, Class<?> mainFeatureClass, Path feature, Path... featureDependencies)
+      throws IOException {
+    assert getBackend() == runtime.getBackend();
+    ClassSubject mainClassSubject = inspector().clazz(SplitRunner.class);
+    assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the main method?",
+        mainClassSubject.mainMethod(),
+        isPresent());
+    ClassSubject mainFeatureClassSubject = featureInspector(feature).clazz(mainFeatureClass);
+    assertThat(
+        "Did you forget a keep rule for the run method?", mainFeatureClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the run method?",
+        mainFeatureClassSubject.uniqueMethodWithOriginalName("run"),
+        isPresent());
+    String[] args = new String[2 + featureDependencies.length];
+    args[0] = mainFeatureClassSubject.getFinalName();
+    args[1] = feature.toString();
+    for (int i = 2; i < args.length; i++) {
+      args[i] = featureDependencies[i - 2].toString();
+    }
+    return runArt(runtime, mainClassSubject.getFinalName(), args);
+  }
+
+  public CodeInspector inspectorForBase() throws IOException {
+    return new CodeInspector(writeToZip());
+  }
+
+  public CodeInspector inspectorForFeature(int index) throws IOException {
+    return new CodeInspector(getFeature(index));
+  }
+
+  public <E extends Throwable> CR inspectBase(ThrowingConsumer<CodeInspector, E> consumer)
+      throws IOException, E {
+    consumer.accept(inspectorForBase());
+    return self();
+  }
+
+  public <E extends Throwable> CR inspectFeature(
+      int index, ThrowingConsumer<CodeInspector, E> consumer) throws IOException, E {
+    consumer.accept(inspectorForFeature(index));
+    return self();
+  }
+
+  public String getProguardMap() {
+    return proguardMap;
+  }
+
+  public CR inspectProguardMap(ThrowableConsumer<String> consumer) throws Throwable {
+    consumer.accept(getProguardMap());
+    return self();
+  }
+
+  public Path writeProguardMap() throws IOException {
+    Path file = state.getNewTempFolder().resolve("out.zip");
+    writeProguardMap(file);
+    return file;
+  }
+
+  public CR writeProguardMap(Path path) throws IOException {
+    FileUtils.writeTextFile(path, getProguardMap());
+    return self();
+  }
+
+  @Override
+  public CR benchmarkCodeSize(BenchmarkResults results) throws IOException, ResourceException {
+    if (results.isBenchmarkingCodeSize()) {
+      int applicationSizeWithFeatures =
+          AndroidApp.builder(app).addProgramFiles(features).build().applicationSize();
+      results.addCodeSizeResult(applicationSizeWithFeatures);
+    }
+    return self();
+  }
+
+  @Override
+  public CR benchmarkInstructionCodeSize(BenchmarkResults results) throws IOException {
+    if (results.isBenchmarkingCodeSize()) {
+      InstructionCodeSizeResult result = getComposableCodeSize(inspector());
+      for (Path feature : features) {
+        result.add(getComposableCodeSize(featureInspector(feature)));
+      }
+      results.addInstructionCodeSizeResult(result.instructionCodeSize);
+      results.addComposableInstructionCodeSizeResult(result.composableInstructionCodeSize);
+    }
+    return self();
+  }
+
+  private InstructionCodeSizeResult getComposableCodeSize(CodeInspector inspector) {
+    DexType composableType =
+        inspector.getFactory().createType("Landroidx/compose/runtime/Composable;");
+    InstructionCodeSizeResult result = new InstructionCodeSizeResult();
+    for (FoundClassSubject classSubject : inspector.allClasses()) {
+      DexProgramClass clazz = classSubject.getDexProgramClass();
+      for (DexEncodedMethod method : clazz.methods(DexEncodedMethod::hasCode)) {
+        int instructionCodeSize = method.getCode().asDexCode().codeSizeInBytes();
+        result.instructionCodeSize += instructionCodeSize;
+        if (method.annotations().hasAnnotation(composableType)) {
+          result.composableInstructionCodeSize += instructionCodeSize;
+        }
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public CR benchmarkDexSegmentsCodeSize(BenchmarkResults results)
+      throws IOException, ResourceException {
+    if (results.isBenchmarkingCodeSize()) {
+      AndroidApp appWithFeatures =
+          features.isEmpty() ? app : AndroidApp.builder(app).addProgramFiles(features).build();
+      results.addDexSegmentsSizeResult(runDexSegments(appWithFeatures));
+    }
+    return self();
+  }
+
+  private Int2ReferenceMap<SegmentInfo> runDexSegments(AndroidApp app)
+      throws IOException, ResourceException {
+    Map<Integer, SegmentInfo> result = DexSegments.runForTesting(app);
+    Int2ReferenceMap<SegmentInfo> rewrittenResult = new Int2ReferenceLinkedOpenHashMap<>();
+    rewrittenResult.putAll(result);
+    return rewrittenResult;
+  }
+
+  @Override
+  public CR benchmarkDex2OatCodeSize(BenchmarkResults results) throws IOException {
+    if (results.isBenchmarkingCodeSize()) {
+      Dex2OatTestRunResult dex2OatTestRunResult =
+          runDex2Oat(new DexRuntime(DexVm.Version.LATEST_DEX2OAT));
+      results.addDex2OatSizeResult(dex2OatTestRunResult.getOatSizeOrDefault(0));
+    }
+    return self();
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBase.java b/src/test/testbase/java/com/android/tools/r8/TestBase.java
index e3cc4c8..8cdcbc0 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -167,6 +167,10 @@
     return R8FullTestBuilder.create(new TestState(temp), backend);
   }
 
+  public static R8PartialTestBuilder testForR8Partial(TemporaryFolder temp, Backend backend) {
+    return R8PartialTestBuilder.create(new TestState(temp), backend);
+  }
+
   public static R8CompatTestBuilder testForR8Compat(
       TemporaryFolder temp, Backend backend, boolean forceProguardCompatibility) {
     return R8CompatTestBuilder.create(new TestState(temp), backend, forceProguardCompatibility);
@@ -206,6 +210,10 @@
     return testForR8(temp, backend);
   }
 
+  public R8PartialTestBuilder testForR8Partial(Backend backend) {
+    return testForR8Partial(temp, backend);
+  }
+
   public R8CompatTestBuilder testForR8Compat(Backend backend) {
     return testForR8Compat(backend, true);
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
index 98cb209..55d667b 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
@@ -74,11 +74,33 @@
     return self();
   }
 
+  public T addProgramResourceProviders(Collection<ProgramResourceProvider> providers) {
+    for (ProgramResourceProvider provider : providers) {
+      builder.addProgramResourceProvider(provider);
+    }
+    return self();
+  }
+
+  public T addProgramResourceProviders(ProgramResourceProvider... providers) {
+    return addProgramResourceProviders(Arrays.asList(providers));
+  }
+
   public T addLibraryProvider(ClassFileResourceProvider provider) {
     builder.addLibraryResourceProvider(provider);
     return self();
   }
 
+  public T addClasspathResourceProviders(Collection<ClassFileResourceProvider> providers) {
+    for (ClassFileResourceProvider provider : providers) {
+      builder.addClasspathResourceProvider(provider);
+    }
+    return self();
+  }
+
+  public T addClasspathResourceProviders(ClassFileResourceProvider... providers) {
+    return addClasspathResourceProviders(Arrays.asList(providers));
+  }
+
   @Override
   public T addLibraryFiles(Collection<Path> files) {
     builder.addLibraryFiles(files);
@@ -91,6 +113,17 @@
     return self();
   }
 
+  public T addLibraryResourceProviders(Collection<ClassFileResourceProvider> providers) {
+    for (ClassFileResourceProvider provider : providers) {
+      builder.addLibraryResourceProvider(provider);
+    }
+    return self();
+  }
+
+  public T addLibraryResourceProviders(ClassFileResourceProvider... providers) {
+    return addLibraryResourceProviders(Arrays.asList(providers));
+  }
+
   public T addMainDexListClassReferences(ClassReference... classes) {
     return addMainDexListClassReferences(Arrays.asList(classes));
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
index 4b18fcb..ea40b20 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
@@ -88,10 +88,10 @@
     return self;
   }
 
-  public T applyIfR8(ThrowableConsumer<? super R8TestBuilder<?>> consumer) {
+  public T applyIfR8(ThrowableConsumer<? super R8TestBuilder<?, ?, ?>> consumer) {
     T self = self();
-    if (this instanceof R8TestBuilder<?>) {
-      consumer.acceptWithRuntimeException((R8TestBuilder<?>) self);
+    if (this instanceof R8TestBuilder<?, ?, ?>) {
+      consumer.acceptWithRuntimeException((R8TestBuilder<?, ?, ?>) self);
     }
     return self;
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
index 0e28732..c9cc10d 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -118,7 +118,7 @@
     return false;
   }
 
-  public R8TestBuilder<?> asR8TestBuilder() {
+  public R8TestBuilder<?, ?, ?> asR8TestBuilder() {
     return null;
   }
 
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index 7b76fc0..1a8a049 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -2792,4 +2792,9 @@
     assert !strings.isEmpty();
     return strings.get(0);
   }
+
+  public static void setReadEmbeddedRulesFromClasspathAndLibrary(
+      R8Command.Builder builder, boolean enabled) {
+    builder.setReadEmbeddedRulesFromClasspathAndLibrary(enabled);
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
index d9a4981..7aab984 100644
--- a/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
@@ -230,11 +230,11 @@
     consumer.accept((D8TestBuilder) builder);
   }
 
-  private void withR8TestBuilder(Consumer<R8TestBuilder<?>> consumer) {
+  private void withR8TestBuilder(Consumer<R8TestBuilder<?, ?, ?>> consumer) {
     if (!builder.isTestShrinkerBuilder()) {
       return;
     }
-    consumer.accept((R8TestBuilder<?>) builder);
+    consumer.accept((R8TestBuilder<?, ?, ?>) builder);
   }
 
   public DesugaredLibraryTestBuilder<T> allowUnusedDontWarnPatterns() {
@@ -267,7 +267,8 @@
     return this;
   }
 
-  public DesugaredLibraryTestBuilder<T> applyIfR8TestBuilder(Consumer<R8TestBuilder<?>> consumer) {
+  public DesugaredLibraryTestBuilder<T> applyIfR8TestBuilder(
+      Consumer<R8TestBuilder<?, ?, ?>> consumer) {
     withR8TestBuilder(consumer);
     return this;
   }
diff --git a/third_party/kotlin/kotlin-compiler-2.1.0-Beta1.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-2.1.0-Beta1.tar.gz.sha1
new file mode 100644
index 0000000..3f0f592
--- /dev/null
+++ b/third_party/kotlin/kotlin-compiler-2.1.0-Beta1.tar.gz.sha1
@@ -0,0 +1 @@
+491ee600cbb5b7febfd1ca4bda0d97e5d14aa370
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index 83f775d..c819865 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -223,6 +223,9 @@
     def main_dex_rules_resource(self):
         return self.if_exists('main-dex-rules.txt')
 
+    def resource_ap_file(self):
+        return self.if_exists('app-res.ap_')
+
     def art_profile_resources(self):
         art_profile_resources = []
         while True:
@@ -633,6 +636,10 @@
             cmd.append('--isolated-splits')
         if dump.library_jar():
             cmd.extend(['--lib', dump.library_jar()])
+        if dump.resource_ap_file():
+            res_output = os.path.join(temp, 'ap-res-out.ap_')
+            cmd.extend(['--android-resources', dump.resource_ap_file(),
+                        res_output])
         if dump.classpath_jar() and not is_l8_compiler(compiler):
             cmd.extend([
                 '--target' if compiler == 'tracereferences' else '--classpath',
@@ -782,6 +789,7 @@
 
 
 def run(args, otherargs):
+    gradle.EnsureJdk()
     if args.summary:
         summarize_dump_files(otherargs)
     elif args.loop:
diff --git a/tools/tag_versions.py b/tools/tag_versions.py
index 7a6dfc3..cdedf53 100755
--- a/tools/tag_versions.py
+++ b/tools/tag_versions.py
@@ -5,6 +5,7 @@
 
 import argparse
 import jdk
+import json
 import os.path
 import re
 import subprocess
@@ -26,7 +27,7 @@
                         help='The R8 branch to tag versions on, eg, origin/3.0')
     parser.add_argument('--agp',
                         help='The AGP to compute the tag for, eg, 4.2.0-beta03')
-    parser.add_argument('--no-push',
+    parser.add_argument('--use-push',
                         default=False,
                         action='store_true',
                         help='To create the tag locally only.')
@@ -40,8 +41,6 @@
 
 def run(options, cmd):
     print(' '.join(cmd))
-    if 'push' in cmd and options.no_push:
-        return
     if options.dry_run:
         return
     try:
@@ -81,6 +80,28 @@
     subprocess.check_output(cmd)
     return temp
 
+# Testing info: To delete a tag use
+#
+# gob-curl --request DELETE https://r8-review.git.corp.google.com/a/projects/r8/tags/<tag>
+#
+def gerrit_tag(args, tag, hash, description):
+    data = json.dumps({
+        "message": description if description else tag,
+        "revision": hash
+    })
+    cmd = ' '.join([
+        'gob-curl',
+        '--header',
+        '"Content-Type: application/json; charset=UTF-8"',
+        '--request',
+        'PUT',
+        '--data',
+        "'{data}'".format(data=data),
+        'https://r8-review.git.corp.google.com/a/projects/r8/tags/{tag}'.format(tag=tag)
+    ])
+    result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+    print(result)
+
 
 def get_tag_info_on_origin(tag):
     output = subprocess.check_output(
@@ -110,8 +131,8 @@
                 tag_agp_version(version, args)
 
 
-def tag_agp_version(agp, args):
-    tag = 'agp-%s' % agp
+def tag_agp_version(agp_version, args):
+    tag = 'agp-%s' % agp_version
     result = get_tag_info_on_origin(tag)
     if result:
         print('Tag %s is already present' % tag)
@@ -119,12 +140,12 @@
         subprocess.call(['git', 'show', '--oneline', '-s', tag])
         return 0
     with utils.TempDir() as temp:
-        url = "%s/%s/builder-%s.jar" % (AGP_MAVEN, agp, agp)
+        url = "%s/%s/builder-%s.jar" % (AGP_MAVEN, agp_version, agp_version)
         jar = os.path.join(temp, "agp.jar")
         try:
             urllib.request.urlretrieve(url, jar)
         except urllib.error.HTTPError as e:
-            print('Could not find jar for agp %s' % agp)
+            print('Could not find jar for agp %s' % agp_version)
             print(e)
             return 1
         print_version_helper = prepare_print_version(utils.R8_JAR, temp)
@@ -133,11 +154,17 @@
             ':'.join([jar, print_version_helper]),
             'com.android.tools.r8.utils.PrintR8Version'
         ]).decode('utf-8')
-        version = output.split(' ')[0]
-        run(args, ['git', 'tag', '-f', tag, '-m', tag, '%s^{}' % version])
-        run(args, [
-            'git', 'push', '-o', 'push-justification=b/313360935', 'origin', tag
-        ])
+        r8_version = output.split(' ')[0]
+        cmd = ' '.join(['git', 'rev-list', '-n', '1', r8_version])
+        hash = subprocess.check_output(cmd, shell=True).decode('utf-8').strip()
+        message = 'AGP version {version}'.format(version=agp_version)
+        run(args, ['git', 'tag', '-f', tag, '-m', message, hash])
+        if args.use_push:
+            run(args, [
+                'git', 'push', '-o', 'push-justification=b/313360935', 'origin', tag
+            ])
+        else:
+            gerrit_tag(args, tag, hash, message)
 
 
 def tag_r8_branch(branch, args):
@@ -156,11 +183,15 @@
         version = m.group(1)
         result = get_tag_info_on_origin(version)
         if not result:
-            run(args, ['git', 'tag', '-a', version, '-m', version, hash])
-            run(args, [
-                'git', 'push', '-o', 'push-justification=b/313360935', 'origin',
-                version
-            ])
+            message = 'Version {version}'.format(version=version)
+            run(args, ['git', 'tag', '-a', version, '-m', message, hash])
+            if args.use_push:
+                run(args, [
+                    'git', 'push', '-o', 'push-justification=b/313360935', 'origin',
+                    version
+                ])
+            else:
+                gerrit_tag(args, version, hash, message)
     if args.dry_run:
         print('Dry run complete. None of the above have been executed.')