Merge commit '825c964d58326b0e68b4baa76ebe8786ede4a0eb' into dev-release

Change-Id: I4d0001805440ef2f7301f85b53c0b356b341ece7
diff --git a/.gitignore b/.gitignore
index 3da002f..1c8e875 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,6 +164,8 @@
 third_party/kotlin/kotlin-compiler-1.7.0
 third_party/kotlin/kotlin-compiler-1.8.0.tar.gz
 third_party/kotlin/kotlin-compiler-1.8.0
+third_party/kotlin/kotlin-compiler-1.9.21.tar.gz
+third_party/kotlin/kotlin-compiler-1.9.21
 third_party/kotlin/kotlin-compiler-dev.tar.gz
 third_party/kotlin/kotlin-compiler-dev
 third_party/kotlinx-coroutines-1.3.6.tar.gz
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
deleted file mode 100644
index dc6f3be..0000000
--- a/buildSrc/build.gradle
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-apply plugin: 'java'
-apply plugin: 'idea'
-
-repositories {
-    maven {
-        url uri('file:../third_party/dependencies')
-    }
-}
-
-ext {
-    guavaVersion = '31.1-jre'
-    asmVersion = '9.5'
-    smaliVersion = '3.0.3'
-}
-
-dependencies {
-    implementation group: 'com.google.guava', name: 'guava', version: guavaVersion
-    implementation group: 'com.android.tools.smali', name: 'smali', version: smaliVersion
-    implementation group: 'org.ow2.asm', name: 'asm', version: asmVersion
-    implementation group: 'org.ow2.asm', name: 'asm-commons', version: asmVersion
-    implementation group: 'org.ow2.asm', name: 'asm-tree', version: asmVersion
-    implementation group: 'org.ow2.asm', name: 'asm-analysis', version: asmVersion
-    implementation group: 'org.ow2.asm', name: 'asm-util', version: asmVersion
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
diff --git a/buildSrc/src/main/java/kotlin/Kotlinc.java b/buildSrc/src/main/java/kotlin/Kotlinc.java
deleted file mode 100644
index efb39aa..0000000
--- a/buildSrc/src/main/java/kotlin/Kotlinc.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package kotlin;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.UncheckedIOException;
-import org.gradle.api.file.FileCollection;
-import org.gradle.api.file.FileTree;
-import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-import org.gradle.process.ExecSpec;
-import utils.Utils;
-
-/**
- * Gradle task to compile Kotlin source files. By default the generated classes target Java 1.6.
- */
-public class Kotlinc extends DefaultTask {
-
-  private static final String kotlincExecName = Utils.toolsDir().equals("windows")
-      ? "kotlinc.bat"
-      : "kotlinc";
-
-  private static final Path kotlincExecPath =
-      Paths.get(
-          "third_party", "kotlin", "kotlin-compiler-1.3.72", "kotlinc", "bin", kotlincExecName);
-
-  enum KotlinTargetVersion {
-    JAVA_6("1.6"),
-    JAVA_8("1.8");
-
-    private final String optionName;
-
-    KotlinTargetVersion(String optionName) {
-      this.optionName = optionName;
-    }
-  }
-
-  private FileTree source;
-
-  @OutputFile
-  private File destination;
-
-  private KotlinTargetVersion targetVersion = KotlinTargetVersion.JAVA_6;
-
-  @InputFiles
-  public FileCollection getInputFiles() {
-    // Note: Using Path object directly causes stack overflow.
-    // See: https://github.com/gradle/gradle/issues/1973
-    return source.plus(getProject().files(kotlincExecPath.toFile()));
-  }
-
-  public FileTree getSource() {
-    return source;
-  }
-
-  public void setSource(FileTree source) {
-    this.source = source;
-  }
-
-  public File getDestination() {
-    return destination;
-  }
-
-  public void setDestination(File destination) {
-    this.destination = destination;
-  }
-
-  public KotlinTargetVersion getTargetVersion() {
-    return targetVersion;
-  }
-
-  public void setTargetVersion(KotlinTargetVersion targetVersion) {
-    this.targetVersion = targetVersion;
-  }
-
-  @TaskAction
-  public void compile() {
-    getProject().exec(new Action<ExecSpec>() {
-      @Override
-      public void execute(ExecSpec execSpec) {
-        try {
-          execSpec.setExecutable(kotlincExecPath.toFile());
-          execSpec.args("-include-runtime");
-          execSpec.args("-nowarn");
-          execSpec.args("-jvm-target", targetVersion.optionName);
-          execSpec.args("-d", destination.getCanonicalPath());
-          execSpec.args(source.getFiles());
-        } catch (IOException e) {
-          throw new UncheckedIOException(e);
-        }
-      }
-    });
-  }
-}
diff --git a/buildSrc/src/main/java/tasks/DownloadDependency.java b/buildSrc/src/main/java/tasks/DownloadDependency.java
deleted file mode 100644
index 6d7a75c..0000000
--- a/buildSrc/src/main/java/tasks/DownloadDependency.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package tasks;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-import javax.inject.Inject;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.file.RegularFileProperty;
-import org.gradle.api.provider.Property;
-import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
-import org.gradle.api.tasks.TaskAction;
-import org.gradle.internal.os.OperatingSystem;
-import org.gradle.workers.WorkAction;
-import org.gradle.workers.WorkParameters;
-import org.gradle.workers.WorkerExecutor;
-
-public class DownloadDependency extends DefaultTask {
-
-  public enum Type {
-    GOOGLE_STORAGE,
-    X20
-  }
-
-  private final WorkerExecutor workerExecutor;
-
-  private Type type;
-  private File outputDir;
-  private File tarGzFile;
-  private File sha1File;
-
-  @Inject
-  public DownloadDependency(WorkerExecutor workerExecutor) {
-    this.workerExecutor = workerExecutor;
-  }
-
-  public void setType(Type type) {
-    this.type = type;
-  }
-
-  public void setDependency(String dependency) {
-    outputDir = new File(dependency);
-    tarGzFile = new File(dependency + ".tar.gz");
-    sha1File = new File(dependency + ".tar.gz.sha1");
-  }
-
-  @InputFiles
-  public Collection<File> getInputFiles() {
-    return Arrays.asList(sha1File, tarGzFile);
-  }
-
-  @OutputDirectory
-  public File getOutputDir() {
-    return outputDir;
-  }
-
-  public File getSha1File() {
-    return sha1File;
-  }
-
-  public File getTarGzFile() {
-    return tarGzFile;
-  }
-
-  @TaskAction
-  public void execute() throws IOException, InterruptedException {
-    if (!sha1File.exists()) {
-      throw new RuntimeException("Missing sha1 file: " + sha1File);
-    }
-
-    // First run will write the tar.gz file, causing the second run to still be out-of-date.
-    // Check if the modification time of the tar is newer than the sha in which case we are done.
-    // Also, check the contents of the out directory because gradle appears to create it for us...
-    if (outputDir.exists()
-        && outputDir.isDirectory()
-        && outputDir.list().length > 0
-        && tarGzFile.exists()
-        && sha1File.lastModified() <= tarGzFile.lastModified()) {
-      return;
-    }
-    if (outputDir.exists() && outputDir.isDirectory()) {
-      outputDir.delete();
-    }
-    workerExecutor
-        .noIsolation()
-        .submit(
-            RunDownload.class,
-            parameters -> {
-              parameters.getType().set(type);
-              parameters.getSha1File().set(sha1File);
-            });
-  }
-
-  public interface RunDownloadParameters extends WorkParameters {
-    Property<Type> getType();
-
-    RegularFileProperty getSha1File();
-  }
-
-  public abstract static class RunDownload implements WorkAction<RunDownloadParameters> {
-
-    @Override
-    public void execute() {
-      try {
-        RunDownloadParameters parameters = getParameters();
-        Type type = parameters.getType().get();
-        File sha1File = parameters.getSha1File().getAsFile().get();
-        if (type == Type.GOOGLE_STORAGE) {
-          downloadFromGoogleStorage(sha1File);
-        } else if (type == Type.X20) {
-          downloadFromX20(sha1File);
-        } else {
-          throw new RuntimeException("Unexpected or missing dependency type: " + type);
-        }
-      } catch (Exception e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    private void downloadFromGoogleStorage(File sha1File) throws IOException, InterruptedException {
-      List<String> args = Arrays.asList("-n", "-b", "r8-deps", "-s", "-u", sha1File.toString());
-      if (OperatingSystem.current().isWindows()) {
-        List<String> command = new ArrayList<>();
-        command.add("download_from_google_storage.bat");
-        command.addAll(args);
-        runProcess(new ProcessBuilder().command(command));
-      } else {
-        runProcess(
-            new ProcessBuilder()
-                .command("bash", "-c", "download_from_google_storage " + String.join(" ", args)));
-      }
-    }
-
-    private void downloadFromX20(File sha1File) throws IOException, InterruptedException {
-      if (OperatingSystem.current().isWindows()) {
-        throw new RuntimeException("Downloading from x20 unsupported on windows");
-      }
-      runProcess(
-          new ProcessBuilder()
-              .command("bash", "-c", "tools/download_from_x20.py " + sha1File.toString()));
-    }
-
-    private static void runProcess(ProcessBuilder builder)
-        throws IOException, InterruptedException {
-      String command = String.join(" ", builder.command());
-      Process p = builder.start();
-      int exit = p.waitFor();
-      if (exit != 0) {
-        throw new IOException(
-            "Process failed for "
-                + command
-                + "\n"
-                + new BufferedReader(
-                new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8))
-                .lines()
-                .collect(Collectors.joining("\n")));
-      }
-    }
-  }
-}
diff --git a/buildSrc/src/main/java/utils/Utils.java b/buildSrc/src/main/java/utils/Utils.java
deleted file mode 100644
index 7f09b81..0000000
--- a/buildSrc/src/main/java/utils/Utils.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package utils;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-public class Utils {
-  public static String toolsDir() {
-    String osName = System.getProperty("os.name");
-    if (osName.equals("Mac OS X")) {
-      return "mac";
-    } else if (osName.contains("Windows")) {
-      return "windows";
-    } else {
-      return "linux";
-    }
-  }
-
-  public static boolean isWindows() {
-    return toolsDir().equals("windows");
-  }
-
-  public static Path dxExecutable() {
-    String dxExecutableName = isWindows() ? "dx.bat" : "dx";
-    return Paths.get("tools", toolsDir(), "dx", "bin", dxExecutableName);
-  }
-
-  public static Path dexMergerExecutable() {
-    String executableName = isWindows() ? "dexmerger.bat" : "dexmerger";
-    return Paths.get("tools", toolsDir(), "dx", "bin", executableName);
-  }
-}
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index 418e0d5..56562ac 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -707,6 +707,7 @@
     "kotlin-compiler-1.6.0",
     "kotlin-compiler-1.7.0",
     "kotlin-compiler-1.8.0",
+    "kotlin-compiler-1.9.21",
     "kotlin-compiler-dev")
     .map { ThirdPartyDependency(
       it,
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index afa21b3..d7746ac 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -21,7 +21,7 @@
   `kotlin-dsl`
   id("dependencies-plugin")
   id("net.ltgt.errorprone") version "3.0.1"
-  id("org.spdx.sbom") version "0.4.0-r8-patch02"
+  id("org.spdx.sbom") version "0.4.0"
 }
 
 java {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java
new file mode 100644
index 0000000..a175b3d
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2023, 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.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A pattern structure for matching names of classes and interfaces.
+ *
+ * <p>If no properties are set, the default pattern matches any name of a class or interface.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface ClassNamePattern {
+
+  /**
+   * Exact simple name of the class or interface.
+   *
+   * <p>For example, the simple name of {@code com.example.MyClass} is {@code MyClass}.
+   *
+   * <p>The default matches any simple name.
+   */
+  String simpleName() default "";
+
+  /**
+   * Exact package name of the class or interface.
+   *
+   * <p>For example, the package of {@code com.example.MyClass} is {@code com.example}.
+   *
+   * <p>The default matches any package.
+   */
+  String packageName() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index 579d46f..3f1152e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -74,6 +74,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -95,6 +96,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -111,6 +113,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -121,6 +124,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index 470266b..74b2add 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -36,6 +36,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -57,6 +58,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -73,6 +75,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -83,6 +86,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index 7bbc2f0..d60525d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -133,6 +133,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -154,6 +155,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -170,6 +172,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -180,6 +183,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
index 1d7e6c6..2fa0ffd 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
@@ -29,7 +29,12 @@
    *
    * <p>For example, {@code "long"} or {@code "java.lang.String"}.
    *
-   * <p>Mutually exclusive with the property `constant` also defining type-pattern.
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>constant
+   *   <li>classNamePattern
+   * </ul>
    */
   String name() default "";
 
@@ -38,7 +43,24 @@
    *
    * <p>For example, {@code String.class}.
    *
-   * <p>Mutually exclusive with the property `name` also defining type-pattern.
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>classNamePattern
+   * </ul>
    */
   Class<?> constant() default Object.class;
+
+  /**
+   * Classes matching the class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>constant
+   * </ul>
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java
new file mode 100644
index 0000000..777e99b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
+
+public abstract class AnnotationVisitorBase extends AnnotationVisitor {
+
+  private final ParsingContext parsingContext;
+
+  AnnotationVisitorBase(ParsingContext parsingContext) {
+    super(KeepEdgeReader.ASM_VERSION);
+    this.parsingContext = parsingContext;
+  }
+
+  private String getTypeName(String descriptor) {
+    return Type.getType(descriptor).getClassName();
+  }
+
+  @Override
+  public void visit(String name, Object value) {
+    throw parsingContext.error("Unexpected value for property " + name + " with value " + value);
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+    throw parsingContext.error(
+        "Unexpected annotation for property "
+            + name
+            + " of annotation type "
+            + getTypeName(descriptor));
+  }
+
+  @Override
+  public void visitEnum(String name, String descriptor, String value) {
+    throw parsingContext.error(
+        "Unexpected enum for property "
+            + name
+            + " of enum type "
+            + getTypeName(descriptor)
+            + " with value "
+            + value);
+  }
+
+  @Override
+  public AnnotationVisitor visitArray(String name) {
+    throw parsingContext.error("Unexpected array for property " + name);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
new file mode 100644
index 0000000..d6c41ac
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.asm.ClassSimpleNameParser.ClassSimpleNameProperty;
+import com.android.tools.r8.keepanno.asm.PackageNameParser.PackageNameProperty;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.google.common.collect.ImmutableList;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class ClassNameParser
+    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty, ClassNameParser> {
+
+  public ClassNameParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  public enum ClassNameProperty {
+    PATTERN
+  }
+
+  @Override
+  public ClassNameParser self() {
+    return this;
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      ClassNameProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepQualifiedClassNamePattern> setValue) {
+    switch (property) {
+      case PATTERN:
+        {
+          AnnotationParsingContext parsingContext =
+              new AnnotationParsingContext(getParsingContext(), descriptor);
+          PackageNameParser packageParser =
+              new PackageNameParser(parsingContext)
+                  .setProperty(PackageNameProperty.NAME, ClassNamePattern.packageName);
+          ClassSimpleNameParser nameParser =
+              new ClassSimpleNameParser(parsingContext)
+                  .setProperty(ClassSimpleNameProperty.NAME, ClassNamePattern.simpleName);
+          return new ParserVisitor(
+              parsingContext,
+              descriptor,
+              ImmutableList.of(packageParser, nameParser),
+              () ->
+                  setValue.accept(
+                      KeepQualifiedClassNamePattern.builder()
+                          .setPackagePattern(
+                              packageParser.getValueOrDefault(KeepPackagePattern.any()))
+                          .setNamePattern(
+                              nameParser.getValueOrDefault(KeepUnqualfiedClassNamePattern.any()))
+                          .build()));
+        }
+      default:
+        return null;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
new file mode 100644
index 0000000..d349898
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassSimpleNameParser.ClassSimpleNameProperty;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.function.Consumer;
+
+public class ClassSimpleNameParser
+    extends PropertyParserBase<
+        KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty, ClassSimpleNameParser> {
+
+  public ClassSimpleNameParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  public enum ClassSimpleNameProperty {
+    NAME
+  }
+
+  @Override
+  public ClassSimpleNameParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean tryProperty(
+      ClassSimpleNameProperty property,
+      String name,
+      Object value,
+      Consumer<KeepUnqualfiedClassNamePattern> setValue) {
+    switch (property) {
+      case NAME:
+        setValue.accept(KeepUnqualfiedClassNamePattern.exact((String) value));
+        return true;
+      default:
+        return false;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 35beddf..00729cb 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Constraints;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
@@ -19,7 +20,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflection;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationParserException;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
 import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
@@ -31,7 +32,6 @@
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
@@ -41,6 +41,7 @@
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepMemberAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberAccessPattern.BuilderBase;
 import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
@@ -50,10 +51,18 @@
 import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.ClassParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.FieldParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.MethodParsingContext;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -189,6 +198,7 @@
   private static class KeepEdgeClassVisitor extends ClassVisitor {
     private final Parent<KeepDeclaration> parent;
     private String className;
+    private ClassParsingContext parsingContext;
 
     KeepEdgeClassVisitor(Parent<KeepDeclaration> parent) {
       super(ASM_VERSION);
@@ -208,7 +218,12 @@
         String superName,
         String[] interfaces) {
       super.visit(version, access, name, signature, superName, interfaces);
-      this.className = binaryNameToTypeName(name);
+      className = binaryNameToTypeName(name);
+      parsingContext = new ClassParsingContext(className);
+    }
+
+    private AnnotationParsingContext annotationParsingContext(String descriptor) {
+      return new AnnotationParsingContext(parsingContext, descriptor);
     }
 
     @Override
@@ -218,48 +233,66 @@
         return null;
       }
       if (descriptor.equals(Edge.DESCRIPTOR)) {
-        return new KeepEdgeVisitor(parent::accept, this::setContext);
+        return new KeepEdgeVisitor(
+            annotationParsingContext(descriptor), parent::accept, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
         KeepClassItemPattern classItem =
             KeepClassItemPattern.builder()
                 .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
                 .build();
-        return new UsesReflectionVisitor(parent::accept, this::setContext, classItem);
+        return new UsesReflectionVisitor(
+            annotationParsingContext(descriptor), parent::accept, this::setContext, classItem);
       }
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
-        return new ForApiClassVisitor(parent::accept, this::setContext, className);
+        return new ForApiClassVisitor(
+            annotationParsingContext(descriptor), parent::accept, this::setContext, className);
       }
       if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionClassVisitor(
-            descriptor, parent::accept, this::setContext, className);
+            annotationParsingContext(descriptor),
+            descriptor,
+            parent::accept,
+            this::setContext,
+            className);
       }
       if (descriptor.equals(AnnotationConstants.CheckRemoved.DESCRIPTOR)) {
         return new CheckRemovedClassVisitor(
-            descriptor, parent::accept, this::setContext, className, KeepCheckKind.REMOVED);
+            annotationParsingContext(descriptor),
+            descriptor,
+            parent::accept,
+            this::setContext,
+            className,
+            KeepCheckKind.REMOVED);
       }
       if (descriptor.equals(AnnotationConstants.CheckOptimizedOut.DESCRIPTOR)) {
         return new CheckRemovedClassVisitor(
-            descriptor, parent::accept, this::setContext, className, KeepCheckKind.OPTIMIZED_OUT);
+            annotationParsingContext(descriptor),
+            descriptor,
+            parent::accept,
+            this::setContext,
+            className,
+            KeepCheckKind.OPTIMIZED_OUT);
       }
       return null;
     }
 
     private void setContext(KeepEdgeMetaInfo.Builder builder) {
-      builder.setContextFromClassDescriptor(KeepEdgeReaderUtils.javaTypeToDescriptor(className));
+      builder.setContextFromClassDescriptor(
+          KeepEdgeReaderUtils.getDescriptorFromJavaType(className));
     }
 
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String descriptor, String signature, String[] exceptions) {
-      return new KeepEdgeMethodVisitor(parent::accept, className, name, descriptor);
+      return new KeepEdgeMethodVisitor(parsingContext, parent::accept, className, name, descriptor);
     }
 
     @Override
     public FieldVisitor visitField(
         int access, String name, String descriptor, String signature, Object value) {
-      return new KeepEdgeFieldVisitor(parent::accept, className, name, descriptor);
+      return new KeepEdgeFieldVisitor(parsingContext, parent::accept, className, name, descriptor);
     }
   }
 
@@ -268,8 +301,10 @@
     private final String className;
     private final String methodName;
     private final String methodDescriptor;
+    private final MethodParsingContext parsingContext;
 
     KeepEdgeMethodVisitor(
+        ClassParsingContext classParsingContext,
         Parent<KeepDeclaration> parent,
         String className,
         String methodName,
@@ -279,6 +314,8 @@
       this.className = className;
       this.methodName = methodName;
       this.methodDescriptor = methodDescriptor;
+      this.parsingContext =
+          new MethodParsingContext(classParsingContext, methodName, methodDescriptor);
     }
 
     private KeepMemberItemPattern createMethodItemContext() {
@@ -304,6 +341,10 @@
           .build();
     }
 
+    private AnnotationParsingContext annotationParsingContext(String descriptor) {
+      return new AnnotationParsingContext(parsingContext, descriptor);
+    }
+
     @Override
     public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
       // Skip any visible annotations as @KeepEdge is not runtime visible.
@@ -311,22 +352,35 @@
         return null;
       }
       if (descriptor.equals(Edge.DESCRIPTOR)) {
-        return new KeepEdgeVisitor(parent::accept, this::setContext);
+        return new KeepEdgeVisitor(
+            annotationParsingContext(descriptor), parent::accept, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
         return new UsesReflectionVisitor(
-            parent::accept, this::setContext, createMethodItemContext());
+            annotationParsingContext(descriptor),
+            parent::accept,
+            this::setContext,
+            createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
-        return new ForApiMemberVisitor(parent::accept, this::setContext, createMethodItemContext());
+        return new ForApiMemberVisitor(
+            annotationParsingContext(descriptor),
+            parent::accept,
+            this::setContext,
+            createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionMemberVisitor(
-            descriptor, parent::accept, this::setContext, createMethodItemContext());
+            annotationParsingContext(descriptor),
+            descriptor,
+            parent::accept,
+            this::setContext,
+            createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.CheckRemoved.DESCRIPTOR)) {
         return new CheckRemovedMemberVisitor(
+            annotationParsingContext(descriptor),
             descriptor,
             parent::accept,
             this::setContext,
@@ -335,6 +389,7 @@
       }
       if (descriptor.equals(AnnotationConstants.CheckOptimizedOut.DESCRIPTOR)) {
         return new CheckRemovedMemberVisitor(
+            annotationParsingContext(descriptor),
             descriptor,
             parent::accept,
             this::setContext,
@@ -346,7 +401,7 @@
 
     private void setContext(KeepEdgeMetaInfo.Builder builder) {
       builder.setContextFromMethodDescriptor(
-          KeepEdgeReaderUtils.javaTypeToDescriptor(className), methodName, methodDescriptor);
+          KeepEdgeReaderUtils.getDescriptorFromJavaType(className), methodName, methodDescriptor);
     }
   }
 
@@ -355,14 +410,25 @@
     private final String className;
     private final String fieldName;
     private final String fieldDescriptor;
+    private final FieldParsingContext parsingContext;
 
     KeepEdgeFieldVisitor(
-        Parent<KeepEdge> parent, String className, String fieldName, String fieldDescriptor) {
+        ClassParsingContext classParsingContext,
+        Parent<KeepEdge> parent,
+        String className,
+        String fieldName,
+        String fieldDescriptor) {
       super(ASM_VERSION);
       this.parent = parent;
       this.className = className;
       this.fieldName = fieldName;
       this.fieldDescriptor = fieldDescriptor;
+      this.parsingContext =
+          new FieldParsingContext(classParsingContext, fieldName, fieldDescriptor);
+    }
+
+    private AnnotationParsingContext annotationParsingContext(String descriptor) {
+      return new AnnotationParsingContext(parsingContext, descriptor);
     }
 
     private KeepMemberItemPattern createMemberItemContext() {
@@ -380,7 +446,7 @@
 
     private void setContext(KeepEdgeMetaInfo.Builder builder) {
       builder.setContextFromFieldDescriptor(
-          KeepEdgeReaderUtils.javaTypeToDescriptor(className), fieldName, fieldDescriptor);
+          KeepEdgeReaderUtils.getDescriptorFromJavaType(className), fieldName, fieldDescriptor);
     }
 
     @Override
@@ -390,18 +456,30 @@
         return null;
       }
       if (descriptor.equals(Edge.DESCRIPTOR)) {
-        return new KeepEdgeVisitor(parent, this::setContext);
+        return new KeepEdgeVisitor(annotationParsingContext(descriptor), parent, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
-        return new UsesReflectionVisitor(parent, this::setContext, createMemberItemContext());
+        return new UsesReflectionVisitor(
+            annotationParsingContext(descriptor),
+            parent,
+            this::setContext,
+            createMemberItemContext());
       }
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
-        return new ForApiMemberVisitor(parent, this::setContext, createMemberItemContext());
+        return new ForApiMemberVisitor(
+            annotationParsingContext(descriptor),
+            parent,
+            this::setContext,
+            createMemberItemContext());
       }
       if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionMemberVisitor(
-            descriptor, parent, this::setContext, createMemberItemContext());
+            annotationParsingContext(descriptor),
+            descriptor,
+            parent,
+            this::setContext,
+            createMemberItemContext());
       }
       return null;
     }
@@ -412,40 +490,6 @@
     void accept(T result);
   }
 
-  private abstract static class AnnotationVisitorBase extends AnnotationVisitor {
-
-    AnnotationVisitorBase() {
-      super(ASM_VERSION);
-    }
-
-    public abstract String getAnnotationName();
-
-    private String errorMessagePrefix() {
-      return " @" + getAnnotationName() + ": ";
-    }
-
-    @Override
-    public void visit(String name, Object value) {
-      throw new KeepEdgeException(
-          "Unexpected value in" + errorMessagePrefix() + name + " = " + value);
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-      throw new KeepEdgeException("Unexpected annotation in" + errorMessagePrefix() + name);
-    }
-
-    @Override
-    public void visitEnum(String name, String descriptor, String value) {
-      throw new KeepEdgeException("Unexpected enum in" + errorMessagePrefix() + name);
-    }
-
-    @Override
-    public AnnotationVisitor visitArray(String name) {
-      throw new KeepEdgeException("Unexpected array in" + errorMessagePrefix() + name);
-    }
-  }
-
   private static class UserBindingsHelper {
     private final KeepBindings.Builder builder = KeepBindings.builder();
     private final Map<String, KeepBindingSymbol> userNames = new HashMap<>();
@@ -474,22 +518,24 @@
   }
 
   private static class KeepEdgeVisitor extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
 
-    KeepEdgeVisitor(Parent<KeepEdge> parent, Consumer<KeepEdgeMetaInfo.Builder> addContext) {
+    KeepEdgeVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
     }
 
     @Override
-    public String getAnnotationName() {
-      return "KeepEdge";
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -501,15 +547,15 @@
     @Override
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(Edge.bindings)) {
-        return new KeepBindingsVisitor(getAnnotationName(), bindingsHelper);
+        return new KeepBindingsVisitor(parsingContext, bindingsHelper);
       }
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            getAnnotationName(), builder::setPreconditions, bindingsHelper);
+            parsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(Edge.consequences)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(), builder::setConsequences, bindingsHelper);
+            parsingContext, builder::setConsequences, bindingsHelper);
       }
       return super.visitArray(name);
     }
@@ -529,6 +575,8 @@
    * properties are encountered.
    */
   private static class ForApiClassVisitor extends KeepItemVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final String className;
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
@@ -537,7 +585,12 @@
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
 
     ForApiClassVisitor(
-        Parent<KeepEdge> parent, Consumer<KeepEdgeMetaInfo.Builder> addContext, String className) {
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        String className) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.className = className;
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
@@ -553,11 +606,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      return ForApi.SIMPLE_NAME;
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -570,7 +618,7 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(ForApi.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(),
+            parsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -591,7 +639,7 @@
       Collection<KeepItemReference> items = getItemsWithoutBinding();
       for (KeepItemReference item : items) {
         if (item.isBindingReference()) {
-          throw new KeepEdgeException("@KeepForApi cannot reference bindings");
+          throw parsingContext.error("cannot reference bindings");
         }
         KeepClassItemPattern classItemPattern = item.asClassItemPattern();
         if (classItemPattern == null) {
@@ -601,13 +649,13 @@
         String descriptor = KeepEdgeReaderUtils.getDescriptorFromClassTypeName(className);
         String itemDescriptor = classItemPattern.getClassNamePattern().getExactDescriptor();
         if (!descriptor.equals(itemDescriptor)) {
-          throw new KeepEdgeException("@KeepForApi must reference its class context " + className);
+          throw parsingContext.error("must reference its class context " + className);
         }
         if (classItemPattern.isMemberItemPattern() && items.size() == 1) {
-            throw new KeepEdgeException("@KeepForApi kind must include its class");
+          throw parsingContext.error("kind must include its class");
         }
         if (!classItemPattern.getInstanceOfPattern().isAny()) {
-          throw new KeepEdgeException("@KeepForApi cannot define an 'extends' pattern.");
+          throw parsingContext.error("cannot define an 'extends' pattern.");
         }
         consequences.addTarget(KeepTarget.builder().setItemReference(item).build());
       }
@@ -626,6 +674,8 @@
    * <p>When used on a member context the annotation does not allow member related patterns.
    */
   private static class ForApiMemberVisitor extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
@@ -633,9 +683,12 @@
     private final KeepConsequences.Builder consequences = KeepConsequences.builder();
 
     ForApiMemberVisitor(
+        AnnotationParsingContext parsingContext,
         Parent<KeepEdge> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepMemberItemPattern context) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
       // Create a binding for the context such that the class and member are shared.
@@ -655,11 +708,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      return ForApi.SIMPLE_NAME;
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -672,7 +720,7 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(ForApi.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(),
+            parsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -700,6 +748,8 @@
    * properties are encountered.
    */
   private static class UsedByReflectionClassVisitor extends KeepItemVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final String annotationDescriptor;
     private final String className;
     private final Parent<KeepEdge> parent;
@@ -710,17 +760,20 @@
     private final OptionsDeclaration optionsDeclaration;
 
     UsedByReflectionClassVisitor(
+        AnnotationParsingContext parsingContext,
         String annotationDescriptor,
         Parent<KeepEdge> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         String className) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.annotationDescriptor = annotationDescriptor;
       this.className = className;
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
       // The class context/holder is the annotated class.
       visit(Item.className, className);
-      optionsDeclaration = new OptionsDeclaration(getAnnotationName());
+      optionsDeclaration = new OptionsDeclaration(parsingContext);
     }
 
     @Override
@@ -729,12 +782,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      int sep = annotationDescriptor.lastIndexOf('/');
-      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -747,11 +794,11 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            getAnnotationName(), builder::setPreconditions, bindingsHelper);
+            parsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(UsedByReflection.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(),
+            parsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -775,7 +822,7 @@
       for (KeepItemReference item : items) {
         if (item.isBindingReference()) {
           // TODO(b/248408342): The edge can have preconditions so it should support bindings!
-          throw new KeepEdgeException("@" + getAnnotationName() + " cannot reference bindings");
+          throw parsingContext.error("cannot reference bindings");
         }
         KeepItemPattern itemPattern = item.asItemPattern();
         KeepClassItemPattern holderPattern =
@@ -785,15 +832,13 @@
         String descriptor = KeepEdgeReaderUtils.getDescriptorFromClassTypeName(className);
         String itemDescriptor = holderPattern.getClassNamePattern().getExactDescriptor();
         if (!descriptor.equals(itemDescriptor)) {
-          throw new KeepEdgeException(
-              "@" + getAnnotationName() + " must reference its class context " + className);
+          throw parsingContext.error("must reference its class context " + className);
         }
         if (itemPattern.isMemberItemPattern() && items.size() == 1) {
-          throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its class");
+          throw parsingContext.error("kind must include its class");
         }
         if (!holderPattern.getInstanceOfPattern().isAny()) {
-          throw new KeepEdgeException(
-              "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
+          throw parsingContext.error("cannot define an 'extends' pattern.");
         }
         consequences.addTarget(
             KeepTarget.builder()
@@ -816,6 +861,8 @@
    * <p>When used on a member context the annotation does not allow member related patterns.
    */
   private static class UsedByReflectionMemberVisitor extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final String annotationDescriptor;
     private final Parent<KeepEdge> parent;
     private final KeepItemPattern context;
@@ -827,21 +874,18 @@
     private final OptionsDeclaration optionsDeclaration;
 
     UsedByReflectionMemberVisitor(
+        AnnotationParsingContext parsingContext,
         String annotationDescriptor,
         Parent<KeepEdge> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepItemPattern context) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.context = context;
       addContext.accept(metaInfoBuilder);
-      optionsDeclaration = new OptionsDeclaration(getAnnotationName());
-    }
-
-    @Override
-    public String getAnnotationName() {
-      int sep = annotationDescriptor.lastIndexOf('/');
-      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
+      optionsDeclaration = new OptionsDeclaration(parsingContext);
     }
 
     @Override
@@ -870,11 +914,11 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            getAnnotationName(), builder::setPreconditions, bindingsHelper);
+            parsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(UsedByReflection.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(),
+            parsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -890,7 +934,7 @@
     @Override
     public void visitEnd() {
       if (kind.isOnlyClass()) {
-        throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
+        throw parsingContext.error("kind must include its member");
       }
       assert context.isMemberItemPattern();
       KeepMemberItemPattern memberContext = context.asMemberItemPattern();
@@ -914,18 +958,20 @@
 
     private void validateConsistentKind(KeepMemberPattern memberPattern) {
       if (memberPattern.isGeneralMember()) {
-        throw new KeepEdgeException("Unexpected general pattern for context.");
+        throw parsingContext.error("Unexpected general pattern for context.");
       }
       if (memberPattern.isMethod() && !kind.includesMethod()) {
-        throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a method");
+        throw parsingContext.error("Kind " + kind + " cannot be use when annotating a method");
       }
       if (memberPattern.isField() && !kind.includesField()) {
-        throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a field");
+        throw parsingContext.error("Kind " + kind + " cannot be use when annotating a field");
       }
     }
   }
 
   private static class UsesReflectionVisitor extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
     private final KeepPreconditions.Builder preconditions = KeepPreconditions.builder();
@@ -933,20 +979,18 @@
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
 
     UsesReflectionVisitor(
+        AnnotationParsingContext parsingContext,
         Parent<KeepEdge> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepItemPattern context) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.parent = parent;
       preconditions.addCondition(KeepCondition.builder().setItemPattern(context).build());
       addContext.accept(metaInfoBuilder);
     }
 
     @Override
-    public String getAnnotationName() {
-      return UsesReflection.SIMPLE_NAME;
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -959,11 +1003,11 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(AnnotationConstants.UsesReflection.value)) {
         return new KeepConsequencesVisitor(
-            getAnnotationName(), builder::setConsequences, bindingsHelper);
+            parsingContext, builder::setConsequences, bindingsHelper);
       }
       if (name.equals(AnnotationConstants.UsesReflection.additionalPreconditions)) {
         return new KeepPreconditionsVisitor(
-            getAnnotationName(),
+            parsingContext,
             additionalPreconditions -> {
               additionalPreconditions.forEach(preconditions::addCondition);
             },
@@ -984,52 +1028,44 @@
   }
 
   private static class KeepBindingsVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final AnnotationParsingContext parsingContext;
     private final UserBindingsHelper helper;
 
-    public KeepBindingsVisitor(String annotationName, UserBindingsHelper helper) {
-      this.annotationName = annotationName;
+    public KeepBindingsVisitor(AnnotationParsingContext parsingContext, UserBindingsHelper helper) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.helper = helper;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName;
-    }
-
-    @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
       if (descriptor.equals(AnnotationConstants.Binding.DESCRIPTOR)) {
-        return new KeepBindingVisitor(helper);
+        return new KeepBindingVisitor(parsingContext, helper);
       }
       return super.visitAnnotation(name, descriptor);
     }
   }
 
   private static class KeepPreconditionsVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final AnnotationParsingContext parsingContext;
     private final Parent<KeepPreconditions> parent;
     private final KeepPreconditions.Builder builder = KeepPreconditions.builder();
     private final UserBindingsHelper bindingsHelper;
 
     public KeepPreconditionsVisitor(
-        String annotationName,
+        AnnotationParsingContext parsingContext,
         Parent<KeepPreconditions> parent,
         UserBindingsHelper bindingsHelper) {
-      this.annotationName = annotationName;
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.parent = parent;
       this.bindingsHelper = bindingsHelper;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName;
-    }
-
-    @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
       if (descriptor.equals(Condition.DESCRIPTOR)) {
-        return new KeepConditionVisitor(builder::addCondition, bindingsHelper);
+        return new KeepConditionVisitor(parsingContext, builder::addCondition, bindingsHelper);
       }
       return super.visitAnnotation(name, descriptor);
     }
@@ -1041,27 +1077,25 @@
   }
 
   private static class KeepConsequencesVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final AnnotationParsingContext parsingContext;
     private final Parent<KeepConsequences> parent;
     private final KeepConsequences.Builder builder = KeepConsequences.builder();
     private final UserBindingsHelper bindingsHelper;
 
     public KeepConsequencesVisitor(
-        String annotationName, Parent<KeepConsequences> parent, UserBindingsHelper bindingsHelper) {
-      this.annotationName = annotationName;
+        AnnotationParsingContext parsingContext,
+        Parent<KeepConsequences> parent,
+        UserBindingsHelper bindingsHelper) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.parent = parent;
       this.bindingsHelper = bindingsHelper;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName;
-    }
-
-    @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
       if (descriptor.equals(Target.DESCRIPTOR)) {
-        return KeepTargetVisitor.create(builder::addTarget, bindingsHelper);
+        return KeepTargetVisitor.create(parsingContext, builder::addTarget, bindingsHelper);
       }
       return super.visitAnnotation(name, descriptor);
     }
@@ -1075,6 +1109,7 @@
   /** Parsing of @CheckRemoved and @CheckOptimizedOut on a class context. */
   private static class CheckRemovedClassVisitor extends AnnotationVisitorBase {
 
+    private final AnnotationParsingContext parsingContext;
     private final String annotationDescriptor;
     private final Parent<KeepCheck> parent;
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
@@ -1082,11 +1117,14 @@
     private final KeepCheckKind kind;
 
     public CheckRemovedClassVisitor(
+        AnnotationParsingContext parsingContext,
         String annotationDescriptor,
         Parent<KeepCheck> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         String className,
         KeepCheckKind kind) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.className = className;
@@ -1095,12 +1133,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      int sep = annotationDescriptor.lastIndexOf('/');
-      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -1113,15 +1145,10 @@
     public void visitEnd() {
       CheckRemovedClassVisitor superVisitor = this;
       KeepItemVisitorBase itemVisitor =
-          new KeepItemVisitorBase() {
+          new KeepItemVisitorBase(parsingContext) {
             @Override
             public UserBindingsHelper getBindingsHelper() {
-              throw new KeepEdgeException("Bindings not supported in @" + getAnnotationName());
-            }
-
-            @Override
-            public String getAnnotationName() {
-              return superVisitor.getAnnotationName();
+              throw parsingContext.error("bindings not supported");
             }
           };
       itemVisitor.visit(Item.className, className);
@@ -1145,11 +1172,13 @@
     private final KeepCheckKind kind;
 
     CheckRemovedMemberVisitor(
+        AnnotationParsingContext parsingContext,
         String annotationDescriptor,
         Parent<KeepDeclaration> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepItemPattern context,
         KeepCheckKind kind) {
+      super(parsingContext);
       this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.context = context;
@@ -1158,12 +1187,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      int sep = annotationDescriptor.lastIndexOf('/');
-      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Edge.description) && value instanceof String) {
         metaInfoBuilder.setDescription((String) value);
@@ -1195,7 +1218,6 @@
       }
       return true;
     }
-    ;
 
     abstract T getValue();
 
@@ -1234,10 +1256,19 @@
   }
 
   private abstract static class SingleDeclaration<T> extends Declaration<T> {
+    private final ParsingContext parsingContext;
     private String declarationName = null;
     private T declarationValue = null;
     private AnnotationVisitor declarationVisitor = null;
 
+    private SingleDeclaration(ParsingContext parsingContext) {
+      this.parsingContext = parsingContext;
+    }
+
+    public ParsingContext getParsingContext() {
+      return parsingContext;
+    }
+
     abstract T getDefaultValue();
 
     abstract T parse(String name, Object value);
@@ -1260,7 +1291,8 @@
     }
 
     private void error(String name) {
-      throw new KeepEdgeException(
+      throw new KeepAnnotationParserException(
+          parsingContext,
           "Multiple declarations defining "
               + kind()
               + ": '"
@@ -1321,6 +1353,10 @@
   private static class ClassNameDeclaration
       extends SingleDeclaration<KeepQualifiedClassNamePattern> {
 
+    private ClassNameDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+    }
+
     @Override
     String kind() {
       return "class-name";
@@ -1341,10 +1377,24 @@
       }
       return null;
     }
+
+    @Override
+    AnnotationVisitor parseAnnotation(
+        String name, String descriptor, Consumer<KeepQualifiedClassNamePattern> setValue) {
+      if (name.equals(Item.classNamePattern) && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
+        return new ClassNamePatternVisitor(
+            new AnnotationParsingContext(getParsingContext(), descriptor), setValue);
+      }
+      return super.parseAnnotation(name, descriptor, setValue);
+    }
   }
 
   private static class InstanceOfDeclaration extends SingleDeclaration<KeepInstanceOfPattern> {
 
+    private InstanceOfDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+    }
+
     @Override
     String kind() {
       return "instance-of";
@@ -1397,14 +1447,26 @@
 
   private static class ClassDeclaration extends Declaration<KeepClassItemReference> {
 
+    private final ParsingContext parsingContext;
     private final Supplier<UserBindingsHelper> getBindingsHelper;
 
     private KeepClassItemReference boundClassItemReference = null;
-    private final ClassNameDeclaration classNameDeclaration = new ClassNameDeclaration();
-    private final InstanceOfDeclaration instanceOfDeclaration = new InstanceOfDeclaration();
+    private final ClassNameDeclaration classNameDeclaration;
+    private final InstanceOfDeclaration instanceOfDeclaration;
+    private final List<Declaration<?>> declarations;
 
-    public ClassDeclaration(Supplier<UserBindingsHelper> getBindingsHelper) {
+    public ClassDeclaration(
+        ParsingContext parsingContext, Supplier<UserBindingsHelper> getBindingsHelper) {
+      this.parsingContext = parsingContext;
       this.getBindingsHelper = getBindingsHelper;
+      classNameDeclaration = new ClassNameDeclaration(parsingContext);
+      instanceOfDeclaration = new InstanceOfDeclaration(parsingContext);
+      declarations = ImmutableList.of(classNameDeclaration, instanceOfDeclaration);
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
     }
 
     private boolean isBindingReferenceDefined() {
@@ -1417,7 +1479,7 @@
 
     private void checkAllowedDefinitions() {
       if (isBindingReferenceDefined() && classPatternsAreDefined()) {
-        throw new KeepEdgeException(
+        throw parsingContext.error(
             "Cannot reference a class binding and class patterns for a single class item");
       }
     }
@@ -1429,11 +1491,12 @@
 
     @Override
     boolean isDefault() {
-      return !isBindingReferenceDefined() && !classPatternsAreDefined();
+      return !isBindingReferenceDefined() && super.isDefault();
     }
 
     @Override
     KeepClassItemReference getValue() {
+      checkAllowedDefinitions();
       if (isBindingReferenceDefined()) {
         return boundClassItemReference;
       }
@@ -1450,7 +1513,7 @@
 
     public void setBindingReference(KeepClassItemReference bindingReference) {
       if (isBindingReferenceDefined()) {
-        throw new KeepEdgeException(
+        throw parsingContext.error(
             "Cannot reference multiple class bindings for a single class item");
       }
       this.boundClassItemReference = bindingReference;
@@ -1463,25 +1526,23 @@
         setBindingReference(KeepBindingReference.forClass(symbol).toClassItemReference());
         return true;
       }
-      if (classNameDeclaration.tryParse(name, value)) {
-        checkAllowedDefinitions();
-        return true;
-      }
-      if (instanceOfDeclaration.tryParse(name, value)) {
-        checkAllowedDefinitions();
-        return true;
-      }
-      return false;
+      return super.tryParse(name, value);
     }
   }
 
   private static class MethodReturnTypeDeclaration
       extends SingleDeclaration<KeepMethodReturnTypePattern> {
 
-    private final Supplier<String> annotationName;
+    private final TypeParser typeParser;
 
-    private MethodReturnTypeDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
+    private MethodReturnTypeDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+      typeParser =
+          new TypeParser(parsingContext)
+              .setKind("return type")
+              .enableTypePattern(Item.methodReturnTypePattern)
+              .enableTypeName(Item.methodReturnType)
+              .enableTypeConstant(Item.methodReturnTypeConstant);
     }
 
     @Override
@@ -1494,44 +1555,46 @@
       return KeepMethodReturnTypePattern.any();
     }
 
-    @Override
-    KeepMethodReturnTypePattern parse(String name, Object value) {
-      if (name.equals(Item.methodReturnType) && value instanceof String) {
-        return KeepEdgeReaderUtils.methodReturnTypeFromTypeName((String) value);
+    KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
+      if (typePattern == null) {
+        return null;
       }
-      if (name.equals(Item.methodReturnTypeConstant) && value instanceof Type) {
-        Type type = (Type) value;
-        return KeepEdgeReaderUtils.methodReturnTypeFromTypeDescriptor(type.getDescriptor());
+      // Special-case method return types to allow void.
+      String descriptor = typePattern.getDescriptor();
+      if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
+        return KeepMethodReturnTypePattern.voidType();
       }
-      return null;
+      return KeepMethodReturnTypePattern.fromType(typePattern);
     }
 
     @Override
-    AnnotationVisitor parseAnnotation(
+    public KeepMethodReturnTypePattern parse(String name, Object value) {
+      return fromType(typeParser.tryParse(name, value));
+    }
+
+    @Override
+    public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepMethodReturnTypePattern> setValue) {
-      if (name.equals(Item.methodReturnTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
-        return new TypePatternVisitor(
-            annotationName, t -> setValue.accept(KeepMethodReturnTypePattern.fromType(t)));
-      }
-      return super.parseAnnotation(name, descriptor, setValue);
+      return typeParser.tryParseAnnotation(name, descriptor, t -> setValue.accept(fromType(t)));
     }
   }
 
   private static class MethodParametersDeclaration
       extends SingleDeclaration<KeepMethodParametersPattern> {
 
-    private final Supplier<String> annotationName;
+    private final ParsingContext parsingContext;
     private KeepMethodParametersPattern pattern = null;
 
-    public MethodParametersDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
+    public MethodParametersDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
     }
 
     private void setPattern(
         KeepMethodParametersPattern pattern, Consumer<KeepMethodParametersPattern> setValue) {
       assert setValue != null;
       if (this.pattern != null) {
-        throw new KeepEdgeException("Cannot declare multiple patterns for the parameter list");
+        throw parsingContext.error("Cannot declare multiple patterns for the parameter list");
       }
       setValue.accept(pattern);
       this.pattern = pattern;
@@ -1556,7 +1619,7 @@
     AnnotationVisitor parseArray(String name, Consumer<KeepMethodParametersPattern> setValue) {
       if (name.equals(Item.methodParameters)) {
         return new StringArrayVisitor(
-            annotationName,
+            getParsingContext(),
             params -> {
               KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
               for (String param : params) {
@@ -1567,7 +1630,7 @@
       }
       if (name.equals(Item.methodParameterTypePatterns)) {
         return new TypePatternsArrayVisitor(
-            annotationName,
+            getParsingContext(),
             params -> {
               KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
               for (KeepTypePattern param : params) {
@@ -1581,7 +1644,8 @@
   }
 
   private static class MethodDeclaration extends Declaration<KeepMethodPattern> {
-    private final Supplier<String> annotationName;
+
+    private final ParsingContext parsingContext;
     private KeepMethodAccessPattern.Builder accessBuilder = null;
     private KeepMethodPattern.Builder builder = null;
     private final MethodReturnTypeDeclaration returnTypeDeclaration;
@@ -1589,10 +1653,10 @@
 
     private final List<Declaration<?>> declarations;
 
-    private MethodDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
-      returnTypeDeclaration = new MethodReturnTypeDeclaration(annotationName);
-      parametersDeclaration = new MethodParametersDeclaration(annotationName);
+    private MethodDeclaration(ParsingContext parsingContext) {
+      this.parsingContext = parsingContext;
+      returnTypeDeclaration = new MethodReturnTypeDeclaration(parsingContext);
+      parametersDeclaration = new MethodParametersDeclaration(parsingContext);
       declarations = ImmutableList.of(returnTypeDeclaration, parametersDeclaration);
     }
 
@@ -1645,7 +1709,7 @@
     AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.methodAccess)) {
         accessBuilder = KeepMethodAccessPattern.builder();
-        return new MethodAccessVisitor(annotationName, accessBuilder);
+        return new MethodAccessVisitor(parsingContext, accessBuilder);
       }
       return super.tryParseArray(name);
     }
@@ -1653,10 +1717,16 @@
 
   private static class FieldTypeDeclaration extends SingleDeclaration<KeepFieldTypePattern> {
 
-    private final Supplier<String> annotationName;
+    private final TypeParser typeParser;
 
-    private FieldTypeDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
+    private FieldTypeDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+      this.typeParser =
+          new TypeParser(parsingContext)
+              .setKind("field type")
+              .enableTypePattern(Item.fieldTypePattern)
+              .enableTypeName(Item.fieldType)
+              .enableTypeConstant(Item.fieldTypeConstant);
     }
 
     @Override
@@ -1670,39 +1740,33 @@
     }
 
     @Override
-    KeepFieldTypePattern parse(String name, Object value) {
-      if (name.equals(Item.fieldType) && value instanceof String) {
-        return KeepFieldTypePattern.fromType(
-            KeepEdgeReaderUtils.typePatternFromString((String) value));
-      }
-      if (name.equals(Item.fieldTypeConstant) && value instanceof Type) {
-        String descriptor = ((Type) value).getDescriptor();
-        return KeepFieldTypePattern.fromType(KeepTypePattern.fromDescriptor(descriptor));
+    public KeepFieldTypePattern parse(String name, Object value) {
+      KeepTypePattern typePattern = typeParser.tryParse(name, value);
+      if (typePattern != null) {
+        return KeepFieldTypePattern.fromType(typePattern);
       }
       return null;
     }
 
     @Override
-    AnnotationVisitor parseAnnotation(
+    public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepFieldTypePattern> setValue) {
-      if (name.equals(Item.fieldTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
-        return new TypePatternVisitor(
-            annotationName, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
-      }
-      return super.parseAnnotation(name, descriptor, setValue);
+      return typeParser.tryParseAnnotation(
+          name, descriptor, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
     }
   }
 
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
-    private final Supplier<String> annotationName;
+
+    private final ParsingContext parsingContext;
     private final FieldTypeDeclaration typeDeclaration;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
     private final List<Declaration<?>> declarations;
 
-    public FieldDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
-      typeDeclaration = new FieldTypeDeclaration(annotationName);
+    public FieldDeclaration(ParsingContext parsingContext) {
+      this.parsingContext = parsingContext;
+      typeDeclaration = new FieldTypeDeclaration(parsingContext);
       declarations = Collections.singletonList(typeDeclaration);
     }
 
@@ -1752,23 +1816,24 @@
     AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.fieldAccess)) {
         accessBuilder = KeepFieldAccessPattern.builder();
-        return new FieldAccessVisitor(annotationName, accessBuilder);
+        return new FieldAccessVisitor(parsingContext, accessBuilder);
       }
       return super.tryParseArray(name);
     }
   }
 
   private static class MemberDeclaration extends Declaration<KeepMemberPattern> {
-    private final Supplier<String> annotationName;
+
+    private final ParsingContext parsingContext;
     private KeepMemberAccessPattern.Builder accessBuilder = null;
     private final MethodDeclaration methodDeclaration;
     private final FieldDeclaration fieldDeclaration;
     private final List<Declaration<?>> declarations;
 
-    MemberDeclaration(Supplier<String> annotationName) {
-      this.annotationName = annotationName;
-      methodDeclaration = new MethodDeclaration(annotationName);
-      fieldDeclaration = new FieldDeclaration(annotationName);
+    MemberDeclaration(ParsingContext parsingContext) {
+      this.parsingContext = parsingContext;
+      methodDeclaration = new MethodDeclaration(parsingContext);
+      fieldDeclaration = new FieldDeclaration(parsingContext);
       declarations = ImmutableList.of(methodDeclaration, fieldDeclaration);
     }
 
@@ -1793,13 +1858,13 @@
       KeepFieldPattern field = fieldDeclaration.getValue();
       if (accessBuilder != null) {
         if (method != null || field != null) {
-          throw new KeepEdgeException(
+          throw parsingContext.error(
               "Cannot define common member access as well as field or method pattern");
         }
         return KeepMemberPattern.memberBuilder().setAccessPattern(accessBuilder.build()).build();
       }
       if (method != null && field != null) {
-        throw new KeepEdgeException("Cannot define both a field and a method pattern");
+        throw parsingContext.error("Cannot define both a field and a method pattern");
       }
       if (method != null) {
         return method;
@@ -1814,37 +1879,42 @@
     AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.memberAccess)) {
         accessBuilder = KeepMemberAccessPattern.memberBuilder();
-        return new MemberAccessVisitor(annotationName, accessBuilder);
+        return new MemberAccessVisitor(parsingContext, accessBuilder);
       }
       return super.tryParseArray(name);
     }
   }
 
   private abstract static class KeepItemVisitorBase extends AnnotationVisitorBase {
+    private final ParsingContext parsingContext;
     private String memberBindingReference = null;
     private ItemKind kind = null;
-    private final ClassDeclaration classDeclaration = new ClassDeclaration(this::getBindingsHelper);
+    private final ClassDeclaration classDeclaration;
     private final MemberDeclaration memberDeclaration;
 
+
     public abstract UserBindingsHelper getBindingsHelper();
 
     // Constructed item available once visitEnd has been called.
     private KeepItemReference itemReference = null;
 
-    KeepItemVisitorBase() {
-      memberDeclaration = new MemberDeclaration(this::getAnnotationName);
+    KeepItemVisitorBase(ParsingContext parsingContext) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      classDeclaration = new ClassDeclaration(parsingContext, this::getBindingsHelper);
+      memberDeclaration = new MemberDeclaration(parsingContext);
     }
 
     public Collection<KeepItemReference> getItemsWithoutBinding() {
       if (itemReference == null) {
-        throw new KeepEdgeException("Item reference not finalized. Missing call to visitEnd()");
+        throw parsingContext.error("Item reference not finalized. Missing call to visitEnd()");
       }
       if (itemReference.isBindingReference()) {
         return Collections.singletonList(itemReference);
       }
       // Kind is only null if item is a "binding reference".
       if (kind == null) {
-        throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+        throw parsingContext.error("Unexpected state: unknown kind for an item pattern");
       }
       if (kind.includesClassAndMembers()) {
         assert !itemReference.isBindingReference();
@@ -1870,14 +1940,14 @@
 
     public Collection<KeepItemReference> getItemsWithBinding() {
       if (itemReference == null) {
-        throw new KeepEdgeException("Item reference not finalized. Missing call to visitEnd()");
+        throw parsingContext.error("Item reference not finalized. Missing call to visitEnd()");
       }
       if (itemReference.isBindingReference()) {
         return Collections.singletonList(itemReference);
       }
       // Kind is only null if item is a "binding reference".
       if (kind == null) {
-        throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+        throw parsingContext.error("Unexpected state: unknown kind for an item pattern");
       }
       if (kind.includesClassAndMembers()) {
         KeepItemPattern itemPattern = itemReference.asItemPattern();
@@ -1914,7 +1984,7 @@
 
     public KeepItemReference getItemReference() {
       if (itemReference == null) {
-        throw new KeepEdgeException("Item reference not finalized. Missing call to visitEnd()");
+        throw parsingContext.error("Item reference not finalized. Missing call to visitEnd()");
       }
       return itemReference;
     }
@@ -1981,7 +2051,7 @@
         if (!classDeclaration.isDefault()
             || !memberDeclaration.getValue().isNone()
             || kind != null) {
-          throw new KeepEdgeException(
+          throw parsingContext.error(
               "Cannot define an item explicitly and via a member-binding reference");
         }
         KeepBindingSymbol symbol = getBindingsHelper().resolveUserBinding(memberBindingReference);
@@ -2003,7 +2073,7 @@
         }
 
         if (kind.isOnlyClass() && !memberPattern.isNone()) {
-          throw new KeepEdgeException("Item pattern for members is incompatible with kind " + kind);
+          throw parsingContext.error("Item pattern for members is incompatible with kind " + kind);
         }
 
         // Refine the member pattern to be as precise as the specified kind.
@@ -2020,8 +2090,7 @@
             memberPattern = KeepMethodPattern.allMethods();
           } else {
             assert memberPattern.isField();
-            throw new KeepEdgeException(
-                "Item pattern for fields is incompatible with kind " + kind);
+            throw parsingContext.error("Item pattern for fields is incompatible with kind " + kind);
           }
         }
 
@@ -2038,7 +2107,7 @@
             memberPattern = KeepFieldPattern.allFields();
           } else {
             assert memberPattern.isMethod();
-            throw new KeepEdgeException(
+            throw parsingContext.error(
                 "Item pattern for methods is incompatible with kind " + kind);
           }
         }
@@ -2064,10 +2133,13 @@
 
   private static class KeepBindingVisitor extends KeepItemVisitorBase {
 
+    private final AnnotationParsingContext parsingContext;
     private final UserBindingsHelper helper;
     private String bindingName;
 
-    public KeepBindingVisitor(UserBindingsHelper helper) {
+    public KeepBindingVisitor(AnnotationParsingContext parsingContext, UserBindingsHelper helper) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.helper = helper;
     }
 
@@ -2077,11 +2149,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      return Binding.SIMPLE_NAME;
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (name.equals(Binding.bindingName) && value instanceof String) {
         bindingName = (String) value;
@@ -2097,7 +2164,7 @@
       // The language currently disallows aliasing bindings, thus a binding cannot directly be
       // defined by a reference to another binding.
       if (item.isBindingReference()) {
-        throw new KeepEdgeException(
+        throw parsingContext.error(
             "Invalid binding reference to '"
                 + item.asBindingReference()
                 + "' in binding definition of '"
@@ -2109,21 +2176,15 @@
   }
 
   private static class StringArrayVisitor extends AnnotationVisitorBase {
-    private final Supplier<String> annotationName;
     private final Consumer<List<String>> fn;
     private final List<String> strings = new ArrayList<>();
 
-    public StringArrayVisitor(Supplier<String> annotationName, Consumer<List<String>> fn) {
-      this.annotationName = annotationName;
+    public StringArrayVisitor(ParsingContext parsingContext, Consumer<List<String>> fn) {
+      super(parsingContext);
       this.fn = fn;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName.get();
-    }
-
-    @Override
     public void visit(String name, Object value) {
       if (value instanceof String) {
         strings.add((String) value);
@@ -2139,25 +2200,134 @@
     }
   }
 
-  private static class TypePatternVisitor extends AnnotationVisitorBase {
-    private final Supplier<String> annotationName;
-    private final Consumer<KeepTypePattern> consumer;
-    private KeepTypePattern result = null;
+  private static class ClassSimpleNameDeclaration
+      extends SingleDeclaration<KeepUnqualfiedClassNamePattern> {
 
-    private TypePatternVisitor(
-        Supplier<String> annotationName, Consumer<KeepTypePattern> consumer) {
-      this.annotationName = annotationName;
-      this.consumer = consumer;
+    private ClassSimpleNameDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName.get();
+    String kind() {
+      return "class-simple-name";
+    }
+
+    @Override
+    KeepUnqualfiedClassNamePattern getDefaultValue() {
+      return KeepUnqualfiedClassNamePattern.any();
+    }
+
+    @Override
+    KeepUnqualfiedClassNamePattern parse(String name, Object value) {
+      if (name.equals(ClassNamePattern.simpleName) && value instanceof String) {
+        return KeepUnqualfiedClassNamePattern.builder().exact((String) value).build();
+      }
+      return null;
+    }
+  }
+
+  private static class PackageDeclaration extends SingleDeclaration<KeepPackagePattern> {
+
+    private PackageDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
+    }
+
+    @Override
+    String kind() {
+      return "package";
+    }
+
+    @Override
+    KeepPackagePattern getDefaultValue() {
+      return KeepPackagePattern.any();
+    }
+
+    @Override
+    KeepPackagePattern parse(String name, Object value) {
+      if (name.equals(ClassNamePattern.packageName) && value instanceof String) {
+        return KeepPackagePattern.builder().exact((String) value).build();
+      }
+      return null;
+    }
+  }
+
+  private static class ClassNamePatternDeclaration
+      extends Declaration<KeepQualifiedClassNamePattern> {
+
+    private final ClassSimpleNameDeclaration nameDeclaration;
+    private final PackageDeclaration packageDeclaration;
+    private final List<Declaration<?>> declarations;
+
+    public ClassNamePatternDeclaration(ParsingContext parsingContext) {
+      nameDeclaration = new ClassSimpleNameDeclaration(parsingContext);
+      packageDeclaration = new PackageDeclaration(parsingContext);
+      declarations = ImmutableList.of(nameDeclaration, packageDeclaration);
+    }
+
+    @Override
+    String kind() {
+      return "class-name";
+    }
+
+    @Override
+    KeepQualifiedClassNamePattern getValue() {
+      if (!packageDeclaration.isDefault() || !nameDeclaration.isDefault()) {
+        return KeepQualifiedClassNamePattern.builder()
+            .setPackagePattern(packageDeclaration.getValue())
+            .setNamePattern(nameDeclaration.getValue())
+            .build();
+      }
+      return null;
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
+    }
+  }
+
+  private static class ClassNamePatternVisitor extends AnnotationVisitorBase {
+
+    private final ClassNamePatternDeclaration declaration;
+    private final Consumer<KeepQualifiedClassNamePattern> setValue;
+
+    public ClassNamePatternVisitor(
+        AnnotationParsingContext parsingContext, Consumer<KeepQualifiedClassNamePattern> setValue) {
+      super(parsingContext);
+      this.setValue = setValue;
+      declaration = new ClassNamePatternDeclaration(parsingContext);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (!declaration.tryParse(name, value)) {
+        super.visit(name, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      if (!declaration.isDefault()) {
+        setValue.accept(declaration.getValue());
+      }
+      super.visitEnd();
+    }
+  }
+
+  private static class TypePatternVisitor extends AnnotationVisitorBase {
+    private final ParsingContext parsingContext;
+    private final Consumer<KeepTypePattern> consumer;
+    private KeepTypePattern result = null;
+
+    private TypePatternVisitor(ParsingContext parsingContext, Consumer<KeepTypePattern> consumer) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.consumer = consumer;
     }
 
     private void setResult(KeepTypePattern result) {
       if (this.result != null) {
-        throw new KeepEdgeException("Invalid type annotation defining multiple properties.");
+        throw parsingContext.error("Invalid type annotation defining multiple properties.");
       }
       this.result = result;
     }
@@ -2177,31 +2347,45 @@
     }
 
     @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      if (TypePattern.classNamePattern.equals(name)
+          && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
+        return new ClassNamePatternVisitor(
+            new AnnotationParsingContext(parsingContext, descriptor),
+            p -> {
+              if (p.isExact()) {
+                setResult(KeepTypePattern.fromDescriptor(p.getExactDescriptor()));
+              } else {
+                // TODO(b/248408342): Extend the AST type patterns.
+                throw new Unimplemented("Non-exact class patterns are not implemented yet");
+              }
+            });
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
     public void visitEnd() {
       consumer.accept(result != null ? result : KeepTypePattern.any());
     }
   }
 
   private static class TypePatternsArrayVisitor extends AnnotationVisitorBase {
-    private final Supplier<String> annotationName;
+    private final ParsingContext parsingContext;
     private final Consumer<List<KeepTypePattern>> fn;
     private final List<KeepTypePattern> patterns = new ArrayList<>();
 
     public TypePatternsArrayVisitor(
-        Supplier<String> annotationName, Consumer<List<KeepTypePattern>> fn) {
-      this.annotationName = annotationName;
+        ParsingContext parsingContext, Consumer<List<KeepTypePattern>> fn) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
       this.fn = fn;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName.get();
-    }
-
-    @Override
     public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
       if (TypePattern.DESCRIPTOR.equals(descriptor)) {
-        return new TypePatternVisitor(annotationName, patterns::add);
+        return new TypePatternVisitor(parsingContext, patterns::add);
       }
       return null;
     }
@@ -2215,10 +2399,8 @@
 
   private static class OptionsDeclaration extends SingleDeclaration<KeepOptions> {
 
-    private final String annotationName;
-
-    public OptionsDeclaration(String annotationName) {
-      this.annotationName = annotationName;
+    public OptionsDeclaration(ParsingContext parsingContext) {
+      super(parsingContext);
     }
 
     @Override
@@ -2240,17 +2422,17 @@
     AnnotationVisitor parseArray(String name, Consumer<KeepOptions> setValue) {
       if (name.equals(AnnotationConstants.Target.constraints)) {
         return new KeepConstraintsVisitor(
-            annotationName,
+            getParsingContext(),
             options -> setValue.accept(KeepOptions.disallowBuilder().addAll(options).build()));
       }
       if (name.equals(AnnotationConstants.Target.disallow)) {
         return new KeepOptionsVisitor(
-            annotationName,
+            getParsingContext(),
             options -> setValue.accept(KeepOptions.disallowBuilder().addAll(options).build()));
       }
       if (name.equals(AnnotationConstants.Target.allow)) {
         return new KeepOptionsVisitor(
-            annotationName,
+            getParsingContext(),
             options -> setValue.accept(KeepOptions.allowBuilder().addAll(options).build()));
       }
       return null;
@@ -2260,18 +2442,25 @@
   private static class KeepTargetVisitor extends KeepItemVisitorBase {
 
     private final Parent<KeepTarget> parent;
-    private final KeepTarget.Builder builder = KeepTarget.builder();
-    private final OptionsDeclaration optionsDeclaration =
-        new OptionsDeclaration(getAnnotationName());
     private final UserBindingsHelper bindingsHelper;
+    private final OptionsDeclaration optionsDeclaration;
+    private final KeepTarget.Builder builder = KeepTarget.builder();
 
-    static KeepTargetVisitor create(Parent<KeepTarget> parent, UserBindingsHelper bindingsHelper) {
-      return new KeepTargetVisitor(parent, bindingsHelper);
+    static KeepTargetVisitor create(
+        ParsingContext parsingContext,
+        Parent<KeepTarget> parent,
+        UserBindingsHelper bindingsHelper) {
+      return new KeepTargetVisitor(parsingContext, parent, bindingsHelper);
     }
 
-    private KeepTargetVisitor(Parent<KeepTarget> parent, UserBindingsHelper bindingsHelper) {
+    private KeepTargetVisitor(
+        ParsingContext parsingContext,
+        Parent<KeepTarget> parent,
+        UserBindingsHelper bindingsHelper) {
+      super(parsingContext);
       this.parent = parent;
       this.bindingsHelper = bindingsHelper;
+      optionsDeclaration = new OptionsDeclaration(parsingContext);
     }
 
     @Override
@@ -2280,11 +2469,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      return Target.SIMPLE_NAME;
-    }
-
-    @Override
     public AnnotationVisitor visitArray(String name) {
       AnnotationVisitor visitor = optionsDeclaration.tryParseArray(name);
       if (visitor != null) {
@@ -2308,7 +2492,11 @@
     private final Parent<KeepCondition> parent;
     private final UserBindingsHelper bindingsHelper;
 
-    public KeepConditionVisitor(Parent<KeepCondition> parent, UserBindingsHelper bindingsHelper) {
+    public KeepConditionVisitor(
+        ParsingContext parsingContext,
+        Parent<KeepCondition> parent,
+        UserBindingsHelper bindingsHelper) {
+      super(parsingContext);
       this.parent = parent;
       this.bindingsHelper = bindingsHelper;
     }
@@ -2319,11 +2507,6 @@
     }
 
     @Override
-    public String getAnnotationName() {
-      return Condition.SIMPLE_NAME;
-    }
-
-    @Override
     public void visitEnd() {
       super.visitEnd();
       parent.accept(KeepCondition.builder().setItemReference(getItemReference()).build());
@@ -2332,21 +2515,16 @@
 
   private static class KeepConstraintsVisitor extends AnnotationVisitorBase {
 
-    private final String annotationName;
     private final Parent<Collection<KeepOption>> parent;
     private final Set<KeepOption> options = new HashSet<>();
 
-    public KeepConstraintsVisitor(String annotationName, Parent<Collection<KeepOption>> parent) {
-      this.annotationName = annotationName;
+    public KeepConstraintsVisitor(
+        ParsingContext parsingContext, Parent<Collection<KeepOption>> parent) {
+      super(parsingContext);
       this.parent = parent;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName;
-    }
-
-    @Override
     public void visitEnum(String ignore, String descriptor, String value) {
       if (!descriptor.equals(AnnotationConstants.Constraints.DESCRIPTOR)) {
         super.visitEnum(ignore, descriptor, value);
@@ -2400,21 +2578,16 @@
 
   private static class KeepOptionsVisitor extends AnnotationVisitorBase {
 
-    private final String annotationName;
     private final Parent<Collection<KeepOption>> parent;
     private final Set<KeepOption> options = new HashSet<>();
 
-    public KeepOptionsVisitor(String annotationName, Parent<Collection<KeepOption>> parent) {
-      this.annotationName = annotationName;
+    public KeepOptionsVisitor(
+        ParsingContext parsingContext, Parent<Collection<KeepOption>> parent) {
+      super(parsingContext);
       this.parent = parent;
     }
 
     @Override
-    public String getAnnotationName() {
-      return annotationName;
-    }
-
-    @Override
     public void visitEnum(String ignore, String descriptor, String value) {
       if (!descriptor.equals(AnnotationConstants.Option.DESCRIPTOR)) {
         super.visitEnum(ignore, descriptor, value);
@@ -2451,20 +2624,13 @@
   }
 
   private static class MemberAccessVisitor extends AnnotationVisitorBase {
-    private final Supplier<String> annotationName;
-    KeepMemberAccessPattern.BuilderBase<?, ?> builder;
+    private KeepMemberAccessPattern.BuilderBase<?, ?> builder;
 
-    public MemberAccessVisitor(
-        Supplier<String> annotationName, KeepMemberAccessPattern.BuilderBase<?, ?> builder) {
-      this.annotationName = annotationName;
+    public MemberAccessVisitor(ParsingContext parsingContext, BuilderBase<?, ?> builder) {
+      super(parsingContext);
       this.builder = builder;
     }
 
-    @Override
-    public String getAnnotationName() {
-      return annotationName.get();
-    }
-
     static boolean withNormalizedAccessFlag(String flag, BiPredicate<String, Boolean> fn) {
       boolean allow = !flag.startsWith(MemberAccess.NEGATION_PREFIX);
       return allow
@@ -2523,13 +2689,12 @@
 
   private static class MethodAccessVisitor extends MemberAccessVisitor {
 
-    @SuppressWarnings("HidingField")
-    KeepMethodAccessPattern.Builder builder;
+    private KeepMethodAccessPattern.Builder methodAccessBuilder;
 
     public MethodAccessVisitor(
-        Supplier<String> annotationName, KeepMethodAccessPattern.Builder builder) {
-      super(annotationName, builder);
-      this.builder = builder;
+        ParsingContext parsingContext, KeepMethodAccessPattern.Builder builder) {
+      super(parsingContext, builder);
+      this.methodAccessBuilder = builder;
     }
 
     @Override
@@ -2543,19 +2708,19 @@
               (flag, allow) -> {
                 switch (flag) {
                   case MethodAccess.SYNCHRONIZED:
-                    builder.setSynchronized(allow);
+                    methodAccessBuilder.setSynchronized(allow);
                     return true;
                   case MethodAccess.BRIDGE:
-                    builder.setBridge(allow);
+                    methodAccessBuilder.setBridge(allow);
                     return true;
                   case MethodAccess.NATIVE:
-                    builder.setNative(allow);
+                    methodAccessBuilder.setNative(allow);
                     return true;
                   case MethodAccess.ABSTRACT:
-                    builder.setAbstract(allow);
+                    methodAccessBuilder.setAbstract(allow);
                     return true;
                   case MethodAccess.STRICT_FP:
-                    builder.setStrictFp(allow);
+                    methodAccessBuilder.setStrictFp(allow);
                     return true;
                   default:
                     return false;
@@ -2570,13 +2735,12 @@
 
   private static class FieldAccessVisitor extends MemberAccessVisitor {
 
-    @SuppressWarnings("HidingField")
-    KeepFieldAccessPattern.Builder builder;
+    private KeepFieldAccessPattern.Builder fieldAccessBuilder;
 
     public FieldAccessVisitor(
-        Supplier<String> annotationName, KeepFieldAccessPattern.Builder builder) {
-      super(annotationName, builder);
-      this.builder = builder;
+        ParsingContext parsingContext, KeepFieldAccessPattern.Builder builder) {
+      super(parsingContext, builder);
+      this.fieldAccessBuilder = builder;
     }
 
     @Override
@@ -2590,10 +2754,10 @@
               (flag, allow) -> {
                 switch (flag) {
                   case FieldAccess.VOLATILE:
-                    builder.setVolatile(allow);
+                    fieldAccessBuilder.setVolatile(allow);
                     return true;
                   case FieldAccess.TRANSIENT:
-                    builder.setTransient(allow);
+                    fieldAccessBuilder.setTransient(allow);
                     return true;
                   default:
                     return false;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
index d88b010..18e5c02 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
@@ -23,19 +23,55 @@
     return "L" + getBinaryNameFromClassTypeName(classTypeName) + ";";
   }
 
+  public static String getJavaTypeFromDescriptor(String descriptor) {
+    if (descriptor.length() == 1) {
+      switch (descriptor.charAt(0)) {
+        case 'Z':
+          return "boolean";
+        case 'B':
+          return "byte";
+        case 'C':
+          return "char";
+        case 'S':
+          return "short";
+        case 'I':
+          return "int";
+        case 'J':
+          return "long";
+        case 'F':
+          return "float";
+        case 'D':
+          return "double";
+        case 'V':
+          return "void";
+        default:
+          throw new IllegalStateException("Unexpected descriptor: " + descriptor);
+      }
+    }
+    if (descriptor.charAt(0) == '[') {
+      return getJavaTypeFromDescriptor(descriptor.substring(1)) + "[]";
+    }
+    if (descriptor.charAt(0) == 'L') {
+      return descriptor.substring(1, descriptor.length() - 1).replace('/', '.');
+    }
+    throw new IllegalStateException("Unexpected descriptor: " + descriptor);
+  }
+
   public static KeepTypePattern typePatternFromString(String string) {
     if (string.equals("<any>")) {
       return KeepTypePattern.any();
     }
-    return KeepTypePattern.fromDescriptor(javaTypeToDescriptor(string));
+    return KeepTypePattern.fromDescriptor(getDescriptorFromJavaType(string));
   }
 
-  public static String javaTypeToDescriptor(String type) {
+  public static String getDescriptorFromJavaType(String type) {
     switch (type) {
       case "boolean":
         return "Z";
       case "byte":
         return "B";
+      case "char":
+        return "C";
       case "short":
         return "S";
       case "int":
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
new file mode 100644
index 0000000..dadb8de
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.PackageNameParser.PackageNameProperty;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.function.Consumer;
+
+public class PackageNameParser
+    extends PropertyParserBase<KeepPackagePattern, PackageNameProperty, PackageNameParser> {
+
+  public PackageNameParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  public enum PackageNameProperty {
+    NAME
+  }
+
+  @Override
+  public PackageNameParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean tryProperty(
+      PackageNameProperty property,
+      String name,
+      Object value,
+      Consumer<KeepPackagePattern> setValue) {
+    switch (property) {
+      case NAME:
+        setValue.accept(KeepPackagePattern.exact((String) value));
+        return true;
+      default:
+        return false;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
new file mode 100644
index 0000000..77d6087
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import java.util.Collections;
+import java.util.List;
+import org.objectweb.asm.AnnotationVisitor;
+
+/** Convert parser(s) into an annotation visitor. */
+public class ParserVisitor extends AnnotationVisitorBase {
+
+  private final List<PropertyParser<?, ?, ?>> parsers;
+  private final Runnable onVisitEnd;
+
+  public ParserVisitor(
+      AnnotationParsingContext parsingContext,
+      String annotationDescriptor,
+      List<PropertyParser<?, ?, ?>> parsers,
+      Runnable onVisitEnd) {
+    super(parsingContext);
+    this.parsers = parsers;
+    this.onVisitEnd = onVisitEnd;
+    assert annotationDescriptor.equals(parsingContext.getAnnotationDescriptor());
+  }
+
+  public ParserVisitor(
+      AnnotationParsingContext parsingContext,
+      String annotationDescriptor,
+      PropertyParser<?, ?, ?> declaration,
+      Runnable onVisitEnd) {
+    this(parsingContext, annotationDescriptor, Collections.singletonList(declaration), onVisitEnd);
+  }
+
+  private <T> void ignore(T unused) {}
+
+  @Override
+  public void visit(String name, Object value) {
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      if (parser.tryParse(name, value, this::ignore)) {
+        return;
+      }
+    }
+    super.visit(name, value);
+  }
+
+  @Override
+  public AnnotationVisitor visitArray(String name) {
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
+      if (visitor != null) {
+        return visitor;
+      }
+    }
+    return super.visitArray(name);
+  }
+
+  @Override
+  public void visitEnum(String name, String descriptor, String value) {
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      if (parser.tryParseEnum(name, descriptor, value, this::ignore)) {
+        return;
+      }
+    }
+    super.visitEnum(name, descriptor, value);
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
+      if (visitor != null) {
+        return visitor;
+      }
+    }
+    return super.visitAnnotation(name, descriptor);
+  }
+
+  @Override
+  public void visitEnd() {
+    onVisitEnd.run();
+    super.visitEnd();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
new file mode 100644
index 0000000..67b181b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public interface PropertyParser<T, P, S> {
+
+  S self();
+
+  String kind();
+
+  S setProperty(P property, String name);
+
+  boolean isDeclared();
+
+  default boolean isDefault() {
+    return !isDeclared();
+  }
+
+  T getValue();
+
+  boolean tryParse(String name, Object value, Consumer<T> setValue);
+
+  boolean tryParseEnum(String name, String descriptor, String value, Consumer<T> setValue);
+
+  AnnotationVisitor tryParseArray(String name, Consumer<T> setValue);
+
+  AnnotationVisitor tryParseAnnotation(String name, String descriptor, Consumer<T> setValue);
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
new file mode 100644
index 0000000..108f086
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
@@ -0,0 +1,152 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+/** Special case of a property parser allowing only a single value callback. */
+public abstract class PropertyParserBase<T, P, S> implements PropertyParser<T, P, S> {
+
+  private final ParsingContext parsingContext;
+
+  private String kind;
+  private final Map<String, P> mapping = new HashMap<>();
+  private String resultPropertyName = null;
+  private T resultValue = null;
+
+  protected PropertyParserBase(ParsingContext parsingContext) {
+    this.parsingContext = parsingContext;
+  }
+
+  public ParsingContext getParsingContext() {
+    return parsingContext;
+  }
+
+  boolean tryProperty(P property, String name, Object value, Consumer<T> setValue) {
+    return false;
+  }
+
+  public boolean tryPropertyEnum(
+      P property, String name, String descriptor, String value, Consumer<T> setValue) {
+    return false;
+  }
+
+  AnnotationVisitor tryPropertyArray(P property, String name, Consumer<T> setValue) {
+    return null;
+  }
+
+  AnnotationVisitor tryPropertyAnnotation(
+      P property, String name, String descriptor, Consumer<T> setValue) {
+    return null;
+  }
+
+  private Consumer<T> wrap(String propertyName, Consumer<T> setValue) {
+    return value -> {
+      assert value != null;
+      if (resultPropertyName != null) {
+        assert resultValue != null;
+        error(propertyName);
+      } else {
+        resultPropertyName = propertyName;
+        resultValue = value;
+        setValue.accept(value);
+      }
+    };
+  }
+
+  private void error(String name) {
+    throw new KeepEdgeException(
+        "Multiple properties defining "
+            + kind()
+            + ": '"
+            + resultPropertyName
+            + "' and '"
+            + name
+            + "'");
+  }
+
+  public final boolean isDeclared() {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return resultPropertyName != null;
+  }
+
+  public T getValue() {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return resultValue;
+  }
+
+  public T getValueOrDefault(T defaultValue) {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return isDeclared() ? resultValue : defaultValue;
+  }
+
+  /** Helper for parsing directly. Returns non-null if the property-name triggered parsing. */
+  public final T tryParse(String name, Object value) {
+    boolean triggered = tryParse(name, value, unused -> {});
+    assert triggered == (resultValue != null);
+    return resultValue;
+  }
+
+  public String kind() {
+    return kind != null ? kind : "";
+  }
+
+  public S setKind(String kind) {
+    this.kind = kind;
+    return self();
+  }
+
+  /** Add property parsing for the given property-name. */
+  public S setProperty(P property, String name) {
+    P old = mapping.put(name, property);
+    if (old != null) {
+      throw new IllegalArgumentException("Unexpected attempt to redefine property " + name);
+    }
+    return self();
+  }
+
+  @Override
+  public final boolean tryParse(String name, Object value, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryProperty(prop, name, value, wrap(name, setValue));
+    }
+    return false;
+  }
+
+  @Override
+  public final boolean tryParseEnum(
+      String name, String descriptor, String value, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyEnum(prop, name, descriptor, value, wrap(name, setValue));
+    }
+    return false;
+  }
+
+  @Override
+  public final AnnotationVisitor tryParseArray(String name, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyArray(prop, name, wrap(name, setValue));
+    }
+    return null;
+  }
+
+  @Override
+  public final AnnotationVisitor tryParseAnnotation(
+      String name, String descriptor, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyAnnotation(prop, name, descriptor, wrap(name, setValue));
+    }
+    return null;
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
new file mode 100644
index 0000000..6b11f70
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2023, 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.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
+
+public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty, TypeParser> {
+
+  public TypeParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  public enum TypeProperty {
+    SELF_PATTERN,
+    TYPE_NAME,
+    TYPE_CONSTANT,
+    CLASS_NAME_PATTERN
+  }
+
+  public TypeParser enableTypePattern(String propertyName) {
+    return setProperty(TypeProperty.SELF_PATTERN, propertyName);
+  }
+
+  public TypeParser enableTypeName(String propertyName) {
+    return setProperty(TypeProperty.TYPE_NAME, propertyName);
+  }
+
+  public TypeParser enableTypeConstant(String propertyName) {
+    return setProperty(TypeProperty.TYPE_CONSTANT, propertyName);
+  }
+
+  public TypeParser enableTypeClassNamePattern(String propertyName) {
+    return setProperty(TypeProperty.CLASS_NAME_PATTERN, propertyName);
+  }
+
+  @Override
+  public TypeParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean tryProperty(
+      TypeProperty property, String name, Object value, Consumer<KeepTypePattern> setValue) {
+    switch (property) {
+      case TYPE_NAME:
+        setValue.accept(KeepEdgeReaderUtils.typePatternFromString((String) value));
+        return true;
+      case TYPE_CONSTANT:
+        setValue.accept(KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  @Override
+  public AnnotationVisitor tryPropertyAnnotation(
+      TypeProperty property, String name, String descriptor, Consumer<KeepTypePattern> setValue) {
+    switch (property) {
+      case SELF_PATTERN:
+        {
+          AnnotationParsingContext parsingContext =
+              new AnnotationParsingContext(getParsingContext(), descriptor);
+          TypeParser typeParser =
+              new TypeParser(parsingContext)
+                  .setKind(kind())
+                  .enableTypeName(TypePattern.name)
+                  .enableTypeConstant(TypePattern.constant)
+                  .enableTypeClassNamePattern(TypePattern.classNamePattern);
+          return new ParserVisitor(
+              parsingContext,
+              descriptor,
+              typeParser,
+              () -> setValue.accept(typeParser.getValueOrDefault(KeepTypePattern.any())));
+        }
+      case CLASS_NAME_PATTERN:
+        {
+          return new ClassNameParser(getParsingContext())
+              .setKind(kind())
+              .tryPropertyAnnotation(
+                  ClassNameProperty.PATTERN,
+                  name,
+                  descriptor,
+                  classNamePattern -> {
+                    if (classNamePattern.isExact()) {
+                      setValue.accept(
+                          KeepTypePattern.fromDescriptor(classNamePattern.getExactDescriptor()));
+                    } else {
+                      // TODO(b/248408342): Extend the AST type patterns.
+                      throw new Unimplemented("Non-exact class patterns are not implemented yet");
+                    }
+                  });
+        }
+      default:
+        return null;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 1ab1561..13a228b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -16,7 +16,6 @@
  */
 public final class AnnotationConstants {
   public static final class Edge {
-    public static final String SIMPLE_NAME = "KeepEdge";
     public static final String DESCRIPTOR = "Lcom/android/tools/r8/keepanno/annotations/KeepEdge;";
     public static final String description = "description";
     public static final String bindings = "bindings";
@@ -25,7 +24,6 @@
   }
 
   public static final class ForApi {
-    public static final String SIMPLE_NAME = "KeepForApi";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepForApi;";
     public static final String description = "description";
@@ -34,7 +32,6 @@
   }
 
   public static final class UsesReflection {
-    public static final String SIMPLE_NAME = "UsesReflection";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/UsesReflection;";
     public static final String description = "description";
@@ -43,7 +40,6 @@
   }
 
   public static final class UsedByReflection {
-    public static final String SIMPLE_NAME = "UsedByReflection";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/UsedByReflection;";
     public static final String description = "description";
@@ -52,20 +48,17 @@
   }
 
   public static final class UsedByNative {
-    public static final String SIMPLE_NAME = "UsedByNative";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/UsedByNative;";
     // Content is the same as UsedByReflection.
   }
 
   public static final class CheckRemoved {
-    public static final String SIMPLE_NAME = "CheckRemoved";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/CheckRemoved;";
   }
 
   public static final class CheckOptimizedOut {
-    public static final String SIMPLE_NAME = "CheckOptimizedOut";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/CheckOptimizedOut;";
   }
@@ -76,6 +69,7 @@
     public static final String memberFromBinding = "memberFromBinding";
     public static final String className = "className";
     public static final String classConstant = "classConstant";
+    public static final String classNamePattern = "classNamePattern";
     public static final String instanceOfClassName = "instanceOfClassName";
     public static final String instanceOfClassNameExclusive = "instanceOfClassNameExclusive";
     public static final String instanceOfClassConstant = "instanceOfClassConstant";
@@ -99,20 +93,17 @@
   }
 
   public static final class Binding {
-    public static final String SIMPLE_NAME = "KeepBinding";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepBinding;";
     public static final String bindingName = "bindingName";
   }
 
   public static final class Condition {
-    public static final String SIMPLE_NAME = "KeepCondition";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepCondition;";
   }
 
   public static final class Target {
-    public static final String SIMPLE_NAME = "KeepTarget";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepTarget;";
     public static final String kind = "kind";
@@ -122,7 +113,6 @@
   }
 
   public static final class Kind {
-    public static final String SIMPLE_NAME = "KeepItemKind";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepItemKind;";
     public static final String ONLY_CLASS = "ONLY_CLASS";
@@ -135,7 +125,6 @@
   }
 
   public static final class Constraints {
-    public static final String SIMPLE_NAME = "KeepConstraint";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepConstraint;";
     public static final String LOOKUP = "LOOKUP";
@@ -154,7 +143,6 @@
   }
 
   public static final class Option {
-    public static final String SIMPLE_NAME = "KeepOption";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepOption;";
     public static final String SHRINKING = "SHRINKING";
@@ -165,7 +153,6 @@
   }
 
   public static final class MemberAccess {
-    public static final String SIMPLE_NAME = "MemberAccessFlags";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/MemberAccessFlags;";
     public static final String NEGATION_PREFIX = "NON_";
@@ -179,7 +166,6 @@
   }
 
   public static final class MethodAccess {
-    public static final String SIMPLE_NAME = "MethodAccessFlags";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/MethodAccessFlags;";
     public static final String SYNCHRONIZED = "SYNCHRONIZED";
@@ -190,7 +176,6 @@
   }
 
   public static final class FieldAccess {
-    public static final String SIMPLE_NAME = "FieldAccessFlags";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/FieldAccessFlags;";
     public static final String VOLATILE = "VOLATILE";
@@ -198,10 +183,17 @@
   }
 
   public static final class TypePattern {
-    public static final String SIMPLE_NAME = "TypePattern";
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/TypePattern;";
     public static final String name = "name";
     public static final String constant = "constant";
+    public static final String classNamePattern = "classNamePattern";
+  }
+
+  public static final class ClassNamePattern {
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/ClassNamePattern;";
+    public static final String simpleName = "simpleName";
+    public static final String packageName = "packageName";
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java
new file mode 100644
index 0000000..d98d410
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2023, 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.keepanno.ast;
+
+public class KeepAnnotationParserException extends KeepEdgeException {
+
+  private final ParsingContext context;
+
+  public KeepAnnotationParserException(ParsingContext context, String message) {
+    super(message);
+    this.context = context;
+  }
+
+  @Override
+  public String getMessage() {
+    return super.getMessage() + getContextAsString();
+  }
+
+  private String getContextAsString() {
+    StringBuilder builder = new StringBuilder();
+    ParsingContext current = context;
+    while (current != null) {
+      builder.append("\n  in ").append(current.getContextFrameAsString());
+      current = current.getParentContext();
+    }
+    return builder.toString();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
index 75094be..4ec248b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
@@ -115,12 +115,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object o) {
       if (this == o) {
         return true;
       }
-      if (o == null || getClass() != o.getClass()) {
+      if (!(o instanceof KeepEdgeClassContext)) {
         return false;
       }
       KeepEdgeClassContext that = (KeepEdgeClassContext) o;
@@ -154,12 +153,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object o) {
       if (this == o) {
         return true;
       }
-      if (o == null || getClass() != o.getClass()) {
+      if (!(o instanceof KeepEdgeMethodContext)) {
         return false;
       }
       KeepEdgeMethodContext that = (KeepEdgeMethodContext) o;
@@ -191,12 +189,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object o) {
       if (this == o) {
         return true;
       }
-      if (o == null || getClass() != o.getClass()) {
+      if (!(o instanceof KeepEdgeFieldContext)) {
         return false;
       }
       KeepEdgeFieldContext that = (KeepEdgeFieldContext) o;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
new file mode 100644
index 0000000..d7ce9a4
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2023, 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.keepanno.ast;
+
+import static com.android.tools.r8.keepanno.asm.KeepEdgeReaderUtils.getJavaTypeFromDescriptor;
+
+import org.objectweb.asm.Type;
+
+public abstract class ParsingContext {
+
+  public KeepAnnotationParserException error(String message) {
+    throw new KeepAnnotationParserException(this, message);
+  }
+
+  public abstract String getHolderName();
+
+  public ParsingContext getParentContext() {
+    return null;
+  }
+
+  public abstract String getContextFrameAsString();
+
+  public static class ClassParsingContext extends ParsingContext {
+    private final String className;
+
+    public ClassParsingContext(String className) {
+      this.className = className;
+    }
+
+    @Override
+    public String getHolderName() {
+      return className;
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return className;
+    }
+  }
+
+  public abstract static class MemberParsingContext extends ParsingContext {
+    private final ClassParsingContext classContext;
+
+    public MemberParsingContext(ClassParsingContext classContext) {
+      this.classContext = classContext;
+    }
+
+    @Override
+    public String getHolderName() {
+      return classContext.getHolderName();
+    }
+
+    @Override
+    public ParsingContext getParentContext() {
+      return classContext;
+    }
+  }
+
+  public static class MethodParsingContext extends MemberParsingContext {
+
+    private final String methodName;
+    private final String methodDescriptor;
+
+    public MethodParsingContext(
+        ClassParsingContext classContext, String methodName, String methodDescriptor) {
+      super(classContext);
+      this.methodName = methodName;
+      this.methodDescriptor = methodDescriptor;
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      Type methodType = Type.getMethodType(methodDescriptor);
+      StringBuilder builder = new StringBuilder();
+      builder
+          .append("method ")
+          .append(getJavaTypeFromDescriptor(methodType.getReturnType().getDescriptor()))
+          .append(' ')
+          .append(methodName)
+          .append('(');
+      boolean first = true;
+      for (Type argument : methodType.getArgumentTypes()) {
+        if (first) {
+          first = false;
+        } else {
+          builder.append(", ");
+        }
+        builder.append(getJavaTypeFromDescriptor(argument.getDescriptor()));
+      }
+      return builder.append(')').toString();
+    }
+  }
+
+  public static class FieldParsingContext extends MemberParsingContext {
+
+    private final String fieldName;
+    private final String fieldDescriptor;
+
+    public FieldParsingContext(
+        ClassParsingContext classContext, String fieldName, String fieldDescriptor) {
+      super(classContext);
+      this.fieldName = fieldName;
+      this.fieldDescriptor = fieldDescriptor;
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return "field " + getJavaTypeFromDescriptor(fieldDescriptor) + " " + fieldName;
+    }
+  }
+
+  public static class AnnotationParsingContext extends ParsingContext {
+    private final ParsingContext parentContext;
+    private final String annotationDescriptor;
+
+    public AnnotationParsingContext(ParsingContext parentContext, String annotationDescriptor) {
+      this.parentContext = parentContext;
+      this.annotationDescriptor = annotationDescriptor;
+    }
+
+    public String getAnnotationDescriptor() {
+      return annotationDescriptor;
+    }
+
+    private String getSimpleAnnotationName() {
+      int i = annotationDescriptor.lastIndexOf('/') + 1;
+      return annotationDescriptor.substring(Math.max(i, 1), annotationDescriptor.length() - 1);
+    }
+
+    @Override
+    public String getHolderName() {
+      return parentContext.getHolderName();
+    }
+
+    @Override
+    public ParsingContext getParentContext() {
+      return parentContext;
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return "@" + getSimpleAnnotationName();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index c67603b..f2c08f3 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -393,6 +393,11 @@
       return self();
     }
 
+    /** Get the consumer for receiving the proguard-map content or null if unset. */
+    public StringConsumer getProguardMapConsumer() {
+      return proguardMapConsumer;
+    }
+
     /**
      * Set an output destination to which partition-map content should be written.
      *
diff --git a/src/main/java/com/android/tools/r8/MapConsumerToPartitionMapConsumer.java b/src/main/java/com/android/tools/r8/MapConsumerToPartitionMapConsumer.java
index 2fd47d9..56a5446 100644
--- a/src/main/java/com/android/tools/r8/MapConsumerToPartitionMapConsumer.java
+++ b/src/main/java/com/android/tools/r8/MapConsumerToPartitionMapConsumer.java
@@ -7,12 +7,9 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
-import com.android.tools.r8.naming.ProguardMapMarkerInfo;
 import com.android.tools.r8.retrace.ProguardMapPartitioner;
 import com.android.tools.r8.retrace.internal.ProguardMapProducerInternal;
-import com.android.tools.r8.utils.ListUtils;
 import java.io.IOException;
-import java.util.List;
 
 public class MapConsumerToPartitionMapConsumer implements MapConsumer {
 
@@ -26,12 +23,8 @@
   @Override
   public void accept(
       DiagnosticsHandler diagnosticsHandler,
-      ProguardMapMarkerInfo makerInfo,
       ClassNameMapper classNameMapper) {
     try {
-      List<String> newPreamble =
-          ListUtils.joinNewArrayList(makerInfo.toPreamble(), classNameMapper.getPreamble());
-      classNameMapper.setPreamble(newPreamble);
       partitionMapConsumer.acceptMappingPartitionMetadata(
           ProguardMapPartitioner.builder(diagnosticsHandler)
               .setProguardMapProducer(new ProguardMapProducerInternal(classNameMapper))
diff --git a/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
index 5e0445e..8bf6828 100644
--- a/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.DexMethodSignatureBiMap;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -52,6 +53,8 @@
   private final SyntheticArgumentClass syntheticArgumentClass;
 
   private final Map<DexProgramClass, DexType> originalSuperTypes = new IdentityHashMap<>();
+
+  protected final DexMethodSignatureSet keptSignatures = DexMethodSignatureSet.create();
   private final DexMethodSignatureBiMap<DexMethodSignature> reservedInterfaceSignatures =
       new DexMethodSignatureBiMap<>();
 
@@ -74,6 +77,7 @@
     }
     timing.begin("Fixup");
     AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+    preprocess();
     Collection<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
     Iterables.filter(classes, DexProgramClass::isInterface).forEach(this::fixupInterfaceClass);
     classes.forEach(this::fixupAttributes);
@@ -94,6 +98,10 @@
 
   public abstract boolean isRunningBeforePrimaryOptimizationPass();
 
+  public void preprocess() {
+    // Intentionally empty.
+  }
+
   public void postprocess() {
     // Intentionally empty.
   }
@@ -155,9 +163,12 @@
   }
 
   private DexEncodedMethod fixupVirtualInterfaceMethod(DexEncodedMethod method) {
-    DexMethod originalMethodReference = method.getReference();
+    if (keptSignatures.contains(method)) {
+      return method;
+    }
 
     // Don't process this method if it does not refer to a merge class type.
+    DexMethod originalMethodReference = method.getReference();
     boolean referencesMergeClass =
         Iterables.any(
             originalMethodReference.getReferencedBaseTypes(dexItemFactory),
@@ -264,64 +275,70 @@
     DexMethod originalMethodReference = method.getReference();
 
     // Fix all type references in the method prototype.
-    DexMethodSignature reservedMethodSignature = newMethodSignatures.get(method);
     DexMethod newMethodReference;
-    if (reservedMethodSignature != null) {
-      newMethodReference = reservedMethodSignature.withHolder(clazz, dexItemFactory);
+    if (keptSignatures.contains(method)) {
+      newMethodReference = method.getReference();
     } else {
-      newMethodReference = fixupMethodReference(originalMethodReference);
-      if (newMethodSignatures.containsValue(newMethodReference.getSignature())) {
-        // If the method collides with a direct method on the same class then rename it to a
-        // globally
-        // fresh name and record the signature.
-        if (method.isInstanceInitializer()) {
-          // If the method is an instance initializer, then add extra nulls.
-          Box<Set<DexType>> usedSyntheticArgumentClasses = new Box<>();
-          newMethodReference =
-              dexItemFactory.createInstanceInitializerWithFreshProto(
-                  newMethodReference,
-                  syntheticArgumentClass.getArgumentClasses(),
-                  tryMethod -> !newMethodSignatures.containsValue(tryMethod.getSignature()),
-                  usedSyntheticArgumentClasses::set);
-          lensBuilder.addExtraParameters(
-              originalMethodReference,
-              newMethodReference,
-              ExtraUnusedNullParameter.computeExtraUnusedNullParameters(
-                  originalMethodReference, newMethodReference));
+      DexMethodSignature reservedMethodSignature = newMethodSignatures.get(method);
+      if (reservedMethodSignature != null) {
+        newMethodReference = reservedMethodSignature.withHolder(clazz, dexItemFactory);
+      } else {
+        newMethodReference = fixupMethodReference(originalMethodReference);
+        if (keptSignatures.contains(newMethodReference)
+            || newMethodSignatures.containsValue(newMethodReference.getSignature())) {
+          // If the method collides with a direct method on the same class then rename it to a
+          // globally
+          // fresh name and record the signature.
+          if (method.isInstanceInitializer()) {
+            // If the method is an instance initializer, then add extra nulls.
+            Box<Set<DexType>> usedSyntheticArgumentClasses = new Box<>();
+            newMethodReference =
+                dexItemFactory.createInstanceInitializerWithFreshProto(
+                    newMethodReference,
+                    syntheticArgumentClass.getArgumentClasses(),
+                    tryMethod -> !newMethodSignatures.containsValue(tryMethod.getSignature()),
+                    usedSyntheticArgumentClasses::set);
+            lensBuilder.addExtraParameters(
+                originalMethodReference,
+                newMethodReference,
+                ExtraUnusedNullParameter.computeExtraUnusedNullParameters(
+                    originalMethodReference, newMethodReference));
 
-          // Amend the art profile collection.
-          if (usedSyntheticArgumentClasses.isSet()) {
-            Set<DexMethod> previousMethodReferences =
-                lensBuilder.getOriginalMethodReferences(originalMethodReference);
-            if (previousMethodReferences.isEmpty()) {
-              profileCollectionAdditions.applyIfContextIsInProfile(
-                  originalMethodReference,
-                  additionsBuilder ->
-                      usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
-            } else {
-              for (DexMethod previousMethodReference : previousMethodReferences) {
+            // Amend the art profile collection.
+            if (usedSyntheticArgumentClasses.isSet()) {
+              Set<DexMethod> previousMethodReferences =
+                  lensBuilder.getOriginalMethodReferences(originalMethodReference);
+              if (previousMethodReferences.isEmpty()) {
                 profileCollectionAdditions.applyIfContextIsInProfile(
-                    previousMethodReference,
+                    originalMethodReference,
                     additionsBuilder ->
                         usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
+              } else {
+                for (DexMethod previousMethodReference : previousMethodReferences) {
+                  profileCollectionAdditions.applyIfContextIsInProfile(
+                      previousMethodReference,
+                      additionsBuilder ->
+                          usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
+                }
               }
             }
+          } else {
+            newMethodReference =
+                dexItemFactory.createFreshMethodNameWithoutHolder(
+                    newMethodReference.getName().toSourceString(),
+                    newMethodReference.getProto(),
+                    newMethodReference.getHolderType(),
+                    tryMethod ->
+                        !keptSignatures.contains(tryMethod)
+                            && !reservedInterfaceSignatures.containsValue(tryMethod.getSignature())
+                            && !remappedVirtualMethods.containsValue(tryMethod.getSignature())
+                            && !newMethodSignatures.containsValue(tryMethod.getSignature()));
           }
-        } else {
-          newMethodReference =
-              dexItemFactory.createFreshMethodNameWithoutHolder(
-                  newMethodReference.getName().toSourceString(),
-                  newMethodReference.getProto(),
-                  newMethodReference.getHolderType(),
-                  tryMethod ->
-                      !reservedInterfaceSignatures.containsValue(tryMethod.getSignature())
-                          && !remappedVirtualMethods.containsValue(tryMethod.getSignature())
-                          && !newMethodSignatures.containsValue(tryMethod.getSignature()));
         }
-      }
 
-      assert !newMethodSignatures.containsValue(newMethodReference.getSignature());
-      newMethodSignatures.put(method, newMethodReference.getSignature());
+        assert !newMethodSignatures.containsValue(newMethodReference.getSignature());
+        newMethodSignatures.put(method, newMethodReference.getSignature());
+      }
     }
 
     return fixupProgramMethod(clazz, method, newMethodReference);
@@ -378,27 +395,33 @@
       DexEncodedMethod method,
       DexMethodSignatureBiMap<DexMethodSignature> renamedClassVirtualMethods,
       MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures) {
-    DexMethodSignature newSignature = newMethodSignatures.get(method);
-    if (newSignature == null) {
-      // Fix all type references in the method prototype.
-      newSignature =
-          dexItemFactory.createFreshMethodSignatureName(
-              method.getName().toSourceString(),
-              null,
-              fixupProto(method.getProto()),
-              trySignature ->
-                  !reservedInterfaceSignatures.containsValue(trySignature)
-                      && !newMethodSignatures.containsValue(trySignature)
-                      && !renamedClassVirtualMethods.containsValue(trySignature));
-      newMethodSignatures.put(method, newSignature);
+    DexMethodSignature newSignature;
+    if (keptSignatures.contains(method)) {
+      newSignature = method.getSignature();
+    } else {
+      newSignature = newMethodSignatures.get(method);
+      if (newSignature == null) {
+        // Fix all type references in the method prototype.
+        newSignature =
+            dexItemFactory.createFreshMethodSignatureName(
+                method.getName().toSourceString(),
+                null,
+                fixupProto(method.getProto()),
+                trySignature ->
+                    !keptSignatures.contains(trySignature)
+                        && !reservedInterfaceSignatures.containsValue(trySignature)
+                        && !newMethodSignatures.containsValue(trySignature)
+                        && !renamedClassVirtualMethods.containsValue(trySignature));
+        newMethodSignatures.put(method, newSignature);
+      }
     }
 
     // If any of the parameter types have been merged, record the signature mapping so that
     // subclasses perform the identical rename.
-    if (!reservedInterfaceSignatures.containsKey(method)
+    if (!keptSignatures.contains(method)
+        && !reservedInterfaceSignatures.containsKey(method)
         && Iterables.any(
-            newSignature.getProto().getParameterBaseTypes(dexItemFactory),
-            mergedClasses::isMergeTarget)) {
+            newSignature.getProto().getBaseTypes(dexItemFactory), mergedClasses::isMergeTarget)) {
       renamedClassVirtualMethods.put(method.getSignature(), newSignature);
     }
 
diff --git a/src/main/java/com/android/tools/r8/classmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/classmerging/MergeGroup.java
new file mode 100644
index 0000000..a811666
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/MergeGroup.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging;
+
+public abstract class MergeGroup {
+
+  public abstract int size();
+}
diff --git a/src/main/java/com/android/tools/r8/classmerging/Policy.java b/src/main/java/com/android/tools/r8/classmerging/Policy.java
new file mode 100644
index 0000000..e7f52f3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/Policy.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging;
+
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.VerticalClassMergerPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.VerticalClassMergerPolicyWithPreprocessing;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * The super class of all class merging policies. Most classes will either implement {@link
+ * SingleClassPolicy} or {@link MultiClassPolicy}.
+ */
+public abstract class Policy {
+
+  /** Counter keeping track of how many classes this policy has removed. For debugging only. */
+  public int numberOfRemovedClasses;
+
+  public int numberOfRemovedInterfaces;
+
+  public void clear() {}
+
+  public abstract String getName();
+
+  public boolean isIdentityForInterfaceGroups() {
+    return false;
+  }
+
+  public boolean isSingleClassPolicy() {
+    return false;
+  }
+
+  public SingleClassPolicy asSingleClassPolicy() {
+    return null;
+  }
+
+  public boolean isMultiClassPolicy() {
+    return false;
+  }
+
+  public MultiClassPolicy asMultiClassPolicy() {
+    return null;
+  }
+
+  public boolean isMultiClassPolicyWithPreprocessing() {
+    return false;
+  }
+
+  public MultiClassPolicyWithPreprocessing<?> asMultiClassPolicyWithPreprocessing() {
+    return null;
+  }
+
+  public boolean isVerticalClassMergerPolicy() {
+    return false;
+  }
+
+  public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+    return null;
+  }
+
+  public boolean isVerticalClassMergerPolicyWithPreprocessing() {
+    return false;
+  }
+
+  public VerticalClassMergerPolicyWithPreprocessing<?>
+      asVerticalClassMergerPolicyWithPreprocessing() {
+    return null;
+  }
+
+  public boolean shouldSkipPolicy() {
+    return false;
+  }
+
+  /**
+   * Remove all groups containing no or only a single class, as there is no point in merging these.
+   */
+  protected Collection<HorizontalMergeGroup> removeTrivialGroups(
+      Collection<HorizontalMergeGroup> groups) {
+    assert !(groups instanceof ArrayList);
+    groups.removeIf(HorizontalMergeGroup::isTrivial);
+    return groups;
+  }
+
+  public boolean recordRemovedClassesForDebugging(
+      boolean isInterfaceGroup, int previousGroupSize, Collection<HorizontalMergeGroup> newGroups) {
+    assert previousGroupSize >= 2;
+    int previousNumberOfRemovedClasses = previousGroupSize - 1;
+    int newNumberOfRemovedClasses = 0;
+    for (HorizontalMergeGroup newGroup : newGroups) {
+      if (newGroup.isNonTrivial()) {
+        newNumberOfRemovedClasses += newGroup.size() - 1;
+      }
+    }
+    assert previousNumberOfRemovedClasses >= newNumberOfRemovedClasses;
+    int change = previousNumberOfRemovedClasses - newNumberOfRemovedClasses;
+    if (isInterfaceGroup) {
+      numberOfRemovedInterfaces += change;
+    } else {
+      numberOfRemovedClasses += change;
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/classmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/classmerging/PolicyExecutor.java
new file mode 100644
index 0000000..595ff58
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/PolicyExecutor.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging;
+
+import com.android.tools.r8.utils.Timing;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * This is a simple policy executor that ensures regular sequential execution of policies. It should
+ * primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
+ * against which more efficient policy executors can be compared.
+ */
+public abstract class PolicyExecutor<MG extends MergeGroup> {
+
+  /**
+   * Given an initial collection of class groups which can potentially be merged, run all of the
+   * policies registered to this policy executor on the class groups yielding a new collection of
+   * class groups.
+   */
+  // TODO(b/270398965): Replace LinkedList.
+  @SuppressWarnings("JdkObsolete")
+  public Collection<MG> run(
+      Collection<MG> inputGroups,
+      Collection<? extends Policy> policies,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    LinkedList<MG> linkedGroups;
+
+    if (inputGroups instanceof LinkedList) {
+      linkedGroups = (LinkedList<MG>) inputGroups;
+    } else {
+      linkedGroups = new LinkedList<>(inputGroups);
+    }
+
+    for (Policy policy : policies) {
+      if (policy.shouldSkipPolicy()) {
+        continue;
+      }
+
+      timing.begin(policy.getName());
+      linkedGroups = apply(policy, linkedGroups, executorService);
+      timing.end();
+
+      policy.clear();
+
+      if (linkedGroups.isEmpty()) {
+        break;
+      }
+
+      // Any policy should not return any trivial groups.
+      assert linkedGroups.stream().allMatch(group -> group.size() >= 2);
+    }
+
+    return linkedGroups;
+  }
+
+  protected abstract LinkedList<MG> apply(
+      Policy policy, LinkedList<MG> linkedGroups, ExecutorService executorService)
+      throws ExecutionException;
+}
diff --git a/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java b/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
index a8dcd49..54cdd43 100644
--- a/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
+++ b/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.synthesis.SyntheticItems.SyntheticKindSelector;
 import com.google.common.base.Suppliers;
@@ -60,7 +60,7 @@
           .createFixedClass(syntheticKindSelector, context, appView, builder -> {});
     }
 
-    public SyntheticArgumentClass build(Collection<MergeGroup> mergeGroups) {
+    public SyntheticArgumentClass build(Collection<HorizontalMergeGroup> mergeGroups) {
       return build(getDeterministicContext(mergeGroups));
     }
 
@@ -87,9 +87,10 @@
       return new SyntheticArgumentClass(syntheticArgumentTypes);
     }
 
-    private static DexProgramClass getDeterministicContext(Collection<MergeGroup> mergeGroups) {
+    private static DexProgramClass getDeterministicContext(
+        Collection<HorizontalMergeGroup> mergeGroups) {
       // Relies on the determinism of the merge groups.
-      MergeGroup mergeGroup = mergeGroups.iterator().next();
+      HorizontalMergeGroup mergeGroup = mergeGroups.iterator().next();
       assert mergeGroup.hasTarget();
       return mergeGroup.getTarget();
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 532abbd..a9ebda5 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -1107,7 +1107,7 @@
                     AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
                     MethodAccessInfoCollection methodAccessInfoCollection =
                         appViewWithLiveness.appInfo().getMethodAccessInfoCollection();
-                    methodAccessInfoCollection.modifier().commit(appViewWithLiveness);
+                    assert methodAccessInfoCollection.verify(appViewWithLiveness);
                   }
                   return true;
                 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 14c4975..ea46ea8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -298,6 +298,10 @@
     return Iterables.filter(virtualMethods(), predicate::test);
   }
 
+  public Iterable<DexClassAndMethod> virtualClassMethods() {
+    return Iterables.transform(virtualMethods(), method -> DexClassAndMethod.create(this, method));
+  }
+
   public void addVirtualMethod(DexEncodedMethod method) {
     methodCollection.addVirtualMethod(method);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 39c4168..1c568cd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -675,6 +675,7 @@
   public final AssertionErrorMethods assertionErrorMethods = new AssertionErrorMethods();
   public final ClassMethods classMethods = new ClassMethods();
   public final ConstructorMethods constructorMethods = new ConstructorMethods();
+  public final MethodMethods methodMethods = new MethodMethods();
   public final EnumMembers enumMembers = new EnumMembers();
   public final AndroidUtilLogMembers androidUtilLogMembers = new AndroidUtilLogMembers();
   public final JavaLangReflectArrayMembers javaLangReflectArrayMembers =
@@ -1946,6 +1947,15 @@
     }
   }
 
+  public class MethodMethods {
+
+    public final DexMethod invoke =
+        createMethod(
+            methodType, createProto(objectType, objectType, objectArrayType), invokeMethodName);
+
+    private MethodMethods() {}
+  }
+
   public class AndroidUtilLogMembers {
 
     public final DexMethod i =
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 95be1fa..4ec188f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -30,6 +30,10 @@
     return identical(this, other);
   }
 
+  public final boolean isNotIdenticalTo(DexString other) {
+    return !isIdenticalTo(other);
+  }
+
   public static final DexString[] EMPTY_ARRAY = {};
   private static final int ARRAY_CHARACTER = '[';
 
diff --git a/src/main/java/com/android/tools/r8/graph/ImmediateProgramSubtypingInfo.java b/src/main/java/com/android/tools/r8/graph/ImmediateProgramSubtypingInfo.java
index 8b9a38b..f7be3cf 100644
--- a/src/main/java/com/android/tools/r8/graph/ImmediateProgramSubtypingInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/ImmediateProgramSubtypingInfo.java
@@ -126,4 +126,8 @@
   public List<DexProgramClass> getSubclasses(DexProgramClass clazz) {
     return immediateSubtypes.getOrDefault(clazz, Collections.emptyList());
   }
+
+  public boolean hasSubclasses(DexProgramClass clazz) {
+    return !getSubclasses(clazz).isEmpty();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
index 542a558..3b8681b 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
@@ -212,6 +212,34 @@
             });
   }
 
+  public boolean verify(AppView<AppInfoWithLiveness> appView) {
+    assert verifyNoNonResolving(appView);
+    return true;
+  }
+
+  public boolean verifyNoNonResolving(AppView<AppInfoWithLiveness> appView) {
+    verifyNoNonResolving(appView, directInvokes);
+    verifyNoNonResolving(appView, interfaceInvokes);
+    verifyNoNonResolving(appView, staticInvokes);
+    verifyNoNonResolving(appView, superInvokes);
+    verifyNoNonResolving(appView, virtualInvokes);
+    return true;
+  }
+
+  private void verifyNoNonResolving(
+      AppView<AppInfoWithLiveness> appView, Map<DexMethod, ?> invokes) {
+    if (!isThrowingMap(invokes)) {
+      for (DexMethod method : invokes.keySet()) {
+        MethodResolutionResult result =
+            appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(method);
+        assert !result.isFailedResolution()
+            : "Unexpected method that does not resolve: " + method.toSourceString();
+        assert !result.isSignaturePolymorphicResolution(method, appView.dexItemFactory())
+            : "Unexpected signature polymorphic resolution: " + method.toSourceString();
+      }
+    }
+  }
+
   public abstract static class Builder<T extends Map<DexMethod, ProgramMethodSet>> {
 
     private final T directInvokes;
@@ -374,29 +402,6 @@
           });
     }
 
-    public boolean verifyNoNonResolving(AppView<AppInfoWithLiveness> appView) {
-      verifyNoNonResolving(appView, directInvokes);
-      verifyNoNonResolving(appView, interfaceInvokes);
-      verifyNoNonResolving(appView, staticInvokes);
-      verifyNoNonResolving(appView, superInvokes);
-      verifyNoNonResolving(appView, virtualInvokes);
-      return true;
-    }
-
-    private void verifyNoNonResolving(
-        AppView<AppInfoWithLiveness> appView, Map<DexMethod, ?> invokes) {
-      if (!isThrowingMap(invokes)) {
-        for (DexMethod method : invokes.keySet()) {
-          MethodResolutionResult result =
-              appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(method);
-          assert !result.isFailedResolution()
-              : "Unexpected method that does not resolve: " + method.toSourceString();
-          assert !result.isSignaturePolymorphicResolution(method, appView.dexItemFactory())
-              : "Unexpected signature polymorphic resolution: " + method.toSourceString();
-        }
-      }
-    }
-
     public MethodAccessInfoCollection build() {
       return new MethodAccessInfoCollection(
           directInvokes, interfaceInvokes, staticInvokes, superInvokes, virtualInvokes);
@@ -437,9 +442,5 @@
       collection.forEachSuperInvoke(this::registerInvokeSuperInContexts);
       collection.forEachVirtualInvoke(this::registerInvokeVirtualInContexts);
     }
-
-    public void commit(AppView<AppInfoWithLiveness> appView) {
-      assert verifyNoNonResolving(appView);
-    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
index a897bd0..77fef31 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
@@ -7,8 +7,11 @@
 import com.android.tools.r8.graph.proto.ArgumentInfo;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintFactory;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
 import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
@@ -157,6 +160,36 @@
     return fixupArgumentInfo(arguments);
   }
 
+  @Override
+  public DynamicType fixupDynamicType(DynamicType dynamicType) {
+    if (!prototypeChanges.hasRewrittenReturnInfo()) {
+      return dynamicType;
+    }
+    RewrittenTypeInfo rewrittenReturnInfo = prototypeChanges.getRewrittenReturnInfo();
+    if (rewrittenReturnInfo.getNewType().isPrimitiveType()
+        || rewrittenReturnInfo.getNewType().isVoidType()) {
+      return DynamicType.unknown();
+    }
+    return dynamicType;
+  }
+
+  @Override
+  public AbstractValue fixupAbstractReturnValue(
+      AppView<AppInfoWithLiveness> appView, AbstractValue returnValue) {
+    if (!prototypeChanges.hasRewrittenReturnInfo()) {
+      return returnValue;
+    }
+    RewrittenTypeInfo rewrittenReturnInfo = prototypeChanges.getRewrittenReturnInfo();
+    if (rewrittenReturnInfo.getNewType().isPrimitiveType()) {
+      // This covers for number unboxing, however, enum unboxing should never have a single
+      // boxed primitive as return value.
+      if (returnValue.isSingleBoxedPrimitive()) {
+        return returnValue.asSingleBoxedPrimitive().toPrimitive(appView.abstractValueFactory());
+      }
+    }
+    return returnValue;
+  }
+
   private BitSet fixupArgumentInfo(BitSet bitSet) {
     if (getArgumentInfoCollection().isEmpty() || bitSet == null) {
       return bitSet;
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java
index b7175d3..5a7c463 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java
@@ -10,16 +10,10 @@
 
   public static Deque<NonIdentityGraphLens> extractNonIdentityLenses(GraphLens lens) {
     Deque<NonIdentityGraphLens> lenses = new ArrayDeque<>();
-    if (lens.isNonIdentityLens()) {
-      lenses.addFirst(lens.asNonIdentityLens());
-      while (true) {
-        GraphLens previous = lenses.getFirst().getPrevious();
-        if (previous.isNonIdentityLens()) {
-          lenses.addFirst(previous.asNonIdentityLens());
-        } else {
-          break;
-        }
-      }
+    while (lens.isNonIdentityLens()) {
+      NonIdentityGraphLens nonIdentityLens = lens.asNonIdentityLens();
+      lenses.addFirst(nonIdentityLens);
+      lens = nonIdentityLens.getPrevious();
     }
     return lenses;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index 8edf836..778132e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -33,7 +33,9 @@
   DexEncodedField[] merge();
 
   static ClassInstanceFieldsMerger create(
-      AppView<?> appView, HorizontalClassMergerGraphLens.Builder lensBuilder, MergeGroup group) {
+      AppView<?> appView,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      HorizontalMergeGroup group) {
     if (appView.hasClassHierarchy()) {
       return new ClassInstanceFieldsMergerImpl(appView.withClassHierarchy(), lensBuilder, group);
     } else {
@@ -137,7 +139,7 @@
   class ClassInstanceFieldsMergerImpl implements ClassInstanceFieldsMerger {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
-    private final MergeGroup group;
+    private final HorizontalMergeGroup group;
 
     @SuppressWarnings("BadImport")
     private final Builder lensBuilder;
@@ -149,7 +151,7 @@
     private ClassInstanceFieldsMergerImpl(
         AppView<? extends AppInfoWithClassHierarchy> appView,
         HorizontalClassMergerGraphLens.Builder lensBuilder,
-        MergeGroup group) {
+        HorizontalMergeGroup group) {
       this.appView = appView;
       this.group = group;
       this.lensBuilder = lensBuilder;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index ecbf167..2097c3a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -60,7 +60,7 @@
 
   private final AppView<?> appView;
   private final Mode mode;
-  private final MergeGroup group;
+  private final HorizontalMergeGroup group;
   private final DexItemFactory dexItemFactory;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
 
@@ -81,7 +81,7 @@
       IRCodeProvider codeProvider,
       Mode mode,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers) {
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
@@ -376,9 +376,10 @@
     private final AppView<?> appView;
     private final IRCodeProvider codeProvider;
     private final Mode mode;
-    private final MergeGroup group;
+    private final HorizontalMergeGroup group;
 
-    public Builder(AppView<?> appView, IRCodeProvider codeProvider, MergeGroup group, Mode mode) {
+    public Builder(
+        AppView<?> appView, IRCodeProvider codeProvider, HorizontalMergeGroup group, Mode mode) {
       this.appView = appView;
       this.codeProvider = codeProvider;
       this.group = group;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
index 81ec45e..dd6ddf2 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
@@ -17,7 +17,7 @@
 
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
-  private final MergeGroup group;
+  private final HorizontalMergeGroup group;
 
   @SuppressWarnings("BadImport")
   private final Builder lensBuilder;
@@ -25,7 +25,9 @@
   private final Map<DexField, DexEncodedField> targetFields = new LinkedHashMap<>();
 
   public ClassStaticFieldsMerger(
-      AppView<?> appView, HorizontalClassMergerGraphLens.Builder lensBuilder, MergeGroup group) {
+      AppView<?> appView,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      HorizontalMergeGroup group) {
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
     this.group = group;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 57ff66a..d7c99ba 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
 
+import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -126,8 +127,9 @@
     // Run the policies on all program classes to produce a final grouping.
     List<Policy> policies =
         PolicyScheduler.getPolicies(appView, codeProvider, mode, runtimeTypeCheckInfo);
-    Collection<MergeGroup> groups =
-        new PolicyExecutor().run(getInitialGroups(), policies, executorService, timing);
+    Collection<HorizontalMergeGroup> groups =
+        new HorizontalClassMergerPolicyExecutor()
+            .run(getInitialGroups(), policies, executorService, timing);
 
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
@@ -186,7 +188,7 @@
     if (mode.isInitial()) {
       fieldAccessInfoCollectionModifier = createFieldAccessInfoCollectionModifier(groups);
     } else {
-      assert groups.stream().noneMatch(MergeGroup::hasClassIdField);
+      assert groups.stream().noneMatch(HorizontalMergeGroup::hasClassIdField);
     }
 
     // Set the new graph lens before finalizing any synthetic code.
@@ -271,11 +273,11 @@
   }
 
   private FieldAccessInfoCollectionModifier createFieldAccessInfoCollectionModifier(
-      Collection<MergeGroup> groups) {
+      Collection<HorizontalMergeGroup> groups) {
     assert mode.isInitial();
     FieldAccessInfoCollectionModifier.Builder builder =
         new FieldAccessInfoCollectionModifier.Builder();
-    for (MergeGroup group : groups) {
+    for (HorizontalMergeGroup group : groups) {
       if (group.hasClassIdField()) {
         DexProgramClass target = group.getTarget();
         target.forEachProgramInstanceInitializerMatching(
@@ -291,7 +293,7 @@
   }
 
   private void transformIncompleteCode(
-      Collection<MergeGroup> groups,
+      Collection<HorizontalMergeGroup> groups,
       HorizontalClassMergerGraphLens horizontalClassMergerGraphLens,
       ExecutorService executorService)
       throws ExecutionException {
@@ -321,7 +323,8 @@
   }
 
   private boolean verifyNoIncompleteCode(
-      Collection<MergeGroup> groups, ExecutorService executorService) throws ExecutionException {
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService)
+      throws ExecutionException {
     ThreadUtils.processItems(
         groups,
         group -> {
@@ -356,9 +359,9 @@
 
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
-  private List<MergeGroup> getInitialGroups() {
-    MergeGroup initialClassGroup = new MergeGroup();
-    MergeGroup initialInterfaceGroup = new MergeGroup();
+  private List<HorizontalMergeGroup> getInitialGroups() {
+    HorizontalMergeGroup initialClassGroup = new HorizontalMergeGroup();
+    HorizontalMergeGroup initialInterfaceGroup = new HorizontalMergeGroup();
     for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
       if (clazz.isInterface()) {
         initialInterfaceGroup.add(clazz);
@@ -366,10 +369,10 @@
         initialClassGroup.add(clazz);
       }
     }
-    List<MergeGroup> initialGroups = new LinkedList<>();
+    List<HorizontalMergeGroup> initialGroups = new LinkedList<>();
     initialGroups.add(initialClassGroup);
     initialGroups.add(initialInterfaceGroup);
-    initialGroups.removeIf(MergeGroup::isTrivial);
+    initialGroups.removeIf(HorizontalMergeGroup::isTrivial);
     return initialGroups;
   }
 
@@ -380,9 +383,9 @@
   private List<ClassMerger> initializeClassMergers(
       IRCodeProvider codeProvider,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Collection<MergeGroup> groups) {
+      Collection<HorizontalMergeGroup> groups) {
     List<ClassMerger> classMergers = new ArrayList<>(groups.size());
-    for (MergeGroup group : groups) {
+    for (HorizontalMergeGroup group : groups) {
       assert group.isNonTrivial();
       assert group.hasInstanceFieldMap();
       assert group.hasTarget();
@@ -438,8 +441,8 @@
 
   @SuppressWarnings("ReferenceEquality")
   private static boolean verifyNoCyclesInInterfaceHierarchies(
-      AppView<?> appView, Collection<MergeGroup> groups) {
-    for (MergeGroup group : groups) {
+      AppView<?> appView, Collection<HorizontalMergeGroup> groups) {
+    for (HorizontalMergeGroup group : groups) {
       if (group.isClassGroup()) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
new file mode 100644
index 0000000..be9f787
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.horizontalclassmerging;
+
+import com.android.tools.r8.classmerging.Policy;
+import com.android.tools.r8.classmerging.PolicyExecutor;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class HorizontalClassMergerPolicyExecutor extends PolicyExecutor<HorizontalMergeGroup> {
+
+  @Override
+  protected LinkedList<HorizontalMergeGroup> apply(
+      Policy policy, LinkedList<HorizontalMergeGroup> linkedGroups, ExecutorService executorService)
+      throws ExecutionException {
+    if (policy.isSingleClassPolicy()) {
+      applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
+    } else {
+      if (policy.isMultiClassPolicy()) {
+        linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
+      } else {
+        assert policy.isMultiClassPolicyWithPreprocessing();
+        linkedGroups =
+            applyMultiClassPolicyWithPreprocessing(
+                policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
+      }
+    }
+    return linkedGroups;
+  }
+
+  void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<HorizontalMergeGroup> groups) {
+    Iterator<HorizontalMergeGroup> i = groups.iterator();
+    while (i.hasNext()) {
+      HorizontalMergeGroup group = i.next();
+      boolean isInterfaceGroup = group.isInterfaceGroup();
+      int previousGroupSize = group.size();
+      group.removeIf(clazz -> !policy.canMerge(clazz));
+      assert policy.recordRemovedClassesForDebugging(
+          isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
+      if (group.isTrivial()) {
+        i.remove();
+      }
+    }
+  }
+
+  // TODO(b/270398965): Replace LinkedList.
+  @SuppressWarnings("JdkObsolete")
+  private LinkedList<HorizontalMergeGroup> applyMultiClassPolicy(
+      MultiClassPolicy policy, LinkedList<HorizontalMergeGroup> groups) {
+    // For each group apply the multi class policy and add all the new groups together.
+    LinkedList<HorizontalMergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
+          Collection<HorizontalMergeGroup> policyGroups = policy.apply(group);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
+  }
+
+  // TODO(b/270398965): Replace LinkedList.
+  @SuppressWarnings("JdkObsolete")
+  private <T> LinkedList<HorizontalMergeGroup> applyMultiClassPolicyWithPreprocessing(
+      MultiClassPolicyWithPreprocessing<T> policy,
+      LinkedList<HorizontalMergeGroup> groups,
+      ExecutorService executorService)
+      throws ExecutionException {
+    // For each group apply the multi class policy and add all the new groups together.
+    T data = policy.preprocess(groups, executorService);
+    LinkedList<HorizontalMergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
+          Collection<HorizontalMergeGroup> policyGroups = policy.apply(group, data);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
similarity index 94%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
index 8062a16..9210ef9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
@@ -6,6 +6,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.MergeGroup;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -26,7 +27,7 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
-public class MergeGroup implements Collection<DexProgramClass> {
+public class HorizontalMergeGroup extends MergeGroup implements Collection<DexProgramClass> {
 
   public static class Metadata {}
 
@@ -42,21 +43,21 @@
 
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
-  public MergeGroup() {
+  public HorizontalMergeGroup() {
     this.classes = new LinkedList<>();
   }
 
-  public MergeGroup(DexProgramClass clazz) {
+  public HorizontalMergeGroup(DexProgramClass clazz) {
     this();
     add(clazz);
   }
 
-  public MergeGroup(Iterable<DexProgramClass> classes) {
+  public HorizontalMergeGroup(Iterable<DexProgramClass> classes) {
     this();
     Iterables.addAll(this.classes, classes);
   }
 
-  public void applyMetadataFrom(MergeGroup group) {
+  public void applyMetadataFrom(HorizontalMergeGroup group) {
     if (metadata == null) {
       metadata = group.metadata;
     }
@@ -67,7 +68,7 @@
     return classes.add(clazz);
   }
 
-  public boolean add(MergeGroup group) {
+  public boolean add(HorizontalMergeGroup group) {
     return classes.addAll(group.getClasses());
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index f832f3a..197f2b8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -112,11 +112,11 @@
       mergedClasses.put(source, target);
     }
 
-    void addMergeGroup(MergeGroup group) {
+    void addMergeGroup(HorizontalMergeGroup group) {
       group.forEachSource(clazz -> add(clazz.getType(), group.getTarget().getType()));
     }
 
-    Builder addMergeGroups(Iterable<MergeGroup> groups) {
+    Builder addMergeGroups(Iterable<HorizontalMergeGroup> groups) {
       groups.forEach(this::addMergeGroup);
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
index 67022ed..f49af82 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -40,7 +40,7 @@
   public static InstanceInitializerDescription analyze(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       IRCodeProvider codeProvider,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       InstanceInitializer instanceInitializer) {
     if (instanceInitializer.isAbsent()) {
       InstanceInitializerDescription.Builder builder =
@@ -69,7 +69,7 @@
   public static InstanceInitializerDescription analyze(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       IRCodeProvider codeProvider,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       ProgramMethod instanceInitializer) {
     InstanceInitializerDescription.Builder builder =
         InstanceInitializerDescription.builder(appView, instanceInitializer);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
index 2360c36..dc5f15e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
@@ -74,7 +74,7 @@
    */
   public IncompleteMergedInstanceInitializerCode createCfCode(
       DexMethod originalMethodReference,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       boolean hasClassId,
       int extraNulls) {
     return new IncompleteMergedInstanceInitializerCode(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
index 54f4898..b522877 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -45,7 +45,7 @@
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final Reference2IntMap<DexType> classIdentifiers;
   private final DexItemFactory dexItemFactory;
-  private final MergeGroup group;
+  private final HorizontalMergeGroup group;
   private final List<ProgramMethod> instanceInitializers;
   private final InstanceInitializerDescription instanceInitializerDescription;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
@@ -54,7 +54,7 @@
   InstanceInitializerMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       Reference2IntMap<DexType> classIdentifiers,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       List<ProgramMethod> instanceInitializers,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode) {
@@ -64,7 +64,7 @@
   InstanceInitializerMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       Reference2IntMap<DexType> classIdentifiers,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       List<ProgramMethod> instanceInitializers,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode,
@@ -214,7 +214,7 @@
       return this;
     }
 
-    public List<InstanceInitializerMerger> build(MergeGroup group) {
+    public List<InstanceInitializerMerger> build(HorizontalMergeGroup group) {
       assert instanceInitializerGroups.stream().noneMatch(List::isEmpty);
       return ListUtils.map(
           instanceInitializerGroups,
@@ -224,7 +224,7 @@
     }
 
     public InstanceInitializerMerger buildSingle(
-        MergeGroup group, InstanceInitializerDescription instanceInitializerDescription) {
+        HorizontalMergeGroup group, InstanceInitializerDescription instanceInitializerDescription) {
       assert instanceInitializerGroups.stream().noneMatch(List::isEmpty);
       assert instanceInitializerGroups.size() == 1;
       List<ProgramMethod> instanceInitializers = ListUtils.first(instanceInitializerGroups);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
index 50a0f23..62d7522 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
@@ -42,7 +42,7 @@
       AppView<?> appView,
       Reference2IntMap<DexType> classIdentifiers,
       IRCodeProvider codeProvider,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode) {
     if (!appView.hasClassHierarchy()) {
@@ -130,7 +130,7 @@
         instanceInitializerMergers, equivalentInstanceInitializerMergers);
   }
 
-  private static boolean verifyNoInstanceInitializers(MergeGroup group) {
+  private static boolean verifyNoInstanceInitializers(HorizontalMergeGroup group) {
     group.forEach(
         clazz -> {
           assert !clazz.programInstanceInitializers().iterator().hasNext();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
index 63420fe..8bad6ec 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.Policy;
 import java.util.Collection;
 
 public abstract class MultiClassPolicy extends Policy {
@@ -16,7 +17,7 @@
    *     merged. If the policy detects no issues then `group` will be returned unchanged. If classes
    *     cannot be merged with any other classes they are returned as singleton lists.
    */
-  public abstract Collection<MergeGroup> apply(MergeGroup group);
+  public abstract Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group);
 
   @Override
   public boolean isMultiClassPolicy() {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java
index d634479..416164b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.Policy;
 import java.util.Collection;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -19,9 +20,10 @@
    *     merged. If the policy detects no issues then `group` will be returned unchanged. If classes
    *     cannot be merged with any other classes they are returned as singleton lists.
    */
-  public abstract Collection<MergeGroup> apply(MergeGroup group, T data);
+  public abstract Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group, T data);
 
-  public abstract T preprocess(Collection<MergeGroup> groups, ExecutorService executorService)
+  public abstract T preprocess(
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService)
       throws ExecutionException;
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
index aa3ca62..40399c0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
@@ -12,12 +12,12 @@
 public abstract class MultiClassSameReferencePolicy<T> extends MultiClassPolicy {
 
   @Override
-  public final Collection<MergeGroup> apply(MergeGroup group) {
-    Map<T, MergeGroup> groups = new LinkedHashMap<>();
+  public final Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
+    Map<T, HorizontalMergeGroup> groups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       T mergeKey = getMergeKey(clazz);
       if (mergeKey != null) {
-        groups.computeIfAbsent(mergeKey, ignore -> new MergeGroup()).add(clazz);
+        groups.computeIfAbsent(mergeKey, ignore -> new HorizontalMergeGroup()).add(clazz);
       }
     }
     removeTrivialGroups(groups.values());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
deleted file mode 100644
index 5bff09e..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-/**
- * The super class of all horizontal class merging policies. Most classes will either implement
- * {@link SingleClassPolicy} or {@link MultiClassPolicy}.
- */
-public abstract class Policy {
-
-  /** Counter keeping track of how many classes this policy has removed. For debugging only. */
-  public int numberOfRemovedClasses;
-
-  public int numberOfRemovedInterfaces;
-
-  public void clear() {}
-
-  public abstract String getName();
-
-  public boolean isIdentityForInterfaceGroups() {
-    return false;
-  }
-
-  public boolean isSingleClassPolicy() {
-    return false;
-  }
-
-  public SingleClassPolicy asSingleClassPolicy() {
-    return null;
-  }
-
-  public boolean isMultiClassPolicy() {
-    return false;
-  }
-
-  public MultiClassPolicy asMultiClassPolicy() {
-    return null;
-  }
-
-  public boolean isMultiClassPolicyWithPreprocessing() {
-    return false;
-  }
-
-  public MultiClassPolicyWithPreprocessing<?> asMultiClassPolicyWithPreprocessing() {
-    return null;
-  }
-
-  public boolean shouldSkipPolicy() {
-    return false;
-  }
-
-  /**
-   * Remove all groups containing no or only a single class, as there is no point in merging these.
-   */
-  protected Collection<MergeGroup> removeTrivialGroups(Collection<MergeGroup> groups) {
-    assert !(groups instanceof ArrayList);
-    groups.removeIf(MergeGroup::isTrivial);
-    return groups;
-  }
-
-  boolean recordRemovedClassesForDebugging(
-      boolean isInterfaceGroup, int previousGroupSize, Collection<MergeGroup> newGroups) {
-    assert previousGroupSize >= 2;
-    int previousNumberOfRemovedClasses = previousGroupSize - 1;
-    int newNumberOfRemovedClasses = 0;
-    for (MergeGroup newGroup : newGroups) {
-      if (newGroup.isNonTrivial()) {
-        newNumberOfRemovedClasses += newGroup.size() - 1;
-      }
-    }
-    assert previousNumberOfRemovedClasses >= newNumberOfRemovedClasses;
-    int change = previousNumberOfRemovedClasses - newNumberOfRemovedClasses;
-    if (isInterfaceGroup) {
-      numberOfRemovedInterfaces += change;
-    } else {
-      numberOfRemovedClasses += change;
-    }
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
deleted file mode 100644
index 28ec89d..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging;
-
-import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-/**
- * This is a simple policy executor that ensures regular sequential execution of policies. It should
- * primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
- * against which more efficient policy executors can be compared.
- */
-public class PolicyExecutor {
-
-  private void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
-    Iterator<MergeGroup> i = groups.iterator();
-    while (i.hasNext()) {
-      MergeGroup group = i.next();
-      boolean isInterfaceGroup = group.isInterfaceGroup();
-      int previousGroupSize = group.size();
-      group.removeIf(clazz -> !policy.canMerge(clazz));
-      assert policy.recordRemovedClassesForDebugging(
-          isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
-      if (group.isTrivial()) {
-        i.remove();
-      }
-    }
-  }
-
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private LinkedList<MergeGroup> applyMultiClassPolicy(
-      MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
-    // For each group apply the multi class policy and add all the new groups together.
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
-    groups.forEach(
-        group -> {
-          boolean isInterfaceGroup = group.isInterfaceGroup();
-          int previousGroupSize = group.size();
-          Collection<MergeGroup> policyGroups = policy.apply(group);
-          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
-          assert policy.recordRemovedClassesForDebugging(
-              isInterfaceGroup, previousGroupSize, policyGroups);
-          newGroups.addAll(policyGroups);
-        });
-    return newGroups;
-  }
-
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
-      MultiClassPolicyWithPreprocessing<T> policy,
-      LinkedList<MergeGroup> groups,
-      ExecutorService executorService)
-      throws ExecutionException {
-    // For each group apply the multi class policy and add all the new groups together.
-    T data = policy.preprocess(groups, executorService);
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
-    groups.forEach(
-        group -> {
-          boolean isInterfaceGroup = group.isInterfaceGroup();
-          int previousGroupSize = group.size();
-          Collection<MergeGroup> policyGroups = policy.apply(group, data);
-          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
-          assert policy.recordRemovedClassesForDebugging(
-              isInterfaceGroup, previousGroupSize, policyGroups);
-          newGroups.addAll(policyGroups);
-        });
-    return newGroups;
-  }
-
-  /**
-   * Given an initial collection of class groups which can potentially be merged, run all of the
-   * policies registered to this policy executor on the class groups yielding a new collection of
-   * class groups.
-   */
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  public Collection<MergeGroup> run(
-      Collection<MergeGroup> inputGroups,
-      Collection<Policy> policies,
-      ExecutorService executorService,
-      Timing timing)
-      throws ExecutionException {
-    LinkedList<MergeGroup> linkedGroups;
-
-    if (inputGroups instanceof LinkedList) {
-      linkedGroups = (LinkedList<MergeGroup>) inputGroups;
-    } else {
-      linkedGroups = new LinkedList<>(inputGroups);
-    }
-
-    for (Policy policy : policies) {
-      if (policy.shouldSkipPolicy()) {
-        continue;
-      }
-
-      timing.begin(policy.getName());
-      if (policy.isSingleClassPolicy()) {
-        applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
-      } else if (policy.isMultiClassPolicy()) {
-        linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
-      } else {
-        assert policy.isMultiClassPolicyWithPreprocessing();
-        linkedGroups =
-            applyMultiClassPolicyWithPreprocessing(
-                policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
-      }
-      timing.end();
-
-      policy.clear();
-
-      if (linkedGroups.isEmpty()) {
-        break;
-      }
-
-      // Any policy should not return any trivial groups.
-      assert linkedGroups.stream().allMatch(group -> group.size() >= 2);
-    }
-
-    return linkedGroups;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index da96bcf..fa18dbb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
index db2d8eb..7c9aedb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.graph.DexProgramClass;
 
 public abstract class SingleClassPolicy extends Policy {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 5b1ee78..6175c5a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -49,13 +49,13 @@
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final DexItemFactory dexItemFactory;
-  private final MergeGroup group;
+  private final HorizontalMergeGroup group;
   private final List<ProgramMethod> methods;
   private final SuperMethodReference superMethod;
 
   public VirtualMethodMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       List<ProgramMethod> methods,
       SuperMethodReference superMethod) {
     this.appView = appView;
@@ -75,7 +75,7 @@
 
     /** Get the super method handle if this method overrides a parent method. */
     private SuperMethodReference superMethod(
-        AppView<? extends AppInfoWithClassHierarchy> appView, MergeGroup group) {
+        AppView<? extends AppInfoWithClassHierarchy> appView, HorizontalMergeGroup group) {
       DexMethod template = methods.iterator().next().getReference();
       SingleResolutionResult<?> resolutionResult =
           appView
@@ -98,7 +98,7 @@
     }
 
     public VirtualMethodMerger build(
-        AppView<? extends AppInfoWithClassHierarchy> appView, MergeGroup group) {
+        AppView<? extends AppInfoWithClassHierarchy> appView, HorizontalMergeGroup group) {
       // If not all the classes are in the merge group, find the fallback super method to call.
       SuperMethodReference superMethod =
           methods.size() < group.size() ? superMethod(appView, group) : null;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index a0b8b9b..19efa91 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -24,7 +24,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.IRMetadata;
@@ -59,7 +59,7 @@
     this.classInitializers = classInitializers;
   }
 
-  public static ClassInitializerMerger create(MergeGroup group) {
+  public static ClassInitializerMerger create(HorizontalMergeGroup group) {
     ClassInitializerMerger.Builder builder = new ClassInitializerMerger.Builder();
     group.forEach(
         clazz -> {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java
index 17bb034..82cc1b5 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java
@@ -7,7 +7,7 @@
 import static com.android.tools.r8.utils.IteratorUtils.createCircularIterator;
 
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
@@ -20,12 +20,12 @@
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     // Create a new merge group for each class that we want at most one of.
-    List<MergeGroup> newGroups = new LinkedList<>();
+    List<HorizontalMergeGroup> newGroups = new LinkedList<>();
     for (DexProgramClass clazz : group) {
       if (atMostOneOf(clazz)) {
-        newGroups.add(new MergeGroup(clazz));
+        newGroups.add(new HorizontalMergeGroup(clazz));
       }
     }
 
@@ -36,7 +36,7 @@
     }
 
     // Otherwise, fill up the new merge groups with the remaining classes.
-    Iterator<MergeGroup> newGroupsIterator = createCircularIterator(newGroups);
+    Iterator<HorizontalMergeGroup> newGroupsIterator = createCircularIterator(newGroups);
     for (DexProgramClass clazz : group) {
       if (!atMostOneOf(clazz)) {
         newGroupsIterator.next().add(clazz);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
index b9c4c3c..16e17f0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
@@ -34,7 +34,7 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     if (appView.enableWholeProgramOptimizations()) {
       if (mode.isInitial() || group.isInterfaceGroup()) {
         group.selectTarget(appView);
@@ -63,7 +63,7 @@
     return true;
   }
 
-  private boolean verifyAlreadyFinalized(MergeGroup group) {
+  private boolean verifyAlreadyFinalized(HorizontalMergeGroup group) {
     assert group.hasTarget();
     assert group.getClasses().contains(group.getTarget());
     assert group.hasInstanceFieldMap();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
index d271464..728cf5e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
@@ -6,7 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import java.util.Collection;
 import java.util.Collections;
@@ -27,13 +27,13 @@
   // TODO(b/270398965): Replace LinkedList.
   @Override
   @SuppressWarnings({"JdkObsolete", "MixedMutabilityReturnType"})
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     if (group.size() <= maxGroupSize || group.isInterfaceGroup()) {
       return Collections.singletonList(group);
     }
 
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
-    MergeGroup newGroup = createNewGroup(newGroups);
+    LinkedList<HorizontalMergeGroup> newGroups = new LinkedList<>();
+    HorizontalMergeGroup newGroup = createNewGroup(newGroups);
     for (DexProgramClass clazz : group) {
       if (newGroup.size() == maxGroupSize) {
         newGroup = createNewGroup(newGroups);
@@ -42,7 +42,7 @@
     }
     if (newGroup.size() == 1) {
       if (maxGroupSize == 2) {
-        MergeGroup removedGroup = newGroups.removeLast();
+        HorizontalMergeGroup removedGroup = newGroups.removeLast();
         assert removedGroup == newGroup;
       } else {
         newGroup.add(newGroups.getFirst().removeLast());
@@ -51,8 +51,8 @@
     return newGroups;
   }
 
-  private MergeGroup createNewGroup(LinkedList<MergeGroup> newGroups) {
-    MergeGroup newGroup = new MergeGroup();
+  private HorizontalMergeGroup createNewGroup(LinkedList<HorizontalMergeGroup> newGroups) {
+    HorizontalMergeGroup newGroup = new HorizontalMergeGroup();
     newGroups.add(newGroup);
     return newGroup;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitInterfaceGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitInterfaceGroups.java
index adee1f4..414003a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitInterfaceGroups.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitInterfaceGroups.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import java.util.Collection;
 import java.util.Collections;
@@ -25,24 +25,24 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     if (group.isClassGroup()) {
       return Collections.singletonList(group);
     }
     // Mapping from new merge groups to their size.
-    Map<MergeGroup, Integer> newGroups = new LinkedHashMap<>();
+    Map<HorizontalMergeGroup, Integer> newGroups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       processClass(clazz, newGroups);
     }
     return removeTrivialGroups(newGroups.keySet());
   }
 
-  private void processClass(DexProgramClass clazz, Map<MergeGroup, Integer> newGroups) {
+  private void processClass(DexProgramClass clazz, Map<HorizontalMergeGroup, Integer> newGroups) {
     int increment = clazz.getMethodCollection().size();
 
     // Find an existing group.
-    for (Entry<MergeGroup, Integer> entry : newGroups.entrySet()) {
-      MergeGroup candidateGroup = entry.getKey();
+    for (Entry<HorizontalMergeGroup, Integer> entry : newGroups.entrySet()) {
+      HorizontalMergeGroup candidateGroup = entry.getKey();
       int candidateGroupSize = entry.getValue();
       int newCandidateGroupSize = candidateGroupSize + increment;
       if (newCandidateGroupSize <= maxGroupSize) {
@@ -53,7 +53,7 @@
     }
 
     // Failed to find an existing group.
-    newGroups.put(new MergeGroup(clazz), increment);
+    newGroups.put(new HorizontalMergeGroup(clazz), increment);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
index 0242766..bfd431f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Multiset;
@@ -20,13 +20,13 @@
 public class MinimizeInstanceFieldCasts extends MultiClassPolicy {
 
   @Override
-  public final Collection<MergeGroup> apply(MergeGroup group) {
+  public final Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     // First find all classes that can be merged without changing field types.
-    Map<Multiset<DexType>, MergeGroup> newGroups = new LinkedHashMap<>();
+    Map<Multiset<DexType>, HorizontalMergeGroup> newGroups = new LinkedHashMap<>();
     group.forEach(clazz -> addExact(clazz, newGroups));
 
     // Create a single group from all trivial groups.
-    MergeGroup pendingGroup = new MergeGroup();
+    HorizontalMergeGroup pendingGroup = new HorizontalMergeGroup();
     newGroups
         .values()
         .removeIf(
@@ -43,13 +43,14 @@
     }
 
     if (!pendingGroup.isTrivial()) {
-      List<MergeGroup> newGroupsIncludingRelaxedGroup = new ArrayList<>(newGroups.values());
+      List<HorizontalMergeGroup> newGroupsIncludingRelaxedGroup =
+          new ArrayList<>(newGroups.values());
       newGroupsIncludingRelaxedGroup.add(pendingGroup);
       return newGroupsIncludingRelaxedGroup;
     }
 
-    MergeGroup smallestNewGroup = null;
-    for (MergeGroup newGroup : newGroups.values()) {
+    HorizontalMergeGroup smallestNewGroup = null;
+    for (HorizontalMergeGroup newGroup : newGroups.values()) {
       if (smallestNewGroup == null || newGroup.size() < smallestNewGroup.size()) {
         smallestNewGroup = newGroup;
       }
@@ -59,8 +60,11 @@
     return newGroups.values();
   }
 
-  private void addExact(DexProgramClass clazz, Map<Multiset<DexType>, MergeGroup> groups) {
-    groups.computeIfAbsent(getExactMergeKey(clazz), ignore -> new MergeGroup()).add(clazz);
+  private void addExact(
+      DexProgramClass clazz, Map<Multiset<DexType>, HorizontalMergeGroup> groups) {
+    groups
+        .computeIfAbsent(getExactMergeKey(clazz), ignore -> new HorizontalMergeGroup())
+        .add(clazz);
   }
 
   private Multiset<DexType> getExactMergeKey(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerCycles.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerCycles.java
index bb60289..9addf2f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerCycles.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerCycles.java
@@ -20,7 +20,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.deadlock.SingleCallerInformation;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -97,7 +97,7 @@
   final AppView<AppInfoWithLiveness> appView;
 
   // Mapping from each merge candidate to its merge group.
-  final Map<DexProgramClass, MergeGroup> allGroups = new IdentityHashMap<>();
+  final Map<DexProgramClass, HorizontalMergeGroup> allGroups = new IdentityHashMap<>();
 
   private SingleCallerInformation singleCallerInformation;
 
@@ -112,24 +112,25 @@
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group, Void nothing) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group, Void nothing) {
     // Partition the merge group into smaller groups that may be merged. If the class initialization
     // of a parent class may initialize a member of the merge group, then this member is not
     // eligible for class merging, unless the only way to class initialize this member is from the
     // class initialization of the parent class. In this case, the member may be merged with other
     // group members that are also guaranteed to only be class initialized from the class
     // initialization of the parent class.
-    List<MergeGroup> partitioning = partitionClassesWithPossibleClassInitializerDeadlock(group);
-    List<MergeGroup> newGroups = new LinkedList<>();
+    List<HorizontalMergeGroup> partitioning =
+        partitionClassesWithPossibleClassInitializerDeadlock(group);
+    List<HorizontalMergeGroup> newGroups = new LinkedList<>();
 
     // Revisit each partition. If the class initialization of a group member may initialize another
     // class (not necessarily a group member), and vice versa, then class initialization could
     // deadlock if the group member is merged with another class that is initialized concurrently.
-    for (MergeGroup partition : partitioning) {
-      List<MergeGroup> newGroupsFromPartition = new LinkedList<>();
+    for (HorizontalMergeGroup partition : partitioning) {
+      List<HorizontalMergeGroup> newGroupsFromPartition = new LinkedList<>();
       Tracer tracer = new Tracer(partition);
       for (DexProgramClass clazz : partition) {
-        MergeGroup newGroup = getOrCreateGroupFor(clazz, newGroupsFromPartition, tracer);
+        HorizontalMergeGroup newGroup = getOrCreateGroupFor(clazz, newGroupsFromPartition, tracer);
         if (newGroup != null) {
           newGroup.add(clazz);
         } else {
@@ -143,22 +144,22 @@
     return newGroups;
   }
 
-  private void commit(MergeGroup oldGroup, List<MergeGroup> newGroups) {
-    for (MergeGroup newGroup : newGroups) {
+  private void commit(HorizontalMergeGroup oldGroup, List<HorizontalMergeGroup> newGroups) {
+    for (HorizontalMergeGroup newGroup : newGroups) {
       for (DexProgramClass member : newGroup) {
         allGroups.put(member, newGroup);
       }
     }
     for (DexProgramClass member : oldGroup) {
-      MergeGroup newGroup = allGroups.get(member);
+      HorizontalMergeGroup newGroup = allGroups.get(member);
       if (newGroup == oldGroup) {
         allGroups.remove(member);
       }
     }
   }
 
-  private MergeGroup getOrCreateGroupFor(
-      DexProgramClass clazz, List<MergeGroup> groups, Tracer tracer) {
+  private HorizontalMergeGroup getOrCreateGroupFor(
+      DexProgramClass clazz, List<HorizontalMergeGroup> groups, Tracer tracer) {
     assert !tracer.hasPossibleClassInitializerDeadlock(clazz);
 
     if (clazz.hasClassInitializer()) {
@@ -174,18 +175,18 @@
       }
     }
 
-    for (MergeGroup group : groups) {
+    for (HorizontalMergeGroup group : groups) {
       if (canMerge(clazz, group, tracer)) {
         return group;
       }
     }
 
-    MergeGroup newGroup = new MergeGroup();
+    HorizontalMergeGroup newGroup = new HorizontalMergeGroup();
     groups.add(newGroup);
     return newGroup;
   }
 
-  private boolean canMerge(DexProgramClass clazz, MergeGroup group, Tracer tracer) {
+  private boolean canMerge(DexProgramClass clazz, HorizontalMergeGroup group, Tracer tracer) {
     for (DexProgramClass member : group) {
       // Check that the class initialization of the given class cannot reach the class initializer
       // of the current group member.
@@ -206,7 +207,8 @@
    * If the class initializer of one of the classes in the merge group is reached, then that class
    * is not eligible for merging.
    */
-  private List<MergeGroup> partitionClassesWithPossibleClassInitializerDeadlock(MergeGroup group) {
+  private List<HorizontalMergeGroup> partitionClassesWithPossibleClassInitializerDeadlock(
+      HorizontalMergeGroup group) {
     Set<DexProgramClass> superclasses = Sets.newIdentityHashSet();
     appView
         .appInfo()
@@ -230,13 +232,15 @@
     }
     tracer.trace();
 
-    MergeGroup notInitializedByInitializationOfParent = new MergeGroup();
-    Map<DexProgramClass, MergeGroup> partitioning = new LinkedHashMap<>();
+    HorizontalMergeGroup notInitializedByInitializationOfParent = new HorizontalMergeGroup();
+    Map<DexProgramClass, HorizontalMergeGroup> partitioning = new LinkedHashMap<>();
     for (DexProgramClass member : group) {
       if (tracer.hasPossibleClassInitializerDeadlock(member)) {
         DexProgramClass nearestLock = getNearestLock(member, superclasses);
         if (nearestLock != null) {
-          partitioning.computeIfAbsent(nearestLock, ignoreKey(MergeGroup::new)).add(member);
+          partitioning
+              .computeIfAbsent(nearestLock, ignoreKey(HorizontalMergeGroup::new))
+              .add(member);
         } else {
           // Ineligible for merging.
         }
@@ -245,7 +249,7 @@
       }
     }
 
-    return ImmutableList.<MergeGroup>builder()
+    return ImmutableList.<HorizontalMergeGroup>builder()
         .add(notInitializedByInitializationOfParent)
         .addAll(partitioning.values())
         .build();
@@ -276,9 +280,9 @@
   }
 
   @Override
-  public Void preprocess(Collection<MergeGroup> groups, ExecutorService executorService)
+  public Void preprocess(Collection<HorizontalMergeGroup> groups, ExecutorService executorService)
       throws ExecutionException {
-    for (MergeGroup group : groups) {
+    for (HorizontalMergeGroup group : groups) {
       for (DexProgramClass clazz : group) {
         allGroups.put(clazz, group);
       }
@@ -296,7 +300,7 @@
 
   private class Tracer {
 
-    final MergeGroup group;
+    final HorizontalMergeGroup group;
 
     // The members of the existing merge group, for efficient membership querying.
     final Set<DexProgramClass> groupMembers;
@@ -314,7 +318,7 @@
     // group).
     private Collection<DexProgramClass> tracingRoots;
 
-    Tracer(MergeGroup group) {
+    Tracer(HorizontalMergeGroup group) {
       this.group = group;
       this.groupMembers = SetUtils.newIdentityHashSet(group);
     }
@@ -469,7 +473,7 @@
             worklist.addIfNotSeen(superClass);
           }
 
-          MergeGroup other = allGroups.get(clazz);
+          HorizontalMergeGroup other = allGroups.get(clazz);
           if (other != null && other != group) {
             worklist.addIfNotSeen(other);
           }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
index 6d83173..f4b929d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.IterableUtils;
@@ -55,9 +55,10 @@
    * filtered group.
    */
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group, Set<DexType> collisionResolution) {
-    MergeGroup newGroup =
-        new MergeGroup(
+  public Collection<HorizontalMergeGroup> apply(
+      HorizontalMergeGroup group, Set<DexType> collisionResolution) {
+    HorizontalMergeGroup newGroup =
+        new HorizontalMergeGroup(
             Iterables.filter(group, clazz -> !collisionResolution.contains(clazz.getType())));
     return newGroup.isTrivial() ? Collections.emptyList() : ListUtils.newLinkedList(newGroup);
   }
@@ -67,10 +68,11 @@
    * lead to constructor collisions.
    */
   @Override
-  public Set<DexType> preprocess(Collection<MergeGroup> groups, ExecutorService executorService) {
+  public Set<DexType> preprocess(
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService) {
     // Build a mapping from types to groups.
-    Map<DexType, MergeGroup> groupsByType = new IdentityHashMap<>();
-    for (MergeGroup group : groups) {
+    Map<DexType, HorizontalMergeGroup> groupsByType = new IdentityHashMap<>();
+    for (HorizontalMergeGroup group : groups) {
       for (DexProgramClass clazz : group) {
         groupsByType.put(clazz.getType(), group);
       }
@@ -109,7 +111,7 @@
     return collisionResolution;
   }
 
-  private DexProto rewriteProto(DexProto proto, Map<DexType, MergeGroup> groups) {
+  private DexProto rewriteProto(DexProto proto, Map<DexType, HorizontalMergeGroup> groups) {
     DexType[] parameters =
         ArrayUtils.map(
             proto.getParameters().values,
@@ -118,7 +120,7 @@
     return dexItemFactory.createProto(rewriteType(proto.getReturnType(), groups), parameters);
   }
 
-  private DexMethod rewriteReference(DexMethod method, Map<DexType, MergeGroup> groups) {
+  private DexMethod rewriteReference(DexMethod method, Map<DexType, HorizontalMergeGroup> groups) {
     return dexItemFactory.createMethod(
         rewriteType(method.getHolderType(), groups),
         rewriteProto(method.getProto(), groups),
@@ -126,7 +128,7 @@
   }
 
   @SuppressWarnings("ReferenceEquality")
-  private DexType rewriteType(DexType type, Map<DexType, MergeGroup> groups) {
+  private DexType rewriteType(DexType type, Map<DexType, HorizontalMergeGroup> groups) {
     if (type.isArrayType()) {
       DexType baseType = type.toBaseType(dexItemFactory);
       DexType rewrittenBaseType = rewriteType(baseType, groups);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
index 7e32fd3..ee9f3a7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
@@ -6,7 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
@@ -28,14 +28,14 @@
   // TODO(b/270398965): Replace LinkedList.
   @Override
   @SuppressWarnings({"JdkObsolete", "MixedMutabilityReturnType"})
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     // Gather all synchronized classes.
-    Collection<MergeGroup> synchronizedGroups = new LinkedList<>();
+    Collection<HorizontalMergeGroup> synchronizedGroups = new LinkedList<>();
     group.removeIf(
         clazz -> {
           boolean synchronizationClass = isSynchronizationClass(clazz);
           if (synchronizationClass) {
-            MergeGroup synchronizedGroup = new MergeGroup();
+            HorizontalMergeGroup synchronizedGroup = new HorizontalMergeGroup();
             synchronizedGroup.add(clazz);
             synchronizedGroups.add(synchronizedGroup);
           }
@@ -46,7 +46,7 @@
       return Collections.singletonList(group);
     }
 
-    Iterator<MergeGroup> synchronizedGroupIterator = synchronizedGroups.iterator();
+    Iterator<HorizontalMergeGroup> synchronizedGroupIterator = synchronizedGroups.iterator();
     for (DexProgramClass clazz : group) {
       if (!synchronizedGroupIterator.hasNext()) {
         synchronizedGroupIterator = synchronizedGroups.iterator();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
index e8494a3..f330807 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -19,7 +19,7 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodCollisions.InterfaceInfo;
 import com.android.tools.r8.utils.IterableUtils;
@@ -82,7 +82,8 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group, Map<DexType, InterfaceInfo> infos) {
+  public Collection<HorizontalMergeGroup> apply(
+      HorizontalMergeGroup group, Map<DexType, InterfaceInfo> infos) {
     if (!group.isInterfaceGroup()) {
       return ImmutableList.of(group);
     }
@@ -95,7 +96,7 @@
     // TODO(b/173990042): Consider forming multiple groups instead of just filtering. In practice,
     //  this rarely leads to much filtering, though, since the use of default methods is somewhat
     //  limited.
-    MergeGroup newGroup = new MergeGroup();
+    HorizontalMergeGroup newGroup = new HorizontalMergeGroup();
     for (DexProgramClass clazz : group) {
       Set<DexMethod> newDefaultMethodsAddedToClassByMerge =
           computeNewDefaultMethodsAddedToClassByMerge(clazz, group, infos);
@@ -107,7 +108,7 @@
   }
 
   private Set<DexMethod> computeNewDefaultMethodsAddedToClassByMerge(
-      DexProgramClass clazz, MergeGroup group, Map<DexType, InterfaceInfo> infos) {
+      DexProgramClass clazz, HorizontalMergeGroup group, Map<DexType, InterfaceInfo> infos) {
     // Run through the other classes in the merge group, and add the default interface methods that
     // they declare (or inherit from a super interface) to a set.
     Set<DexMethod> newDefaultMethodsAddedToClassByMerge = Sets.newIdentityHashSet();
@@ -146,7 +147,7 @@
 
   @Override
   public Map<DexType, InterfaceInfo> preprocess(
-      Collection<MergeGroup> groups, ExecutorService executorService) {
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService) {
     SubtypingInfo subtypingInfo = SubtypingInfo.create(appView);
     Collection<DexProgramClass> classesOfInterest = computeClassesOfInterest(subtypingInfo);
     Map<DexType, DexMethodSignatureSet> inheritedClassMethodsPerClass =
@@ -163,7 +164,7 @@
 
     // Store the computed information for each interface that is subject to merging.
     Map<DexType, InterfaceInfo> infos = new IdentityHashMap<>();
-    for (MergeGroup group : groups) {
+    for (HorizontalMergeGroup group : groups) {
       if (group.isInterfaceGroup()) {
         for (DexProgramClass clazz : group) {
           infos.put(
@@ -277,13 +278,13 @@
       computeDefaultMethodsInheritedBySubclassesPerProgramClass(
           Collection<DexProgramClass> classesOfInterest,
           Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerClass,
-          Collection<MergeGroup> groups,
+          Collection<HorizontalMergeGroup> groups,
           SubtypingInfo subtypingInfo) {
     // Build a mapping from class types to their merge group.
     Map<DexType, Iterable<DexProgramClass>> classGroupsByType =
         MapUtils.newIdentityHashMap(
             builder ->
-                Iterables.filter(groups, MergeGroup::isClassGroup)
+                Iterables.filter(groups, HorizontalMergeGroup::isClassGroup)
                     .forEach(group -> group.forEach(clazz -> builder.put(clazz.getType(), group))));
 
     // Copy the map from classes to their inherited default methods.
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
index e466650..318a2af 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
@@ -43,11 +43,11 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     // Split the group into smaller groups such that no default methods collide.
     // TODO(b/229951607): This fixes the ICCE issue for synthetic lambda classes, but a more
     //  general solution possibly extending the policy NoDefaultInterfaceMethodCollisions.
-    Map<MergeGroup, DexMethodSignatureMap<DexType>> newGroups = new LinkedHashMap<>();
+    Map<HorizontalMergeGroup, DexMethodSignatureMap<DexType>> newGroups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       addClassToGroup(
           clazz,
@@ -63,14 +63,14 @@
   @SuppressWarnings("ReferenceEquality")
   private void addClassToGroup(
       DexProgramClass clazz,
-      Map<MergeGroup, DexMethodSignatureMap<DexType>> newGroups,
+      Map<HorizontalMergeGroup, DexMethodSignatureMap<DexType>> newGroups,
       Function<DexProgramClass, DexMethodSignatureMap<DexType>> fn) {
     DexMethodSignatureMap<DexType> classSignatures = fn.apply(clazz);
 
     // Find a group that does not have any collisions with `clazz`.
     nextGroup:
-    for (Entry<MergeGroup, DexMethodSignatureMap<DexType>> entry : newGroups.entrySet()) {
-      MergeGroup group = entry.getKey();
+    for (Entry<HorizontalMergeGroup, DexMethodSignatureMap<DexType>> entry : newGroups.entrySet()) {
+      HorizontalMergeGroup group = entry.getKey();
       DexMethodSignatureMap<DexType> groupSignatures = entry.getValue();
       if (!groupSignatures.containsAnyKeyOf(classSignatures.keySet())) {
         groupSignatures.putAll(classSignatures);
@@ -92,7 +92,7 @@
     }
 
     // Else create a new group.
-    newGroups.put(new MergeGroup(clazz), classSignatures);
+    newGroups.put(new HorizontalMergeGroup(clazz), classSignatures);
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
index aef65d3..b792e84 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -19,13 +19,13 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.ClassInstanceFieldsMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.AbsentInstanceInitializer;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.InstanceInitializer;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis.PresentInstanceInitializer;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerDescription;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
@@ -73,7 +73,7 @@
   @Override
   @SuppressWarnings("MixedMutabilityReturnType")
   public Map<DexProgramClass, Set<DexMethod>> preprocess(
-      Collection<MergeGroup> groups, ExecutorService executorService) {
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService) {
     if (!appView.options().canHaveNonReboundConstructorInvoke()) {
       return Collections.emptyMap();
     }
@@ -118,8 +118,8 @@
 
   @Override
   @SuppressWarnings("MixedMutabilityReturnType")
-  public Collection<MergeGroup> apply(
-      MergeGroup group, Map<DexProgramClass, Set<DexMethod>> absentInstanceInitializers) {
+  public Collection<HorizontalMergeGroup> apply(
+      HorizontalMergeGroup group, Map<DexProgramClass, Set<DexMethod>> absentInstanceInitializers) {
     assert !group.hasTarget();
     assert !group.hasInstanceFieldMap();
 
@@ -148,7 +148,8 @@
     group.selectTarget(appView);
     group.selectInstanceFieldMap(appView);
 
-    Map<MergeGroup, Map<DexMethodSignature, InstanceInitializer>> newGroups = new LinkedHashMap<>();
+    Map<HorizontalMergeGroup, Map<DexMethodSignature, InstanceInitializer>> newGroups =
+        new LinkedHashMap<>();
 
     // Caching of instance initializer descriptions, which are used to determine equivalence.
     // TODO(b/181846319): Make this cache available to the instance initializer merger so that we
@@ -164,12 +165,12 @@
     // Partition group into smaller groups where there are no (non-equivalent) instance initializer
     // collisions.
     for (DexProgramClass clazz : group) {
-      MergeGroup newGroup = null;
+      HorizontalMergeGroup newGroup = null;
       Map<DexMethodSignature, InstanceInitializer> classInstanceInitializers =
           getInstanceInitializersByRelaxedSignature(clazz, absentInstanceInitializers);
-      for (Entry<MergeGroup, Map<DexMethodSignature, InstanceInitializer>> entry :
+      for (Entry<HorizontalMergeGroup, Map<DexMethodSignature, InstanceInitializer>> entry :
           newGroups.entrySet()) {
-        MergeGroup candidateGroup = entry.getKey();
+        HorizontalMergeGroup candidateGroup = entry.getKey();
         Map<DexMethodSignature, InstanceInitializer> groupInstanceInitializers = entry.getValue();
         if (canAddClassToGroup(
             classInstanceInitializers,
@@ -183,12 +184,12 @@
       if (newGroup != null) {
         newGroup.add(clazz);
       } else {
-        newGroups.put(new MergeGroup(clazz), classInstanceInitializers);
+        newGroups.put(new HorizontalMergeGroup(clazz), classInstanceInitializers);
       }
     }
 
     // Remove trivial groups and finalize the newly created groups.
-    Collection<MergeGroup> newNonTrivialGroups = removeTrivialGroups(newGroups.keySet());
+    Collection<HorizontalMergeGroup> newNonTrivialGroups = removeTrivialGroups(newGroups.keySet());
     setInstanceFieldMaps(newNonTrivialGroups, group);
     return newNonTrivialGroups;
   }
@@ -265,7 +266,7 @@
   }
 
   private Optional<InstanceInitializerDescription> getOrComputeInstanceInitializerDescription(
-      MergeGroup group,
+      HorizontalMergeGroup group,
       InstanceInitializer instanceInitializer,
       Map<DexMethod, Optional<InstanceInitializerDescription>> instanceInitializerDescriptions) {
     return instanceInitializerDescriptions.computeIfAbsent(
@@ -296,8 +297,9 @@
         : instanceInitializerReference;
   }
 
-  private void setInstanceFieldMaps(Iterable<MergeGroup> newGroups, MergeGroup group) {
-    for (MergeGroup newGroup : newGroups) {
+  private void setInstanceFieldMaps(
+      Iterable<HorizontalMergeGroup> newGroups, HorizontalMergeGroup group) {
+    for (HorizontalMergeGroup newGroup : newGroups) {
       // Set target.
       newGroup.selectTarget(appView);
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
index 07b2fc7..21283e1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.SetUtils;
@@ -40,17 +40,19 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
-    Map<MergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups = new LinkedHashMap<>();
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
+    Map<HorizontalMergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups =
+        new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       Map<DexMethodSignature, ProgramMethod> classMethods = new HashMap<>();
       clazz.forEachProgramVirtualMethodMatching(
           DexEncodedMethod::isNonAbstractVirtualMethod,
           method -> classMethods.put(method.getMethodSignature(), method));
 
-      MergeGroup newGroup = null;
-      for (Entry<MergeGroup, Map<DexMethodSignature, ProgramMethod>> entry : newGroups.entrySet()) {
-        MergeGroup candidateGroup = entry.getKey();
+      HorizontalMergeGroup newGroup = null;
+      for (Entry<HorizontalMergeGroup, Map<DexMethodSignature, ProgramMethod>> entry :
+          newGroups.entrySet()) {
+        HorizontalMergeGroup candidateGroup = entry.getKey();
         Map<DexMethodSignature, ProgramMethod> groupMethods = entry.getValue();
         if (canAddNonAbstractVirtualMethodsToGroup(
             clazz, classMethods.values(), candidateGroup, groupMethods)) {
@@ -63,7 +65,7 @@
       if (newGroup != null) {
         newGroup.add(clazz);
       } else {
-        newGroups.put(new MergeGroup(clazz), classMethods);
+        newGroups.put(new HorizontalMergeGroup(clazz), classMethods);
       }
     }
     return removeTrivialGroups(newGroups.keySet());
@@ -72,7 +74,7 @@
   private boolean canAddNonAbstractVirtualMethodsToGroup(
       DexProgramClass clazz,
       Collection<ProgramMethod> methods,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       Map<DexMethodSignature, ProgramMethod> groupMethods) {
     // For each of clazz' virtual methods, check that adding these methods to the group does not
     // require method merging.
@@ -92,7 +94,8 @@
     return true;
   }
 
-  private boolean hasNonAbstractDefinitionInHierarchy(MergeGroup group, ProgramMethod method) {
+  private boolean hasNonAbstractDefinitionInHierarchy(
+      HorizontalMergeGroup group, ProgramMethod method) {
     return hasNonAbstractDefinitionInSuperClass(group.getSuperType(), method)
         || hasNonAbstractDefinitionInSuperInterface(
             SetUtils.newIdentityHashSet(IterableUtils.flatMap(group, DexClass::getInterfaces)),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoWeakerAccessPrivileges.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoWeakerAccessPrivileges.java
index 2305438..3daa113 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoWeakerAccessPrivileges.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoWeakerAccessPrivileges.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
@@ -46,14 +46,14 @@
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
-    List<MergeGroup> newMergeGroups = new LinkedList<>();
-    Map<MergeGroup, DexMethodSignatureSet> inheritedInterfaceMethodsPerGroup =
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
+    List<HorizontalMergeGroup> newMergeGroups = new LinkedList<>();
+    Map<HorizontalMergeGroup, DexMethodSignatureSet> inheritedInterfaceMethodsPerGroup =
         new IdentityHashMap<>();
     for (DexProgramClass clazz : group) {
       // Find an existing merge group that the current class can be added to.
-      MergeGroup newMergeGroup = null;
-      for (MergeGroup candidateMergeGroup : newMergeGroups) {
+      HorizontalMergeGroup newMergeGroup = null;
+      for (HorizontalMergeGroup candidateMergeGroup : newMergeGroups) {
         DexMethodSignatureSet inheritedInterfaceMethodsInGroup =
             inheritedInterfaceMethodsPerGroup.get(candidateMergeGroup);
         if (canAddToGroup(clazz, candidateMergeGroup, inheritedInterfaceMethodsInGroup)) {
@@ -65,7 +65,7 @@
       DexMethodSignatureSet inheritedInterfaceMethodsInGroup;
       if (newMergeGroup == null) {
         // Form a new singleton merge group from the current class.
-        newMergeGroup = new MergeGroup(clazz);
+        newMergeGroup = new HorizontalMergeGroup(clazz);
         newMergeGroups.add(newMergeGroup);
         inheritedInterfaceMethodsInGroup = DexMethodSignatureSet.create();
         inheritedInterfaceMethodsPerGroup.put(newMergeGroup, inheritedInterfaceMethodsInGroup);
@@ -86,7 +86,7 @@
 
   private boolean canAddToGroup(
       DexProgramClass clazz,
-      MergeGroup group,
+      HorizontalMergeGroup group,
       DexMethodSignatureSet inheritedInterfaceMethodsInGroup) {
     // We need to ensure that adding class to the group is OK.
     DexMethodSignatureSet nonPublicVirtualMethodSignaturesInClassComponent =
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
index 9812a26..db74f25 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
@@ -62,7 +62,7 @@
   private final Mode mode;
 
   // The interface merge groups that this policy has committed to so far.
-  private final Map<DexProgramClass, MergeGroup> committed = new IdentityHashMap<>();
+  private final Map<DexProgramClass, HorizontalMergeGroup> committed = new IdentityHashMap<>();
 
   public OnlyDirectlyConnectedOrUnrelatedInterfaces(
       AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
@@ -73,7 +73,8 @@
   // TODO(b/270398965): Replace LinkedList.
   @Override
   @SuppressWarnings({"JdkObsolete", "MixedMutabilityReturnType"})
-  public Collection<MergeGroup> apply(MergeGroup group, SubtypingInfo subtypingInfo) {
+  public Collection<HorizontalMergeGroup> apply(
+      HorizontalMergeGroup group, SubtypingInfo subtypingInfo) {
     if (!group.isInterfaceGroup()) {
       return ImmutableList.of(group);
     }
@@ -104,9 +105,9 @@
       committed.put(clazz, newGroup.getGroup());
     }
 
-    List<MergeGroup> newGroups = new LinkedList<>();
+    List<HorizontalMergeGroup> newGroups = new LinkedList<>();
     for (MergeGroupWithInfo newGroupWithInfo : newGroupsWithInfo) {
-      MergeGroup newGroup = newGroupWithInfo.getGroup();
+      HorizontalMergeGroup newGroup = newGroupWithInfo.getGroup();
       if (newGroup.isTrivial()) {
         assert !newGroup.isEmpty();
         committed.remove(newGroup.getClasses().getFirst());
@@ -135,7 +136,7 @@
     workList.addIgnoringSeenSet(clazz);
     workList.process(
         interfaceDefinition -> {
-          MergeGroup group = committed.get(interfaceDefinition);
+          HorizontalMergeGroup group = committed.get(interfaceDefinition);
           if (group != null) {
             workList.addIfNotSeen(group);
           }
@@ -163,7 +164,8 @@
   }
 
   @Override
-  public SubtypingInfo preprocess(Collection<MergeGroup> groups, ExecutorService executorService) {
+  public SubtypingInfo preprocess(
+      Collection<HorizontalMergeGroup> groups, ExecutorService executorService) {
     return SubtypingInfo.create(appView);
   }
 
@@ -174,7 +176,7 @@
 
   static class MergeGroupWithInfo {
 
-    private final MergeGroup group;
+    private final HorizontalMergeGroup group;
     private final Set<DexProgramClass> members;
     private final Set<DexProgramClass> superInterfaces;
     private final Set<DexProgramClass> subInterfaces;
@@ -183,7 +185,7 @@
         DexProgramClass clazz,
         Set<DexProgramClass> superInterfaces,
         Set<DexProgramClass> subInterfaces) {
-      this.group = new MergeGroup(clazz);
+      this.group = new HorizontalMergeGroup(clazz);
       this.members = SetUtils.newIdentityHashSet(clazz);
       this.superInterfaces = superInterfaces;
       this.subInterfaces = subInterfaces;
@@ -206,7 +208,7 @@
       subInterfaces.remove(clazz);
     }
 
-    MergeGroup getGroup() {
+    HorizontalMergeGroup getGroup() {
       return group;
     }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
index 63e334b..b891bcd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.OptionalBool;
@@ -101,10 +101,10 @@
 
   public static class TargetGroup {
 
-    private final MergeGroup group = new MergeGroup();
+    private final HorizontalMergeGroup group = new HorizontalMergeGroup();
     private final Map<DexMethodSignature, MethodCharacteristics> methodMap = new HashMap<>();
 
-    public MergeGroup getGroup() {
+    public HorizontalMergeGroup getGroup() {
       return group;
     }
 
@@ -139,7 +139,7 @@
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     List<TargetGroup> groups = new ArrayList<>();
 
     for (DexProgramClass clazz : group) {
@@ -152,7 +152,7 @@
       }
     }
 
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    LinkedList<HorizontalMergeGroup> newGroups = new LinkedList<>();
     for (TargetGroup newGroup : groups) {
       if (!newGroup.getGroup().isTrivial()) {
         newGroups.add(newGroup.getGroup());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
index 2a3602c..06e39d7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.google.common.collect.ImmutableList;
@@ -151,7 +151,7 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     // This policy is specific to issues that may arise from merging (non-interface) classes.
     if (group.isInterfaceGroup()) {
       return ImmutableList.of(group);
@@ -162,7 +162,7 @@
       signatures.addAllMethods(clazz.methods());
     }
 
-    Map<DispatchSignature, MergeGroup> newGroups = new LinkedHashMap<>();
+    Map<DispatchSignature, HorizontalMergeGroup> newGroups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       DexMethodSignatureSet clazzReserved = computeReservedSignaturesForClass(clazz);
       DispatchSignature dispatchSignature = new DispatchSignature();
@@ -178,7 +178,7 @@
         }
         dispatchSignature.addSignature(signature, category);
       }
-      newGroups.computeIfAbsent(dispatchSignature, ignore -> new MergeGroup()).add(clazz);
+      newGroups.computeIfAbsent(dispatchSignature, ignore -> new HorizontalMergeGroup()).add(clazz);
     }
     return removeTrivialGroups(newGroups.values());
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
index 29a9147..bfc4cd1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
@@ -15,7 +15,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.verticalclassmerging.IllegalAccessDetector;
@@ -122,10 +122,12 @@
 
   /** Sort unrestricted classes into restricted classes if they are in the same package. */
   void tryFindRestrictedPackage(
-      MergeGroup unrestrictedClasses, Map<String, MergeGroup> restrictedClasses) {
+      HorizontalMergeGroup unrestrictedClasses,
+      Map<String, HorizontalMergeGroup> restrictedClasses) {
     unrestrictedClasses.removeIf(
         clazz -> {
-          MergeGroup restrictedPackage = restrictedClasses.get(clazz.type.getPackageDescriptor());
+          HorizontalMergeGroup restrictedPackage =
+              restrictedClasses.get(clazz.type.getPackageDescriptor());
           if (restrictedPackage != null) {
             restrictedPackage.add(clazz);
             return true;
@@ -135,15 +137,16 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
-    Map<String, MergeGroup> restrictedClasses = new LinkedHashMap<>();
-    MergeGroup unrestrictedClasses = new MergeGroup();
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
+    Map<String, HorizontalMergeGroup> restrictedClasses = new LinkedHashMap<>();
+    HorizontalMergeGroup unrestrictedClasses = new HorizontalMergeGroup();
 
     // Sort all restricted classes into packages.
     for (DexProgramClass clazz : group) {
       if (shouldRestrictMergingAcrossPackageBoundary(clazz)) {
         restrictedClasses
-            .computeIfAbsent(clazz.getType().getPackageDescriptor(), ignore -> new MergeGroup())
+            .computeIfAbsent(
+                clazz.getType().getPackageDescriptor(), ignore -> new HorizontalMergeGroup())
             .add(clazz);
       } else {
         unrestrictedClasses.add(clazz);
@@ -155,7 +158,7 @@
 
     // TODO(b/166577694): Add the unrestricted classes to restricted groups, but ensure they aren't
     // the merge target.
-    Collection<MergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
+    Collection<HorizontalMergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
     if (unrestrictedClasses.size() > 1) {
       groups.add(unrestrictedClasses);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForNonGlobalMergeSynthetic.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForNonGlobalMergeSynthetic.java
index db6d282..a95bafe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForNonGlobalMergeSynthetic.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForNonGlobalMergeSynthetic.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.google.common.collect.Iterables;
@@ -28,10 +28,12 @@
 
   /** Sort unrestricted classes into restricted classes if they are in the same package. */
   private void tryFindRestrictedPackage(
-      MergeGroup unrestrictedClasses, Map<String, MergeGroup> restrictedClasses) {
+      HorizontalMergeGroup unrestrictedClasses,
+      Map<String, HorizontalMergeGroup> restrictedClasses) {
     unrestrictedClasses.removeIf(
         clazz -> {
-          MergeGroup restrictedPackage = restrictedClasses.get(clazz.type.getPackageDescriptor());
+          HorizontalMergeGroup restrictedPackage =
+              restrictedClasses.get(clazz.type.getPackageDescriptor());
           if (restrictedPackage != null) {
             restrictedPackage.add(clazz);
             return true;
@@ -41,9 +43,9 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
-    Map<String, MergeGroup> restrictedClasses = new LinkedHashMap<>();
-    MergeGroup unrestrictedClasses = new MergeGroup();
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
+    Map<String, HorizontalMergeGroup> restrictedClasses = new LinkedHashMap<>();
+    HorizontalMergeGroup unrestrictedClasses = new HorizontalMergeGroup();
     SyntheticItems syntheticItems = appView.getSyntheticItems();
 
     // Sort all restricted classes into packages.
@@ -56,7 +58,7 @@
                   || !kind.asSyntheticMethodKind().isAllowGlobalMerging())) {
         restrictedClasses
             .computeIfAbsent(
-                clazz.getType().getPackageDescriptor(), ignoreArgument(MergeGroup::new))
+                clazz.getType().getPackageDescriptor(), ignoreArgument(HorizontalMergeGroup::new))
             .add(clazz);
       } else {
         unrestrictedClasses.add(clazz);
@@ -66,7 +68,7 @@
     tryFindRestrictedPackage(unrestrictedClasses, restrictedClasses);
     removeTrivialGroups(restrictedClasses.values());
 
-    Collection<MergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
+    Collection<HorizontalMergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
     if (unrestrictedClasses.size() > 1) {
       groups.add(unrestrictedClasses);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyMultiClassPolicyAlwaysSatisfied.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyMultiClassPolicyAlwaysSatisfied.java
index 7ad1e60..e62195b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyMultiClassPolicyAlwaysSatisfied.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyMultiClassPolicyAlwaysSatisfied.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
@@ -29,15 +29,15 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<HorizontalMergeGroup> apply(HorizontalMergeGroup group) {
     assert verifySameAppliedGroup(group);
     return Collections.singletonList(group);
   }
 
-  private boolean verifySameAppliedGroup(MergeGroup group) {
-    Collection<MergeGroup> applied = policy.apply(group);
+  private boolean verifySameAppliedGroup(HorizontalMergeGroup group) {
+    Collection<HorizontalMergeGroup> applied = policy.apply(group);
     assert applied.size() == 1;
-    MergeGroup appliedGroup = applied.iterator().next();
+    HorizontalMergeGroup appliedGroup = applied.iterator().next();
     assert appliedGroup.size() == group.size() && group.containsAll(appliedGroup);
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 1b14342..1ef539a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -445,13 +445,14 @@
           if (instanceGet.instructionMayHaveSideEffects(appView, context)) {
             return false;
           }
+          NewInstance newInstance = null;
           if (instanceGet.object().isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
-            NewInstance newInstance = instanceGet.object().getDefinition().asNewInstance();
+            newInstance = instanceGet.object().getDefinition().asNewInstance();
             if (newInstance.getUniqueConstructorInvoke(appView.dexItemFactory()) == null) {
               return false;
             }
           }
-          if (!isReadOfEffectivelyFinalFieldOutsideInitializer(instanceGet)) {
+          if (!isReadOfEffectivelyFinalFieldOutsideInitializer(instanceGet, newInstance)) {
             return false;
           }
           if (getOrComputeIneligibleInstanceGetInstructions().contains(instanceGet)) {
@@ -484,7 +485,12 @@
     return true;
   }
 
-  private boolean isReadOfEffectivelyFinalFieldOutsideInitializer(FieldGet fieldGet) {
+  private boolean isReadOfEffectivelyFinalFieldOutsideInitializer(StaticGet staticGet) {
+    return isReadOfEffectivelyFinalFieldOutsideInitializer(staticGet, null);
+  }
+
+  private boolean isReadOfEffectivelyFinalFieldOutsideInitializer(
+      FieldGet fieldGet, NewInstance newInstance) {
     if (getOrComputeIsAccessingVolatileField()) {
       // A final field may be initialized concurrently. A requirement for this is that the field is
       // volatile. However, the reading or writing of another volatile field also allows for
@@ -512,6 +518,14 @@
     if (!resolvedField.isFinalOrEffectivelyFinal(appViewWithClassHierarchy)) {
       return false;
     }
+    if (!resolvedField.getAccessFlags().isFinal() && newInstance != null) {
+      // The effectively final property captured in the enqueuer may be invalidated by constructor
+      // inlining (in particular, fields that used only to be written in instance initializers from
+      // the enclosing class may now be written outside such constructors). If we see an
+      // instance-get on a newly created instance, we therefore bail-out since the field may in
+      // principle not be effectively final in this method.
+      return false;
+    }
     if (appView.getKeepInfo(resolvedField).isPinned(appView.options())) {
       // The final flag could be unset using reflection.
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/constraint/ConditionalClassInlinerMethodConstraint.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/constraint/ConditionalClassInlinerMethodConstraint.java
index bd524a5..e1e605d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/constraint/ConditionalClassInlinerMethodConstraint.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/constraint/ConditionalClassInlinerMethodConstraint.java
@@ -53,9 +53,9 @@
                 return;
               }
               if (argumentInfo.isRewrittenTypeInfo()
-                  && argumentInfo.asRewrittenTypeInfo().getNewType().isIntType()) {
-                // This is due to enum unboxing. After enum unboxing, we no longer need information
-                // about the usages of this parameter for class inlining.
+                  && argumentInfo.asRewrittenTypeInfo().getNewType().isPrimitiveType()) {
+                // This is due to number/enum unboxing. After enum unboxing, we no longer need
+                // information about the usages of this parameter for class inlining.
                 return;
               }
               backing.put(changes.getNewArgumentIndex(argumentIndex), usagePerContext);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index c0a7655..5f61f9b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -73,15 +73,13 @@
     }
 
     List<DexProgramClass> subtypes = subtypingInfo.getSubclasses(clazz);
-    ImmutableSet.Builder<DexProgramClass> subEnumClassesBuilder = ImmutableSet.builder();
     for (DexProgramClass subEnum : subtypes) {
       if (!isSubEnumUnboxingCandidate(subEnum)) {
         return;
       }
-      subEnumClassesBuilder.add(subEnum);
     }
     enumToUnboxCandidates.addCandidate(
-        appView, clazz, subEnumClassesBuilder.build(), graphLensForPrimaryOptimizationPass);
+        appView, clazz, ImmutableSet.copyOf(subtypes), graphLensForPrimaryOptimizationPass);
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
index 01b4a7d..3314582 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
@@ -94,7 +94,8 @@
             parameterChanges.getArgumentInfo(parameterIndex).asRewrittenTypeInfo();
         if (rewrittenTypeInfo != null
             && rewrittenTypeInfo.getOldType().isReferenceType()
-            && rewrittenTypeInfo.getNewType().isIntType()) {
+            && rewrittenTypeInfo.getNewType().isPrimitiveType()) {
+          // Clear information for number/enum unboxing.
           rewrittenParameterIndex++;
           continue;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfoFixer.java
index a593d91..c90d850 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfoFixer.java
@@ -7,6 +7,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintFactory;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
 import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
@@ -74,4 +76,15 @@
   public BitSet fixupArguments(BitSet arguments) {
     return arguments;
   }
+
+  @Override
+  public DynamicType fixupDynamicType(DynamicType dynamicType) {
+    return dynamicType;
+  }
+
+  @Override
+  public AbstractValue fixupAbstractReturnValue(
+      AppView<AppInfoWithLiveness> appView, AbstractValue returnValue) {
+    return returnValue;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
index 4279b3c..06ef441 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
@@ -7,6 +7,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintFactory;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
 import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
@@ -44,4 +46,9 @@
       SimpleInliningConstraintFactory factory);
 
   public abstract BitSet fixupArguments(BitSet arguments);
+
+  public abstract DynamicType fixupDynamicType(DynamicType dynamicType);
+
+  public abstract AbstractValue fixupAbstractReturnValue(
+      AppView<AppInfoWithLiveness> appView, AbstractValue returnValue);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 685fc0f..602b555 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -159,6 +159,8 @@
     return fixupArgumentInfos(method, fixer)
         .fixupBridgeInfo(fixer)
         .fixupClassInlinerMethodConstraint(appView, fixer)
+        .fixupDynamicType(fixer)
+        .fixupAbstractReturnValue(appView, fixer)
         .fixupEnumUnboxerMethodClassification(fixer)
         .fixupInstanceInitializerInfo(appView, fixer)
         .fixupNonNullParamOnNormalExits(fixer)
@@ -169,6 +171,13 @@
         .fixupUnusedArguments(fixer);
   }
 
+  private MutableMethodOptimizationInfo fixupDynamicType(MethodOptimizationInfoFixer fixer) {
+    if (dynamicType.isUnknown()) {
+      return this;
+    }
+    return setDynamicType(fixer.fixupDynamicType(dynamicType));
+  }
+
   public MutableMethodOptimizationInfo fixupClassTypeReferences(
       AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     return fixupClassTypeReferences(appView, lens, emptySet());
@@ -202,6 +211,15 @@
   }
 
   public MutableMethodOptimizationInfo fixupAbstractReturnValue(
+      AppView<AppInfoWithLiveness> appView, MethodOptimizationInfoFixer fixer) {
+    if (abstractReturnValue.isUnknown()) {
+      return this;
+    }
+    abstractReturnValue = fixer.fixupAbstractReturnValue(appView, abstractReturnValue);
+    return this;
+  }
+
+  public MutableMethodOptimizationInfo fixupAbstractReturnValue(
       AppView<AppInfoWithLiveness> appView,
       DexEncodedMethod method,
       GraphLens lens,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
new file mode 100644
index 0000000..db81e4b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class ClassOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod getConstructor;
+  private final DexMethod getDeclaredConstructor;
+  private final DexMethod getMethod;
+  private final DexMethod getDeclaredMethod;
+
+  ClassOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    getConstructor = dexItemFactory.classMethods.getConstructor;
+    getDeclaredConstructor = dexItemFactory.classMethods.getDeclaredConstructor;
+    getMethod = dexItemFactory.classMethods.getMethod;
+    getDeclaredMethod = dexItemFactory.classMethods.getDeclaredMethod;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.classType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(getConstructor)
+        || singleTargetReference.isIdenticalTo(getDeclaredConstructor)
+        || singleTargetReference.isIdenticalTo(getMethod)
+        || singleTargetReference.isIdenticalTo(getDeclaredMethod)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getLastArgument(), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java
new file mode 100644
index 0000000..718ed8b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class ConstructorOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod newInstance;
+
+  ConstructorOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    newInstance = dexItemFactory.constructorMethods.newInstance;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.constructorType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(newInstance)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getArgument(1), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java
new file mode 100644
index 0000000..a81a0c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+
+public class EmptyVarargsUtil {
+  public static void replaceWithNullIfEmptyArray(
+      Value value,
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InternalOptions options,
+      AffectedValues affectedValues) {
+    if (value.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)
+        && value.definition.asNewArrayEmpty().sizeIfConst() == 0) {
+      instructionIterator.previous();
+      value.replaceUsers(
+          instructionIterator.insertConstNullInstruction(code, options), affectedValues);
+      instructionIterator.next();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index 1dc3ca2..1ea01a3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -46,6 +46,9 @@
     this.appView = appView;
     timing.begin("Register optimizers");
     PrimitiveMethodOptimizer.forEachPrimitiveOptimizer(appView, this::register);
+    register(new ClassOptimizer(appView));
+    register(new ConstructorOptimizer(appView));
+    register(new MethodOptimizer(appView));
     register(new ObjectMethodOptimizer(appView));
     register(new ObjectsMethodOptimizer(appView));
     register(new StringBuilderMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java
new file mode 100644
index 0000000..78b0149
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class MethodOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod invoke;
+
+  MethodOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    invoke = dexItemFactory.methodMethods.invoke;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.methodType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(this.invoke)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getArgument(2), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
index 0c471b0..2b30686 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
@@ -110,18 +110,20 @@
     Map<DexMethod, DexMethod> vMethodRepresentative = new IdentityHashMap<>();
     for (List<ProgramMethod> vMethods : componentVirtualMethods.values()) {
       if (vMethods.size() > 1) {
-        if (Iterables.all(vMethods, this::shouldConsiderForUnboxing)) {
+        if (Iterables.all(vMethods, this::shouldConsiderForUnboxing)
+            && Iterables.any(vMethods, m -> !m.getDefinition().isAbstract())) {
           vMethods.sort(Comparator.comparing(DexClassAndMember::getReference));
           ProgramMethod representative = vMethods.get(0);
           for (int i = 1; i < vMethods.size(); i++) {
             vMethodRepresentative.put(
                 vMethods.get(i).getReference(), representative.getReference());
           }
+          candidateBoxingStatus.put(representative.getReference(), UNPROCESSED_CANDIDATE);
         }
       } else {
         assert vMethods.size() == 1;
         ProgramMethod candidate = vMethods.get(0);
-        if (shouldConsiderForUnboxing(candidate)) {
+        if (shouldConsiderForUnboxing(candidate) && !candidate.getDefinition().isAbstract()) {
           candidateBoxingStatus.put(candidate.getReference(), UNPROCESSED_CANDIDATE);
         }
       }
@@ -131,8 +133,7 @@
 
   private void registerMethodUnboxingStatusIfNeeded(
       ProgramMethod method, ValueBoxingStatus returnStatus, ValueBoxingStatus[] args) {
-    DexMethod representative =
-        virtualMethodsRepresentative.getOrDefault(method.getReference(), method.getReference());
+    DexMethod representative = representative(method.getReference());
     if (args == null && (returnStatus == null || returnStatus.isNotUnboxable())) {
       // Effectively NOT_UNBOXABLE, remove the candidate.
       // TODO(b/307872552): Do we need to remove at the end of the wave for determinism?
@@ -153,6 +154,10 @@
     }
   }
 
+  private DexMethod representative(DexMethod method) {
+    return virtualMethodsRepresentative.getOrDefault(method, method);
+  }
+
   /**
    * Analysis phase: Figures out in each method if parameters, invoke, field accesses and return
    * values are used in boxing operations.
@@ -260,7 +265,9 @@
       if (definition.isArgument()) {
         int shift = BooleanUtils.intValue(!context.getDefinition().isStatic());
         return ValueBoxingStatus.with(
-            new MethodArg(definition.asArgument().getIndex() - shift, context.getReference()));
+            new MethodArg(
+                definition.asArgument().getIndex() - shift,
+                representative(context.getReference())));
       }
       if (definition.isInvokeMethod()) {
         if (boxPrimitiveMethod.isIdenticalTo(definition.asInvokeMethod().getInvokedMethod())) {
@@ -279,7 +286,8 @@
                 .resolveMethodLegacy(invoke.getInvokedMethod(), invoke.getInterfaceBit())
                 .getResolvedProgramMethod();
         if (resolvedMethod != null) {
-          return ValueBoxingStatus.with(new MethodRet(invoke.getInvokedMethod()));
+          return ValueBoxingStatus.with(
+              new MethodRet(representative(resolvedMethod.getReference())));
         }
       }
     }
@@ -346,7 +354,8 @@
     }
 
     NumberUnboxerLens numberUnboxerLens =
-        new NumberUnboxerTreeFixer(appView, unboxingResult).fixupTree(executorService, timing);
+        new NumberUnboxerTreeFixer(appView, unboxingResult, virtualMethodsRepresentative)
+            .fixupTree(executorService, timing);
     appView.rewriteWithLens(numberUnboxerLens, executorService, timing);
     new NumberUnboxerMethodReprocessingEnqueuer(appView, numberUnboxerLens)
         .enqueueMethodsForReprocessing(postMethodProcessorBuilder, executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
index 8e7c278..e33ceb1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
@@ -82,12 +83,14 @@
       ArgumentInfoCollection.Builder builder =
           ArgumentInfoCollection.builder()
               .setArgumentInfosSize(from.getNumberOfArguments(staticMethod));
+      int shift = BooleanUtils.intValue(!staticMethod);
       for (int i = 0; i < from.getParameters().size(); i++) {
         DexType fromType = from.getParameter(i);
         DexType toType = to.getParameter(i);
         if (!fromType.isIdenticalTo(toType)) {
           builder.addArgumentInfo(
-              i, RewrittenTypeInfo.builder().setOldType(fromType).setNewType(toType).build());
+              shift + i,
+              RewrittenTypeInfo.builder().setOldType(fromType).setNewType(toType).build());
         }
       }
       RewrittenTypeInfo returnInfo =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java
index 166cccd..0239e02 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java
@@ -26,16 +26,19 @@
 
 public class NumberUnboxerTreeFixer implements ProgramClassFixer {
 
-  private final Map<DexMethod, MethodBoxingStatusResult> unboxingResult;
   private final AppView<AppInfoWithLiveness> appView;
+  private final Map<DexMethod, MethodBoxingStatusResult> unboxingResult;
+  private final Map<DexMethod, DexMethod> virtualMethodsRepresentative;
 
   private final NumberUnboxerLens.Builder lensBuilder = NumberUnboxerLens.builder();
 
   public NumberUnboxerTreeFixer(
       AppView<AppInfoWithLiveness> appView,
-      Map<DexMethod, MethodBoxingStatusResult> unboxingResult) {
-    this.unboxingResult = unboxingResult;
+      Map<DexMethod, MethodBoxingStatusResult> unboxingResult,
+      Map<DexMethod, DexMethod> virtualMethodsRepresentative) {
     this.appView = appView;
+    this.unboxingResult = unboxingResult;
+    this.virtualMethodsRepresentative = virtualMethodsRepresentative;
   }
 
   public NumberUnboxerLens fixupTree(ExecutorService executorService, Timing timing)
@@ -56,12 +59,16 @@
   public boolean shouldReserveAsIfPinned(ProgramMethod method) {
     // We don't reprocess dependencies of unchanged methods so we have to maintain them
     // with the same signature.
-    return !unboxingResult.containsKey(method.getReference());
+    return !unboxingResult.containsKey(representative(method.getReference()));
+  }
+
+  private DexMethod representative(DexMethod method) {
+    return virtualMethodsRepresentative.getOrDefault(method, method);
   }
 
   private DexEncodedMethod fixupEncodedMethod(
       DexEncodedMethod method, MethodNamingUtility utility) {
-    if (!unboxingResult.containsKey(method.getReference())) {
+    if (!unboxingResult.containsKey(representative(method.getReference()))) {
       assert method
           .getReference()
           .isIdenticalTo(
@@ -69,7 +76,8 @@
                   method, method.getProto(), appView.dexItemFactory().shortType));
       return method;
     }
-    MethodBoxingStatusResult methodBoxingStatus = unboxingResult.get(method.getReference());
+    MethodBoxingStatusResult methodBoxingStatus =
+        unboxingResult.get(representative(method.getReference()));
     assert !methodBoxingStatus.isNoneUnboxable();
     DexProto newProto = fixupProto(method.getProto(), methodBoxingStatus);
     DexMethod newMethod =
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
index 2f4b4b9..f15bba1 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -126,13 +127,12 @@
               }
               ReservedFieldNamingState reservationState =
                   getOrCreateReservedFieldNamingState(frontier);
-              for (DexEncodedField field : clazz.fields()) {
-                DexString reservedName = strategy.getReservedName(field, clazz);
+              for (DexClassAndField field : clazz.classFields()) {
+                DexString reservedName = strategy.getReservedName(field);
                 if (reservedName != null) {
-                  reservationState.markReserved(
-                      reservedName, field.getReference().name, field.getReference().type);
+                  reservationState.markReserved(reservedName, field);
                   // TODO(b/148846065): Consider lazily computing the renaming on actual lookups.
-                  if (reservedName != field.getReference().name) {
+                  if (reservedName.isNotIdenticalTo(field.getName())) {
                     renaming.put(field.getReference(), reservedName);
                   }
                 }
@@ -214,16 +214,15 @@
             });
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private void renameFieldsInUnrelatedClasspathClasses() {
     if (appView.options().getProguardConfiguration().hasApplyMappingFile()) {
       appView
           .appInfo()
           .forEachReferencedClasspathClass(
               clazz -> {
-                for (DexEncodedField field : clazz.fields()) {
-                  DexString reservedName = strategy.getReservedName(field, clazz);
-                  if (reservedName != null && reservedName != field.getReference().name) {
+                for (DexClassAndField field : clazz.classFields()) {
+                  DexString reservedName = strategy.getReservedName(field);
+                  if (reservedName != null && reservedName.isNotIdenticalTo(field.getName())) {
                     renaming.put(field.getReference(), reservedName);
                   }
                 }
@@ -267,8 +266,7 @@
             .forEachProgramField(
                 field -> {
                   DexString newName = renameField(field, state);
-                  namesToBeReservedInImplementsSubclasses.markReserved(
-                      newName, field.getReference().name, field.getReference().type);
+                  namesToBeReservedInImplementsSubclasses.markReserved(newName, field);
                 });
       }
     }
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNamingState.java b/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
index e507112..6490228 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
@@ -49,7 +49,7 @@
   }
 
   public DexString getOrCreateNameFor(ProgramField field) {
-    DexString reservedName = strategy.getReservedName(field.getDefinition(), field.getHolder());
+    DexString reservedName = strategy.getReservedName(field);
     if (reservedName != null) {
       return reservedName;
     }
@@ -57,10 +57,6 @@
     return getOrCreateInternalState(field.getReference()).createNewName(field);
   }
 
-  public void includeReservations(ReservedFieldNamingState reservedNames) {
-    this.reservedNames.includeReservations(reservedNames);
-  }
-
   @Override
   public InternalState createInternalState() {
     return new InternalState();
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNamingStateBase.java b/src/main/java/com/android/tools/r8/naming/FieldNamingStateBase.java
index 693f373..b9f8454 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNamingStateBase.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNamingStateBase.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.ProguardConfiguration;
 import java.util.Map;
 
 abstract class FieldNamingStateBase<T> {
@@ -36,11 +35,10 @@
     return internalStates.computeIfAbsent(internalStateKey, key -> createInternalState());
   }
 
+  @SuppressWarnings("UnusedVariable")
   private DexType getInternalStateKey(DexType type) {
-    ProguardConfiguration proguardConfiguration = appView.options().getProguardConfiguration();
-    return proguardConfiguration.isOverloadAggressively()
-        ? type
-        : appView.dexItemFactory().voidType;
+    // Returning the given type instead of void will implement aggressive overloading.
+    return appView.dexItemFactory().voidType;
   }
 
   abstract T createInternalState();
diff --git a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
index 4f1544e..d3bfef8 100644
--- a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
@@ -3,9 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
@@ -16,11 +19,12 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DisjointSets;
 import com.android.tools.r8.utils.MethodJavaSignatureEquivalence;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.DexClassAndMethodMap;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import java.io.PrintStream;
 import java.util.ArrayList;
@@ -110,11 +114,11 @@
       this.iface = iface;
     }
 
-    DexString getReservedName(DexEncodedMethod method) {
+    DexString getReservedName(DexClassAndMethod method) {
       // If an interface is kept and we are using applymapping, the renamed name for this method
       // is tracked on this level.
       if (appView.options().getProguardConfiguration().hasApplyMappingFile()) {
-        DexString reservedName = minifierState.getReservedName(method, iface);
+        DexString reservedName = minifierState.getReservedName(method);
         if (reservedName != null) {
           return reservedName;
         }
@@ -143,7 +147,7 @@
       this.reservationTypes.add(type);
     }
 
-    void reserveName(DexString reservedName, DexEncodedMethod method) {
+    void reserveName(DexString reservedName, DexClassAndMethod method) {
       forAll(
           s -> {
             s.reservationTypes.forEach(
@@ -154,7 +158,7 @@
           });
     }
 
-    boolean isAvailable(DexString candidate, DexEncodedMethod method) {
+    boolean isAvailable(DexString candidate, DexClassAndMethod method) {
       Boolean result =
           forAny(
               s -> {
@@ -169,14 +173,14 @@
       return result == null || result;
     }
 
-    void addRenaming(DexString newName, DexEncodedMethod method) {
+    void addRenaming(DexString newName, DexClassAndMethod method) {
       forAll(
           s ->
               s.reservationTypes.forEach(
                   resType -> minifierState.getNamingState(resType).addRenaming(newName, method)));
     }
 
-    <T> void forAll(Consumer<InterfaceReservationState> action) {
+    void forAll(Consumer<InterfaceReservationState> action) {
       forAny(
           s -> {
             action.accept(s);
@@ -238,20 +242,19 @@
   class InterfaceMethodGroupState implements Comparable<InterfaceMethodGroupState> {
 
     private final Set<DexCallSite> callSites = new HashSet<>();
-    private final Map<DexEncodedMethod, Set<InterfaceReservationState>> methodStates =
-        new HashMap<>();
-    private final List<DexEncodedMethod> callSiteCollidingMethods = new ArrayList<>();
+    private final DexClassAndMethodMap<Set<InterfaceReservationState>> methodStates =
+        DexClassAndMethodMap.create();
+    private final List<DexClassAndMethod> callSiteCollidingMethods = new ArrayList<>();
 
-    void addState(DexEncodedMethod method, InterfaceReservationState interfaceState) {
-      methodStates.computeIfAbsent(method, m -> new HashSet<>()).add(interfaceState);
+    void addState(DexClassAndMethod method, InterfaceReservationState interfaceState) {
+      methodStates.computeIfAbsent(method, ignoreKey(HashSet::new)).add(interfaceState);
     }
 
     void appendMethodGroupState(InterfaceMethodGroupState state) {
       callSites.addAll(state.callSites);
       callSiteCollidingMethods.addAll(state.callSiteCollidingMethods);
-      for (DexEncodedMethod key : state.methodStates.keySet()) {
-        methodStates.computeIfAbsent(key, k -> new HashSet<>()).addAll(state.methodStates.get(key));
-      }
+      state.methodStates.forEach(
+          (key, value) -> methodStates.computeIfAbsent(key, ignoreKey(HashSet::new)).addAll(value));
     }
 
     void addCallSite(DexCallSite callSite) {
@@ -260,7 +263,6 @@
       callSites.add(callSite);
     }
 
-    @SuppressWarnings("ReferenceEquality")
     DexString getReservedName() {
       if (methodStates.isEmpty()) {
         return null;
@@ -268,13 +270,11 @@
       // It is perfectly fine to have multiple reserved names inside a group. If we have an identity
       // reservation, we have to prioritize that over the others, otherwise we just propose the
       // first ordered reserved name since we do not allow overwriting the name.
-      List<DexEncodedMethod> sortedMethods = Lists.newArrayList(methodStates.keySet());
-      sortedMethods.sort((x, y) -> x.getReference().compareTo(y.getReference()));
       DexString reservedName = null;
-      for (DexEncodedMethod method : sortedMethods) {
+      for (DexClassAndMethod method : methodStates.getKeysSorted()) {
         for (InterfaceReservationState state : methodStates.get(method)) {
           DexString stateReserved = state.getReservedName(method);
-          if (stateReserved == method.getName()) {
+          if (method.getName().isIdenticalTo(stateReserved)) {
             return method.getName();
           } else if (stateReserved != null) {
             reservedName = stateReserved;
@@ -320,7 +320,7 @@
           });
     }
 
-    void forEachState(BiConsumer<DexEncodedMethod, InterfaceReservationState> action) {
+    void forEachState(BiConsumer<DexClassAndMethod, InterfaceReservationState> action) {
       forAnyState(
           (s, i) -> {
             action.accept(s, i);
@@ -328,21 +328,25 @@
           });
     }
 
-    <T> T forAnyState(BiFunction<DexEncodedMethod, InterfaceReservationState, T> callback) {
-      T returnValue;
-      for (Map.Entry<DexEncodedMethod, Set<InterfaceReservationState>> entry :
-          methodStates.entrySet()) {
-        for (InterfaceReservationState state : entry.getValue()) {
-          returnValue = callback.apply(entry.getKey(), state);
-          if (returnValue != null) {
-            return returnValue;
-          }
-        }
+    <T> T forAnyState(BiFunction<DexClassAndMethod, InterfaceReservationState, T> callback) {
+      TraversalContinuation<T, Void> traversalContinuation =
+          methodStates.traverse(
+              (key, value) -> {
+                for (InterfaceReservationState state : value) {
+                  T returnValue = callback.apply(key, state);
+                  if (returnValue != null) {
+                    return TraversalContinuation.doBreak(returnValue);
+                  }
+                }
+                return TraversalContinuation.doContinue();
+              });
+      if (traversalContinuation.isBreak()) {
+        return traversalContinuation.asBreak().getValue();
       }
       return null;
     }
 
-    boolean containsReservation(DexEncodedMethod method, DexType reservationType) {
+    boolean containsReservation(DexClassAndMethod method, DexType reservationType) {
       Set<InterfaceReservationState> states = methodStates.get(method);
       if (states != null) {
         for (InterfaceReservationState state : states) {
@@ -361,15 +365,30 @@
     }
   }
 
+  // Replacing the use of MethodJavaSignatureEquivalence by MethodSignatureEquivalence implements
+  // aggressive overloading.
+  private static final Equivalence<DexClassAndMethod> equivalence =
+      new Equivalence<>() {
+
+        @Override
+        protected boolean doEquivalent(DexClassAndMethod method, DexClassAndMethod other) {
+          return MethodJavaSignatureEquivalence.get()
+              .equivalent(method.getReference(), other.getReference());
+        }
+
+        @Override
+        protected int doHash(DexClassAndMethod method) {
+          return MethodJavaSignatureEquivalence.get().hash(method.getReference());
+        }
+      };
+
   private final AppView<AppInfoWithLiveness> appView;
   private final SubtypingInfo subtypingInfo;
-  private final Equivalence<DexMethod> equivalence;
-  private final Equivalence<DexEncodedMethod> definitionEquivalence;
   private final MethodNameMinifier.State minifierState;
 
   /** A map from DexMethods to all the states linked to interfaces they appear in. */
-  private final Map<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> globalStateMap =
-      new HashMap<>();
+  private final DexClassAndMethodMap<InterfaceMethodGroupState> globalStateMap =
+      createDexClassAndMethodMap();
 
   /** A map for caching all interface states. */
   private final Map<DexType, InterfaceReservationState> interfaceStateMap = new HashMap<>();
@@ -379,25 +398,19 @@
     this.appView = appView;
     this.minifierState = minifierState;
     this.subtypingInfo = subtypingInfo;
-    this.equivalence =
-        appView.options().getProguardConfiguration().isOverloadAggressively()
-            ? MethodSignatureEquivalence.get()
-            : MethodJavaSignatureEquivalence.get();
-    this.definitionEquivalence =
-        new Equivalence<>() {
-          @Override
-          protected boolean doEquivalent(DexEncodedMethod method, DexEncodedMethod other) {
-            return equivalence.equivalent(method.getReference(), other.getReference());
-          }
-
-          @Override
-          protected int doHash(DexEncodedMethod method) {
-            return equivalence.hash(method.getReference());
-          }
-        };
   }
 
-  private Comparator<Wrapper<DexEncodedMethod>> getDefaultInterfaceMethodOrdering() {
+  private static <V> DexClassAndMethodMap<V> createDexClassAndMethodMap() {
+    return new DexClassAndMethodMap<>(new HashMap<>()) {
+
+      @Override
+      protected Wrapper<DexClassAndMethod> wrap(DexClassAndMethod method) {
+        return equivalence.wrap(method);
+      }
+    };
+  }
+
+  private Comparator<DexClassAndMethod> getDefaultInterfaceMethodOrdering() {
     return Comparator.comparing(globalStateMap::get);
   }
 
@@ -430,10 +443,9 @@
     for (DexClass iface : interfaces) {
       InterfaceReservationState inheritanceState = interfaceStateMap.get(iface.type);
       assert inheritanceState != null;
-      for (DexEncodedMethod method : iface.methods()) {
-        Wrapper<DexEncodedMethod> key = definitionEquivalence.wrap(method);
+      for (DexClassAndMethod method : iface.classMethods()) {
         globalStateMap
-            .computeIfAbsent(key, k -> new InterfaceMethodGroupState())
+            .computeIfAbsent(method, ignoreKey(InterfaceMethodGroupState::new))
             .addState(method, inheritanceState);
       }
     }
@@ -451,21 +463,21 @@
     // Note that if the input does not use multi-interface lambdas unificationParent will remain
     // empty.
     timing.begin("Union-find");
-    DisjointSets<Wrapper<DexEncodedMethod>> unification = new DisjointSets<>();
+    DisjointSets<Wrapper<DexClassAndMethod>> unification = new DisjointSets<>();
 
     liveCallSites.forEach(
         callSite -> {
-          Set<Wrapper<DexEncodedMethod>> callSiteMethods = new HashSet<>();
+          Set<Wrapper<DexClassAndMethod>> callSiteMethods = new HashSet<>();
           // Don't report errors, as the set of call sites is a conservative estimate, and can
           // refer to interfaces which has been removed.
-          Set<DexEncodedMethod> implementedMethods =
+          DexClassAndMethodSet implementedMethods =
               appView.appInfo().lookupLambdaImplementedMethods(callSite, appView);
-          for (DexEncodedMethod method : implementedMethods) {
-            Wrapper<DexEncodedMethod> wrapped = definitionEquivalence.wrap(method);
-            InterfaceMethodGroupState groupState = globalStateMap.get(wrapped);
-            assert groupState != null : wrapped;
+          for (DexClassAndMethod method : implementedMethods) {
+            Wrapper<DexClassAndMethod> wrapper = equivalence.wrap(method);
+            InterfaceMethodGroupState groupState = globalStateMap.get(wrapper);
+            assert groupState != null : wrapper;
             groupState.addCallSite(callSite);
-            callSiteMethods.add(wrapped);
+            callSiteMethods.add(wrapper);
           }
           if (callSiteMethods.isEmpty()) {
             return;
@@ -480,8 +492,8 @@
               // name.
               DexClass iface = appView.definitionFor(implementedInterfaces.get(i));
               assert iface.isInterface();
-              for (DexEncodedMethod implementedMethod : implementedMethods) {
-                for (DexEncodedMethod virtualMethod : iface.virtualMethods()) {
+              for (DexClassAndMethod implementedMethod : implementedMethods) {
+                for (DexClassAndMethod virtualMethod : iface.virtualClassMethods()) {
                   boolean differentName = implementedMethod.getName() != virtualMethod.getName();
                   if (differentName
                       && MethodJavaSignatureEquivalence.getEquivalenceIgnoreName()
@@ -489,8 +501,7 @@
                               implementedMethod.getReference(), virtualMethod.getReference())) {
                     InterfaceMethodGroupState interfaceMethodGroupState =
                         globalStateMap.computeIfAbsent(
-                            definitionEquivalence.wrap(implementedMethod),
-                            k -> new InterfaceMethodGroupState());
+                            implementedMethod, ignoreKey(InterfaceMethodGroupState::new));
                     interfaceMethodGroupState.callSiteCollidingMethods.add(virtualMethod);
                   }
                 }
@@ -499,9 +510,9 @@
           }
           if (callSiteMethods.size() > 1) {
             // Implemented interfaces have different protos. Unify them.
-            Wrapper<DexEncodedMethod> mainKey = callSiteMethods.iterator().next();
-            Wrapper<DexEncodedMethod> representative = unification.findOrMakeSet(mainKey);
-            for (Wrapper<DexEncodedMethod> key : callSiteMethods) {
+            Wrapper<DexClassAndMethod> mainKey = callSiteMethods.iterator().next();
+            Wrapper<DexClassAndMethod> representative = unification.findOrMakeSet(mainKey);
+            for (Wrapper<DexClassAndMethod> key : callSiteMethods) {
               unification.unionWithMakeSet(representative, key);
             }
           }
@@ -512,32 +523,32 @@
     // We now have roots for all unions. Add all of the states for the groups to the method state
     // for the unions to allow consistent naming across different protos.
     timing.begin("States for union");
-    Map<Wrapper<DexEncodedMethod>, Set<Wrapper<DexEncodedMethod>>> unions =
-        unification.collectSets();
-
-    for (Wrapper<DexEncodedMethod> wrapped : unions.keySet()) {
-      InterfaceMethodGroupState groupState = globalStateMap.get(wrapped);
-      assert groupState != null;
-
-      for (Wrapper<DexEncodedMethod> groupedMethod : unions.get(wrapped)) {
-        DexEncodedMethod method = groupedMethod.get();
-        assert method != null;
-        groupState.appendMethodGroupState(globalStateMap.get(groupedMethod));
-      }
-    }
+    DexClassAndMethodMap<Set<Wrapper<DexClassAndMethod>>> unions = createDexClassAndMethodMap();
+    unification.consumeSets(
+        (representative, element) ->
+            unions.computeIfAbsent(representative, ignoreKey(HashSet::new)).add(element));
+    unions.forEach(
+        (representative, elements) -> {
+          InterfaceMethodGroupState groupState = globalStateMap.get(representative);
+          assert groupState != null;
+          for (Wrapper<DexClassAndMethod> groupedMethod : elements) {
+            groupState.appendMethodGroupState(globalStateMap.get(groupedMethod));
+          }
+        });
     timing.end();
 
     timing.begin("Sort");
     // Filter out the groups that is included both in the unification and in the map. We sort the
     // methods by the number of dependent states, so that we use short names for method that are
     // referenced in many places.
-    List<Wrapper<DexEncodedMethod>> interfaceMethodGroups =
-        globalStateMap.keySet().stream()
+    List<DexClassAndMethod> interfaceMethodGroups =
+        globalStateMap
+            .streamWrappedKeys()
             .filter(unification::isRepresentativeOrNotPresent)
+            .map(Wrapper::get)
             .sorted(
                 appView
-                    .options()
-                    .testing
+                    .testing()
                     .minifier
                     .getInterfaceMethodOrderingOrDefault(getDefaultInterfaceMethodOrdering()))
             .collect(Collectors.toList());
@@ -549,8 +560,8 @@
     timing.begin("Reserve in groups");
     // It is important that this entire phase is run before given new names, to ensure all
     // reservations are propagated to all naming states.
-    List<Wrapper<DexEncodedMethod>> nonReservedMethodGroups = new ArrayList<>();
-    for (Wrapper<DexEncodedMethod> interfaceMethodGroup : interfaceMethodGroups) {
+    List<DexClassAndMethod> nonReservedMethodGroups = new ArrayList<>();
+    for (DexClassAndMethod interfaceMethodGroup : interfaceMethodGroups) {
       InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
       assert groupState != null;
       DexString reservedName = groupState.getReservedName();
@@ -564,39 +575,43 @@
     timing.end();
 
     timing.begin("Rename in groups");
-    for (Wrapper<DexEncodedMethod> interfaceMethodGroup : nonReservedMethodGroups) {
+    for (DexClassAndMethod interfaceMethodGroup : nonReservedMethodGroups) {
       InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
       assert groupState != null;
       assert groupState.getReservedName() == null;
-      DexString newName = assignNewName(interfaceMethodGroup.get(), groupState);
+      DexString newName = assignNewName(interfaceMethodGroup, groupState);
       assert newName != null;
       Set<String> loggingFilter = appView.options().extensiveInterfaceMethodMinifierLoggingFilter;
       if (!loggingFilter.isEmpty()) {
-        Set<DexEncodedMethod> sourceMethods = groupState.methodStates.keySet();
-        if (sourceMethods.stream()
-            .map(DexEncodedMethod::toSourceString)
+        if (groupState
+            .methodStates
+            .streamKeys()
+            .map(DexClassAndMethod::toSourceString)
             .anyMatch(loggingFilter::contains)) {
-          print(interfaceMethodGroup.get().getReference(), sourceMethods, System.out);
+          print(
+              interfaceMethodGroup.getReference(),
+              groupState.methodStates.getKeysSorted(),
+              System.out);
         }
       }
     }
 
     // After all naming is completed for callsites, we must ensure to rename all interface methods
     // that can collide with the callsite method name.
-    for (Wrapper<DexEncodedMethod> interfaceMethodGroup : nonReservedMethodGroups) {
+    for (DexClassAndMethod interfaceMethodGroup : nonReservedMethodGroups) {
       InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
       if (groupState.callSiteCollidingMethods.isEmpty()) {
         continue;
       }
-      DexEncodedMethod key = interfaceMethodGroup.get();
-      MethodNamingState<?> keyNamingState = minifierState.getNamingState(key.getHolderType());
-      DexString existingRenaming = keyNamingState.newOrReservedNameFor(key);
+      MethodNamingState<?> keyNamingState =
+          minifierState.getNamingState(interfaceMethodGroup.getHolderType());
+      DexString existingRenaming = keyNamingState.newOrReservedNameFor(interfaceMethodGroup);
       assert existingRenaming != null;
-      for (DexEncodedMethod collidingMethod : groupState.callSiteCollidingMethods) {
+      for (DexClassAndMethod collidingMethod : groupState.callSiteCollidingMethods) {
         DexString newNameInGroup = newNameInGroup(collidingMethod, keyNamingState, groupState);
         minifierState.putRenaming(collidingMethod, newNameInGroup);
         MethodNamingState<?> methodNamingState =
-            minifierState.getNamingState(collidingMethod.getReference().holder);
+            minifierState.getNamingState(collidingMethod.getHolderType());
         methodNamingState.addRenaming(newNameInGroup, collidingMethod);
         keyNamingState.addRenaming(newNameInGroup, collidingMethod);
       }
@@ -606,7 +621,7 @@
     timing.end(); // end compute timing
   }
 
-  private DexString assignNewName(DexEncodedMethod method, InterfaceMethodGroupState groupState) {
+  private DexString assignNewName(DexClassAndMethod method, InterfaceMethodGroupState groupState) {
     assert groupState.getReservedName() == null;
     assert groupState.methodStates.containsKey(method);
     assert groupState.containsReservation(method, method.getHolderType());
@@ -620,7 +635,7 @@
   }
 
   private DexString newNameInGroup(
-      DexEncodedMethod method,
+      DexClassAndMethod method,
       MethodNamingState<?> namingState,
       InterfaceMethodGroupState groupState) {
     // Check if the name is available in all states.
@@ -663,50 +678,52 @@
                     }));
   }
 
-  private boolean verifyAllCallSitesAreRepresentedIn(List<Wrapper<DexEncodedMethod>> groups) {
-    Set<Wrapper<DexEncodedMethod>> unifiedMethods = new HashSet<>(groups);
+  private boolean verifyAllCallSitesAreRepresentedIn(List<DexClassAndMethod> groups) {
+    Set<Wrapper<DexClassAndMethod>> unifiedMethods = new HashSet<>(groups.size());
+    groups.forEach(group -> unifiedMethods.add(equivalence.wrap(group)));
     Set<DexCallSite> unifiedSeen = new HashSet<>();
     Set<DexCallSite> seen = new HashSet<>();
-    for (Map.Entry<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> state :
-        globalStateMap.entrySet()) {
-      for (DexCallSite callSite : state.getValue().callSites) {
-        seen.add(callSite);
-        if (unifiedMethods.contains(state.getKey())) {
-          boolean added = unifiedSeen.add(callSite);
-          assert added;
-        }
-      }
-    }
+    globalStateMap.forEach(
+        (key, value) -> {
+          for (DexCallSite callSite : value.callSites) {
+            seen.add(callSite);
+            if (unifiedMethods.contains(equivalence.wrap(key))) {
+              boolean added = unifiedSeen.add(callSite);
+              assert added;
+            }
+          }
+        });
     assert seen.size() == unifiedSeen.size();
     assert unifiedSeen.containsAll(seen);
     return true;
   }
 
-  private boolean verifyAllMethodsAreRepresentedIn(List<Wrapper<DexEncodedMethod>> groups) {
-    Set<Wrapper<DexEncodedMethod>> unifiedMethods = new HashSet<>(groups);
+  private boolean verifyAllMethodsAreRepresentedIn(List<DexClassAndMethod> groups) {
+    Set<Wrapper<DexClassAndMethod>> unifiedMethods = new HashSet<>(groups.size());
+    groups.forEach(group -> unifiedMethods.add(equivalence.wrap(group)));
     Set<DexEncodedMethod> unifiedSeen = Sets.newIdentityHashSet();
     Set<DexEncodedMethod> seen = Sets.newIdentityHashSet();
-    for (Map.Entry<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> state :
-        globalStateMap.entrySet()) {
-      for (DexEncodedMethod method : state.getValue().methodStates.keySet()) {
-        seen.add(method);
-        if (unifiedMethods.contains(state.getKey())) {
-          boolean added = unifiedSeen.add(method);
-          assert added;
-        }
-      }
-    }
+    globalStateMap.forEach(
+        (representative, value) ->
+            value.methodStates.forEachKey(
+                method -> {
+                  seen.add(method.getDefinition());
+                  if (unifiedMethods.contains(equivalence.wrap(representative))) {
+                    boolean added = unifiedSeen.add(method.getDefinition());
+                    assert added;
+                  }
+                }));
     assert seen.size() == unifiedSeen.size();
     assert unifiedSeen.containsAll(seen);
     return true;
   }
 
-  private void print(DexMethod method, Set<DexEncodedMethod> sourceMethods, PrintStream out) {
+  private void print(DexMethod method, List<DexClassAndMethod> sourceMethods, PrintStream out) {
     out.println("-----------------------------------------------------------------------");
     out.println("assignNameToInterfaceMethod(`" + method.toSourceString() + "`)");
     out.println("-----------------------------------------------------------------------");
     out.println("Source methods:");
-    for (DexEncodedMethod sourceMethod : sourceMethods) {
+    for (DexClassAndMethod sourceMethod : sourceMethods) {
       out.println("  " + sourceMethod.toSourceString());
     }
     out.println("States:");
diff --git a/src/main/java/com/android/tools/r8/naming/MapConsumer.java b/src/main/java/com/android/tools/r8/naming/MapConsumer.java
index 236c2b7..01c497f 100644
--- a/src/main/java/com/android/tools/r8/naming/MapConsumer.java
+++ b/src/main/java/com/android/tools/r8/naming/MapConsumer.java
@@ -15,6 +15,5 @@
 
   void accept(
       DiagnosticsHandler diagnosticsHandler,
-      ProguardMapMarkerInfo makerInfo,
       ClassNameMapper classNameMapper);
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java b/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
index 58a4dc6..15b2aae 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
@@ -5,8 +5,9 @@
 package com.android.tools.r8.naming;
 
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.ProgramField;
@@ -15,7 +16,7 @@
 public interface MemberNamingStrategy {
 
   DexString next(
-      DexEncodedMethod method,
+      DexClassAndMethod method,
       InternalNamingState internalState,
       BiPredicate<DexString, DexMethod> isAvailable);
 
@@ -24,9 +25,13 @@
       InternalNamingState internalState,
       BiPredicate<DexString, ProgramField> isAvailable);
 
-  DexString getReservedName(DexEncodedMethod method, DexClass holder);
+  DexString getReservedName(DexClassAndMethod method);
 
-  DexString getReservedName(DexEncodedField field, DexClass holder);
+  DexString getReservedName(DexClassAndField field);
 
-  boolean allowMemberRenaming(DexClass holder);
+  boolean allowMemberRenaming(DexClass clazz);
+
+  default boolean allowMemberRenaming(DexClassAndMember<?, ?> member) {
+    return allowMemberRenaming(member.getHolder());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index 4dd3d15..fa5089d 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -8,6 +8,8 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
@@ -19,15 +21,17 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
@@ -105,9 +109,8 @@
   // from the method name minifier to the interface method name minifier.
   class State {
 
-    @SuppressWarnings("ReferenceEquality")
-    void putRenaming(DexEncodedMethod key, DexString newName) {
-      if (newName != key.getName()) {
+    void putRenaming(DexClassAndMethod key, DexString newName) {
+      if (newName.isNotIdenticalTo(key.getName())) {
         renaming.put(key.getReference(), newName);
       }
     }
@@ -129,8 +132,8 @@
       return frontiers.getOrDefault(type, type);
     }
 
-    DexString getReservedName(DexEncodedMethod method, DexClass holder) {
-      return strategy.getReservedName(method, holder);
+    DexString getReservedName(DexClassAndMethod method) {
+      return strategy.getReservedName(method);
     }
   }
 
@@ -163,14 +166,9 @@
   }
 
   private Function<DexMethod, ?> getReservationKeyTransform() {
-    if (appView.options().getProguardConfiguration().isOverloadAggressively()
-        && appView.options().isGeneratingClassFiles()) {
-      // Use the full proto as key, hence reuse names based on full signature.
-      return method -> method.proto;
-    } else {
-      // Only use the parameters as key, hence do not reuse names on return type.
-      return method -> method.proto.parameters;
-    }
+    // Only use the parameters as key, hence do not reuse names on return type. Returning the full
+    // proto here implements aggressive overloading.
+    return DexMethod::getParameters;
   }
 
   private Function<DexMethod, ?> getNamingKeyTransform() {
@@ -244,23 +242,27 @@
                               .getOrDefault(clazz.superType, rootNamingState)
                               .createChild(reservationState));
               if (strategy.allowMemberRenaming(clazz)) {
-                for (DexEncodedMethod method : clazz.allMethodsSorted()) {
-                  assignNameToMethod(clazz, method, namingState);
+                List<DexClassAndMethod> allMethodsSorted =
+                    ListUtils.sort(
+                        clazz.classMethods(),
+                        Comparator.comparing(DexClassAndMember::getReference),
+                        clazz.getMethodCollection().size());
+                for (DexClassAndMethod method : allMethodsSorted) {
+                  assignNameToMethod(method, namingState);
                 }
               }
             });
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private void renameMethodsInUnrelatedClasspathClasses() {
     if (appView.options().getProguardConfiguration().hasApplyMappingFile()) {
       appView
           .appInfo()
           .forEachReferencedClasspathClass(
               clazz -> {
-                for (DexEncodedMethod method : clazz.methods()) {
-                  DexString reservedName = strategy.getReservedName(method, clazz);
-                  if (reservedName != null && reservedName != method.getReference().name) {
+                for (DexClassAndMethod method : clazz.classMethods()) {
+                  DexString reservedName = strategy.getReservedName(method);
+                  if (reservedName != null && reservedName.isNotIdenticalTo(method.getName())) {
                     renaming.put(method.getReference(), reservedName);
                   }
                 }
@@ -268,20 +270,18 @@
     }
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  private void assignNameToMethod(
-      DexClass holder, DexEncodedMethod method, MethodNamingState<?> state) {
-    if (method.isInitializer()) {
+  private void assignNameToMethod(DexClassAndMethod method, MethodNamingState<?> state) {
+    if (method.getDefinition().isInitializer()) {
       return;
     }
     // The strategy may have an explicit naming for this member which we query first. It may be that
     // the strategy will return the identity name, for which we have to look into a previous
     // renaming tracked by the state.
-    DexString newName = strategy.getReservedName(method, holder);
-    if (newName == null || newName == method.getName()) {
+    DexString newName = strategy.getReservedName(method);
+    if (newName == null || newName.isIdenticalTo(method.getName())) {
       newName = state.newOrReservedNameFor(method);
     }
-    if (method.getName() != newName) {
+    if (newName.isNotIdenticalTo(method.getName())) {
       renaming.put(method.getReference(), newName);
     }
     state.addRenaming(newName, method);
@@ -335,20 +335,22 @@
       // have to do byte-code rewriting against a mapping file to observe the issue. Doing that they
       // may as well just adjust the keep rules to keep the targets of bridges.
       // See b/290711987 for an actual issue regarding this.
-      Set<DexEncodedMethod> bridgeMethodCandidates = Sets.newIdentityHashSet();
-      Iterable<DexEncodedMethod> methods = shuffleMethods(holder.methods(), appView.options());
-      for (DexEncodedMethod method : methods) {
-        DexString reservedName = strategy.getReservedName(method, holder);
+      DexClassAndMethodSet bridgeMethodCandidates = DexClassAndMethodSet.create();
+      Iterable<DexClassAndMethod> methods =
+          shuffleMethods(holder.classMethods(), appView.options());
+      for (DexClassAndMethod method : methods) {
+        DexString reservedName = strategy.getReservedName(method);
         if (reservedName != null) {
           state.reserveName(reservedName, method);
-        } else if (appView.options().isGeneratingClassFiles() && method.isSyntheticBridgeMethod()) {
+        } else if (appView.options().isGeneratingClassFiles()
+            && method.getDefinition().isSyntheticBridgeMethod()) {
           bridgeMethodCandidates.add(method);
         }
       }
       Map<DexString, Set<Integer>> methodNamesToReserve =
           computeBridgesThatAreReserved(holder, bridgeMethodCandidates);
       if (!methodNamesToReserve.isEmpty()) {
-        for (DexEncodedMethod method : methods) {
+        for (DexClassAndMethod method : methods) {
           if (methodNamesToReserve
               .getOrDefault(method.getName(), Collections.emptySet())
               .contains(method.getProto().getArity())) {
@@ -360,7 +362,7 @@
   }
 
   private Map<DexString, Set<Integer>> computeBridgesThatAreReserved(
-      DexClass holder, Set<DexEncodedMethod> methods) {
+      DexClass holder, DexClassAndMethodSet methods) {
     if (methods.isEmpty()) {
       return Collections.emptyMap();
     }
@@ -495,8 +497,8 @@
 
   // Shuffles the given methods if assertions are enabled and deterministic debugging is disabled.
   // Used to ensure that the generated output is deterministic.
-  private static Iterable<DexEncodedMethod> shuffleMethods(
-      Iterable<DexEncodedMethod> methods, InternalOptions options) {
-    return options.testing.irOrdering.order(methods);
+  private static Iterable<DexClassAndMethod> shuffleMethods(
+      Iterable<DexClassAndMethod> methods, InternalOptions options) {
+    return options.testing.irOrdering.orderClassMethods(methods);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNamingState.java b/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
index 922c062..7a38768 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.naming.MethodNamingState.InternalNewNameState;
@@ -45,12 +45,12 @@
         this, this.keyTransform, this.namingStrategy, frontierReservationState);
   }
 
-  DexString newOrReservedNameFor(DexEncodedMethod method) {
+  DexString newOrReservedNameFor(DexClassAndMethod method) {
     return newOrReservedNameFor(method, this::isAvailable);
   }
 
   DexString newOrReservedNameFor(
-      DexEncodedMethod method, BiPredicate<DexString, DexMethod> isAvailable) {
+      DexClassAndMethod method, BiPredicate<DexString, DexMethod> isAvailable) {
     DexString newName = getAssignedName(method.getReference());
     if (newName != null) {
       return newName;
@@ -67,14 +67,14 @@
     return nextName(method, isAvailable);
   }
 
-  DexString nextName(DexEncodedMethod method, BiPredicate<DexString, DexMethod> isAvailable) {
+  DexString nextName(DexClassAndMethod method, BiPredicate<DexString, DexMethod> isAvailable) {
     InternalNewNameState internalState = getOrCreateInternalState(method.getReference());
     DexString newName = namingStrategy.next(method, internalState, isAvailable);
     assert newName != null;
     return newName;
   }
 
-  void addRenaming(DexString newName, DexEncodedMethod method) {
+  void addRenaming(DexString newName, DexClassAndMethod method) {
     InternalNewNameState internalState = getOrCreateInternalState(method.getReference());
     internalState.addRenaming(newName, method.getReference());
   }
diff --git a/src/main/java/com/android/tools/r8/naming/MethodReservationState.java b/src/main/java/com/android/tools/r8/naming/MethodReservationState.java
index 61aed28..68d0378 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodReservationState.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodReservationState.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.naming;
 
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.naming.MethodReservationState.InternalReservationState;
@@ -37,7 +37,7 @@
     return new MethodReservationState<>(this, this.keyTransform);
   }
 
-  void reserveName(DexString reservedName, DexEncodedMethod method) {
+  void reserveName(DexString reservedName, DexClassAndMethod method) {
     try {
       getOrCreateInternalState(method.getReference()).reserveName(method, reservedName);
     } catch (AssertionError err) {
@@ -92,7 +92,7 @@
       return originalToReservedNames.get(MethodSignatureEquivalence.get().wrap(method));
     }
 
-    void reserveName(DexEncodedMethod method, DexString name) {
+    void reserveName(DexClassAndMethod method, DexString name) {
       if (reservedNames == null) {
         assert originalToReservedNames == null;
         originalToReservedNames = new HashMap<>();
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index 96275ab..6c08cef 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -3,19 +3,22 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.utils.StringUtils.EMPTY_CHAR_ARRAY;
 import static com.android.tools.r8.utils.SymbolGenerationUtils.RESERVED_NAMES;
 
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassNamingStrategy;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassRenaming;
@@ -206,8 +209,9 @@
 
     @Override
     public DexString reservedDescriptor(DexType type) {
-      if (!appView.appInfo().isMinificationAllowed(type)) {
-        return type.descriptor;
+      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+      if (clazz == null || !appView.getKeepInfo(clazz).isMinificationAllowed(appView.options())) {
+        return type.getDescriptor();
       }
       return null;
     }
@@ -277,10 +281,14 @@
 
     @Override
     public DexString next(
-        DexEncodedMethod method,
+        DexClassAndMethod method,
         InternalNamingState internalState,
         BiPredicate<DexString, DexMethod> isAvailable) {
-      assert checkAllowMemberRenaming(method.getHolderType());
+      if (!method.isProgramMethod()) {
+        assert isAvailable.test(method.getName(), method.getReference());
+        return method.getName();
+      }
+      assert allowMemberRenaming(method);
       DexString candidate;
       do {
         candidate = getNextName(internalState);
@@ -293,7 +301,7 @@
         ProgramField field,
         InternalNamingState internalState,
         BiPredicate<DexString, ProgramField> isAvailable) {
-      assert checkAllowMemberRenaming(field.getHolderType());
+      assert allowMemberRenaming(field);
       DexString candidate;
       do {
         candidate = getNextName(internalState);
@@ -306,40 +314,39 @@
     }
 
     @Override
-    public DexString getReservedName(DexEncodedMethod method, DexClass holder) {
-      if (!allowMemberRenaming(holder)
-          || holder.accessFlags.isAnnotation()
-          || method.accessFlags.isConstructor()
-          || !appView.appInfo().isMinificationAllowed(method)) {
-        return method.getReference().name;
+    public DexString getReservedName(DexClassAndMethod method) {
+      if (!allowMemberRenaming(method)) {
+        return method.getName();
+      }
+      assert method.isProgramMethod();
+      ProgramMethod programMethod = method.asProgramMethod();
+      if (method.getHolder().isAnnotation()
+          || method.getAccessFlags().isConstructor()
+          || !appView.getKeepInfo(programMethod).isMinificationAllowed(appView.options())) {
+        return method.getName();
       }
       if (desugaredLibraryRenaming
-          && method.isLibraryMethodOverride().isTrue()
-          && appView.typeRewriter.hasRewrittenTypeInSignature(
-              method.getReference().proto, appView)) {
+          && method.getDefinition().isLibraryMethodOverride().isTrue()
+          && appView.typeRewriter.hasRewrittenTypeInSignature(method.getProto(), appView)) {
         // With desugared library, call-backs names are reserved here.
-        return method.getReference().name;
+        return method.getName();
       }
       return null;
     }
 
     @Override
-    public DexString getReservedName(DexEncodedField field, DexClass holder) {
-      if (holder.isLibraryClass() || !appView.appInfo().isMinificationAllowed(field)) {
-        return field.getReference().name;
+    public DexString getReservedName(DexClassAndField field) {
+      ProgramField programField = field.asProgramField();
+      if (programField == null
+          || !appView.getKeepInfo(programField).isMinificationAllowed(appView.options())) {
+        return field.getName();
       }
       return null;
     }
 
     @Override
-    public boolean allowMemberRenaming(DexClass holder) {
-      return holder.isProgramClass();
-    }
-
-    public boolean checkAllowMemberRenaming(DexType holder) {
-      DexClass clazz = appView.definitionFor(holder);
-      assert clazz != null && allowMemberRenaming(clazz);
-      return true;
+    public boolean allowMemberRenaming(DexClass clazz) {
+      return clazz.isProgramClass();
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/NamingLens.java b/src/main/java/com/android/tools/r8/naming/NamingLens.java
index ed4bbbb..3e551cf 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingLens.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -20,6 +21,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import com.google.common.collect.Sets;
 import java.util.Arrays;
 import java.util.Set;
@@ -59,7 +61,7 @@
       return callSite.methodName;
     }
     AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-    Set<DexEncodedMethod> lambdaImplementedMethods =
+    DexClassAndMethodSet lambdaImplementedMethods =
         appViewWithLiveness.appInfo().lookupLambdaImplementedMethods(callSite, appViewWithLiveness);
     if (lambdaImplementedMethods.isEmpty()) {
       return callSite.methodName;
@@ -70,7 +72,7 @@
         lookupMethod(lambdaImplementedMethodReference, appView.dexItemFactory()).getName();
     // Verify that all lambda implemented methods are renamed consistently.
     assert lambdaImplementedMethods.stream()
-        .map(DexEncodedMethod::getReference)
+        .map(DexClassAndMethod::getReference)
         .map(reference -> lookupMethod(reference, appView.dexItemFactory()))
         .map(DexMethod::getName)
         .allMatch(name -> name == renamedMethodName);
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java
index 8394ecf..7084d2d 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.Version;
 import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapId;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.VersionProperties;
 import java.util.ArrayList;
 import java.util.List;
@@ -63,10 +62,6 @@
     return preamble;
   }
 
-  public String serializeToString() {
-    return StringUtils.unixLines(toPreamble());
-  }
-
   public static Builder builder() {
     return new Builder();
   }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
index 588666a..ddb57d6 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.naming;
 
 import static com.android.tools.r8.graph.DexApplication.classesWithDeterministicOrder;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.defaultAsMethodOfCompanionClass;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.getInterfaceClassType;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.isCompanionClassType;
@@ -13,12 +14,15 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexDefinition;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -187,10 +191,9 @@
           memberNaming -> addMemberNamings(type, memberNaming, nonPrivateMembers, false));
     } else {
       // We have to ensure we do not rename to an existing member, that cannot be renamed.
-      if (clazz == null || !appView.options().isMinifying()) {
-        notMappedReferences.add(type);
-      } else if (appView.options().isMinifying()
-          && !appView.appInfo().isMinificationAllowed(type)) {
+      DexProgramClass programClass = asProgramClassOrNull(clazz);
+      if (programClass == null
+          || !appView.getKeepInfo(programClass).isMinificationAllowed(appView.options())) {
         notMappedReferences.add(type);
       }
     }
@@ -400,7 +403,9 @@
     public DexString next(
         DexType type, char[] packagePrefix, InternalNamingState state, Predicate<String> isUsed) {
       assert !mappings.containsKey(type);
-      assert appView.appInfo().isMinificationAllowed(type);
+      assert appView
+          .getKeepInfo(appView.definitionFor(type).asProgramClass())
+          .isMinificationAllowed(appView.options());
       return super.next(
           type,
           packagePrefix,
@@ -420,19 +425,21 @@
       //  members that can be reserved differently in the hierarchy.
       DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(type);
       if (clazz == null) {
-        return type.descriptor;
-      }
-      if (clazz.isNotProgramClass() && mappings.containsKey(type)) {
-        return mappings.get(type);
+        return type.getDescriptor();
       }
       if (clazz.isProgramClass()) {
-        if (appView.appInfo().isMinificationAllowed(clazz.asProgramClass())) {
+        DexProgramClass programClass = clazz.asProgramClass();
+        if (appView.getKeepInfo(programClass).isMinificationAllowed(appView.options())) {
           return mappings.get(type);
         }
         // TODO(b/136694827): Report a warning here if in the mapping since the user may find this
         //  non intuitive.
+      } else {
+        if (mappings.containsKey(type)) {
+          return mappings.get(type);
+        }
       }
-      return type.descriptor;
+      return type.getDescriptor();
     }
 
     @Override
@@ -456,13 +463,11 @@
     @Override
     @SuppressWarnings("ReferenceEquality")
     public DexString next(
-        DexEncodedMethod method,
+        DexClassAndMethod method,
         InternalNamingState internalState,
         BiPredicate<DexString, DexMethod> isAvailable) {
       DexMethod reference = method.getReference();
-      DexClass holder = appView.definitionForHolder(reference);
-      assert holder != null;
-      DexString reservedName = getReservedName(method, reference.name, holder);
+      DexString reservedName = getReservedName(method, reference.getName());
       DexString nextName;
       if (reservedName != null) {
         if (!isAvailable.test(reservedName, reference)) {
@@ -471,11 +476,14 @@
         nextName = reservedName;
       } else {
         assert !mappedNames.containsKey(reference);
-        assert appView.appInfo().isMinificationAllowed(method);
+        assert !method.isProgramMethod()
+            || appView
+                .getKeepInfo(method.asProgramMethod())
+                .isMinificationAllowed(appView.options());
         nextName = super.next(method, internalState, isAvailable);
       }
-      assert nextName == reference.name || !method.isInitializer();
-      assert nextName == reference.name || !holder.isAnnotation();
+      assert nextName.isIdenticalTo(reference.getName()) || !method.getDefinition().isInitializer();
+      assert nextName.isIdenticalTo(reference.getName()) || !method.getHolder().isAnnotation();
       return nextName;
     }
 
@@ -485,8 +493,7 @@
         InternalNamingState internalState,
         BiPredicate<DexString, ProgramField> isAvailable) {
       DexField reference = field.getReference();
-      DexString reservedName =
-          getReservedName(field.getDefinition(), reference.name, field.getHolder());
+      DexString reservedName = getReservedName(field, reference.getName());
       if (reservedName != null) {
         if (!isAvailable.test(reservedName, field)) {
           reportReservationError(reference, reservedName);
@@ -494,35 +501,34 @@
         return reservedName;
       }
       assert !mappedNames.containsKey(reference);
-      assert appView.appInfo().isMinificationAllowed(field);
+      assert appView.getKeepInfo(field).isMinificationAllowed(appView.options());
       return super.next(field, internalState, isAvailable);
     }
 
     @Override
-    public DexString getReservedName(DexEncodedMethod method, DexClass holder) {
-      return getReservedName(method, method.getReference().name, holder);
+    public DexString getReservedName(DexClassAndMethod method) {
+      return getReservedName(method, method.getName());
     }
 
     @Override
-    public DexString getReservedName(DexEncodedField field, DexClass holder) {
-      return getReservedName(field, field.getReference().name, holder);
+    public DexString getReservedName(DexClassAndField field) {
+      return getReservedName(field, field.getName());
     }
 
-    private DexString getReservedName(DexDefinition definition, DexString name, DexClass holder) {
-      assert definition.isDexEncodedMethod() || definition.isDexEncodedField();
+    private DexString getReservedName(DexClassAndMember<?, ?> definition, DexString name) {
       // Always consult the mapping for renamed members that are not on program path.
       DexReference reference = definition.getReference();
-      if (holder.isNotProgramClass()) {
+      if (definition.getHolder().isNotProgramClass()) {
         if (mappedNames.containsKey(reference)) {
           return factory.createString(mappedNames.get(reference).getRenamedName());
         }
         return name;
       }
-      assert holder.isProgramClass();
+      assert definition.isProgramMember();
       DexString reservedName =
-          definition.isDexEncodedMethod()
-              ? super.getReservedName(definition.asDexEncodedMethod(), holder)
-              : super.getReservedName(definition.asDexEncodedField(), holder);
+          definition.isMethod()
+              ? super.getReservedName(definition.asMethod())
+              : super.getReservedName(definition.asField());
       if (reservedName != null) {
         return reservedName;
       }
@@ -533,7 +539,7 @@
     }
 
     @Override
-    public boolean allowMemberRenaming(DexClass holder) {
+    public boolean allowMemberRenaming(DexClass clazz) {
       return true;
     }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 559d7f0..7d60962 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -652,6 +652,24 @@
 
   // Parsing of components
 
+  private static boolean isAllowedIdentifierStart(int codePoint) {
+    if (IdentifierUtils.isDexIdentifierStart(codePoint)) {
+      return true;
+    }
+    // Proguard sometimes outputs a ? as a method name. We have tools (dexsplitter) that depends
+    // on being able to map class names back to the original, but does not care if methods are
+    // correctly mapped. Using this on proguard output for anything else might not give correct
+    // remappings.
+    if (IdentifierUtils.isQuestionMark(codePoint)) {
+      return true;
+    }
+    // Some mapping files contain entries starting with a '.', allow those for compatibility.
+    if (codePoint == '.') {
+      return true;
+    }
+    return false;
+  }
+
   private void skipIdentifier(boolean allowInit) {
     boolean isInit = false;
     if (allowInit && peekChar(0) == '<') {
@@ -659,12 +677,7 @@
       nextChar();
       isInit = true;
     }
-    // Proguard sometimes outputs a ? as a method name. We have tools (dexsplitter) that depends
-    // on being able to map class names back to the original, but does not care if methods are
-    // correctly mapped. Using this on proguard output for anything else might not give correct
-    // remappings.
-    if (!IdentifierUtils.isDexIdentifierStart(peekCodePoint())
-        && !IdentifierUtils.isQuestionMark(peekCodePoint())) {
+    if (!isAllowedIdentifierStart(peekCodePoint())) {
       throw new ParseException("Identifier expected");
     }
     nextCodePoint();
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java b/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java
index 71b60d9..6f2091c 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java
@@ -26,10 +26,8 @@
   @Override
   public void accept(
       DiagnosticsHandler diagnosticsHandler,
-      ProguardMapMarkerInfo markerInfo,
       ClassNameMapper classNameMapper) {
     this.diagnosticsHandler = diagnosticsHandler;
-    accept(markerInfo.serializeToString());
     accept(StringUtils.unixLines(classNameMapper.getPreamble()));
     classNameMapper.write(this);
   }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
index 0637406..ed3cc24 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
@@ -66,16 +67,20 @@
 
   public ProguardMapId writeProguardMap() {
     ProguardMapId proguardMapId = computeProguardMapId();
-    consumer.accept(
-        reporter,
+    ProguardMapMarkerInfo markerInfo =
         ProguardMapMarkerInfo.builder()
             .setCompilerName(compiler.name())
             .setProguardMapId(proguardMapId)
             .setGeneratingDex(options.isGeneratingDex())
             .setApiLevel(options.getMinApiLevel())
             .setMapVersion(options.getMapFileVersion())
-            .build(),
-        classNameMapper);
+            .build();
+
+    // Set or compose the marker in the preamble information.
+    classNameMapper.setPreamble(
+        ListUtils.concat(markerInfo.toPreamble(), classNameMapper.getPreamble()));
+
+    consumer.accept(reporter, classNameMapper);
     ExceptionUtils.withConsumeResourceHandler(reporter, this.consumer::finished);
     return proguardMapId;
   }
diff --git a/src/main/java/com/android/tools/r8/naming/ReservedFieldNamingState.java b/src/main/java/com/android/tools/r8/naming/ReservedFieldNamingState.java
index 2b66d18..fe4799b 100644
--- a/src/main/java/com/android/tools/r8/naming/ReservedFieldNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/ReservedFieldNamingState.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.ReservedFieldNamingState.InternalState;
@@ -43,8 +44,8 @@
     return internalState == null ? null : internalState.getReservedByName(name);
   }
 
-  void markReserved(DexString name, DexString originalName, DexType type) {
-    getOrCreateInternalState(type).markReserved(name, originalName);
+  void markReserved(DexString name, DexClassAndField field) {
+    getOrCreateInternalState(field.getType()).markReserved(name, field.getName());
   }
 
   void includeReservations(ReservedFieldNamingState reservedNames) {
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
index e07407b..50c7bf7 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
@@ -9,14 +9,12 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepClassInfo;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -31,7 +29,7 @@
   private final AccessModifier accessModifier;
   private final AccessModifierNamingState namingState;
 
-  private final Map<DexType, TraversalState> states = new IdentityHashMap<>();
+  private final Map<DexProgramClass, TraversalState> states = new IdentityHashMap<>();
 
   AccessModifierTraversal(
       AppView<AppInfoWithLiveness> appView,
@@ -57,29 +55,13 @@
     // TODO(b/279126633): Store a top down traversal state for the current class, which contains the
     //  protected and public method signatures when traversing downwards to enable publicizing of
     //  package private methods with illegal overrides.
-    states.put(clazz.getType(), TopDownTraversalState.empty());
+    states.put(clazz, TopDownTraversalState.empty());
   }
 
   /** Called during backtracking when all subclasses of {@param clazz} have been processed. */
   @Override
   public void prune(DexProgramClass clazz) {
-    // Remove the traversal state since all subclasses have now been processed.
-    states.remove(clazz.getType());
-
-    // Remove and join the bottom up traversal states of the subclasses.
-    KeepClassInfo keepInfo = appView.getKeepInfo(clazz);
-    InternalOptions options = appView.options();
-    BottomUpTraversalState state =
-        new BottomUpTraversalState(
-            !keepInfo.isMinificationAllowed(options) && !keepInfo.isShrinkingAllowed(options));
-    forEachSubClass(
-        clazz,
-        subclass -> {
-          BottomUpTraversalState subState =
-              MapUtils.removeOrDefault(states, subclass.getType(), BottomUpTraversalState.empty())
-                  .asBottomUpTraversalState();
-          state.add(subState);
-        });
+    BottomUpTraversalState state = getOrCreateBottomUpTraversalState(clazz);
 
     // Apply access modification to the class and its members.
     accessModifier.processClass(clazz, namingState, state);
@@ -88,19 +70,48 @@
     clazz.forEachProgramVirtualMethod(state::addMethod);
 
     // Store the bottom up traversal state for the current class.
-    if (state.isEmpty()) {
-      states.remove(clazz.getType());
-    } else {
-      states.put(clazz.getType(), state);
+    if (!state.isEmpty()) {
+      immediateSubtypingInfo.forEachImmediateProgramSuperClass(
+          clazz,
+          superClass -> {
+            BottomUpTraversalState superState = getOrCreateBottomUpTraversalState(superClass);
+            superState.add(state);
+          });
     }
+
+    // Done processing the current class and all subclasses.
+    states.remove(clazz);
   }
 
-  abstract static class TraversalState {
+  private BottomUpTraversalState getOrCreateBottomUpTraversalState(DexProgramClass clazz) {
+    TraversalState traversalState = states.get(clazz);
+    if (traversalState == null || traversalState.isTopDownTraversalState()) {
+      KeepClassInfo keepInfo = appView.getKeepInfo(clazz);
+      InternalOptions options = appView.options();
+      BottomUpTraversalState newState =
+          new BottomUpTraversalState(
+              !keepInfo.isMinificationAllowed(options) && !keepInfo.isShrinkingAllowed(options));
+      states.put(clazz, newState);
+      return newState;
+    }
+    assert traversalState.isBottomUpTraversalState();
+    return traversalState.asBottomUpTraversalState();
+  }
+
+  private abstract static class TraversalState {
+
+    boolean isBottomUpTraversalState() {
+      return false;
+    }
 
     BottomUpTraversalState asBottomUpTraversalState() {
       return null;
     }
 
+    boolean isTopDownTraversalState() {
+      return false;
+    }
+
     TopDownTraversalState asTopDownTraversalState() {
       return null;
     }
@@ -108,7 +119,7 @@
 
   // TODO(b/279126633): Collect the protected and public method signatures when traversing downwards
   //  to enable publicizing of package private methods with illegal overrides.
-  static class TopDownTraversalState extends TraversalState {
+  private static class TopDownTraversalState extends TraversalState {
 
     private static final TopDownTraversalState EMPTY = new TopDownTraversalState();
 
@@ -117,12 +128,13 @@
     }
 
     @Override
-    TopDownTraversalState asTopDownTraversalState() {
-      return this;
+    boolean isTopDownTraversalState() {
+      return true;
     }
 
-    boolean isEmpty() {
-      return true;
+    @Override
+    TopDownTraversalState asTopDownTraversalState() {
+      return this;
     }
   }
 
@@ -136,20 +148,29 @@
     // The set of non-private virtual methods below the current class.
     DexMethodSignatureMap<Set<String>> nonPrivateVirtualMethods;
 
-    BottomUpTraversalState(boolean isKept) {
+    private BottomUpTraversalState(boolean isKept) {
       this(DexMethodSignatureMap.create());
       this.isKeptOrHasKeptSubclass = isKept;
     }
 
-    BottomUpTraversalState(DexMethodSignatureMap<Set<String>> packagePrivateMethods) {
+    private BottomUpTraversalState(DexMethodSignatureMap<Set<String>> packagePrivateMethods) {
       this.nonPrivateVirtualMethods = packagePrivateMethods;
     }
 
+    static BottomUpTraversalState asBottomUpTraversalStateOrNull(TraversalState traversalState) {
+      return (BottomUpTraversalState) traversalState;
+    }
+
     static BottomUpTraversalState empty() {
       return EMPTY;
     }
 
     @Override
+    boolean isBottomUpTraversalState() {
+      return true;
+    }
+
+    @Override
     BottomUpTraversalState asBottomUpTraversalState() {
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
index 4b4b611..7503fa5 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -114,7 +114,7 @@
                 assert false;
               }
             });
-        methodAccessInfoCollectionModifier.commit(appView);
+        methodAccessInfoCollection.verify(appView);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index e222e20..6fb0c2f 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -42,7 +42,6 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
-import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
@@ -71,6 +70,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.Visibility;
 import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.collections.ThrowingSet;
 import com.android.tools.r8.utils.structural.Ordered;
@@ -764,14 +764,14 @@
    * @return Methods implemented by the lambda expression that created the {@code callSite}.
    */
   @SuppressWarnings("ReferenceEquality")
-  public Set<DexEncodedMethod> lookupLambdaImplementedMethods(
+  public DexClassAndMethodSet lookupLambdaImplementedMethods(
       DexCallSite callSite, AppView<AppInfoWithLiveness> appView) {
     assert checkIfObsolete();
     List<DexType> callSiteInterfaces = LambdaDescriptor.getInterfaces(callSite, appView);
     if (callSiteInterfaces == null || callSiteInterfaces.isEmpty()) {
-      return Collections.emptySet();
+      return DexClassAndMethodSet.empty();
     }
-    Set<DexEncodedMethod> result = Sets.newIdentityHashSet();
+    DexClassAndMethodSet result = DexClassAndMethodSet.create();
     Deque<DexType> worklist = new ArrayDeque<>(callSiteInterfaces);
     Set<DexType> visited = Sets.newIdentityHashSet();
     while (!worklist.isEmpty()) {
@@ -794,8 +794,9 @@
         continue;
       }
       assert clazz.isInterface();
-      for (DexEncodedMethod method : clazz.virtualMethods()) {
-        if (method.getReference().name == callSite.methodName && method.accessFlags.isAbstract()) {
+      for (DexClassAndMethod method : clazz.virtualClassMethods()) {
+        if (method.getName().isIdenticalTo(callSite.methodName)
+            && method.getAccessFlags().isAbstract()) {
           result.add(method);
         }
       }
@@ -1009,30 +1010,6 @@
     return this;
   }
 
-  @Deprecated
-  public boolean isMinificationAllowed(DexProgramClass clazz) {
-    return options().isMinificationEnabled()
-        && keepInfo.getInfo(clazz).isMinificationAllowed(options());
-  }
-
-  @Deprecated
-  public boolean isMinificationAllowed(ProgramDefinition definition) {
-    return options().isMinificationEnabled()
-        && keepInfo.getInfo(definition).isMinificationAllowed(options());
-  }
-
-  @Deprecated
-  public boolean isMinificationAllowed(DexDefinition definition) {
-    return options().isMinificationEnabled()
-        && keepInfo.getInfo(definition, this).isMinificationAllowed(options());
-  }
-
-  @Deprecated
-  public boolean isMinificationAllowed(DexType reference) {
-    return options().isMinificationEnabled()
-        && keepInfo.getClassInfo(reference, this).isMinificationAllowed(options());
-  }
-
   public boolean isRepackagingAllowed(DexProgramClass clazz, AppView<?> appView) {
     if (!keepInfo.getInfo(clazz).isRepackagingAllowed(options())) {
       return false;
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 4cdfc22..63e6bbb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -5177,7 +5177,9 @@
     int parametersSize =
         newArrayEmpty != null
             ? newArrayEmpty.sizeIfConst()
-            : newArrayFilled != null ? newArrayFilled.size() : -1;
+            : newArrayFilled != null
+                ? newArrayFilled.size()
+                : parametersValue.isAlwaysNull(appView) ? 0 : -1;
     if (parametersSize < 0) {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java b/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
index 1410247..933cc33 100644
--- a/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
+++ b/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
@@ -328,7 +328,9 @@
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     ProguardIfRule materializedRule = rule.materialize(dexItemFactory, preconditions);
 
-    if (enqueuer.getMode().isInitialTreeShaking() && !rule.isUsed()) {
+    if (enqueuer.getMode().isInitialTreeShaking()
+        && !rule.isUsed()
+        && !rule.isTrivalAllClassMatch()) {
       // We need to abort class inlining of classes that could be matched by the condition of this
       // -if rule.
       ClassInlineRule neverClassInlineRuleForCondition =
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardAccessFlags.java b/src/main/java/com/android/tools/r8/shaking/ProguardAccessFlags.java
index cdf0264..4c35b96 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardAccessFlags.java
@@ -239,6 +239,10 @@
     return this.flags == ((ProguardAccessFlags) obj).flags;
   }
 
+  public boolean isDefaultFlags() {
+    return flags == 0;
+  }
+
   @Override
   public int hashCode() {
     return flags;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index d7e4ff2..98bb147 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -609,10 +609,6 @@
     return rules;
   }
 
-  public boolean isOverloadAggressively() {
-    return false;
-  }
-
   public List<String> getObfuscationDictionary() {
     return obfuscationDictionary;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index 6099834..01c9118 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -60,6 +61,23 @@
         memberRules);
   }
 
+  public boolean isTrivalAllClassMatch() {
+    BooleanBox booleanBox = new BooleanBox(true);
+    getClassNames()
+        .forEachTypeMatcher(
+            unused -> booleanBox.set(false),
+            proguardTypeMatcher -> !proguardTypeMatcher.isMatchAnyClassPattern());
+    return booleanBox.get()
+        && getClassAnnotations().isEmpty()
+        && getClassAccessFlags().isDefaultFlags()
+        && getNegatedClassAccessFlags().isDefaultFlags()
+        && !getClassTypeNegated()
+        && getClassType() == ProguardClassType.CLASS
+        && getInheritanceAnnotations().isEmpty()
+        && getInheritanceClassName() == null
+        && getMemberRules().isEmpty();
+  }
+
   public boolean isUsed() {
     return used;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
index 8e19b25..79b6f5f 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.util.List;
 import java.util.Map;
@@ -195,7 +194,7 @@
             ? null
             : getInheritanceClassName().materialize(dexItemFactory),
         getInheritanceIsExtends(),
-        ImmutableList.of(),
+        getMemberRules(),
         ClassInlineRule.Type.NEVER);
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
index a8233bf..f9aef9b 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
@@ -43,6 +43,10 @@
   // Evaluates this matcher on the given type.
   public abstract boolean matches(DexType type);
 
+  public boolean isMatchAnyClassPattern() {
+    return false;
+  }
+
   // Evaluates this matcher on the given type, and on all types that have been merged into the given
   // type, if any.
   public final boolean matches(DexType type, AppView<?> appView) {
@@ -167,6 +171,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return true;
+    }
+
+    @Override
     public boolean matches(DexType type) {
       wildcard.setCaptured(type.toSourceString());
       return true;
@@ -249,6 +258,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return true;
+    }
+
+    @Override
     public boolean matches(DexType type) {
       if (type.isClassType()) {
         wildcard.setCaptured(type.toSourceString());
diff --git a/src/main/java/com/android/tools/r8/utils/DexClassAndMethodEquivalence.java b/src/main/java/com/android/tools/r8/utils/DexClassAndMethodEquivalence.java
new file mode 100644
index 0000000..acb221d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DexClassAndMethodEquivalence.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.google.common.base.Equivalence;
+
+public class DexClassAndMethodEquivalence extends Equivalence<DexClassAndMethod> {
+
+  private static final DexClassAndMethodEquivalence INSTANCE = new DexClassAndMethodEquivalence();
+
+  private DexClassAndMethodEquivalence() {}
+
+  public static DexClassAndMethodEquivalence get() {
+    return INSTANCE;
+  }
+
+  @Override
+  protected boolean doEquivalent(DexClassAndMethod method, DexClassAndMethod other) {
+    return method.getDefinition() == other.getDefinition();
+  }
+
+  @Override
+  protected int doHash(DexClassAndMethod method) {
+    return method.getReference().hashCode();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DisjointSets.java b/src/main/java/com/android/tools/r8/utils/DisjointSets.java
index d8e42b9..bc5e129 100644
--- a/src/main/java/com/android/tools/r8/utils/DisjointSets.java
+++ b/src/main/java/com/android/tools/r8/utils/DisjointSets.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 /**
  * Disjoint sets of instances of type T. Each of the sets will be represented by one of the
@@ -134,12 +137,18 @@
   /** Returns the sets currently represented. */
   public Map<T, Set<T>> collectSets() {
     Map<T, Set<T>> unification = new HashMap<>();
+    consumeSets(
+        (representative, element) ->
+            unification.computeIfAbsent(representative, ignoreKey(HashSet::new)).add(element));
+    return unification;
+  }
+
+  public void consumeSets(BiConsumer<T, T> consumer) {
     for (T element : parent.keySet()) {
       // Find root with path-compression.
       T representative = findSet(element);
-      unification.computeIfAbsent(representative, k -> new HashSet<>()).add(element);
+      consumer.accept(representative, element);
     }
-    return unification;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/DominatorChecker.java b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
index 46c6716..c2fd047 100644
--- a/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
+++ b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
@@ -16,7 +16,6 @@
 public interface DominatorChecker {
   boolean check(BasicBlock targetBlock);
 
-  DominatorChecker TRUE_CHECKER = targetBlock -> true;
   DominatorChecker FALSE_CHECKER = targetBlock -> false;
 
   class PrecomputedDominatorChecker implements DominatorChecker {
diff --git a/src/main/java/com/android/tools/r8/utils/IROrdering.java b/src/main/java/com/android/tools/r8/utils/IROrdering.java
index db97cfa..4d1508d 100644
--- a/src/main/java/com/android/tools/r8/utils/IROrdering.java
+++ b/src/main/java/com/android/tools/r8/utils/IROrdering.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.google.common.collect.Lists;
 import java.util.Collection;
@@ -16,6 +17,8 @@
 
   Iterable<DexEncodedMethod> order(Iterable<DexEncodedMethod> methods);
 
+  Iterable<DexClassAndMethod> orderClassMethods(Iterable<DexClassAndMethod> methods);
+
   Collection<DexEncodedMethod> order(Collection<DexEncodedMethod> methods);
 
   Set<DexEncodedMethod> order(Set<DexEncodedMethod> methods);
@@ -36,6 +39,11 @@
     }
 
     @Override
+    public Iterable<DexClassAndMethod> orderClassMethods(Iterable<DexClassAndMethod> methods) {
+      return methods;
+    }
+
+    @Override
     public Collection<DexEncodedMethod> order(Collection<DexEncodedMethod> methods) {
       return methods;
     }
@@ -64,6 +72,13 @@
     }
 
     @Override
+    public List<DexClassAndMethod> orderClassMethods(Iterable<DexClassAndMethod> methods) {
+      List<DexClassAndMethod> toShuffle = Lists.newArrayList(methods);
+      Collections.shuffle(toShuffle);
+      return toShuffle;
+    }
+
+    @Override
     public List<DexEncodedMethod> order(Collection<DexEncodedMethod> methods) {
       return order((Iterable<DexEncodedMethod>) methods);
     }
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 e069c30..6fe8388 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.Version;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.debuginfo.DebugRepresentation;
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.dex.Constants;
@@ -50,6 +51,7 @@
 import com.android.tools.r8.graph.AppView.WholeProgramOptimizations;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItem;
@@ -66,7 +68,6 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
-import com.android.tools.r8.horizontalclassmerging.Policy;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -114,7 +115,6 @@
 import com.android.tools.r8.verticalclassmerging.VerticalClassMergerOptions;
 import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -2476,9 +2476,6 @@
     // specified.
     public boolean enableD8ResourcesPassThrough = false;
 
-    // TODO(b/144781417): This is disabled by default as some test apps appear to have such classes.
-    public boolean allowNonAbstractClassesWithAbstractMethods = true;
-
     public boolean verifyKeptGraphInfo = false;
 
     public boolean readInputStackMaps = true;
@@ -2502,11 +2499,10 @@
 
       public Comparator<DexMethod> interfaceMethodOrdering = null;
 
-      public Comparator<Wrapper<DexEncodedMethod>> getInterfaceMethodOrderingOrDefault(
-          Comparator<Wrapper<DexEncodedMethod>> comparator) {
+      public Comparator<? super DexClassAndMethod> getInterfaceMethodOrderingOrDefault(
+          Comparator<DexClassAndMethod> comparator) {
         if (interfaceMethodOrdering != null) {
-          return (a, b) ->
-              interfaceMethodOrdering.compare(a.get().getReference(), b.get().getReference());
+          return (a, b) -> interfaceMethodOrdering.compare(a.getReference(), b.getReference());
         }
         return comparator;
       }
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 769905a..1210325 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -297,14 +298,22 @@
     void accept(T item, int index);
   }
 
+  public static <T> List<T> sort(Iterable<T> items, Comparator<T> comparator, int numberOfItems) {
+    List<T> sorted = new ArrayList<>(numberOfItems);
+    Iterables.addAll(sorted, items);
+    sorted.sort(comparator);
+    return sorted;
+  }
+
   public static <T> List<T> sort(Collection<T> items, Comparator<T> comparator) {
     List<T> sorted = new ArrayList<>(items);
     sorted.sort(comparator);
     return sorted;
   }
 
-  public static <T> void destructiveSort(List<T> items, Comparator<T> comparator) {
+  public static <T> List<T> destructiveSort(List<T> items, Comparator<T> comparator) {
     items.sort(comparator);
+    return items;
   }
 
   // Utility to add a slow verification of a comparator as part of sorting. Note that this
@@ -358,7 +367,13 @@
     return true;
   }
 
-  public static <T> List<T> joinNewArrayList(List<T> one, List<T> other) {
+  public static <T> List<T> concat(List<T> one, List<T> other) {
+    if (one.isEmpty()) {
+      return other;
+    }
+    if (other.isEmpty()) {
+      return one;
+    }
     ArrayList<T> ts = new ArrayList<>(one.size() + other.size());
     ts.addAll(one);
     ts.addAll(other);
diff --git a/src/main/java/com/android/tools/r8/utils/MapConsumerUtils.java b/src/main/java/com/android/tools/r8/utils/MapConsumerUtils.java
index b04dd82..c6056ea 100644
--- a/src/main/java/com/android/tools/r8/utils/MapConsumerUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapConsumerUtils.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
-import com.android.tools.r8.naming.ProguardMapMarkerInfo;
 import java.util.function.Function;
 
 public class MapConsumerUtils {
@@ -19,12 +18,9 @@
     }
     return new MapConsumer() {
       @Override
-      public void accept(
-          DiagnosticsHandler diagnosticsHandler,
-          ProguardMapMarkerInfo makerInfo,
-          ClassNameMapper classNameMapper) {
-        existingMapConsumer.accept(diagnosticsHandler, makerInfo, classNameMapper);
-        newConsumer.accept(diagnosticsHandler, makerInfo, classNameMapper);
+      public void accept(DiagnosticsHandler diagnosticsHandler, ClassNameMapper classNameMapper) {
+        existingMapConsumer.accept(diagnosticsHandler, classNameMapper);
+        newConsumer.accept(diagnosticsHandler, classNameMapper);
       }
 
       @Override
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramMethodEquivalence.java b/src/main/java/com/android/tools/r8/utils/ProgramMethodEquivalence.java
index cdb98b8..91553b8 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramMethodEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramMethodEquivalence.java
@@ -18,7 +18,6 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   protected boolean doEquivalent(ProgramMethod method, ProgramMethod other) {
     return method.getDefinition() == other.getDefinition();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ValueUtils.java b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
index 509816c..68d3364 100644
--- a/src/main/java/com/android/tools/r8/utils/ValueUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
@@ -170,8 +170,11 @@
 
     // Ensure that all paths from new-array-empty to |usage| contain all array-put instructions.
     DominatorChecker dominatorChecker = DominatorChecker.create(definition.getBlock(), usageBlock);
-    for (Instruction user : arrayValue.uniqueUsers()) {
-      if (!dominatorChecker.check(user.getBlock())) {
+    // Visit in reverse order because array-puts generally appear in order, and DominatorChecker's
+    // cache is more effective when visiting in reverse order.
+    for (int i = arraySize - 1; i >= 0; --i) {
+      ArrayPut arrayPut = arrayPutsByIndex[i];
+      if (arrayPut != null && !dominatorChecker.check(arrayPut.getBlock())) {
         return null;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndFieldMapBase.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndFieldMapBase.java
index 522ff17..ae458e4 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndFieldMapBase.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndFieldMapBase.java
@@ -18,7 +18,7 @@
   }
 
   @Override
-  Wrapper<K> wrap(K field) {
+  protected Wrapper<K> wrap(K field) {
     return DexClassAndFieldEquivalence.get().wrap(field);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
index eb747e4..6dd73cf 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
@@ -5,8 +5,12 @@
 package com.android.tools.r8.utils.collections;
 
 import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.TriPredicate;
 import com.google.common.base.Equivalence.Wrapper;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.function.BiConsumer;
@@ -15,6 +19,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 public abstract class DexClassAndMemberMap<K extends DexClassAndMember<?, ?>, V> {
 
@@ -37,7 +42,11 @@
   }
 
   public V computeIfAbsent(K member, Function<K, V> fn) {
-    return backing.computeIfAbsent(wrap(member), key -> fn.apply(key.get()));
+    return computeIfAbsent(wrap(member), fn);
+  }
+
+  public V computeIfAbsent(Wrapper<K> wrapper, Function<K, V> fn) {
+    return backing.computeIfAbsent(wrapper, key -> fn.apply(key.get()));
   }
 
   public boolean containsKey(K member) {
@@ -48,6 +57,10 @@
     backing.forEach((wrapper, value) -> consumer.accept(wrapper.get(), value));
   }
 
+  public void forEachKey(Consumer<K> consumer) {
+    backing.keySet().forEach(wrapper -> consumer.accept(wrapper.get()));
+  }
+
   public void forEachValue(Consumer<V> consumer) {
     backing.values().forEach(consumer);
   }
@@ -56,6 +69,16 @@
     return backing.get(wrap(member));
   }
 
+  public V get(Wrapper<K> wrapper) {
+    return backing.get(wrapper);
+  }
+
+  public List<K> getKeysSorted() {
+    List<K> keys = new ArrayList<>(size());
+    backing.keySet().forEach(key -> keys.add(key.get()));
+    return ListUtils.sort(keys, (x, y) -> x.getReference().compareTo(y.getReference()));
+  }
+
   public V getOrDefault(K member, V defaultValue) {
     return backing.getOrDefault(wrap(member), defaultValue);
   }
@@ -94,5 +117,25 @@
     return backing.size();
   }
 
-  abstract Wrapper<K> wrap(K member);
+  public Stream<K> streamKeys() {
+    return streamWrappedKeys().map(Wrapper::get);
+  }
+
+  public Stream<Wrapper<K>> streamWrappedKeys() {
+    return backing.keySet().stream();
+  }
+
+  public <TB, TC> TraversalContinuation<TB, TC> traverse(
+      BiFunction<K, V, TraversalContinuation<TB, TC>> fn) {
+    for (Entry<Wrapper<K>, V> entry : backing.entrySet()) {
+      TraversalContinuation<TB, TC> traversalContinuation =
+          fn.apply(entry.getKey().get(), entry.getValue());
+      if (traversalContinuation.shouldBreak()) {
+        return traversalContinuation;
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  protected abstract Wrapper<K> wrap(K member);
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodMap.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodMap.java
new file mode 100644
index 0000000..0728fc0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodMap.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.utils.DexClassAndMethodEquivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ImmutableMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+public class DexClassAndMethodMap<V> extends DexClassAndMemberMap<DexClassAndMethod, V> {
+
+  private static final DexClassAndMethodMap<?> EMPTY = new DexClassAndMethodMap<>(ImmutableMap::of);
+
+  private DexClassAndMethodMap(Supplier<Map<Wrapper<DexClassAndMethod>, V>> backingFactory) {
+    super(backingFactory);
+  }
+
+  protected DexClassAndMethodMap(Map<Wrapper<DexClassAndMethod>, V> backing) {
+    super(backing);
+  }
+
+  public static <V> DexClassAndMethodMap<V> create() {
+    return new DexClassAndMethodMap<>(HashMap::new);
+  }
+
+  public static <V> DexClassAndMethodMap<V> create(int capacity) {
+    return new DexClassAndMethodMap<>(new HashMap<>(capacity));
+  }
+
+  public static <V> DexClassAndMethodMap<V> createConcurrent() {
+    return new DexClassAndMethodMap<>(ConcurrentHashMap::new);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <V> DexClassAndMethodMap<V> empty() {
+    return (DexClassAndMethodMap<V>) EMPTY;
+  }
+
+  @Override
+  protected Wrapper<DexClassAndMethod> wrap(DexClassAndMethod method) {
+    return DexClassAndMethodEquivalence.get().wrap(method);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
index 007aa51..9b77365 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
@@ -42,7 +42,6 @@
   @Override
   public boolean add(T method) {
     T existing = backing.put(method.getReference(), method);
-    assert existing == null || existing.isStructurallyEqualTo(method);
     return existing == null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
index b5c3c26..5a0a814 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
@@ -43,7 +43,7 @@
   }
 
   @Override
-  Wrapper<ProgramMethod> wrap(ProgramMethod method) {
+  protected Wrapper<ProgramMethod> wrap(ProgramMethod method) {
     return ProgramMethodEquivalence.get().wrap(method);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
index bf8a075..a198871 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -37,18 +37,14 @@
 import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper;
 import com.android.tools.r8.graph.GenericSignaturePartialTypeArgumentApplier;
 import com.android.tools.r8.graph.MethodAccessFlags;
-import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.ObjectUtils;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Streams;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -57,9 +53,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import java.util.function.Predicate;
-import java.util.stream.Stream;
 
 class ClassMerger {
 
@@ -73,34 +67,33 @@
       OptimizationFeedback.getSimpleFeedback();
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final VerticalClassMergerGraphLens.Builder deferredRenamings;
   private final DexItemFactory dexItemFactory;
   private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final VerticalClassMergerGraphLens.Builder outerLensBuilder;
   private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
 
   private final DexProgramClass source;
   private final DexProgramClass target;
 
-  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
-
-  private boolean abortMerge = false;
+  private final List<SynthesizedBridgeCode> synthesizedBridges;
 
   ClassMerger(
       AppView<AppInfoWithLiveness> appView,
-      VerticalClassMergerGraphLens.Builder lensBuilder,
+      VerticalClassMergerGraphLens.Builder outerLensBuilder,
+      List<SynthesizedBridgeCode> synthesizedBridges,
       VerticallyMergedClasses.Builder verticallyMergedClassesBuilder,
-      DexProgramClass source,
-      DexProgramClass target) {
+      VerticalMergeGroup group) {
     this.appView = appView;
-    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder();
     this.dexItemFactory = appView.dexItemFactory();
-    this.lensBuilder = lensBuilder;
+    this.lensBuilder = new VerticalClassMergerGraphLens.Builder();
+    this.outerLensBuilder = outerLensBuilder;
+    this.synthesizedBridges = synthesizedBridges;
     this.verticallyMergedClassesBuilder = verticallyMergedClassesBuilder;
-    this.source = source;
-    this.target = target;
+    this.source = group.getSource();
+    this.target = group.getTarget();
   }
 
-  public boolean merge() throws ExecutionException {
+  public void merge() {
     // Merge the class [clazz] into [targetClass] by adding all methods to
     // targetClass that are not currently contained.
     // Step 1: Merge methods
@@ -137,7 +130,7 @@
                     availableMethodSignatures,
                     definition.isClassInitializer() ? Rename.NEVER : Rename.IF_NEEDED);
             add(directMethods, resultingDirectMethod, MethodSignatureEquivalence.get());
-            deferredRenamings.recordMove(directMethod.getDefinition(), resultingDirectMethod);
+            lensBuilder.recordMove(directMethod.getDefinition(), resultingDirectMethod);
             blockRedirectionOfSuperCalls(resultingDirectMethod);
 
             // Private methods in the parent class may be targeted with invoke-super if the two
@@ -146,10 +139,8 @@
                 && definition.isPrivate()
                 && AccessControl.isMemberAccessible(directMethod, source, target, appView)
                     .isTrue()) {
-              // TODO(b/315283465): Add a test for correct rewriting of invoke-super to nest members
-              //  and determine if we need to record something here or not.
-              // deferredRenamings.mapVirtualMethodToDirectInType(
-              //    directMethod.getReference(), target.getType());
+              lensBuilder.mapVirtualMethodToDirectInType(
+                  definition.getReference(), definition, target.getType());
             }
           }
         });
@@ -161,7 +152,7 @@
           // Remove abstract/interface methods that are shadowed. The identity mapping below is
           // needed to ensure we correctly fixup the mapping in case the signature refers to
           // merged classes.
-          deferredRenamings.recordSplit(virtualMethod, shadowedBy, null, null);
+          lensBuilder.recordSplit(virtualMethod, shadowedBy, null, null);
 
           // The override now corresponds to the method in the parent, so unset its synthetic flag
           // if the method in the parent is not synthetic.
@@ -171,28 +162,16 @@
           continue;
         }
       } else {
-        if (abortMerge) {
-          // If [virtualMethod] does not resolve to a single method in [target], abort.
-          assert restoreDebuggingState(
-              Streams.concat(directMethods.values().stream(), virtualMethods.values().stream()));
-          return false;
-        }
-
         // The method is not shadowed. If it is abstract, we can simply move it to the subclass.
         // Non-abstract methods are handled below (they cannot simply be moved to the subclass as
         // a virtual method, because they might be the target of an invoke-super instruction).
         if (virtualMethod.isAbstract()) {
-          // Abort if target is non-abstract and does not override the abstract method.
-          if (!target.isAbstract()) {
-            assert appView.options().testing.allowNonAbstractClassesWithAbstractMethods;
-            abortMerge = true;
-            return false;
-          }
+          assert target.isAbstract();
           // Update the holder of [virtualMethod] using renameMethod().
           DexEncodedMethod resultingVirtualMethod =
               renameMethod(virtualMethod, availableMethodSignatures, Rename.NEVER);
           resultingVirtualMethod.setLibraryMethodOverride(virtualMethod.isLibraryMethodOverride());
-          deferredRenamings.recordMove(virtualMethod, resultingVirtualMethod);
+          lensBuilder.recordMove(virtualMethod, resultingVirtualMethod);
           add(virtualMethods, resultingVirtualMethod, MethodSignatureEquivalence.get());
           continue;
         }
@@ -261,13 +240,7 @@
                                   .getMethodInfo(virtualMethod, source)
                                   .joiner())));
 
-      deferredRenamings.recordSplit(virtualMethod, override, bridge, resultingMethod);
-    }
-
-    if (abortMerge) {
-      assert restoreDebuggingState(
-          Streams.concat(directMethods.values().stream(), virtualMethods.values().stream()));
-      return false;
+      lensBuilder.recordSplit(virtualMethod, override, bridge, resultingMethod);
     }
 
     // Rewrite generic signatures before we merge a base with a generic signature.
@@ -349,12 +322,12 @@
       target.setNestMemberAttributes(source.getNestMembersClassAttributes());
     }
     // Step 6: Record merging.
-    assert !abortMerge;
     assert GenericSignatureCorrectnessHelper.createForVerification(
             appView, GenericSignatureContextBuilder.createForSingleClass(appView, target))
         .evaluateSignaturesForClass(target)
         .isValid();
-    return true;
+    outerLensBuilder.merge(lensBuilder);
+    verticallyMergedClassesBuilder.add(source, target);
   }
 
   /**
@@ -495,31 +468,6 @@
         type -> true);
   }
 
-  private boolean restoreDebuggingState(Stream<DexEncodedMethod> toBeDiscarded) {
-    toBeDiscarded.forEach(
-        method -> {
-          assert !method.isObsolete();
-          method.setObsolete();
-        });
-    source.forEachMethod(
-        method -> {
-          if (method.isObsolete()) {
-            method.unsetObsolete();
-          }
-        });
-    assert Streams.concat(Streams.stream(source.methods()), Streams.stream(target.methods()))
-        .allMatch(method -> !method.isObsolete());
-    return true;
-  }
-
-  public VerticalClassMergerGraphLens.Builder getRenamings() {
-    return deferredRenamings;
-  }
-
-  public List<SynthesizedBridgeCode> getSynthesizedBridges() {
-    return synthesizedBridges;
-  }
-
   private void redirectSuperCallsInTarget(DexEncodedMethod oldTarget) {
     DexMethod oldTargetReference = oldTarget.getReference();
     if (source.isInterface()) {
@@ -530,8 +478,7 @@
       // rewrite any invocations on the form "invoke-super J.m()" to "invoke-direct C.m$I()",
       // if I has a supertype J. This is due to the fact that invoke-super instructions that
       // resolve to a method on an interface never hit an implementation below that interface.
-      deferredRenamings.mapVirtualMethodToDirectInType(
-          oldTargetReference, oldTarget, target.getType());
+      lensBuilder.mapVirtualMethodToDirectInType(oldTargetReference, oldTarget, target.getType());
     } else {
       // If we merge class B into class C, and class C contains an invocation super.m(), then it
       // is insufficient to rewrite "invoke-super B.m()" to "invoke-{direct,virtual} C.m$B()" (the
@@ -548,8 +495,7 @@
         boolean resolutionSucceeds =
             appView.appInfo().resolveMethodOnClass(holder, signatureInHolder).isSingleResolution();
         if (resolutionSucceeds) {
-          deferredRenamings.mapVirtualMethodToDirectInType(
-              signatureInHolder, oldTarget, target.type);
+          lensBuilder.mapVirtualMethodToDirectInType(signatureInHolder, oldTarget, target.type);
         } else {
           break;
         }
@@ -567,12 +513,11 @@
           // TODO(b/315283244): Should not rely on lens for this. Instead precompute this before
           //  merging any classes.
           boolean resolutionSucceededBeforeMerge =
-              lensBuilder.hasMappingForSignatureInContext(holder, signatureInType)
+              outerLensBuilder.hasMappingForSignatureInContext(holder, signatureInType)
                   || appView.appInfo().lookupSuperTarget(signatureInHolder, holder, appView)
                       != null;
           if (resolutionSucceededBeforeMerge) {
-            deferredRenamings.mapVirtualMethodToDirectInType(
-                signatureInType, oldTarget, target.type);
+            lensBuilder.mapVirtualMethodToDirectInType(signatureInType, oldTarget, target.type);
           }
         }
         holder =
@@ -600,7 +545,7 @@
     //   class C extends B {
     //     public void m() { super.m(); } <- invoke needs to be rewritten to invoke-direct
     //   }
-    deferredRenamings.markMethodAsMerged(method);
+    lensBuilder.markMethodAsMerged(method);
   }
 
   private DexEncodedMethod buildBridgeMethod(
@@ -647,20 +592,15 @@
 
   // Returns the method that shadows the given method, or null if method is not shadowed.
   private DexEncodedMethod findMethodInTarget(DexEncodedMethod method) {
-    SingleResolutionResult<?> resolutionResult =
-        appView.appInfo().resolveMethodOnLegacy(target, method.getReference()).asSingleResolution();
-    if (resolutionResult == null) {
-      // May happen in case of missing classes, or if multiple implementations were found.
-      abortMerge = true;
-      return null;
+    DexEncodedMethod resolvedMethod =
+        appView.appInfo().resolveMethodOnLegacy(target, method.getReference()).getResolvedMethod();
+    assert resolvedMethod != null;
+    if (resolvedMethod != method) {
+      assert resolvedMethod.isVirtualMethod() == method.isVirtualMethod();
+      return resolvedMethod;
     }
-    DexEncodedMethod actual = resolutionResult.getResolvedMethod();
-    if (ObjectUtils.notIdentical(actual, method)) {
-      assert actual.isVirtualMethod() == method.isVirtualMethod();
-      return actual;
-    }
-    // The method is not actually overridden. This means that we will move `method` to the
-    // subtype. If `method` is abstract, then so should the subtype be.
+    // The method is not actually overridden. This means that we will move `method` to the subtype.
+    // If `method` is abstract, then so should the subtype be.
     return null;
   }
 
@@ -694,7 +634,7 @@
     for (DexEncodedField field : sourceFields) {
       DexEncodedField resultingField = renameFieldIfNeeded(field, availableFieldSignatures);
       existingFieldNames.add(resultingField.getName());
-      deferredRenamings.recordMove(field, resultingField);
+      lensBuilder.recordMove(field, resultingField);
       result[i] = resultingField;
       i++;
     }
@@ -732,7 +672,7 @@
     DexEncodedMethod result =
         method.toTypeSubstitutedMethodAsInlining(newSignature, dexItemFactory);
     result.getMutableOptimizationInfo().markForceInline();
-    deferredRenamings.recordMove(method, result);
+    lensBuilder.recordMove(method, result);
     // Renamed constructors turn into ordinary private functions. They can be private, as
     // they are only references from their direct subclass, which they were merged into.
     result.getAccessFlags().unsetConstructor();
@@ -823,10 +763,6 @@
 
   private void makeStatic(DexEncodedMethod method) {
     method.getAccessFlags().setStatic();
-    if (!method.getCode().isCfCode()) {
-      // Due to member rebinding we may have inserted bridge methods with synthesized code.
-      // Currently, there is no easy way to make such code static.
-      abortMerge = true;
-    }
+    assert method.getCode().isCfCode();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
index 22316f3c..fdd9383 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
@@ -4,20 +4,17 @@
 package com.android.tools.r8.verticalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ListUtils;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
 
 public class ConnectedComponentVerticalClassMerger {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final Set<DexProgramClass> classesToMerge;
+  private final Collection<VerticalMergeGroup> classesToMerge;
 
   // The resulting graph lens that should be used after class merging.
   private final VerticalClassMergerGraphLens.Builder lensBuilder;
@@ -29,7 +26,7 @@
       VerticallyMergedClasses.builder();
 
   ConnectedComponentVerticalClassMerger(
-      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> classesToMerge) {
+      AppView<AppInfoWithLiveness> appView, Collection<VerticalMergeGroup> classesToMerge) {
     this.appView = appView;
     this.classesToMerge = classesToMerge;
     this.lensBuilder = new VerticalClassMergerGraphLens.Builder();
@@ -39,35 +36,16 @@
     return classesToMerge.isEmpty();
   }
 
-  public VerticalClassMergerResult.Builder run(ImmediateProgramSubtypingInfo immediateSubtypingInfo)
-      throws ExecutionException {
-    List<DexProgramClass> classesToMergeSorted =
-        ListUtils.sort(classesToMerge, Comparator.comparing(DexProgramClass::getType));
-    for (DexProgramClass clazz : classesToMergeSorted) {
-      mergeClassIfPossible(clazz, immediateSubtypingInfo);
+  public VerticalClassMergerResult.Builder run() {
+    List<VerticalMergeGroup> classesToMergeSorted =
+        ListUtils.sort(classesToMerge, Comparator.comparing(group -> group.getSource().getType()));
+    for (VerticalMergeGroup group : classesToMergeSorted) {
+      ClassMerger classMerger =
+          new ClassMerger(
+              appView, lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder, group);
+      classMerger.merge();
     }
     return VerticalClassMergerResult.builder(
         lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
   }
-
-  private void mergeClassIfPossible(
-      DexProgramClass sourceClass, ImmediateProgramSubtypingInfo immediateSubtypingInfo)
-      throws ExecutionException {
-    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
-    assert subclasses.size() == 1;
-    DexProgramClass targetClass = ListUtils.first(subclasses);
-    if (verticallyMergedClassesBuilder.isMergeSource(targetClass)
-        || verticallyMergedClassesBuilder.isMergeTarget(sourceClass)) {
-      return;
-    }
-    ClassMerger merger =
-        new ClassMerger(
-            appView, lensBuilder, verticallyMergedClassesBuilder, sourceClass, targetClass);
-    if (merger.merge()) {
-      verticallyMergedClassesBuilder.add(sourceClass, targetClass);
-      // Commit the changes to the graph lens.
-      lensBuilder.merge(merger.getRenamings());
-      synthesizedBridges.addAll(merger.getSynthesizedBridges());
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
index 506a2af..8c76978 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
@@ -10,7 +10,7 @@
 public class InvokeSpecialToDefaultLibraryMethodUseRegistry
     extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
-  InvokeSpecialToDefaultLibraryMethodUseRegistry(
+  public InvokeSpecialToDefaultLibraryMethodUseRegistry(
       AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
     super(appView, context, false);
     assert context.getHolder().isInterface();
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java b/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java
index c69002e..3c20af4 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
-class MergeMayLeadToNoSuchMethodErrorUseRegistry
+public class MergeMayLeadToNoSuchMethodErrorUseRegistry
     extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
   private final AppView<AppInfoWithLiveness> appViewWithLiveness;
@@ -22,7 +22,7 @@
   private final GraphLens codeLens;
   private final DexProgramClass source;
 
-  MergeMayLeadToNoSuchMethodErrorUseRegistry(
+  public MergeMayLeadToNoSuchMethodErrorUseRegistry(
       AppView<AppInfoWithLiveness> appView, ProgramMethod context, DexProgramClass source) {
     super(appView, context, Boolean.FALSE);
     assert context.getHolder().getSuperType().isIdenticalTo(source.getType());
@@ -32,7 +32,7 @@
     this.source = source;
   }
 
-  boolean mayLeadToNoSuchMethodError() {
+  public boolean mayLeadToNoSuchMethodError() {
     return getResult();
   }
 
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
index cb482b9..9b0626b 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -182,6 +182,7 @@
   }
 
   private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    appView.appInfo().getMethodAccessInfoCollection().verifyNoNonResolving(appView);
     timing.begin("Setup");
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
@@ -224,8 +225,7 @@
     Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
         getConnectedComponentMergers(
             connectedComponents, immediateSubtypingInfo, executorService, timing);
-    return applyConnectedComponentMergers(
-        connectedComponentMergers, immediateSubtypingInfo, executorService, timing);
+    return applyConnectedComponentMergers(connectedComponentMergers, executorService, timing);
   }
 
   private Collection<ConnectedComponentVerticalClassMerger> getConnectedComponentMergers(
@@ -244,8 +244,9 @@
             connectedComponent -> {
               Timing threadTiming = Timing.create("Compute classes to merge in component", options);
               ConnectedComponentVerticalClassMerger connectedComponentMerger =
-                  new VerticalClassMergerPolicyExecutor(appView, pinnedClasses)
-                      .run(connectedComponent, immediateSubtypingInfo);
+                  new VerticalClassMergerPolicyExecutor(
+                          appView, immediateSubtypingInfo, pinnedClasses)
+                      .run(connectedComponent, executorService, threadTiming);
               if (!connectedComponentMerger.isEmpty()) {
                 synchronized (connectedComponentMergers) {
                   connectedComponentMergers.add(connectedComponentMerger);
@@ -263,7 +264,6 @@
 
   private VerticalClassMergerResult applyConnectedComponentMergers(
       Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       ExecutorService executorService,
       Timing timing)
       throws ExecutionException {
@@ -276,7 +276,7 @@
             connectedComponentMerger -> {
               Timing threadTiming = Timing.create("Merge classes in component", options);
               VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
-                  connectedComponentMerger.run(immediateSubtypingInfo);
+                  connectedComponentMerger.run();
               verticalClassMergerResult.merge(verticalClassMergerComponentResult);
               threadTiming.end();
               return threadTiming;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index ad653bd..a3d1257 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -281,8 +281,7 @@
       return InvokeType.STATIC;
     }
     if (type.isInterface()
-        && mergedClasses.hasInterfaceBeenMergedIntoClass(
-            previousMethod.getHolderType(), newMethod.getHolderType())) {
+        && mergedClasses.hasInterfaceBeenMergedIntoClass(previousMethod.getHolderType())) {
       return InvokeType.VIRTUAL;
     }
     return type;
@@ -429,8 +428,14 @@
         for (Entry<DexMethod, DexMethod> innerEntry : entry.getValue().entrySet()) {
           DexMethod virtualMethod = innerEntry.getValue();
           DexMethod implementationMethod = extraNewMethodSignatures.get(virtualMethod);
-          assert implementationMethod != null;
-          innerEntry.setValue(implementationMethod);
+          if (implementationMethod != null) {
+            // Handle invoke-super to non-private virtual method.
+            innerEntry.setValue(implementationMethod);
+          } else {
+            // Handle invoke-super to private virtual method (nest access).
+            assert newMethodSignatures.containsKey(virtualMethod);
+            innerEntry.setValue(newMethodSignatures.get(virtualMethod));
+          }
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
index e2bfb17..bab7656 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -3,436 +3,135 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging;
 
-import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
-
-import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
-import com.android.tools.r8.androidapi.ComputedApiLevel;
-import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
+import com.android.tools.r8.classmerging.Policy;
+import com.android.tools.r8.classmerging.PolicyExecutor;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
-import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.MainDexInfo;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.ObjectUtils;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.verticalclassmerging.policies.NoAbstractMethodsOnAbstractClassesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoAnnotationClassesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoClassInitializationChangesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoDirectlyInstantiatedClassesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoEnclosingMethodAttributesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoFieldResolutionChangesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoIllegalAccessesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoInnerClassAttributesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoInterfacesWithUnknownSubtypesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoInvokeSuperNoSuchMethodErrorsPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoKeptClassesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoLockMergingPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoMethodResolutionChangesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoNestedMergingPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoNonSerializableClassIntoSerializableClassPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoServiceInterfacesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SafeConstructorInliningPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SameApiReferenceLevelPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SameFeatureSplitPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SameMainDexGroupPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SameNestPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SameStartupPartitionPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.SuccessfulVirtualMethodResolutionInTargetPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.VerticalClassMergerPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.VerticalClassMergerPolicyWithPreprocessing;
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
-// TODO(b/315252934): Parallelize policy execution over connected program components.
-public class VerticalClassMergerPolicyExecutor {
+public class VerticalClassMergerPolicyExecutor extends PolicyExecutor<VerticalMergeGroup> {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final InternalOptions options;
-  private final MainDexInfo mainDexInfo;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
   private final Set<DexProgramClass> pinnedClasses;
 
   VerticalClassMergerPolicyExecutor(
-      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> pinnedClasses) {
     this.appView = appView;
-    this.options = appView.options();
-    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.pinnedClasses = pinnedClasses;
   }
 
   ConnectedComponentVerticalClassMerger run(
-      Set<DexProgramClass> connectedComponent,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    Set<DexProgramClass> mergeCandidates = Sets.newIdentityHashSet();
-    for (DexProgramClass sourceClass : connectedComponent) {
-      List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
-      if (subclasses.size() != 1) {
-        continue;
-      }
-      DexProgramClass targetClass = ListUtils.first(subclasses);
-      if (!isMergeCandidate(sourceClass, targetClass)) {
-        continue;
-      }
-      if (!isStillMergeCandidate(sourceClass, targetClass)) {
-        continue;
-      }
-      if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
-          || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
-        continue;
-      }
-      mergeCandidates.add(sourceClass);
-    }
-    return new ConnectedComponentVerticalClassMerger(appView, mergeCandidates);
+      Set<DexProgramClass> connectedComponent, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    Collection<VerticalMergeGroup> groups =
+        createInitialMergeGroupsWithDeterministicOrder(connectedComponent);
+    Collection<Policy> policies =
+        List.of(
+            new NoDirectlyInstantiatedClassesPolicy(appView),
+            new NoInterfacesWithUnknownSubtypesPolicy(appView),
+            new NoKeptClassesPolicy(appView, pinnedClasses),
+            new SameFeatureSplitPolicy(appView),
+            new SameStartupPartitionPolicy(appView),
+            new NoServiceInterfacesPolicy(appView),
+            new NoAnnotationClassesPolicy(),
+            new NoNonSerializableClassIntoSerializableClassPolicy(appView),
+            new SafeConstructorInliningPolicy(appView),
+            new NoEnclosingMethodAttributesPolicy(),
+            new NoInnerClassAttributesPolicy(),
+            new SameNestPolicy(),
+            new SameMainDexGroupPolicy(appView),
+            new NoLockMergingPolicy(appView),
+            new SameApiReferenceLevelPolicy(appView),
+            new NoFieldResolutionChangesPolicy(appView),
+            new NoMethodResolutionChangesPolicy(appView),
+            new NoIllegalAccessesPolicy(appView),
+            new NoClassInitializationChangesPolicy(appView),
+            new NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy(appView),
+            new NoInvokeSuperNoSuchMethodErrorsPolicy(appView),
+            new SuccessfulVirtualMethodResolutionInTargetPolicy(appView),
+            new NoAbstractMethodsOnAbstractClassesPolicy(appView),
+            new NoNestedMergingPolicy());
+    groups = run(groups, policies, executorService, timing);
+    return new ConnectedComponentVerticalClassMerger(appView, groups);
   }
 
-  // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
-  // method do not change in response to any class merges.
-  private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    assert targetClass != null;
-    ObjectAllocationInfoCollection allocationInfo =
-        appView.appInfo().getObjectAllocationInfoCollection();
-    if (allocationInfo.isInstantiatedDirectly(sourceClass)
-        || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
-        || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
-        || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
-        || pinnedClasses.contains(sourceClass)) {
-      return false;
-    }
-
-    assert sourceClass
-        .traverseProgramMembers(
-            member -> {
-              assert !appView.getKeepInfo(member).isPinned(options);
-              return TraversalContinuation.doContinue();
-            })
-        .shouldContinue();
-
-    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
-        && appView.getKeepInfo(targetClass).isPinned(options)) {
-      return false;
-    }
-    if (sourceClass.isAnnotation()) {
-      return false;
-    }
-    if (!sourceClass.isInterface()
-        && targetClass.isSerializable(appView)
-        && !appView.appInfo().isSerializable(sourceClass.getType())) {
-      // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
-      //   1.10 The Serializable Interface
-      //   ...
-      //   A Serializable class must do the following:
-      //   ...
-      //     * Have access to the no-arg constructor of its first non-serializable superclass
-      return false;
-    }
-
-    // If there is a constructor in the target, make sure that all source constructors can be
-    // inlined.
-    if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
-      TraversalContinuation<?, ?> result =
-          sourceClass.traverseProgramInstanceInitializers(
-              method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
-      if (result.shouldBreak()) {
-        return false;
+  private Collection<VerticalMergeGroup> createInitialMergeGroupsWithDeterministicOrder(
+      Set<DexProgramClass> connectedComponent) {
+    List<VerticalMergeGroup> groups = new ArrayList<>();
+    for (DexProgramClass mergeCandidate : connectedComponent) {
+      List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
+      if (subclasses.size() == 1) {
+        groups.add(new VerticalMergeGroup(mergeCandidate, ListUtils.first(subclasses)));
       }
     }
-    if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
-      return false;
-    }
-    // We abort class merging when merging across nests or from a nest to non-nest.
-    // Without nest this checks null == null.
-    if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
-      return false;
-    }
-
-    // If there is an invoke-special to a default interface method and we are not merging into an
-    // interface, then abort, since invoke-special to a virtual class method requires desugaring.
-    if (sourceClass.isInterface() && !targetClass.isInterface()) {
-      TraversalContinuation<?, ?> result =
-          sourceClass.traverseProgramMethods(
-              method -> {
-                boolean foundInvokeSpecialToDefaultLibraryMethod =
-                    method.registerCodeReferencesWithResult(
-                        new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
-                return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
-              });
-      if (result.shouldBreak()) {
-        return false;
-      }
-    }
-
-    // Check with main dex classes to see if we are allowed to merge.
-    if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
-      return false;
-    }
-
-    return true;
+    return ListUtils.destructiveSort(
+        groups, Comparator.comparing(group -> group.getSource().getType()));
   }
 
-  /**
-   * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
-   * in this method may change in response to class merges. Therefore, this method should always be
-   * called before merging {@param sourceClass} into {@param targetClass}.
-   */
-  boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    // For interface types, this is more complicated, see:
-    // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
-    // We basically can't move the clinit, since it is not called when implementing classes have
-    // their clinit called - except when the interface has a default method.
-    if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
-        || targetClass.classInitializationMayHaveSideEffects(
-            appView, type -> type.isIdenticalTo(sourceClass.getType()))
-        || (sourceClass.isInterface()
-            && sourceClass.classInitializationMayHaveSideEffects(appView))) {
-      return false;
+  @Override
+  protected LinkedList<VerticalMergeGroup> apply(
+      Policy policy, LinkedList<VerticalMergeGroup> linkedGroups, ExecutorService executorService)
+      throws ExecutionException {
+    if (policy.isVerticalClassMergerPolicy()) {
+      return apply(policy.asVerticalClassMergerPolicy(), linkedGroups);
+    } else {
+      assert policy.isVerticalClassMergerPolicyWithPreprocessing();
+      return apply(policy.asVerticalClassMergerPolicyWithPreprocessing(), linkedGroups);
     }
-    boolean sourceCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(sourceClass)
-            || sourceClass.hasStaticSynchronizedMethods();
-    boolean targetCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(targetClass)
-            || targetClass.hasStaticSynchronizedMethods();
-    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
-      return false;
-    }
-    if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
-      return false;
-    }
-    if (methodResolutionMayChange(sourceClass, targetClass)) {
-      return false;
-    }
-    // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
-    // to the super class.
-    if (fieldResolutionMayChange(sourceClass, targetClass)) {
-      return false;
-    }
-    // Only merge if api reference level of source class is equal to target class. The check is
-    // somewhat expensive.
-    if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
-      AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
-      ComputedApiLevel sourceApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
-      ComputedApiLevel targetApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
-      if (!sourceApiLevel.equals(targetApiLevel)) {
-        return false;
-      }
-    }
-    return true;
   }
 
-  private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
-    if (!appView.options().inlinerOptions().enableInlining) {
-      return true;
-    }
-    Code code = method.getDefinition().getCode();
-    if (code.isCfCode()) {
-      CfCode cfCode = code.asCfCode();
-      ConstraintWithTarget constraint =
-          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
-      if (constraint.isNever()) {
-        return true;
-      }
-      // Constructors can have references beyond the root main dex classes. This can increase the
-      // size of the main dex dependent classes and we should bail out.
-      if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
-        return true;
-      }
-      return false;
-    }
-    if (code.isDefaultInstanceInitializerCode()) {
-      return false;
-    }
-    return true;
+  private LinkedList<VerticalMergeGroup> apply(
+      VerticalClassMergerPolicy policy, LinkedList<VerticalMergeGroup> linkedGroups) {
+    linkedGroups.removeIf(group -> !policy.canMerge(group));
+    return linkedGroups;
   }
 
-  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
-    if (source.getType().isIdenticalTo(target.getSuperType())) {
-      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
-      // Target implements an interface that declares a static final field f, this should yield an
-      // IncompatibleClassChangeError.
-      // TODO(christofferqa): In the following we only check if a static field from an interface
-      //  shadows an instance field from [source]. We could actually check if there is an iget/iput
-      //  instruction whose resolution would be affected by the merge. The situation where a static
-      //  field shadows an instance field is probably not widespread in practice, though.
-      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
-      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
-      for (DexType interfaceType : target.getInterfaces()) {
-        DexClass clazz = appView.definitionFor(interfaceType);
-        for (DexEncodedField staticField : clazz.staticFields()) {
-          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
-        }
-      }
-      for (DexEncodedField instanceField : source.instanceFields()) {
-        if (staticFieldsInInterfacesOfTarget.contains(
-            equivalence.wrap(instanceField.getReference()))) {
-          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
-          // interface would now hit an instance field from [source], so that an IncompatibleClass-
-          // ChangeError would no longer be thrown. Abort merge.
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
-    if (source.isSamePackage(target)) {
-      // When merging two classes from the same package, we only need to make sure that [source]
-      // does not get less visible, since that could make a valid access to [source] from another
-      // package illegal after [source] has been merged into [target].
-      assert source.getAccessFlags().isPackagePrivateOrPublic();
-      assert target.getAccessFlags().isPackagePrivateOrPublic();
-      // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
-      return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
-    }
-
-    // Check that all accesses to [source] and its members from inside the current package of
-    // [source] will continue to work. This is guaranteed if [target] is public and all members of
-    // [source] are either private or public.
-    //
-    // (Deliberately not checking all accesses to [source] since that would be expensive.)
-    if (!target.isPublic()) {
-      return true;
-    }
-    for (DexType sourceInterface : source.getInterfaces()) {
-      DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
-      if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
-        return true;
-      }
-    }
-    for (DexEncodedField field : source.fields()) {
-      if (!(field.isPublic() || field.isPrivate())) {
-        return true;
-      }
-    }
-    for (DexEncodedMethod method : source.methods()) {
-      if (!(method.isPublic() || method.isPrivate())) {
-        return true;
-      }
-      // Check if the target is overriding and narrowing the access.
-      if (method.isPublic()) {
-        DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
-        if (targetOverride != null && !targetOverride.isPublic()) {
-          return true;
-        }
-      }
-    }
-    // Check that all accesses from [source] to classes or members from the current package of
-    // [source] will continue to work. This is guaranteed if the methods of [source] do not access
-    // any private or protected classes or members from the current package of [source].
-    TraversalContinuation<?, ?> result =
-        source.traverseProgramMethods(
-            method -> {
-              boolean foundIllegalAccess =
-                  method.registerCodeReferencesWithResult(
-                      new IllegalAccessDetector(appView, method));
-              if (foundIllegalAccess) {
-                return TraversalContinuation.doBreak();
-              }
-              return TraversalContinuation.doContinue();
-            },
-            DexEncodedMethod::hasCode);
-    return result.shouldBreak();
-  }
-
-  // TODO: maybe skip this check if target does not implement any interfaces (directly or
-  // indirectly)?
-  private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
-    // This only returns true when an invoke-super instruction is found that targets a default
-    // interface method.
-    if (!options.canUseDefaultAndStaticInterfaceMethods()) {
-      return false;
-    }
-    // This problem may only arise when merging (non-interface) classes into classes.
-    if (source.isInterface() || target.isInterface()) {
-      return false;
-    }
-    return target
-        .traverseProgramMethods(
-            method -> {
-              MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
-                  new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
-              method.registerCodeReferencesWithResult(registry);
-              return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
-            },
-            DexEncodedMethod::hasCode)
-        .shouldBreak();
-  }
-
-  private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
-    for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
-      DexEncodedMethod directTargetMethod =
-          target.lookupDirectMethod(virtualSourceMethod.getReference());
-      if (directTargetMethod != null) {
-        // A private method shadows a virtual method. This situation is rare, since it is not
-        // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
-        // possible to rename the private method in the subclass, and then move the virtual method
-        // to the subclass without changing its name.)
-        return true;
-      }
-    }
-
-    // When merging an interface into a class, all instructions on the form "invoke-interface
-    // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
-    // transformation could hide IncompatibleClassChangeErrors.
-    if (source.isInterface() && !target.isInterface()) {
-      List<DexEncodedMethod> defaultMethods = new ArrayList<>();
-      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
-        if (!virtualMethod.accessFlags.isAbstract()) {
-          defaultMethods.add(virtualMethod);
-        }
-      }
-
-      // For each of the default methods, the subclass [target] could inherit another default method
-      // with the same signature from another interface (i.e., there is a conflict). In such cases,
-      // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
-      // ClassChangeError.
-      //
-      // Example:
-      //   interface I1 { default void m() {} }
-      //   interface I2 { default void m() {} }
-      //   class C implements I1, I2 {
-      //     ... invoke-interface I1.m ... <- IncompatibleClassChangeError
-      //   }
-      for (DexEncodedMethod method : defaultMethods) {
-        // Conservatively find all possible targets for this method.
-        LookupResultSuccess lookupResult =
-            appView
-                .appInfo()
-                .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
-                .lookupVirtualDispatchTargets(target, appView)
-                .asLookupResultSuccess();
-        assert lookupResult != null;
-        if (lookupResult == null) {
-          return true;
-        }
-        if (lookupResult.contains(method)) {
-          Box<Boolean> found = new Box<>(false);
-          lookupResult.forEach(
-              interfaceTarget -> {
-                if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
-                  return;
-                }
-                DexClass enclosingClass = interfaceTarget.getHolder();
-                if (enclosingClass != null && enclosingClass.isInterface()) {
-                  // Found a default method that is different from the one in [source], aborting.
-                  found.set(true);
-                }
-              },
-              lambdaTarget -> {
-                // The merger should already have excluded lambda implemented interfaces.
-                assert false;
-              });
-          if (found.get()) {
-            return true;
-          }
-        }
-      }
-    }
-    return false;
+  private <T> LinkedList<VerticalMergeGroup> apply(
+      VerticalClassMergerPolicyWithPreprocessing<T> policy,
+      LinkedList<VerticalMergeGroup> linkedGroups) {
+    T data = policy.preprocess(linkedGroups);
+    linkedGroups.removeIf(group -> !policy.canMerge(group, data));
+    return linkedGroups;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
index 6b9955c..d814695 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
@@ -46,6 +46,19 @@
   }
 
   @Override
+  public void preprocess() {
+    appView
+        .getKeepInfo()
+        .forEachPinnedMethod(
+            method -> {
+              if (!method.isInstanceInitializer(dexItemFactory)) {
+                keptSignatures.add(method);
+              }
+            },
+            appView.options());
+  }
+
+  @Override
   public void postprocess() {
     lensBuilder.fixupContextualVirtualToDirectMethodMaps();
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
new file mode 100644
index 0000000..1b434ea
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2023, 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.verticalclassmerging;
+
+import com.android.tools.r8.classmerging.MergeGroup;
+import com.android.tools.r8.graph.DexProgramClass;
+
+public class VerticalMergeGroup extends MergeGroup {
+
+  private final DexProgramClass source;
+  private final DexProgramClass target;
+
+  VerticalMergeGroup(DexProgramClass source, DexProgramClass target) {
+    this.source = source;
+    this.target = target;
+  }
+
+  public DexProgramClass getSource() {
+    return source;
+  }
+
+  public DexProgramClass getTarget() {
+    return target;
+  }
+
+  @Override
+  public int size() {
+    return 2;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
index 0003478..9728275 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
@@ -84,8 +84,8 @@
     return mergedClasses.containsKey(type);
   }
 
-  public boolean hasInterfaceBeenMergedIntoClass(DexType interfaceType, DexType classType) {
-    return classType.isIdenticalTo(mergedInterfacesToClasses.get(interfaceType));
+  public boolean hasInterfaceBeenMergedIntoClass(DexType interfaceType) {
+    return mergedInterfacesToClasses.containsKey(interfaceType);
   }
 
   public boolean hasInterfaceBeenMergedIntoSubtype(DexType type) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAbstractMethodsOnAbstractClassesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAbstractMethodsOnAbstractClassesPolicy.java
new file mode 100644
index 0000000..f6d7d4b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAbstractMethodsOnAbstractClassesPolicy.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoAbstractMethodsOnAbstractClassesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoAbstractMethodsOnAbstractClassesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    if (!group.getSource().isAbstract() || group.getTarget().isAbstract()) {
+      return true;
+    }
+    for (ProgramMethod method :
+        group.getSource().virtualProgramMethods(DexEncodedMethod::isAbstract)) {
+      DexClassAndMethod resolvedMethod =
+          appView
+              .appInfo()
+              .resolveMethodOn(group.getTarget(), method.getReference())
+              .getResolutionPair();
+      // If the method and resolved method are different, then the abstract method will be removed
+      // and references will be rewritten to the resolved method.
+      if (resolvedMethod.getDefinition() == method.getDefinition()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public String getName() {
+    return "NoAbstractMethodsOnAbstractClassesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAnnotationClassesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAnnotationClassesPolicy.java
new file mode 100644
index 0000000..0c35617
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoAnnotationClassesPolicy.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoAnnotationClassesPolicy extends VerticalClassMergerPolicy {
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return !group.getSource().isAnnotation();
+  }
+
+  @Override
+  public String getName() {
+    return "NoAnnotationClassesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java
new file mode 100644
index 0000000..d8b6933
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoClassInitializationChangesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoClassInitializationChangesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    // For interface types, this is more complicated, see:
+    // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
+    // We basically can't move the clinit, since it is not called when implementing classes have
+    // their clinit called - except when the interface has a default method.
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return (!sourceClass.hasClassInitializer() || !targetClass.hasClassInitializer())
+        && !targetClass.classInitializationMayHaveSideEffects(
+            appView, type -> type.isIdenticalTo(sourceClass.getType()))
+        && (!sourceClass.isInterface()
+            || !sourceClass.classInitializationMayHaveSideEffects(appView));
+  }
+
+  @Override
+  public String getName() {
+    return "NoClassInitializationChangesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoDirectlyInstantiatedClassesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoDirectlyInstantiatedClassesPolicy.java
new file mode 100644
index 0000000..8487780
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoDirectlyInstantiatedClassesPolicy.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoDirectlyInstantiatedClassesPolicy extends VerticalClassMergerPolicy {
+
+  private final ObjectAllocationInfoCollection allocationInfo;
+
+  public NoDirectlyInstantiatedClassesPolicy(AppView<AppInfoWithLiveness> appView) {
+    allocationInfo = appView.appInfo().getObjectAllocationInfoCollection();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    return !allocationInfo.isInstantiatedDirectly(sourceClass);
+  }
+
+  @Override
+  public String getName() {
+    return "NoDirectlyInstantiatedClassesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoEnclosingMethodAttributesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoEnclosingMethodAttributesPolicy.java
new file mode 100644
index 0000000..c0273cc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoEnclosingMethodAttributesPolicy.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoEnclosingMethodAttributesPolicy extends VerticalClassMergerPolicy {
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return !group.getSource().hasEnclosingMethodAttribute()
+        && !group.getTarget().hasEnclosingMethodAttribute();
+  }
+
+  @Override
+  public String getName() {
+    return "NoEnclosingMethodAttributesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java
new file mode 100644
index 0000000..7d38449
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.HashSet;
+import java.util.Set;
+
+public class NoFieldResolutionChangesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoFieldResolutionChangesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+    // to the super class.
+    return !fieldResolutionMayChange(group.getSource(), group.getTarget());
+  }
+
+  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+    if (source.getType().isIdenticalTo(target.getSuperType())) {
+      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+      // Target implements an interface that declares a static final field f, this should yield an
+      // IncompatibleClassChangeError.
+      // TODO(christofferqa): In the following we only check if a static field from an interface
+      //  shadows an instance field from [source]. We could actually check if there is an iget/iput
+      //  instruction whose resolution would be affected by the merge. The situation where a static
+      //  field shadows an instance field is probably not widespread in practice, though.
+      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+      for (DexType interfaceType : target.getInterfaces()) {
+        DexClass clazz = appView.definitionFor(interfaceType);
+        for (DexEncodedField staticField : clazz.staticFields()) {
+          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
+        }
+      }
+      for (DexEncodedField instanceField : source.instanceFields()) {
+        if (staticFieldsInInterfacesOfTarget.contains(
+            equivalence.wrap(instanceField.getReference()))) {
+          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+          // interface would now hit an instance field from [source], so that an IncompatibleClass-
+          // ChangeError would no longer be thrown. Abort merge.
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String getName() {
+    return "NoFieldResolutionChangesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoIllegalAccessesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoIllegalAccessesPolicy.java
new file mode 100644
index 0000000..4322b34
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoIllegalAccessesPolicy.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.verticalclassmerging.IllegalAccessDetector;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoIllegalAccessesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoIllegalAccessesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return !mergeMayLeadToIllegalAccesses(group.getSource(), group.getTarget());
+  }
+
+  private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
+    if (source.isSamePackage(target)) {
+      // When merging two classes from the same package, we only need to make sure that [source]
+      // does not get less visible, since that could make a valid access to [source] from another
+      // package illegal after [source] has been merged into [target].
+      assert source.getAccessFlags().isPackagePrivateOrPublic();
+      assert target.getAccessFlags().isPackagePrivateOrPublic();
+      // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
+      return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
+    }
+
+    // Check that all accesses to [source] and its members from inside the current package of
+    // [source] will continue to work. This is guaranteed if [target] is public and all members of
+    // [source] are either private or public.
+    //
+    // (Deliberately not checking all accesses to [source] since that would be expensive.)
+    if (!target.isPublic()) {
+      return true;
+    }
+    for (DexType sourceInterface : source.getInterfaces()) {
+      DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
+      if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
+        return true;
+      }
+    }
+    for (DexEncodedField field : source.fields()) {
+      if (!(field.isPublic() || field.isPrivate())) {
+        return true;
+      }
+    }
+    for (DexEncodedMethod method : source.methods()) {
+      if (!(method.isPublic() || method.isPrivate())) {
+        return true;
+      }
+      // Check if the target is overriding and narrowing the access.
+      if (method.isPublic()) {
+        DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
+        if (targetOverride != null && !targetOverride.isPublic()) {
+          return true;
+        }
+      }
+    }
+    // Check that all accesses from [source] to classes or members from the current package of
+    // [source] will continue to work. This is guaranteed if the methods of [source] do not access
+    // any private or protected classes or members from the current package of [source].
+    TraversalContinuation<?, ?> result =
+        source.traverseProgramMethods(
+            method -> {
+              boolean foundIllegalAccess =
+                  method.registerCodeReferencesWithResult(
+                      new IllegalAccessDetector(appView, method));
+              if (foundIllegalAccess) {
+                return TraversalContinuation.doBreak();
+              }
+              return TraversalContinuation.doContinue();
+            },
+            DexEncodedMethod::hasCode);
+    return result.shouldBreak();
+  }
+
+  @Override
+  public String getName() {
+    return "NoIllegalAccessesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInnerClassAttributesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInnerClassAttributesPolicy.java
new file mode 100644
index 0000000..e6b01c0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInnerClassAttributesPolicy.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoInnerClassAttributesPolicy extends VerticalClassMergerPolicy {
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return group.getSource().getInnerClasses().isEmpty()
+        && group.getTarget().getInnerClasses().isEmpty();
+  }
+
+  @Override
+  public String getName() {
+    return "NoInnerClassAttributesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java
new file mode 100644
index 0000000..13b1342
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.verticalclassmerging.InvokeSpecialToDefaultLibraryMethodUseRegistry;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy
+    extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy(
+      AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    // If there is an invoke-special to a default interface method and we are not merging into an
+    // interface, then abort, since invoke-special to a virtual class method requires desugaring.
+    if (!sourceClass.isInterface() || targetClass.isInterface()) {
+      return true;
+    }
+    TraversalContinuation<?, ?> result =
+        sourceClass.traverseProgramMethods(
+            method -> {
+              boolean foundInvokeSpecialToDefaultLibraryMethod =
+                  method.registerCodeReferencesWithResult(
+                      new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
+              return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
+            });
+    return result.shouldContinue();
+  }
+
+  @Override
+  public String getName() {
+    return "NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithUnknownSubtypesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithUnknownSubtypesPolicy.java
new file mode 100644
index 0000000..67fde09
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithUnknownSubtypesPolicy.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoInterfacesWithUnknownSubtypesPolicy extends VerticalClassMergerPolicy {
+
+  private final ObjectAllocationInfoCollection allocationInfo;
+
+  public NoInterfacesWithUnknownSubtypesPolicy(AppView<AppInfoWithLiveness> appView) {
+    allocationInfo = appView.appInfo().getObjectAllocationInfoCollection();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    return !allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
+        && !allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass);
+  }
+
+  @Override
+  public String getName() {
+    return "NoInterfacesWithUnknownSubtypesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInvokeSuperNoSuchMethodErrorsPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInvokeSuperNoSuchMethodErrorsPolicy.java
new file mode 100644
index 0000000..1a7ae24
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInvokeSuperNoSuchMethodErrorsPolicy.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.verticalclassmerging.MergeMayLeadToNoSuchMethodErrorUseRegistry;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoInvokeSuperNoSuchMethodErrorsPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
+
+  public NoInvokeSuperNoSuchMethodErrorsPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.options = appView.options();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return !mergeMayLeadToNoSuchMethodError(group.getSource(), group.getTarget());
+  }
+
+  private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
+    // This only returns true when an invoke-super instruction is found that targets a default
+    // interface method.
+    if (!options.canUseDefaultAndStaticInterfaceMethods()) {
+      return false;
+    }
+    // This problem may only arise when merging (non-interface) classes into classes.
+    if (source.isInterface() || target.isInterface()) {
+      return false;
+    }
+    return target
+        .traverseProgramMethods(
+            method -> {
+              MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
+                  new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
+              method.registerCodeReferencesWithResult(registry);
+              return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
+            },
+            DexEncodedMethod::hasCode)
+        .shouldBreak();
+  }
+
+  @Override
+  public String getName() {
+    return "NoInvokeSuperNoSuchMethodErrorsPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java
new file mode 100644
index 0000000..959d28e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import java.util.Set;
+
+public class NoKeptClassesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final Set<DexProgramClass> keptClasses;
+  private final InternalOptions options;
+
+  public NoKeptClassesPolicy(
+      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> keptClasses) {
+    this.appView = appView;
+    this.keptClasses = keptClasses;
+    this.options = appView.options();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    return appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
+        && !keptClasses.contains(sourceClass);
+  }
+
+  @Override
+  public String getName() {
+    return "NoKeptClassesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoLockMergingPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoLockMergingPolicy.java
new file mode 100644
index 0000000..26ee674
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoLockMergingPolicy.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoLockMergingPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoLockMergingPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    boolean sourceCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(sourceClass)
+            || sourceClass.hasStaticSynchronizedMethods();
+    boolean targetCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(targetClass)
+            || targetClass.hasStaticSynchronizedMethods();
+    return !sourceCanBeSynchronizedOn || !targetCanBeSynchronizedOn;
+  }
+
+  @Override
+  public String getName() {
+    return "NoLockMergingPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoMethodResolutionChangesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoMethodResolutionChangesPolicy.java
new file mode 100644
index 0000000..c51ebce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoMethodResolutionChangesPolicy.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import java.util.ArrayList;
+import java.util.List;
+
+public class NoMethodResolutionChangesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoMethodResolutionChangesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return !methodResolutionMayChange(group.getSource(), group.getTarget());
+  }
+
+  private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
+    for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
+      DexEncodedMethod directTargetMethod =
+          target.lookupDirectMethod(virtualSourceMethod.getReference());
+      if (directTargetMethod != null) {
+        // A private method shadows a virtual method. This situation is rare, since it is not
+        // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
+        // possible to rename the private method in the subclass, and then move the virtual method
+        // to the subclass without changing its name.)
+        return true;
+      }
+    }
+
+    // When merging an interface into a class, all instructions on the form "invoke-interface
+    // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
+    // transformation could hide IncompatibleClassChangeErrors.
+    if (source.isInterface() && !target.isInterface()) {
+      List<DexEncodedMethod> defaultMethods = new ArrayList<>();
+      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+        if (!virtualMethod.accessFlags.isAbstract()) {
+          defaultMethods.add(virtualMethod);
+        }
+      }
+
+      // For each of the default methods, the subclass [target] could inherit another default method
+      // with the same signature from another interface (i.e., there is a conflict). In such cases,
+      // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
+      // ClassChangeError.
+      //
+      // Example:
+      //   interface I1 { default void m() {} }
+      //   interface I2 { default void m() {} }
+      //   class C implements I1, I2 {
+      //     ... invoke-interface I1.m ... <- IncompatibleClassChangeError
+      //   }
+      for (DexEncodedMethod method : defaultMethods) {
+        // Conservatively find all possible targets for this method.
+        LookupResultSuccess lookupResult =
+            appView
+                .appInfo()
+                .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
+                .lookupVirtualDispatchTargets(target, appView)
+                .asLookupResultSuccess();
+        assert lookupResult != null;
+        if (lookupResult == null) {
+          return true;
+        }
+        if (lookupResult.contains(method)) {
+          Box<Boolean> found = new Box<>(false);
+          lookupResult.forEach(
+              interfaceTarget -> {
+                if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
+                  return;
+                }
+                DexClass enclosingClass = interfaceTarget.getHolder();
+                if (enclosingClass != null && enclosingClass.isInterface()) {
+                  // Found a default method that is different from the one in [source], aborting.
+                  found.set(true);
+                }
+              },
+              lambdaTarget -> {
+                // The merger should already have excluded lambda implemented interfaces.
+                assert false;
+              });
+          if (found.get()) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String getName() {
+    return "NoMethodResolutionChangesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNestedMergingPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNestedMergingPolicy.java
new file mode 100644
index 0000000..55cae02
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNestedMergingPolicy.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import java.util.Collection;
+import java.util.Map;
+
+public class NoNestedMergingPolicy
+    extends VerticalClassMergerPolicyWithPreprocessing<Map<DexProgramClass, VerticalMergeGroup>> {
+
+  @Override
+  public boolean canMerge(
+      VerticalMergeGroup group, Map<DexProgramClass, VerticalMergeGroup> groups) {
+    if (groups.containsKey(group.getTarget())) {
+      VerticalMergeGroup removedGroup = groups.remove(group.getSource());
+      assert removedGroup == group;
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public Map<DexProgramClass, VerticalMergeGroup> preprocess(
+      Collection<VerticalMergeGroup> groups) {
+    return MapUtils.newIdentityHashMap(
+        builder -> {
+          for (VerticalMergeGroup group : groups) {
+            builder.put(group.getSource(), group);
+          }
+        });
+  }
+
+  @Override
+  public String getName() {
+    return "NoNestedMergingPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNonSerializableClassIntoSerializableClassPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNonSerializableClassIntoSerializableClassPolicy.java
new file mode 100644
index 0000000..f9c4a47
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoNonSerializableClassIntoSerializableClassPolicy.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoNonSerializableClassIntoSerializableClassPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoNonSerializableClassIntoSerializableClassPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
+    //   1.10 The Serializable Interface
+    //   ...
+    //   A Serializable class must do the following:
+    //   ...
+    //     * Have access to the no-arg constructor of its first non-serializable superclass
+    return sourceClass.isInterface()
+        || !targetClass.isSerializable(appView)
+        || sourceClass.isSerializable(appView);
+  }
+
+  @Override
+  public String getName() {
+    return "NoNonSerializableClassIntoSerializableClassPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoServiceInterfacesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoServiceInterfacesPolicy.java
new file mode 100644
index 0000000..28f915f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoServiceInterfacesPolicy.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class NoServiceInterfacesPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
+
+  public NoServiceInterfacesPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.options = appView.options();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return !appView.appServices().allServiceTypes().contains(sourceClass.getType())
+        || !appView.getKeepInfo(targetClass).isPinned(options);
+  }
+
+  @Override
+  public String getName() {
+    return "NoServiceInterfacesPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SafeConstructorInliningPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SafeConstructorInliningPolicy.java
new file mode 100644
index 0000000..15bcf11
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SafeConstructorInliningPolicy.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import com.google.common.collect.Iterables;
+
+public class SafeConstructorInliningPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final MainDexInfo mainDexInfo;
+
+  public SafeConstructorInliningPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    // If there is a constructor in the target, make sure that all source constructors can be
+    // inlined.
+    if (Iterables.isEmpty(targetClass.programInstanceInitializers())) {
+      return true;
+    }
+    TraversalContinuation<?, ?> result =
+        sourceClass.traverseProgramInstanceInitializers(
+            method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
+    return result.shouldContinue();
+  }
+
+  private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
+    if (!appView.options().inlinerOptions().enableInlining) {
+      return true;
+    }
+    Code code = method.getDefinition().getCode();
+    if (code.isCfCode()) {
+      CfCode cfCode = code.asCfCode();
+      ConstraintWithTarget constraint =
+          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
+      if (constraint.isNever()) {
+        return true;
+      }
+      // Constructors can have references beyond the root main dex classes. This can increase the
+      // size of the main dex dependent classes and we should bail out.
+      if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
+        return true;
+      }
+      return false;
+    }
+    if (code.isDefaultInstanceInitializerCode()) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String getName() {
+    return "SafeConstructorInliningPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameApiReferenceLevelPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameApiReferenceLevelPolicy.java
new file mode 100644
index 0000000..736ca06
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameApiReferenceLevelPolicy.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
+
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
+import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SameApiReferenceLevelPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SameApiReferenceLevelPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    assert appView.options().apiModelingOptions().isApiCallerIdentificationEnabled();
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    // Only merge if api reference level of source class is equal to target class. The check is
+    // somewhat expensive.
+    AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
+    ComputedApiLevel sourceApiLevel = getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
+    ComputedApiLevel targetApiLevel = getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
+    return sourceApiLevel.equals(targetApiLevel);
+  }
+
+  @Override
+  public String getName() {
+    return "SameApiReferenceLevelPolicy";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return !appView.options().apiModelingOptions().isApiCallerIdentificationEnabled();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameFeatureSplitPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameFeatureSplitPolicy.java
new file mode 100644
index 0000000..851c5ca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameFeatureSplitPolicy.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SameFeatureSplitPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SameFeatureSplitPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView);
+  }
+
+  @Override
+  public String getName() {
+    return "SameFeatureSplitPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameMainDexGroupPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameMainDexGroupPolicy.java
new file mode 100644
index 0000000..fc8edeb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameMainDexGroupPolicy.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SameMainDexGroupPolicy extends VerticalClassMergerPolicy {
+
+  private final MainDexInfo mainDexInfo;
+  private final SyntheticItems syntheticItems;
+
+  public SameMainDexGroupPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.syntheticItems = appView.getSyntheticItems();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    assert !mainDexInfo.isNone();
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return mainDexInfo.canMerge(sourceClass, targetClass, syntheticItems);
+  }
+
+  @Override
+  public String getName() {
+    return "SameMainDexGroupPolicy";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return mainDexInfo.isNone();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameNestPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameNestPolicy.java
new file mode 100644
index 0000000..a0cda38
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameNestPolicy.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SameNestPolicy extends VerticalClassMergerPolicy {
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    // We abort class merging when merging across nests or from a nest to non-nest.
+    // Without nest this checks null == null.
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return ObjectUtils.identical(targetClass.getNestHost(), sourceClass.getNestHost());
+  }
+
+  @Override
+  public String getName() {
+    return "SameNestPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameStartupPartitionPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameStartupPartitionPolicy.java
new file mode 100644
index 0000000..6651ade
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SameStartupPartitionPolicy.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SameStartupPartitionPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SameStartupPartitionPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    return StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView);
+  }
+
+  @Override
+  public String getName() {
+    return "SameStartupPartitionPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SuccessfulVirtualMethodResolutionInTargetPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SuccessfulVirtualMethodResolutionInTargetPolicy.java
new file mode 100644
index 0000000..a3cee74
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/SuccessfulVirtualMethodResolutionInTargetPolicy.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public class SuccessfulVirtualMethodResolutionInTargetPolicy extends VerticalClassMergerPolicy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SuccessfulVirtualMethodResolutionInTargetPolicy(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    for (ProgramMethod method : group.getSource().virtualProgramMethods()) {
+      MethodResolutionResult resolutionResult =
+          appView.appInfo().resolveMethodOn(group.getTarget(), method.getReference());
+      if (!resolutionResult.isSingleResolution()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public String getName() {
+    return "SuccessfulVirtualMethodResolutionInTargetPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicy.java
new file mode 100644
index 0000000..49f179f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicy.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.classmerging.Policy;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+
+public abstract class VerticalClassMergerPolicy extends Policy {
+
+  public abstract boolean canMerge(VerticalMergeGroup group);
+
+  @Override
+  public boolean isVerticalClassMergerPolicy() {
+    return true;
+  }
+
+  @Override
+  public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicyWithPreprocessing.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicyWithPreprocessing.java
new file mode 100644
index 0000000..7737ebc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/VerticalClassMergerPolicyWithPreprocessing.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2023, 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.classmerging.Policy;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import java.util.Collection;
+
+public abstract class VerticalClassMergerPolicyWithPreprocessing<T> extends Policy {
+
+  public abstract boolean canMerge(VerticalMergeGroup group, T data);
+
+  public abstract T preprocess(Collection<VerticalMergeGroup> groups);
+
+  @Override
+  public boolean isVerticalClassMergerPolicyWithPreprocessing() {
+    return true;
+  }
+
+  @Override
+  public VerticalClassMergerPolicyWithPreprocessing<T>
+      asVerticalClassMergerPolicyWithPreprocessing() {
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index a0821d2..1075fd9 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -74,10 +74,11 @@
     KOTLINC_1_6_0("kotlin-compiler-1.6.0"),
     KOTLINC_1_7_0("kotlin-compiler-1.7.0"),
     KOTLINC_1_8_0("kotlin-compiler-1.8.0"),
+    KOTLINC_1_9_21("kotlin-compiler-1.9.21"),
     KOTLIN_DEV("kotlin-compiler-dev");
 
-    public static final KotlinCompilerVersion MIN_SUPPORTED_VERSION = KOTLINC_1_6_0;
-    public static final KotlinCompilerVersion MAX_SUPPORTED_VERSION = KOTLINC_1_8_0;
+    public static final KotlinCompilerVersion MIN_SUPPORTED_VERSION = KOTLINC_1_7_0;
+    public static final KotlinCompilerVersion MAX_SUPPORTED_VERSION = KOTLINC_1_9_21;
 
     private final String folder;
 
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 0e08d8f..b64e03a 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -86,7 +86,6 @@
   private final List<Path> features = new ArrayList<>();
   private Path resourceShrinkerOutput = null;
   private HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
-  private PartitionMapConsumer partitionMapConsumer = null;
 
   @Override
   public boolean isR8TestBuilder() {
@@ -115,21 +114,7 @@
     if (enableMinification.isFalse()) {
       builder.setDisableMinification(true);
     }
-    StringBuilder proguardMapBuilder = new StringBuilder();
-    builder.setProguardMapConsumer(
-        new StringConsumer() {
-          @Override
-          public void accept(String string, DiagnosticsHandler handler) {
-            proguardMapBuilder.append(string);
-          }
-
-          @Override
-          public void finished(DiagnosticsHandler handler) {
-            // Nothing to do.
-          }
-        });
-    builder.setPartitionMapConsumer(partitionMapConsumer);
-
+    StringBuilder proguardMapBuilder = wrapProguardMapConsumer(builder);
     if (!applyMappingMaps.isEmpty()) {
       try {
         Path mappingsDir = getState().getNewTempFolder();
@@ -207,6 +192,25 @@
     return compileResult;
   }
 
+  private static StringBuilder wrapProguardMapConsumer(Builder builder) {
+    StringBuilder pgMapOutput = new StringBuilder();
+    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 = new StringBuilder();
     StringConsumer pgConfConsumer = builder.getProguardConfigurationConsumer();
@@ -900,7 +904,7 @@
   }
 
   public T setPartitionMapConsumer(PartitionMapConsumer partitionMapConsumer) {
-    this.partitionMapConsumer = partitionMapConsumer;
+    getBuilder().setPartitionMapConsumer(partitionMapConsumer);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 0515b8d..1792e97 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -196,6 +196,11 @@
     return isCfRuntime() ? ToolHelper.getJava8RuntimeJar() : getDefaultAndroidJar();
   }
 
+  public CfRuntime getCfRuntime() {
+    assert isCfRuntime();
+    return runtime.asCf();
+  }
+
   // Access to underlying runtime/wrapper.
   public TestRuntime getRuntime() {
     return runtime;
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/PackagePrivateOverrideWithInterfacePublicizerTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/PackagePrivateOverrideWithInterfacePublicizerTest.java
new file mode 100644
index 0000000..8e3c527
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/PackagePrivateOverrideWithInterfacePublicizerTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.accessrelaxation;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.resolution.virtualtargets.package_a.ViewModel;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Regression test for b/314984596. */
+@RunWith(Parameterized.class)
+public class PackagePrivateOverrideWithInterfacePublicizerTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, ViewModel.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertSuccessOutput);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, ViewModel.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules("wtf.I")
+        .allowAccessModification()
+        .allowStdoutMessages()
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertSuccessOutput);
+  }
+
+  public List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(I.class).setClassDescriptor("Lwtf/I;").transform(),
+        transformer(SubViewModel.class).setImplementsClassDescriptors("Lwtf/I;").transform());
+  }
+
+  private void assertSuccessOutput(TestRunResult<?> result) {
+    if (parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik()) {
+      result.assertFailureWithErrorThatMatches(containsString("overrides final"));
+    } else {
+      result.assertSuccessWithOutputLines("SubViewModel.clear()", "ViewModel.clear()");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      SubViewModel subViewModel = new SubViewModel();
+      subViewModel.clear();
+      subViewModel.clearBridge();
+    }
+  }
+
+  // Repackaged to wtf.I using transformer so that it sorts higher than the ViewModel class.
+  // This ensures that I is processed before ViewModel in the access modifier, which reproduces
+  // the bug in b/314984596.
+  public interface /*wtf.*/ I {}
+
+  @NeverClassInline
+  public static class SubViewModel extends ViewModel implements I {
+
+    @NeverInline
+    public void clear() {
+      System.out.println("SubViewModel.clear()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClinitDeadlockAfterMergingMultipleGroupsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClinitDeadlockAfterMergingMultipleGroupsTest.java
index 635ff0e..b2dc326 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClinitDeadlockAfterMergingMultipleGroupsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClinitDeadlockAfterMergingMultipleGroupsTest.java
@@ -7,9 +7,9 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
-import com.android.tools.r8.horizontalclassmerging.Policy;
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
index 513561a..662a717 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
@@ -6,6 +6,7 @@
 
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -43,7 +44,10 @@
               options.testing.enableVerticalClassMergerLensAssertion = verifyLensLookup;
             })
         .enableInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
         .addDontObfuscate()
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class, ArgType.class))
         .setMinApi(parameters)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Caught NPE");
@@ -78,6 +82,7 @@
     }
   }
 
+  @NoVerticalClassMerging
   static class B extends A {
 
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerCollisionWithOverridesTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerCollisionWithOverridesTest.java
new file mode 100644
index 0000000..d620d5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerCollisionWithOverridesTest.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+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 VerticalClassMergerCollisionWithOverridesTest 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(A.class, B.class, Main.class)
+        .addProgramClassFileData(getTransformedClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C C.m()", "B B.m()");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addProgramClassFileData(getTransformedClass())
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C C.m()", "B B.m()");
+  }
+
+  private static byte[] getTransformedClass() throws Exception {
+    return transformer(C.class)
+        .removeMethods(
+            (access, name, descriptor, signature, exceptions) ->
+                name.equals("m") && descriptor.equals("()" + descriptor(B.class)))
+        .transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      C c = new C();
+      c.callOnA();
+      c.callOnB();
+    }
+  }
+
+  abstract static class A {
+
+    abstract A m();
+
+    A callOnA() {
+      return m();
+    }
+  }
+
+  @NoVerticalClassMerging
+  abstract static class B extends A {
+
+    @NeverInline
+    B m() {
+      System.out.println("B B.m()");
+      return this;
+    }
+
+    B callOnB() {
+      return m();
+    }
+  }
+
+  @NeverClassInline
+  static class C extends B {
+
+    @NeverInline
+    @Override
+    C m() {
+      System.out.println("C C.m()");
+      return this;
+    }
+
+    // DELETED BY TRANSFORMER: synthetic bridge B m() { ...}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeInterfaceToVirtualInSuperClassTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeInterfaceToVirtualInSuperClassTest.java
new file mode 100644
index 0000000..ef58833
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeInterfaceToVirtualInSuperClassTest.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023git add, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+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 VerticalClassMergerInvokeInterfaceToVirtualInSuperClassTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(I.class))
+        .enableInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I i = new B();
+      i.m();
+    }
+  }
+
+  interface I {
+
+    void m();
+  }
+
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    public void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  static class B extends A implements I {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodCollisionTest.java
new file mode 100644
index 0000000..1458079
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodCollisionTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.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 VerticalClassMergerPinnedMethodCollisionTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keep class " + UserSub.class.getTypeName() + " {",
+            "  void f(" + B.class.getTypeName() + ");",
+            "}")
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("B", "B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new User().f(new B());
+      new UserSub().f(new B());
+    }
+  }
+
+  static class A {}
+
+  static class B extends A {
+
+    @Override
+    public String toString() {
+      return "B";
+    }
+  }
+
+  static class User {
+
+    @NeverInline
+    void f(A a) {
+      System.out.println(a);
+    }
+  }
+
+  static class UserSub extends User {
+
+    void f(B b) {
+      System.out.println(b);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodInterfaceCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodInterfaceCollisionTest.java
new file mode 100644
index 0000000..84fa122
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerPinnedMethodInterfaceCollisionTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+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 VerticalClassMergerPinnedMethodInterfaceCollisionTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .addKeepRules(
+            "-keep class " + OtherUser.class.getTypeName() + " {",
+            "  void f(" + B.class.getTypeName() + ");",
+            "}")
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("B", "B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      keep(new UserImpl());
+      new OtherUser().f(new B());
+    }
+
+    static void keep(User user) {
+      user.f(new B());
+    }
+  }
+
+  static class A {}
+
+  static class B extends A {
+
+    @Override
+    public String toString() {
+      return "B";
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface User {
+
+    void f(A a);
+  }
+
+  static class UserImpl implements User {
+
+    @Override
+    public void f(A a) {
+      System.out.println(a);
+    }
+  }
+
+  static class OtherUser {
+
+    void f(B b) { // f(B)
+      System.out.println(b);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 7254cd5..0e87692 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -388,7 +388,7 @@
     Set<String> preservedClassNames =
         ImmutableSet.of(
             "classmerging.NestedDefaultInterfaceMethodsTest",
-            "classmerging.NestedDefaultInterfaceMethodsTest$B",
+            "classmerging.NestedDefaultInterfaceMethodsTest$A",
             "classmerging.NestedDefaultInterfaceMethodsTest$C");
     runTest(
         testForR8(parameters.getBackend())
@@ -413,7 +413,7 @@
     Set<String> preservedClassNames =
         ImmutableSet.of(
             "classmerging.NestedDefaultInterfaceMethodsTest",
-            "classmerging.NestedDefaultInterfaceMethodsTest$B",
+            "classmerging.NestedDefaultInterfaceMethodsTest$A",
             "classmerging.NestedDefaultInterfaceMethodsTest$C");
     runTestOnInput(
         testForR8(parameters.getBackend())
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingInvokeSuperToNestMemberTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingInvokeSuperToNestMemberTest.java
new file mode 100644
index 0000000..b8147e0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingInvokeSuperToNestMemberTest.java
@@ -0,0 +1,162 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoAccessModification;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.classmerging.vertical.VerticalClassMergingInvokeSuperToNestMemberTest.Foo.Bar;
+import com.android.tools.r8.classmerging.vertical.VerticalClassMergingInvokeSuperToNestMemberTest.Foo.Baz;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingInvokeSuperToNestMemberTest extends TestBase {
+
+  @Parameter(0)
+  public boolean emitNestAnnotationsInDex;
+
+  @Parameter(1)
+  public boolean enableVerticalClassMerging;
+
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameters(name = "{2}, nest in dex: {0}, vertical: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    assumeFalse(emitNestAnnotationsInDex);
+    assumeFalse(enableVerticalClassMerging);
+    testForJvm(parameters)
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.getCfRuntime().isOlderThan(CfVm.JDK11),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class),
+            runResult -> runResult.assertSuccessWithOutputLines("Bar.bar()"));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || !emitNestAnnotationsInDex);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.emitNestAnnotationsInDex = emitNestAnnotationsInDex)
+        .addVerticallyMergedClassesInspector(
+            inspector -> {
+              if (enableVerticalClassMerging) {
+                inspector.assertMergedIntoSubtype(Bar.class);
+              } else {
+                inspector.assertNoClassesMerged();
+              }
+            })
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoAccessModificationAnnotationsForMembers()
+        .enableNoHorizontalClassMergingAnnotations()
+        .applyIf(
+            enableVerticalClassMerging,
+            R8TestBuilder::addNoVerticalClassMergingAnnotations,
+            R8TestBuilder::enableNoVerticalClassMergingAnnotations)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.canUseNestBasedAccesses()
+                || (parameters.isDexRuntime() && !emitNestAnnotationsInDex),
+            runResult -> runResult.assertSuccessWithOutputLines("Bar.bar()"),
+            parameters.isCfRuntime(),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class),
+            runResult -> runResult.assertFailureWithErrorThatThrows(IllegalAccessError.class));
+  }
+
+  private Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(Foo.class).setNest(Foo.class, Bar.class, Baz.class).transform(),
+        transformer(Bar.class)
+            .setNest(Foo.class, Bar.class, Baz.class)
+            .setPrivate(Bar.class.getDeclaredMethod("bar"))
+            .transform(),
+        transformer(Baz.class)
+            .setNest(Foo.class, Bar.class, Baz.class)
+            .transformMethodInsnInMethod(
+                "test",
+                (opcode, owner, name, descriptor, isInterface, continuation) -> {
+                  assertEquals(Opcodes.INVOKEVIRTUAL, opcode);
+                  assertEquals(binaryName(Baz.class), owner);
+                  assertEquals("bar", name);
+                  continuation.visitMethodInsn(
+                      Opcodes.INVOKESPECIAL, binaryName(Bar.class), name, descriptor, isInterface);
+                })
+            .transform());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Foo.test();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class Foo {
+
+    @NeverInline
+    static void test() {
+      new Baz().test();
+    }
+
+    @NoHorizontalClassMerging
+    @NoVerticalClassMerging
+    static class Bar {
+
+      @NeverInline
+      @NoAccessModification
+      /*private*/ void bar() {
+        System.out.println("Bar.bar()");
+      }
+    }
+
+    @NeverClassInline
+    @NoHorizontalClassMerging
+    static class Baz extends Bar {
+
+      void test() {
+        bar();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingNestHostIntoNestHostTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingNestHostIntoNestHostTest.java
new file mode 100644
index 0000000..717a561
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingNestHostIntoNestHostTest.java
@@ -0,0 +1,175 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoAccessModification;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.classmerging.vertical.VerticalClassMergingNestHostIntoNestHostTest.Bar.InnerBar;
+import com.android.tools.r8.classmerging.vertical.VerticalClassMergingNestHostIntoNestHostTest.Foo.InnerFoo;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingNestHostIntoNestHostTest extends TestBase {
+
+  @Parameter(0)
+  public boolean emitNestAnnotationsInDex;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, nest in dex: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || !emitNestAnnotationsInDex);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.emitNestAnnotationsInDex = emitNestAnnotationsInDex)
+        // TODO(b/315283663): Could allow merging two classes that do not have the same nest.
+        .addVerticallyMergedClassesInspector(
+            inspector -> {
+              if (parameters.isCfRuntime() || emitNestAnnotationsInDex) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector.assertMergedIntoSubtype(Foo.class);
+              }
+            })
+        .enableInliningAnnotations()
+        .enableNoAccessModificationAnnotationsForMembers()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isDexRuntime() && emitNestAnnotationsInDex,
+            runResult -> runResult.assertFailureWithErrorThatThrows(IllegalAccessError.class),
+            parameters.isCfRuntime() && parameters.getRuntime().asCf().isOlderThan(CfVm.JDK11),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class),
+            runResult ->
+                runResult.assertSuccessWithOutputLines(
+                    "InnerFoo.innerFoo()", "Foo.foo()", "InnerBar.innerBar()", "Bar.bar()"));
+  }
+
+  private Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(Foo.class)
+            .setNest(Foo.class, InnerFoo.class)
+            .transformMethodInsnInMethod(
+                "test",
+                (opcode, owner, name, descriptor, isInterface, continuation) ->
+                    continuation.visitMethodInsn(
+                        opcode, owner, "innerFoo", descriptor, isInterface))
+            .transform(),
+        transformer(InnerFoo.class)
+            .setNest(Foo.class, InnerFoo.class)
+            .transformMethodInsnInMethod(
+                "test",
+                (opcode, owner, name, descriptor, isInterface, continuation) ->
+                    continuation.visitMethodInsn(opcode, owner, "foo", descriptor, isInterface))
+            .transform(),
+        transformer(Bar.class)
+            .setNest(Bar.class, InnerBar.class)
+            .transformMethodInsnInMethod(
+                "test",
+                (opcode, owner, name, descriptor, isInterface, continuation) ->
+                    continuation.visitMethodInsn(
+                        opcode, owner, "innerBar", descriptor, isInterface))
+            .transform(),
+        transformer(InnerBar.class)
+            .setNest(Bar.class, InnerBar.class)
+            .transformMethodInsnInMethod(
+                "test",
+                (opcode, owner, name, descriptor, isInterface, continuation) ->
+                    continuation.visitMethodInsn(opcode, owner, "bar", descriptor, isInterface))
+            .transform());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Foo.test();
+      InnerFoo.test();
+      Bar.test();
+      InnerBar.test();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class Foo {
+
+    @NeverInline
+    @NoAccessModification
+    private static void foo() {
+      System.out.println("Foo.foo()");
+    }
+
+    static void test() {
+      InnerFoo.innerFoo();
+    }
+
+    @NoHorizontalClassMerging
+    static class InnerFoo {
+
+      @NeverInline
+      @NoAccessModification
+      private static void innerFoo() {
+        System.out.println("InnerFoo.innerFoo()");
+      }
+
+      static void test() {
+        Foo.foo();
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class Bar extends Foo {
+
+    @NeverInline
+    @NoAccessModification
+    private static void bar() {
+      System.out.println("Bar.bar()");
+    }
+
+    static void test() {
+      InnerBar.innerBar();
+    }
+
+    @NoHorizontalClassMerging
+    static class InnerBar {
+
+      @NeverInline
+      @NoAccessModification
+      private static void innerBar() {
+        System.out.println("InnerBar.innerBar()");
+      }
+
+      static void test() {
+        Bar.bar();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index e043042..867d2a7 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -1572,6 +1572,14 @@
         return internalInstanceField(getMirror(), thisObjectId, fieldId);
       }
 
+      public void setFieldOnThis(String fieldName, String fieldSignature, Value value) {
+        long thisObjectId = getMirror().getThisObject(getThreadId(), getFrameId());
+        long classId = getMirror().getReferenceType(thisObjectId);
+        // TODO(zerny): Search supers too. This will only get the field if directly on the class.
+        long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+        internalSetInstanceField(getMirror(), thisObjectId, fieldId, value);
+      }
+
       private long findField(VmMirror mirror, long classId, String fieldName,
           String fieldSignature) {
 
@@ -1633,6 +1641,19 @@
       }
     }
 
+    private static void internalSetInstanceField(
+        VmMirror mirror, long objectId, long fieldId, Value value) {
+      CommandPacket commandPacket =
+          new CommandPacket(
+              ObjectReferenceCommandSet.CommandSetID, ObjectReferenceCommandSet.SetValuesCommand);
+      commandPacket.setNextValueAsObjectID(objectId);
+      commandPacket.setNextValueAsInt(1); // field count.
+      commandPacket.setNextValueAsFieldID(fieldId);
+      commandPacket.setNextValueAsUntaggedValue(value);
+      ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+      assert replyPacket.getErrorCode() == Error.NONE : "Error code: " + replyPacket.getErrorCode();
+    }
+
     private static Value internalInstanceField(VmMirror mirror, long objectId, long fieldId) {
       CommandPacket commandPacket =
           new CommandPacket(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/EffectivelyFinalInstanceFieldCanonicalizationAfterConstructorInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/EffectivelyFinalInstanceFieldCanonicalizationAfterConstructorInliningTest.java
new file mode 100644
index 0000000..d5865d7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/EffectivelyFinalInstanceFieldCanonicalizationAfterConstructorInliningTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2023, 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.canonicalization;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoRedundantFieldLoadElimination;
+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;
+
+/** Regression test for b/315877832. */
+@RunWith(Parameterized.class)
+public class EffectivelyFinalInstanceFieldCanonicalizationAfterConstructorInliningTest
+    extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoRedundantFieldLoadEliminationAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Object alwaysNull = System.currentTimeMillis() > 0 ? null : new Object();
+      A a = new A(alwaysNull);
+      System.out.println(a);
+    }
+  }
+
+  static class A {
+
+    @NoRedundantFieldLoadElimination Object f;
+
+    A(Object o) {
+      f = o;
+      if (f == null) {
+        f = "Hello";
+      }
+      print(f);
+    }
+
+    @NeverInline
+    static void print(Object o) {
+      System.out.print(o);
+    }
+
+    @Override
+    public String toString() {
+      return ", world!";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java
new file mode 100644
index 0000000..2384387
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyVarargsTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EmptyVarargsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAnnotation()
+        .addKeepRules("-keepclassmembers class * { @com.android.tools.r8.Keep *; }")
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject testMethod =
+                  mainClassSubject.uniqueMethodWithOriginalName("testDeclared");
+              assertTrue(testMethod.streamInstructions().noneMatch(InstructionSubject::isNewArray));
+              testMethod = mainClassSubject.uniqueMethodWithOriginalName("testNonDeclared");
+              assertTrue(testMethod.streamInstructions().noneMatch(InstructionSubject::isNewArray));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("hi", "hi");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      try {
+        testDeclared();
+        testNonDeclared();
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Keep
+    public Main() {}
+
+    @Keep
+    public void doPrint() {
+      System.out.println("hi");
+    }
+
+    @NeverInline
+    static void testNonDeclared() throws Exception {
+      Constructor<Main> ctor = Main.class.getConstructor();
+      Main instance = ctor.newInstance();
+      Method method = Main.class.getMethod("doPrint");
+      method.invoke(instance);
+    }
+
+    @NeverInline
+    static void testDeclared() throws Exception {
+      Constructor<Main> ctor = Main.class.getDeclaredConstructor();
+      Main instance = ctor.newInstance();
+      Method method = Main.class.getDeclaredMethod("doPrint");
+      method.invoke(instance);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
new file mode 100644
index 0000000..3468b4c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
@@ -0,0 +1,192 @@
+// Copyright (c) 2023, 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.keepanno.classpatterns;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ClassNamePatternsTest extends TestBase {
+
+  static final Class<?> A1 = com.android.tools.r8.keepanno.classpatterns.pkg1.A.class;
+  static final Class<?> B1 = com.android.tools.r8.keepanno.classpatterns.pkg1.B.class;
+  static final Class<?> A2 = com.android.tools.r8.keepanno.classpatterns.pkg2.A.class;
+  static final Class<?> B2 = com.android.tools.r8.keepanno.classpatterns.pkg2.B.class;
+
+  static final String EXPECTED_ALL = StringUtils.lines("pkg1.A", "pkg1.B", "pkg2.A", "pkg2.B");
+  static final String EXPECTED_PKG = StringUtils.lines("pkg1.A", "pkg1.B");
+  static final String EXPECTED_NAME = StringUtils.lines("pkg1.B", "pkg2.B");
+  static final String EXPECTED_SINGLE = StringUtils.lines("pkg2.A");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ClassNamePatternsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getBaseInputClasses())
+        .addProgramClasses(TestAll.class)
+        .run(parameters.getRuntime(), TestAll.class)
+        .assertSuccessWithOutput(EXPECTED_ALL);
+  }
+
+  private void runTestR8(Class<?> mainClass, String expected) throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getBaseInputClasses())
+        .addProgramClasses(mainClass)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), mainClass)
+        .assertSuccessWithOutput(expected);
+  }
+
+  @Test
+  public void testAllR8() throws Exception {
+    runTestR8(TestAll.class, EXPECTED_ALL);
+  }
+
+  @Test
+  public void testPkgR8() throws Exception {
+    runTestR8(TestPkg.class, EXPECTED_PKG);
+  }
+
+  @Test
+  public void testNameR8() throws Exception {
+    runTestR8(TestName.class, EXPECTED_NAME);
+  }
+
+  @Test
+  public void testSingleR8() throws Exception {
+    runTestR8(TestSingle.class, EXPECTED_SINGLE);
+  }
+
+  public List<Class<?>> getBaseInputClasses() {
+    return ImmutableList.of(Util.class, A1, B1, A2, B2);
+  }
+
+  static class Util {
+    private static void lookupClassesAndInvokeMethods() {
+      for (String pkg : Arrays.asList("pkg1", "pkg2")) {
+        for (String name : Arrays.asList("A", "B")) {
+          String type = "com.android.tools.r8.keepanno.classpatterns." + pkg + "." + name;
+          try {
+            Class<?> clazz = Class.forName(type);
+            System.out.println(clazz.getDeclaredMethod("foo").invoke(null));
+          } catch (ClassNotFoundException ignored) {
+          } catch (IllegalAccessException ignored) {
+          } catch (InvocationTargetException ignored) {
+          } catch (NoSuchMethodException ignored) {
+          }
+        }
+      }
+    }
+  }
+
+  static class TestAll {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          // The empty class pattern is equivalent to "any class".
+          classNamePattern = @ClassNamePattern(),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestAll().foo();
+    }
+  }
+
+  static class TestPkg {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          classNamePattern =
+              @ClassNamePattern(packageName = "com.android.tools.r8.keepanno.classpatterns.pkg1"),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestPkg().foo();
+    }
+  }
+
+  static class TestName {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          classNamePattern = @ClassNamePattern(simpleName = "B"),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestName().foo();
+    }
+  }
+
+  static class TestSingle {
+
+    @UsesReflection(
+        @KeepTarget(
+            kind = KeepItemKind.CLASS_AND_METHODS,
+            classNamePattern =
+                @ClassNamePattern(
+                    simpleName = "A",
+                    packageName = "com.android.tools.r8.keepanno.classpatterns.pkg2"),
+            methodName = "foo",
+            methodReturnTypePattern =
+                @TypePattern(
+                    classNamePattern =
+                        @ClassNamePattern(packageName = "java.lang", simpleName = "String"))))
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestSingle().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
new file mode 100644
index 0000000..eab0e5f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.keepanno.classpatterns.pkg1;
+
+public class A {
+
+  public static String foo() {
+    return "pkg1.A";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
new file mode 100644
index 0000000..edeb6b8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.keepanno.classpatterns.pkg1;
+
+public class B {
+
+  public static String foo() {
+    return "pkg1.B";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
new file mode 100644
index 0000000..ca0667e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.keepanno.classpatterns.pkg2;
+
+public class A {
+
+  public static String foo() {
+    return "pkg2.A";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
new file mode 100644
index 0000000..c98e746
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.keepanno.classpatterns.pkg2;
+
+public class B {
+
+  public static String foo() {
+    return "pkg2.B";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index bd3d82f..312e8bb 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.cfmethodgeneration.CodeGenerationBase;
 import com.android.tools.r8.keepanno.annotations.CheckOptimizedOut;
 import com.android.tools.r8.keepanno.annotations.CheckRemoved;
+import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
 import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
 import com.android.tools.r8.keepanno.annotations.KeepBinding;
 import com.android.tools.r8.keepanno.annotations.KeepCondition;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
 import com.google.common.base.Strings;
@@ -51,6 +53,11 @@
     Generator.run();
   }
 
+  private static final String DEFAULT_INVALID_TYPE_PATTERN =
+      "@" + simpleName(TypePattern.class) + "(name = \"\")";
+  private static final String DEFAULT_INVALID_CLASS_NAME_PATTERN =
+      "@" + simpleName(ClassNamePattern.class) + "(simpleName = \"\")";
+
   public static String quote(String str) {
     return "\"" + str + "\"";
   }
@@ -69,6 +76,16 @@
       this.name = name;
     }
 
+    public GroupMember setType(String type) {
+      valueType = type;
+      return this;
+    }
+
+    public GroupMember setValue(String value) {
+      valueDefault = value;
+      return this;
+    }
+
     @Override
     public GroupMember self() {
       return this;
@@ -90,50 +107,40 @@
       generator.println("public static final String " + name + " = " + quote(name) + ";");
     }
 
+    public GroupMember requiredValue(Class<?> type) {
+      assert valueDefault == null;
+      return setType(simpleName(type));
+    }
+
+    public GroupMember requiredArrayValue(Class<?> type) {
+      assert valueDefault == null;
+      return setType(simpleName(type) + "[]");
+    }
+
     public GroupMember requiredStringValue() {
-      assert valueDefault == null;
-      return defaultType("String");
+      return requiredValue(String.class);
     }
 
-    public GroupMember requiredValueOfType(String type) {
-      assert valueDefault == null;
-      return defaultType(type);
+    public GroupMember defaultValue(Class<?> type, String value) {
+      setType(simpleName(type));
+      return setValue(value);
     }
 
-    public GroupMember requiredValueOfType(Class<?> type) {
-      assert valueDefault == null;
-      return defaultType(simpleName(type));
-    }
-
-    public GroupMember requiredValueOfArrayType(Class<?> type) {
-      assert valueDefault == null;
-      return defaultType(simpleName(type) + "[]");
-    }
-
-    public GroupMember defaultType(String type) {
-      valueType = type;
-      return this;
-    }
-
-    public GroupMember defaultValue(String value) {
-      valueDefault = value;
-      return this;
+    public GroupMember defaultArrayValue(Class<?> type, String value) {
+      setType(simpleName(type) + "[]");
+      return setValue("{" + value + "}");
     }
 
     public GroupMember defaultEmptyString() {
-      return defaultType("String").defaultValue(quote(""));
+      return defaultValue(String.class, quote(""));
     }
 
     public GroupMember defaultObjectClass() {
-      return defaultType("Class<?>").defaultValue("Object.class");
+      return setType("Class<?>").setValue("Object.class");
     }
 
-    public GroupMember defaultEmptyArray(String valueType) {
-      return defaultType(valueType + "[]").defaultValue("{}");
-    }
-
-    public GroupMember defaultEmptyArray(Class<?> type) {
-      return defaultEmptyArray(simpleName(type));
+    public GroupMember defaultArrayEmpty(Class<?> type) {
+      return defaultArrayValue(type, "");
     }
   }
 
@@ -279,7 +286,7 @@
 
     private Group createBindingsGroup() {
       return new Group("bindings")
-          .addMember(new GroupMember("bindings").defaultEmptyArray(KeepBinding.class));
+          .addMember(new GroupMember("bindings").defaultArrayEmpty(KeepBinding.class));
     }
 
     private Group createPreconditionsGroup() {
@@ -291,7 +298,7 @@
                   .setDocReturn(
                       "The list of preconditions. "
                           + "Defaults to no conditions, thus trivially/unconditionally satisfied.")
-                  .defaultEmptyArray(KeepCondition.class));
+                  .defaultArrayEmpty(KeepCondition.class));
     }
 
     private Group createConsequencesGroup() {
@@ -300,7 +307,7 @@
               new GroupMember("consequences")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
                   .setDocReturn("The list of target consequences.")
-                  .requiredValueOfArrayType(KeepTarget.class));
+                  .requiredArrayValue(KeepTarget.class));
     }
 
     private Group createConsequencesAsValueGroup() {
@@ -309,7 +316,7 @@
               new GroupMember("value")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
                   .setDocReturn("The list of target consequences.")
-                  .requiredValueOfArrayType(KeepTarget.class));
+                  .requiredArrayValue(KeepTarget.class));
     }
 
     private Group createAdditionalPreconditionsGroup() {
@@ -320,7 +327,7 @@
                   .setDocReturn(
                       "The list of additional preconditions. "
                           + "Defaults to no additional preconditions.")
-                  .defaultEmptyArray("KeepCondition"));
+                  .defaultArrayEmpty(KeepCondition.class));
     }
 
     private Group createAdditionalTargetsGroup(String docTitle) {
@@ -331,7 +338,7 @@
                   .setDocReturn(
                       "List of additional target consequences. "
                           + "Defaults to no additional target consequences.")
-                  .defaultEmptyArray("KeepTarget"));
+                  .defaultArrayEmpty(KeepTarget.class));
     }
 
     private Group typePatternGroup() {
@@ -345,7 +352,11 @@
               new GroupMember("constant")
                   .setDocTitle("Exact type from a class constant.")
                   .addParagraph("For example, {@code String.class}.")
-                  .defaultObjectClass());
+                  .defaultObjectClass())
+          .addMember(
+              new GroupMember("classNamePattern")
+                  .setDocTitle("Classes matching the class-name pattern.")
+                  .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN));
       // TODO(b/248408342): Add more injections on type pattern variants.
       // /** Exact type name as a string to match any array with that type as member. */
       // String arrayOf() default "";
@@ -366,14 +377,37 @@
       // boolean anyReference() default false;
     }
 
+    private Group classNamePatternSimpleNameGroup() {
+      return new Group("class-simple-name")
+          .addMember(
+              new GroupMember("simpleName")
+                  .setDocTitle("Exact simple name of the class or interface.")
+                  .addParagraph(
+                      "For example, the simple name of {@code com.example.MyClass} is {@code"
+                          + " MyClass}.")
+                  .addParagraph("The default matches any simple name.")
+                  .defaultEmptyString());
+    }
+
+    private Group classNamePatternPackageGroup() {
+      return new Group("class-package-name")
+          .addMember(
+              new GroupMember("packageName")
+                  .setDocTitle("Exact package name of the class or interface.")
+                  .addParagraph(
+                      "For example, the package of {@code com.example.MyClass} is {@code"
+                          + " com.example}.")
+                  .addParagraph("The default matches any package.")
+                  .defaultEmptyString());
+    }
+
     private Group getKindGroup() {
       return new Group(KIND_GROUP).addMember(getKindMember());
     }
 
     private static GroupMember getKindMember() {
       return new GroupMember("kind")
-          .defaultType("KeepItemKind")
-          .defaultValue("KeepItemKind.DEFAULT")
+          .defaultValue(KeepItemKind.class, "KeepItemKind.DEFAULT")
           .setDocTitle("Specify the kind of this item pattern.")
           .setDocReturn("The kind for this pattern.")
           .addParagraph("Possible values are:")
@@ -406,7 +440,7 @@
                       "The specified option constraints do not need to be preserved for the"
                           + " target.")
                   .setDocReturn("Option constraints allowed to be modified for the target.")
-                  .defaultEmptyArray("KeepOption"))
+                  .defaultArrayEmpty(KeepOption.class))
           .addMember(
               new GroupMember("disallow")
                   .setDeprecated("Use " + docLink(constraints()) + " instead.")
@@ -415,7 +449,7 @@
                   .addParagraph(
                       "The specified option constraints *must* be preserved for the target.")
                   .setDocReturn("Option constraints not allowed to be modified for the target.")
-                  .defaultEmptyArray("KeepOption"))
+                  .defaultArrayEmpty(KeepOption.class))
           .addDocFooterParagraph(
               "If nothing is specified for "
                   + CONSTRAINTS_GROUP
@@ -447,7 +481,7 @@
                       KeepConstraint.FIELD_GET,
                       KeepConstraint.FIELD_SET))
           .setDocReturn("Usage constraints for the target.")
-          .defaultEmptyArray(KeepConstraint.class);
+          .defaultArrayEmpty(KeepConstraint.class);
     }
 
     private GroupMember bindingName() {
@@ -487,10 +521,19 @@
           .defaultObjectClass();
     }
 
+    private GroupMember classNamePattern() {
+      return new GroupMember("classNamePattern")
+          .setDocTitle(
+              "Define the " + CLASS_NAME_GROUP + " pattern by reference to a class-name pattern.")
+          .setDocReturn("The class-name pattern that defines the class.")
+          .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN);
+    }
+
     private Group createClassNamePatternGroup() {
       return new Group(CLASS_NAME_GROUP)
           .addMember(className())
           .addMember(classConstant())
+          .addMember(classNamePattern())
           .addDocFooterParagraph("If none are specified the default is to match any class name.");
     }
 
@@ -604,7 +647,7 @@
                       "Mutually exclusive with all field and method properties",
                       "as use restricts the match to both types of members.")
                   .setDocReturn("The member access-flag constraints that must be met.")
-                  .defaultEmptyArray("MemberAccessFlags"));
+                  .defaultArrayEmpty(MemberAccessFlags.class));
     }
 
     private String getMutuallyExclusiveForMethodProperties() {
@@ -635,7 +678,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any method-access flags"))
                   .setDocReturn("The method access-flag constraints that must be met.")
-                  .defaultEmptyArray("MethodAccessFlags"));
+                  .defaultArrayEmpty(MethodAccessFlags.class));
     }
 
     private Group createMethodNameGroup() {
@@ -672,8 +715,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any return type"))
                   .setDocReturn("The pattern of the method return type.")
-                  .defaultType("TypePattern")
-                  .defaultValue("@TypePattern(name = \"\")"));
+                  .defaultValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private Group createMethodParametersGroup() {
@@ -685,8 +727,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any parameters"))
                   .setDocReturn("The list of qualified type names of the method parameters.")
-                  .defaultType("String[]")
-                  .defaultValue("{\"\"}"))
+                  .defaultArrayValue(String.class, quote("")))
           .addMember(
               new GroupMember("methodParameterTypePatterns")
                   .setDocTitle(
@@ -694,8 +735,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any parameters"))
                   .setDocReturn("The list of type patterns for the method parameters.")
-                  .defaultType("TypePattern[]")
-                  .defaultValue("{@TypePattern(name = \"\")}"));
+                  .defaultArrayValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private Group createFieldAccessGroup() {
@@ -706,7 +746,7 @@
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any field-access flags"))
                   .setDocReturn("The field access-flag constraints that must be met.")
-                  .defaultEmptyArray("FieldAccessFlags"));
+                  .defaultArrayEmpty(FieldAccessFlags.class));
     }
 
     private Group createFieldNameGroup() {
@@ -742,8 +782,7 @@
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any type"))
                   .setDocReturn("The type pattern for the field type.")
-                  .defaultType("TypePattern")
-                  .defaultValue("@TypePattern(name = \"\")"));
+                  .defaultValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private void generateClassAndMemberPropertiesWithClassAndMemberBinding() {
@@ -814,13 +853,37 @@
           .printDoc(this::println);
       println("@Target(ElementType.ANNOTATION_TYPE)");
       println("@Retention(RetentionPolicy.CLASS)");
-      println("public @interface TypePattern {");
+      println("public @interface " + simpleName(TypePattern.class) + " {");
       println();
       withIndent(() -> typePatternGroup().generate(this));
       println();
       println("}");
     }
 
+    private void generateClassNamePattern() {
+      printCopyRight(2023);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A pattern structure for matching names of classes and interfaces.")
+          .addParagraph(
+              "If no properties are set, the default pattern matches any name of a class or"
+                  + " interface.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + simpleName(ClassNamePattern.class) + " {");
+      println();
+      withIndent(
+          () -> {
+            classNamePatternSimpleNameGroup().generate(this);
+            println();
+            classNamePatternPackageGroup().generate(this);
+          });
+      println();
+      println("}");
+    }
+
     private void generateKeepBinding() {
       printCopyRight(2022);
       printPackage("annotations");
@@ -1151,14 +1214,13 @@
             generateFieldAccessConstants();
 
             generateTypePatternConstants();
+            generateClassNamePatternConstants();
           });
       println("}");
     }
 
     private void generateAnnotationConstants(Class<?> clazz) {
-      String name = simpleName(clazz);
       String desc = TestBase.descriptor(clazz);
-      println("public static final String SIMPLE_NAME = " + quote(name) + ";");
       println("public static final String DESCRIPTOR = " + quote(desc) + ";");
     }
 
@@ -1431,6 +1493,18 @@
       println();
     }
 
+    private void generateClassNamePatternConstants() {
+      println("public static final class ClassNamePattern {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(ClassNamePattern.class);
+            classNamePatternSimpleNameGroup().generateConstants(this);
+            classNamePatternPackageGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private static void writeFile(Path file, Consumer<Generator> fn) throws IOException {
       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
       PrintStream printStream = new PrintStream(byteStream);
@@ -1443,26 +1517,31 @@
       Files.write(Paths.get(ToolHelper.getProjectRoot()).resolve(file), formatted.getBytes());
     }
 
+    public static Path source(Path pkg, Class<?> clazz) {
+      return pkg.resolve(simpleName(clazz) + ".java");
+    }
+
     public static void run() throws IOException {
       writeFile(Paths.get("doc/keepanno-guide.md"), KeepAnnoMarkdownGenerator::generateMarkdownDoc);
 
       Path keepAnnoRoot = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno");
 
       Path astPkg = keepAnnoRoot.resolve("ast");
-      writeFile(astPkg.resolve("AnnotationConstants.java"), Generator::generateConstants);
+      writeFile(source(astPkg, AnnotationConstants.class), Generator::generateConstants);
 
       Path annoPkg = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno/annotations");
-      writeFile(annoPkg.resolve("TypePattern.java"), Generator::generateTypePattern);
-      writeFile(annoPkg.resolve("KeepBinding.java"), Generator::generateKeepBinding);
-      writeFile(annoPkg.resolve("KeepTarget.java"), Generator::generateKeepTarget);
-      writeFile(annoPkg.resolve("KeepCondition.java"), Generator::generateKeepCondition);
-      writeFile(annoPkg.resolve("KeepForApi.java"), Generator::generateKeepForApi);
-      writeFile(annoPkg.resolve("UsesReflection.java"), Generator::generateUsesReflection);
+      writeFile(source(annoPkg, TypePattern.class), Generator::generateTypePattern);
+      writeFile(source(annoPkg, ClassNamePattern.class), Generator::generateClassNamePattern);
+      writeFile(source(annoPkg, KeepBinding.class), Generator::generateKeepBinding);
+      writeFile(source(annoPkg, KeepTarget.class), Generator::generateKeepTarget);
+      writeFile(source(annoPkg, KeepCondition.class), Generator::generateKeepCondition);
+      writeFile(source(annoPkg, KeepForApi.class), Generator::generateKeepForApi);
+      writeFile(source(annoPkg, UsesReflection.class), Generator::generateUsesReflection);
       writeFile(
-          annoPkg.resolve("UsedByReflection.java"),
+          source(annoPkg, UsedByReflection.class),
           g -> g.generateUsedByX("UsedByReflection", "accessed reflectively"));
       writeFile(
-          annoPkg.resolve("UsedByNative.java"),
+          source(annoPkg, UsedByNative.class),
           g -> g.generateUsedByX("UsedByNative", "accessed from native code via JNI"));
     }
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
index 0248459..cc58dbe 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
@@ -100,6 +100,19 @@
       assertThat(
           assertionError.getMessage(),
           containsString("compiled with an incompatible version of Kotlin"));
+    } else if (kotlinParameters.is(KotlinCompilerVersion.KOTLINC_1_6_0)) {
+      AssertionError assertionError =
+          assertThrows(
+              AssertionError.class,
+              () -> {
+                runTest(kotlinParameters.getCompiler().getCompilerVersion(), libJar, stdLibJar);
+              });
+      // TODO(b/317019265): Triage this.
+      assertThat(
+          assertionError.getMessage(),
+          containsString(
+              "Trying to inline an anonymous object which is not part of the public ABI"));
+
     } else {
       runTest(kotlinParameters.getCompiler().getCompilerVersion(), libJar, stdLibJar);
     }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
index 6954d57..d732c25 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
@@ -8,14 +8,17 @@
 import static org.junit.Assert.fail;
 import static org.objectweb.asm.Opcodes.ASM7;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinCompilerTool.KotlinTargetVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StreamUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.AnnotationSubject;
@@ -58,6 +61,7 @@
     final R8FullTestBuilder testBuilder = testForR8(parameters.getBackend());
     rewriteMetadataVersion(testBuilder::addProgramClassFileData, new int[] {1, 1, 16});
     testBuilder
+        .apply(this::addLibrary)
         .addProgramFiles(kotlinc.getKotlinAnnotationJar())
         .setMinApi(parameters)
         .addOptionsModification(options -> options.testing.keepMetadataInR8IfNotRewritten = false)
@@ -72,6 +76,7 @@
     final R8FullTestBuilder testBuilder = testForR8(parameters.getBackend());
     rewriteMetadataVersion(testBuilder::addProgramClassFileData, new int[] {1, 4, 0});
     testBuilder
+        .apply(this::addLibrary)
         .addProgramFiles(kotlinc.getKotlinAnnotationJar())
         .setMinApi(parameters)
         .addKeepAllClassesRuleWithAllowObfuscation()
@@ -85,6 +90,7 @@
     final R8FullTestBuilder testBuilder = testForR8(parameters.getBackend());
     rewriteMetadataVersion(testBuilder::addProgramClassFileData, new int[] {1, 4, 2});
     testBuilder
+        .apply(this::addLibrary)
         .addProgramFiles(kotlinc.getKotlinAnnotationJar())
         .setMinApi(parameters)
         .addKeepAllClassesRuleWithAllowObfuscation()
@@ -93,6 +99,16 @@
         .inspect(inspector -> inspectMetadataVersion(inspector, "1.4.2"));
   }
 
+  private void addLibrary(R8FullTestBuilder testBuilder) {
+    // Starting with version 1.6 kotlin-stdlib references java.lang.annotation.Repeatable. This
+    // annotation was added in Android N.
+    testBuilder.applyIf(
+        kotlinParameters.isNewerThanOrEqualTo(KotlinCompilerVersion.KOTLINC_1_6_0)
+            && parameters.isDexRuntime()
+            && parameters.getApiLevel().isLessThan(AndroidApiLevel.N),
+        b -> b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.N)));
+  }
+
   private void rewriteMetadataVersion(Consumer<byte[]> rewrittenBytesConsumer, int[] newVersion)
       throws IOException {
     ZipUtils.iter(
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index 4292c7b..55dffb8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -10,10 +10,11 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.KotlinCompilerTool;
-import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 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.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.util.List;
@@ -52,6 +53,8 @@
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
+        // Use android.jar with java.lang.ClassValue.
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.U))
         .addProgramFiles(
             kotlinJars.getForConfiguration(kotlinParameters), kotlinc.getKotlinAnnotationJar())
         .addKeepMainRule("enumswitch.EnumSwitchKt")
@@ -62,13 +65,6 @@
             })
         .setMinApi(parameters)
         .addDontObfuscate()
-        // This will probably start failing when the CL
-        // https://github.com/JetBrains/kotlin/commit/79f6d4b590573e6adccd7e8899d3b15ddb42d185
-        // is propagated to the build for kotlin-reflect.
-        .applyIf(
-            parameters.isDexRuntime()
-                && kotlinParameters.isNewerThan(KotlinCompilerVersion.KOTLINC_1_8_0),
-            b -> b.addDontWarn("java.lang.ClassValue"))
         .allowDiagnosticWarningMessages()
         .compile()
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
index 9b536f6..955048d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_3_72;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_8_0;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
 import com.android.tools.r8.KotlinTestBase;
@@ -108,9 +109,11 @@
         .assertNoErrorMessages()
         // -keepattributes Signature is added in kotlin-reflect from version 1.4.20.
         .applyIf(kotlinParameters.is(KOTLINC_1_3_72), TestCompileResult::assertNoInfoMessages)
-        // TODO(b/269794485): Figure out why generic signatures fail using kotlin-dev.
+        // TODO(b/269794485): Figure out why generic signatures fail using 1.9.
         .applyIf(
-            kotlinParameters.getCompiler().isNot(KOTLINC_1_3_72) && !kotlinParameters.isKotlinDev(),
+            kotlinParameters.getCompiler().isNot(KOTLINC_1_3_72)
+                && kotlinParameters.getCompiler().isNot(KOTLINC_1_9_21)
+                && !kotlinParameters.isKotlinDev(),
             TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .apply(KotlinMetadataTestBase::verifyExpectedWarningsFromKotlinReflectAndStdLib)
         .writeToZip(foo.toPath())
diff --git a/src/test/java/com/android/tools/r8/naming/MappingHeaderContentTest.java b/src/test/java/com/android/tools/r8/naming/MappingHeaderContentTest.java
new file mode 100644
index 0000000..ae080ed
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MappingHeaderContentTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.PartitionMapConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.retrace.MappingPartition;
+import com.android.tools.r8.retrace.MappingPartitionMetadata;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MappingHeaderContentTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public MappingHeaderContentTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void test() throws Exception {
+    Box<MappingPartitionMetadata> metadataBox = new Box<>();
+    List<MappingPartition> partitions = new ArrayList<>();
+    StringBuilder builder = new StringBuilder();
+    testForR8(Backend.DEX)
+        .addInnerClasses(MappingHeaderContentTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(AndroidApiLevel.B)
+        .apply(b -> b.getBuilder().setProguardMapConsumer((s, unused) -> builder.append(s)))
+        .apply(
+            b ->
+                b.getBuilder()
+                    .setPartitionMapConsumer(
+                        new PartitionMapConsumer() {
+                          @Override
+                          public void acceptMappingPartition(MappingPartition mappingPartition) {
+                            partitions.add(mappingPartition);
+                          }
+
+                          @Override
+                          public void acceptMappingPartitionMetadata(
+                              MappingPartitionMetadata mappingPartitionMetadata) {
+                            assertNull(metadataBox.get());
+                            metadataBox.set(mappingPartitionMetadata);
+                          }
+                        }))
+        .compile();
+    assertNotNull(metadataBox.get());
+    assertFalse(partitions.isEmpty());
+    String mapping = builder.toString();
+    List<String> mapIdLines =
+        StringUtils.splitLines(mapping).stream()
+            .filter(s -> s.startsWith("# pg_map_id:"))
+            .collect(Collectors.toList());
+    assertEquals(
+        "Expected single pg_map_id line, found multiple:\n" + String.join("\n", mapIdLines) + "\n",
+        1,
+        mapIdLines.size());
+  }
+
+  static class A {}
+
+  static class B {}
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(args.length == 0 ? A.class : B.class);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/InitializerNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/InitializerNumberUnboxingTest.java
new file mode 100644
index 0000000..a430224
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/InitializerNumberUnboxingTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2023, 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.numberunboxing;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InitializerNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InitializerNumberUnboxingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNumberUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertUnboxing)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "Main[-1;-1]", "Main[-2;-2]", "Main[1;1]", "Main[2;2]", "Main[3;4]");
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    ClassSubject mainClass = codeInspector.clazz(Main.class);
+    assertThat(mainClass, isPresent());
+
+    List<FoundMethodSubject> inits =
+        mainClass.allMethods(FoundMethodSubject::isInstanceInitializer);
+    assertEquals(3, inits.size());
+    inits.forEach(
+        m ->
+            assertTrue(
+                m.getParameters().stream().allMatch(p -> p.getTypeReference().isPrimitive())));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new Main(-1));
+      System.out.println(new Main(-2L));
+      System.out.println(new Main(1));
+      System.out.println(new Main(2L));
+      System.out.println(new Main(3, 4L));
+    }
+
+    private final int i;
+    private final long l;
+
+    @NeverInline
+    Main(Long l) {
+      this(l.intValue(), l);
+    }
+
+    @NeverInline
+    Main(Integer i) {
+      this(i, Long.valueOf(i));
+    }
+
+    @NeverInline
+    Main(Integer i, Long l) {
+      this.i = i;
+      this.l = l;
+    }
+
+    @Override
+    public String toString() {
+      return "Main[" + i + ";" + l + "]";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsOverrideNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsOverrideNumberUnboxingTest.java
new file mode 100644
index 0000000..67c4241
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsOverrideNumberUnboxingTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2023, 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.numberunboxing;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 VirtualMethodsOverrideNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public VirtualMethodsOverrideNumberUnboxingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNumberUnboxing() throws Throwable {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertUnboxing)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("3", "1", "43", "5", "0", "43");
+  }
+
+  private void assertUnboxed(MethodSubject methodSubject) {
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(0).isDoubleType());
+    assertTrue(methodSubject.getProgramMethod().getParameter(1).isIntType());
+    assertTrue(methodSubject.getProgramMethod().getReturnType().isLongType());
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    codeInspector.forAllClasses(c -> c.forAllVirtualMethods(this::assertUnboxed));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new Add().convert(1.3, 1) + 1L);
+      System.out.println(new Sub().convert(1.4, 2) + 1L);
+      System.out.println(new Cst().convert(1.4, 2) + 1L);
+      run(new Add());
+      run(new Sub());
+      run(new Cst());
+    }
+
+    @NeverInline
+    private static void run(Top top) {
+      System.out.println(top.convert(1.5, 3) + 1L);
+    }
+  }
+
+  @NeverClassInline
+  interface Top {
+    @NeverInline
+    Long convert(Double d, Integer i);
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class Add implements Top {
+    @Override
+    @NeverInline
+    public Long convert(Double d, Integer i) {
+      return Long.valueOf((long) (d.doubleValue() + i.intValue()));
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class Sub implements Top {
+    @Override
+    @NeverInline
+    public Long convert(Double d, Integer i) {
+      return Long.valueOf((long) (d.doubleValue() - i.intValue()));
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class Cst implements Top {
+    @Override
+    @NeverInline
+    public Long convert(Double d, Integer i) {
+      return Long.valueOf(42L);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java
new file mode 100644
index 0000000..e3e6058
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2023, 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.regress.b316744331;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.StringUtils;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.Tag;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class Regress316744331Test extends DebugTestBase {
+
+  static final String EXPECTED = StringUtils.lines("No null fields");
+
+  static final int ENTRY_LINE = 23;
+  static final int IN_STREAM_CHECK_LINE = ENTRY_LINE + 1;
+  static final int IN_STREAM_IS_NULL_LINE = ENTRY_LINE + 2;
+  static final int OUT_STREAM_CHECK_LINE = ENTRY_LINE + 7;
+  static final int OUT_STREAM_IS_NULL_LINE = ENTRY_LINE + 8;
+  static final int NORMAL_EXIT_LINE = ENTRY_LINE + 11;
+
+  static final Value NULL_VALUE = Value.createObjectValue(Tag.OBJECT_TAG, 0);
+
+  private final TestParameters parameters;
+  private final MethodReference fooMethod;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDefaultCfRuntime()
+        .withDefaultDexRuntime()
+        .withAllApiLevels()
+        .build();
+  }
+
+  public Regress316744331Test(TestParameters parameters) {
+    this.parameters = parameters;
+    try {
+      this.fooMethod = Reference.methodFromMethod(Regress316744331TestClass.class.getMethod("foo"));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private TestBuilder<? extends SingleTestRunResult<?>, ?> getTestBuilder() {
+    return testForRuntime(parameters)
+        .addClasspathClasses(Regress316744331TestClass.class)
+        .addProgramClasses(Regress316744331TestClass.class);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    getTestBuilder()
+        .run(parameters.getRuntime(), Regress316744331TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testModifyInStreamField() throws Throwable {
+    runDebugTest(
+        getTestBuilder().debugConfig(parameters.getRuntime()),
+        Regress316744331TestClass.class,
+        breakpoint(fooMethod, ENTRY_LINE),
+        run(),
+        checkLine(ENTRY_LINE),
+        inspect(t -> assertEquals(NULL_VALUE, t.getFieldOnThis("m_instream", null))),
+        stepOver(),
+        checkLine(IN_STREAM_CHECK_LINE),
+        inspect(t -> assertNotEquals(NULL_VALUE, t.getFieldOnThis("m_instream", null))),
+        inspect(t -> t.setFieldOnThis("m_instream", null, NULL_VALUE)),
+        // Install a break point on the possible exits.
+        breakpoint(fooMethod, IN_STREAM_IS_NULL_LINE),
+        breakpoint(fooMethod, NORMAL_EXIT_LINE),
+        run(),
+        // TODO(b/316744331): D8 incorrectly optimizing out the code after the null check.
+        checkLine(parameters.isCfRuntime() ? IN_STREAM_IS_NULL_LINE : NORMAL_EXIT_LINE),
+        run());
+  }
+
+  @Test
+  public void testModifyOutStreamField() throws Throwable {
+    runDebugTest(
+        getTestBuilder().debugConfig(parameters.getRuntime()),
+        Regress316744331TestClass.class,
+        breakpoint(fooMethod, OUT_STREAM_CHECK_LINE),
+        run(),
+        checkLine(OUT_STREAM_CHECK_LINE),
+        inspect(t -> assertNotEquals(NULL_VALUE, t.getFieldOnThis("m_outstream", null))),
+        inspect(t -> t.setFieldOnThis("m_outstream", null, NULL_VALUE)),
+        // Install a break point on the possible exits.
+        breakpoint(fooMethod, OUT_STREAM_IS_NULL_LINE),
+        breakpoint(fooMethod, NORMAL_EXIT_LINE),
+        run(),
+        // TODO(b/316744331): D8 incorrectly optimizing out the code after the null check.
+        checkLine(parameters.isCfRuntime() ? OUT_STREAM_IS_NULL_LINE : NORMAL_EXIT_LINE),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java
new file mode 100644
index 0000000..1a57586
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, 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.regress.b316744331;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class Regress316744331TestClass {
+
+  String filename;
+  FileReader m_instream;
+  PrintWriter m_outstream;
+
+  Regress316744331TestClass(String filename) {
+    this.filename = filename;
+  }
+
+  public void foo() throws IOException {
+    m_instream = new FileReader(filename);
+    if (null == m_instream) {
+      System.out.println("Reader is null!");
+      return;
+    }
+    m_outstream =
+        new PrintWriter(new java.io.BufferedWriter(new java.io.FileWriter(filename + ".java")));
+    if (null == m_outstream) {
+      System.out.println("Writer is null!");
+      return;
+    }
+    System.out.println("No null fields");
+  }
+
+  public static void main(String[] args) throws IOException {
+    // The debugger testing infra does not allow passing runtime arguments.
+    // Classpath should have an entry in normal and debugger runs so use it as the "file".
+    String cp = System.getProperty("java.class.path");
+    int jarIndex = cp.indexOf(".jar");
+    if (jarIndex < 0) {
+      jarIndex = cp.indexOf(".zip");
+    }
+    int start = cp.lastIndexOf(File.pathSeparatorChar, jarIndex);
+    String filename = cp.substring(Math.max(start, 0), jarIndex + 4);
+    new Regress316744331TestClass(filename).foo();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
index 2917464..908da85 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
@@ -74,8 +74,6 @@
         .addProgramClassFileData(transformB())
         .addKeepMainRule(Main.class)
         .setMinApi(parameters)
-        .addOptionsModification(
-            options -> options.testing.allowNonAbstractClassesWithAbstractMethods = true)
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatMatches(containsString("AbstractMethodError"));
   }
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
index 6c16dfd..d5e9fe3 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
@@ -74,8 +74,6 @@
         .addProgramClassFileData(transformB())
         .addKeepMainRule(Main.class)
         .setMinApi(parameters)
-        .addOptionsModification(
-            options -> options.testing.allowNonAbstractClassesWithAbstractMethods = true)
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatMatches(containsString("AbstractMethodError"));
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java b/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java
index 09ca4e0..c5eaf59 100644
--- a/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java
+++ b/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java
@@ -36,7 +36,9 @@
           "    11:2:void a() -> a", // Unexpected line range [11:2] - interpreting as [2:11]
           "    12:21:void a(android.content.Intent) -> a",
           // Allow identifier content to follow <init>/<clinit>.
-          "    22:41:void <clinit>$more$stuff() -> clinit$move$stuff");
+          "    22:41:void <clinit>$more$stuff() -> clinit$move$stuff",
+          // Allow type identifiers to start with '.'
+          ".Foo -> o.bar:");
 
   @Test
   public void test() throws Exception {
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 faba9a5..c0211ef 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -233,7 +233,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector, true, true);
+    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector);
 
     GraphInspector ifThenKeepClassesWithMembersInspector =
         testForR8(Backend.CF)
@@ -252,8 +252,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(
-        referenceInspector, ifThenKeepClassesWithMembersInspector, true, true);
+    assertRetainedClassesEqual(referenceInspector, ifThenKeepClassesWithMembersInspector);
 
     GraphInspector ifHasMemberThenKeepClassInspector =
         testForR8(Backend.CF)
@@ -274,7 +273,7 @@
             .apply(this::suppressZipFileAssignmentsToJavaLangAutoCloseable)
             .compile()
             .graphInspector();
-    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector, true, true);
+    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector);
   }
 
   private void configureHorizontalClassMerging(R8FullTestBuilder testBuilder) {
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/AllowHorizontalClassMergingWithIfRuleTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/AllowHorizontalClassMergingWithIfRuleTest.java
new file mode 100644
index 0000000..af28109
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/AllowHorizontalClassMergingWithIfRuleTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2023, 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.ifrule;
+
+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 AllowHorizontalClassMergingWithIfRuleTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-if class * { void foo(); } -keep class " + Main.class.getTypeName())
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(B.class, C.class).assertNoOtherClassesMerged())
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Object aOrBOrC =
+          System.currentTimeMillis() > 0
+              ? new A()
+              : System.currentTimeMillis() > 1 ? new B() : new C();
+      System.out.print(aOrBOrC);
+      A.foo();
+    }
+  }
+
+  static class A {
+
+    static void foo() {
+      System.out.println();
+    }
+
+    @Override
+    public String toString() {
+      return "Hello, world!";
+    }
+  }
+
+  static class B {}
+
+  static class C {}
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/PinningStarPatternTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/PinningStarPatternTest.java
new file mode 100644
index 0000000..d65f8bd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/PinningStarPatternTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2023, 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.ifrule;
+
+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.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PinningStarPatternTest extends TestBase {
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().build();
+  }
+
+  public static final List<Class<?>> EXPECTED_ABSENT = ImmutableList.of(A.class);
+  public static final List<Class<?>> EXPECTED_PRESENT = ImmutableList.of(TestClass.class, B.class);
+
+  private TestParameters parameters;
+
+  public PinningStarPatternTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNoKeep() throws Exception {
+    testKeepRule("", ImmutableList.of(A.class, B.class), ImmutableList.of(TestClass.class));
+  }
+
+  @Test
+  public void testNoIf() throws Exception {
+    testKeepRule("-keep class **B { *; }", EXPECTED_ABSENT, EXPECTED_PRESENT);
+  }
+
+  @Test
+  public void testR8IfStar() throws Exception {
+    testKeepRule("-if class * -keep class **B { *; }", EXPECTED_ABSENT, EXPECTED_PRESENT);
+  }
+
+  @Test
+  public void testR8IfStarWithMethod() throws Exception {
+    testKeepRule("-if class * { z(); } -keep class **B { *; }", EXPECTED_ABSENT, EXPECTED_PRESENT);
+  }
+
+  @Test
+  public void testR8IfStarWithEmptyMethod() throws Exception {
+    // We should also not keep B, since y() is dead (empty).
+    testKeepRule("-if class * { y(); } -keep class **B { *; }", EXPECTED_ABSENT, EXPECTED_PRESENT);
+  }
+
+  @Test
+  public void testR8IfStarField() throws Exception {
+    // We should also not keep B, since the only_read field is dead.
+    testKeepRule(
+        "-if class * { int only_read; } -keep class **B { *; }", EXPECTED_ABSENT, EXPECTED_PRESENT);
+  }
+
+  private void testKeepRule(String keepRule, List<Class<?>> absent, List<Class<?>> present)
+      throws IOException, ExecutionException, CompilationFailedException {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, TestClass.class)
+        .addKeepRules(keepRule)
+        .addKeepMainRule(TestClass.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("42", "0")
+        .inspect(
+            inspector -> {
+              absent.forEach(clazz -> assertThat(inspector.clazz(clazz), isAbsent()));
+              present.forEach(clazz -> assertThat(inspector.clazz(clazz), isPresent()));
+            });
+  }
+
+  static class A {
+    boolean use = true;
+
+    public A() {
+      if (System.currentTimeMillis() == 0) {
+        use = false;
+      }
+    }
+
+    public void bar() {
+      if (use) {
+        System.out.println(42);
+      }
+    }
+  }
+
+  static class B {
+    public static void foo() {
+      System.out.println(0);
+    }
+  }
+
+  static class TestClass {
+
+    public static int only_read = 88;
+
+    public void z() {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println("foobar");
+      }
+    }
+
+    public void y() {}
+
+    public static void main(String[] args) {
+      new TestClass().z();
+      new TestClass().y();
+      int a = TestClass.only_read;
+      new A().bar();
+      B.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
index 01f8190..aa46e96 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.shaking.ifrule.verticalclassmerging;
 
+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 static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
@@ -114,7 +114,7 @@
 
       if (enableVerticalClassMerging) {
         // Verify that SuperTestClass has been merged into TestClass.
-        assertThat(inspector.clazz(SuperTestClass.class), not(isPresent()));
+        assertThat(inspector.clazz(SuperTestClass.class), isAbsent());
         assertEquals(
             "java.lang.Object", testClassSubject.getDexProgramClass().superType.toSourceString());
 
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
index ed578cd..9f7bb91 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.shaking.ifrule.verticalclassmerging;
 
+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 static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NoAccessModification;
@@ -97,8 +97,8 @@
 
     // Verify that A and I are no longer present when vertical class merging is enabled.
     if (enableVerticalClassMerging) {
-      assertThat(inspector.clazz(A.class), not(isPresent()));
-      assertThat(inspector.clazz(I.class), not(isPresent()));
+      assertThat(inspector.clazz(A.class), isAbsent());
+      assertThat(inspector.clazz(I.class), isAbsent());
     }
   }
 
diff --git a/third_party/dependencies_plugin.tar.gz.sha1 b/third_party/dependencies_plugin.tar.gz.sha1
index ba75d12..f85351e 100644
--- a/third_party/dependencies_plugin.tar.gz.sha1
+++ b/third_party/dependencies_plugin.tar.gz.sha1
@@ -1 +1 @@
-369870d45ae8721ad1379b8bf108e107ad1b5175
\ No newline at end of file
+a58a2edb72ff3a3126e4846057dd55eb0e7c27f8
\ No newline at end of file
diff --git a/third_party/kotlin/kotlin-compiler-1.9.21.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-1.9.21.tar.gz.sha1
new file mode 100644
index 0000000..8e93ad6
--- /dev/null
+++ b/third_party/kotlin/kotlin-compiler-1.9.21.tar.gz.sha1
@@ -0,0 +1 @@
+17c6c5c9cefab179553adaaf860c86e4e598a002
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index 3abe651..cc12d2a 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -477,7 +477,7 @@
     if line.lstrip().startswith('#'):
         return False
     if ('-injars' in line or '-libraryjars' in line or '-print' in line or
-            '-applymapping' in line):
+            '-applymapping' in line or '-tracing' in line):
         return True
     if minify == 'force-enable' and '-dontobfuscate' in line:
         return True
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 05abfb5..e2f0366 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -101,11 +101,7 @@
   'org.gradle.kotlin.kotlin-dsl:org.gradle.kotlin.kotlin-dsl.gradle.plugin:4.1.0',
   'org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.10',
   'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.0.1',
-
-  # Patched version of org.spdx.sbom:org.spdx.sbom.gradle.plugin:0.4.0.
-  # See
-  # https://github.com/spdx/spdx-gradle-plugin/issues/69#issuecomment-1799122543.
-  'org.spdx.sbom:org.spdx.sbom.gradle.plugin:0.4.0-r8-patch02',
+  'org.spdx.sbom:org.spdx.sbom.gradle.plugin:0.4.0',
   # See https://github.com/FasterXML/jackson-core/issues/999.
   'ch.randelshofer:fastdoubleparser:0.8.0',
 ]