Merge commit 'c9ec33d96b64bc2820eef206914e993a6f9c60e0' into dev-release

Change-Id: I3687dd49a93681717da3b58d4547b1df39135e94
diff --git a/.gitignore b/.gitignore
index 8eab4b8..3da002f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -223,6 +223,8 @@
 third_party/opensource-apps/applymapping.tar.gz
 third_party/opensource-apps/chanu
 third_party/opensource-apps/chanu.tar.gz
+third_party/opensource-apps/compose-examples/changed-bitwise-value-propagation
+third_party/opensource-apps/compose-examples/changed-bitwise-value-propagation.tar.gz
 third_party/opensource-apps/empty-activity
 third_party/opensource-apps/empty-activity.tar.gz
 third_party/opensource-apps/empty-compose-activity
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index 1d566dc..1ec0e8b 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -417,6 +417,14 @@
       "third_party",
       "binary_compatibility_tests",
       "compiler_api_tests.tar.gz.sha1").toFile())
+  val composeExamplesChangedBitwiseValuePropagation = ThirdPartyDependency(
+    "compose-examples-changed-bitwise-value-propagation",
+    Paths.get(
+      "third_party", "opensource-apps", "compose-examples",
+      "changed-bitwise-value-propagation").toFile(),
+    Paths.get(
+      "third_party", "opensource-apps", "compose-examples",
+      "changed-bitwise-value-propagation.tar.gz.sha1").toFile())
   val coreLambdaStubs = ThirdPartyDependency(
     "coreLambdaStubs",
     Paths.get("third_party", "core-lambda-stubs").toFile(),
diff --git a/d8_r8/keepanno/build.gradle.kts b/d8_r8/keepanno/build.gradle.kts
index 376472a..3d981f6 100644
--- a/d8_r8/keepanno/build.gradle.kts
+++ b/d8_r8/keepanno/build.gradle.kts
@@ -34,4 +34,9 @@
     dependsOn(gradle.includedBuild("shared").task(":downloadDeps"))
     from(sourceSets.main.get().output)
   }
+
+  val keepAnnoAnnotationsDoc by registering(Javadoc::class) {
+    source = sourceSets.main.get().allJava
+    include("com/android/tools/r8/keepanno/annotations/*")
+  }
 }
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index c441284..ddb244f 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-patch01"
+  id("org.spdx.sbom") version "0.4.0-r8-patch02"
 }
 
 java {
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 35c7e74..30432af 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see BuildbucketCfg message:
-#   https://luci-config.appspot.com/schemas/projects:buildbucket.cfg
+#   https://config.luci.app/schemas/projects:buildbucket.cfg
 
 buckets {
   name: "ci"
@@ -1336,6 +1336,78 @@
       }
     }
     builders {
+      name: "linux-jdk21"
+      swarming_host: "chrome-swarming.appspot.com"
+      swarming_tags: "vpython:native-python-wrapper"
+      dimensions: "cpu:x86-64"
+      dimensions: "normal:true"
+      dimensions: "os:Ubuntu-20.04"
+      dimensions: "pool:luci.r8.ci"
+      exe {
+        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "builder_group": "internal.client.r8",'
+        '  "recipe": "rex",'
+        '  "test_options": ['
+        '    "--runtimes=jdk21",'
+        '    "--command_cache_dir=/tmp/ccache",'
+        '    "--tool=r8",'
+        '    "--no_internal",'
+        '    "--one_line_per_test",'
+        '    "--archive_failures"'
+        '  ]'
+        '}'
+      priority: 26
+      execution_timeout_secs: 21600
+      expiration_secs: 126000
+      build_numbers: YES
+      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+    }
+    builders {
+      name: "linux-jdk21_release"
+      swarming_host: "chrome-swarming.appspot.com"
+      swarming_tags: "vpython:native-python-wrapper"
+      dimensions: "cpu:x86-64"
+      dimensions: "normal:true"
+      dimensions: "os:Ubuntu-20.04"
+      dimensions: "pool:luci.r8.ci"
+      exe {
+        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "builder_group": "internal.client.r8",'
+        '  "recipe": "rex",'
+        '  "test_options": ['
+        '    "--runtimes=jdk21",'
+        '    "--command_cache_dir=/tmp/ccache",'
+        '    "--tool=r8",'
+        '    "--no_internal",'
+        '    "--one_line_per_test",'
+        '    "--archive_failures"'
+        '  ]'
+        '}'
+      priority: 26
+      execution_timeout_secs: 21600
+      expiration_secs: 126000
+      build_numbers: YES
+      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+    }
+    builders {
       name: "linux-jdk8"
       swarming_host: "chrome-swarming.appspot.com"
       swarming_tags: "vpython:native-python-wrapper"
diff --git a/infra/config/global/generated/luci-logdog.cfg b/infra/config/global/generated/luci-logdog.cfg
index 57dada0..6f58579 100644
--- a/infra/config/global/generated/luci-logdog.cfg
+++ b/infra/config/global/generated/luci-logdog.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see ProjectConfig message:
-#   https://luci-config.appspot.com/schemas/projects:luci-logdog.cfg
+#   https://config.luci.app/schemas/projects:luci-logdog.cfg
 
 reader_auth_groups: "all"
 writer_auth_groups: "luci-logdog-r8-writers"
diff --git a/infra/config/global/generated/luci-milo.cfg b/infra/config/global/generated/luci-milo.cfg
index 0f163d5..05b414b 100644
--- a/infra/config/global/generated/luci-milo.cfg
+++ b/infra/config/global/generated/luci-milo.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see Project message:
-#   https://luci-config.appspot.com/schemas/projects:luci-milo.cfg
+#   https://config.luci.app/schemas/projects:luci-milo.cfg
 
 consoles {
   id: "main"
@@ -51,6 +51,11 @@
     short_name: "jdk17"
   }
   builders {
+    name: "buildbucket/luci.r8.ci/linux-jdk21"
+    category: "R8"
+    short_name: "jdk21"
+  }
+  builders {
     name: "buildbucket/luci.r8.ci/linux-android-4.0.4"
     category: "R8"
     short_name: "4.0.4"
@@ -186,6 +191,11 @@
     short_name: "jdk17"
   }
   builders {
+    name: "buildbucket/luci.r8.ci/linux-jdk21_release"
+    category: "Release|R8"
+    short_name: "jdk21"
+  }
+  builders {
     name: "buildbucket/luci.r8.ci/linux-android-4.0.4_release"
     category: "Release|R8"
     short_name: "4.0.4"
diff --git a/infra/config/global/generated/luci-notify.cfg b/infra/config/global/generated/luci-notify.cfg
index ff77a2b..92de734 100644
--- a/infra/config/global/generated/luci-notify.cfg
+++ b/infra/config/global/generated/luci-notify.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see ProjectConfig message:
-#   https://luci-config.appspot.com/schemas/projects:luci-notify.cfg
+#   https://config.luci.app/schemas/projects:luci-notify.cfg
 
 notifiers {
   notifications {
@@ -420,6 +420,30 @@
   }
   builders {
     bucket: "ci"
+    name: "linux-jdk21"
+    repository: "https://r8.googlesource.com/r8"
+  }
+}
+notifiers {
+  notifications {
+    on_failure: true
+    on_new_failure: true
+    notify_blamelist {}
+  }
+  builders {
+    bucket: "ci"
+    name: "linux-jdk21_release"
+    repository: "https://r8.googlesource.com/r8"
+  }
+}
+notifiers {
+  notifications {
+    on_failure: true
+    on_new_failure: true
+    notify_blamelist {}
+  }
+  builders {
+    bucket: "ci"
     name: "linux-jdk8"
     repository: "https://r8.googlesource.com/r8"
   }
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index 60c9a25..55b6ba6 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see ProjectConfig message:
-#   https://luci-config.appspot.com/schemas/projects:luci-scheduler.cfg
+#   https://config.luci.app/schemas/projects:luci-scheduler.cfg
 
 job {
   id: "archive"
@@ -545,6 +545,35 @@
   }
 }
 job {
+  id: "linux-jdk21"
+  realm: "ci"
+  acl_sets: "ci"
+  triggering_policy {
+    kind: GREEDY_BATCHING
+    max_concurrent_invocations: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "linux-jdk21"
+  }
+}
+job {
+  id: "linux-jdk21_release"
+  realm: "ci"
+  acl_sets: "ci"
+  triggering_policy {
+    kind: GREEDY_BATCHING
+    max_concurrent_invocations: 1
+    max_batch_size: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "linux-jdk21_release"
+  }
+}
+job {
   id: "linux-jdk8"
   realm: "ci"
   acl_sets: "ci"
@@ -767,6 +796,17 @@
   }
 }
 trigger {
+  id: "branch-gitiles-8.3-forward"
+  realm: "ci"
+  acl_sets: "ci"
+  triggers: "linux-jdk21_release"
+  gitiles {
+    repo: "https://r8.googlesource.com/r8"
+    refs: "regexp:refs/heads/([8]\\.[3-9]+(\\.[0-9]+)?|[9]\\.[0-9]+(\\.[0-9]+)?)"
+    path_regexps: "src/main/java/com/android/tools/r8/Version.java"
+  }
+}
+trigger {
   id: "branch-gitiles-trigger"
   realm: "ci"
   acl_sets: "ci"
@@ -815,6 +855,7 @@
   triggers: "linux-internal"
   triggers: "linux-jdk11"
   triggers: "linux-jdk17"
+  triggers: "linux-jdk21"
   triggers: "linux-jdk8"
   triggers: "linux-jdk9"
   triggers: "linux-kotlin_dev"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index dcf12da..d9a3a1e 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -2,12 +2,12 @@
 # Do not modify manually.
 #
 # For the schema of this file, see ProjectCfg message:
-#   https://luci-config.appspot.com/schemas/projects:project.cfg
+#   https://config.luci.app/schemas/projects:project.cfg
 
 name: "r8"
 access: "group:all"
 lucicfg {
-  version: "1.39.20"
+  version: "1.40.0"
   package_dir: ".."
   config_dir: "generated"
   entry_point: "main.star"
diff --git a/infra/config/global/generated/realms.cfg b/infra/config/global/generated/realms.cfg
index de02ce6..e05c5b4 100644
--- a/infra/config/global/generated/realms.cfg
+++ b/infra/config/global/generated/realms.cfg
@@ -2,7 +2,7 @@
 # Do not modify manually.
 #
 # For the schema of this file, see RealmsCfg message:
-#   https://luci-config.appspot.com/schemas/projects:realms.cfg
+#   https://config.luci.app/schemas/projects:realms.cfg
 
 realms {
   name: "@root"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 11fbd09..35e29c7 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -109,6 +109,14 @@
 )
 
 luci.gitiles_poller(
+  name = "branch-gitiles-8.3-forward",
+  bucket = "ci",
+  repo = "https://r8.googlesource.com/r8",
+  refs = ["refs/heads/([8]\\.[3-9]+(\\.[0-9]+)?|[9]\\.[0-9]+(\\.[0-9]+)?)"],
+  path_regexps = ["src/main/java/com/android/tools/r8/Version.java"]
+)
+
+luci.gitiles_poller(
   name = "branch-gitiles-8.1-forward",
   bucket = "ci",
   repo = "https://r8.googlesource.com/r8",
@@ -327,7 +335,9 @@
 r8_tester_with_default("linux-jdk17",
         ["--runtimes=jdk17", "--command_cache_dir=/tmp/ccache"],
         release_trigger=["branch-gitiles-3.3-forward"])
-
+r8_tester_with_default("linux-jdk21",
+        ["--runtimes=jdk21", "--command_cache_dir=/tmp/ccache"],
+        release_trigger=["branch-gitiles-8.3-forward"])
 
 r8_tester_with_default("linux-android-4.0.4",
     ["--dex_vm=4.0.4", "--all_tests", "--command_cache_dir=/tmp/ccache"],
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 37713a9..9657a22 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
@@ -1,6 +1,11 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
 package com.android.tools.r8.keepanno.annotations;
 
 import java.lang.annotation.ElementType;
@@ -11,11 +16,16 @@
 /**
  * A binding of a keep item.
  *
- * <p>A binding allows referencing the exact instance of a match from a condition in other
- * conditions and/or targets. It can also be used to reduce duplication of targets by sharing
- * patterns.
+ * <p>Bindings allow referencing the exact instance of a match from a condition in other conditions
+ * and/or targets. It can also be used to reduce duplication of targets by sharing patterns.
  *
- * <p>See KeepTarget for documentation on specifying an item pattern.
+ * <p>An item can be:
+ *
+ * <ul>
+ *   <li>a pattern on classes;
+ *   <li>a pattern on methods; or
+ *   <li>a pattern on fields.
+ * </ul>
  */
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.CLASS)
@@ -24,25 +34,268 @@
   /** Name with which other bindings, conditions or targets can reference the bound item pattern. */
   String bindingName();
 
+  /**
+   * Specify the kind of this item pattern.
+   *
+   * <p>Possible values are:
+   *
+   * <ul>
+   *   <li>ONLY_CLASS
+   *   <li>ONLY_MEMBERS
+   *   <li>CLASS_AND_MEMBERS
+   * </ul>
+   *
+   * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
+   * have member patterns the default is ONLY_MEMBERS
+   */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
+  /**
+   * Define the class pattern by reference to a binding.
+   *
+   * <p>Mutually exclusive with the following other properties defining class:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class.
+   */
   String classFromBinding() default "";
 
+  /**
+   * Define the class-name pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   */
   String className() default "";
 
+  /**
+   * Define the class-name pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   */
   Class<?> classConstant() default Object.class;
 
+  /**
+   * 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:
+   *
+   * <ul>
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassName() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances of the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassNameExclusive() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstant() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstantExclusive() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes extending the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>This property is deprecated, use instanceOfClassName instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
   String extendsClassName() default "";
 
+  /**
+   * Define the instance-of pattern as classes extending the referenced Class constant.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>This property is deprecated, use instanceOfClassConstant instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
   Class<?> extendsClassConstant() default Object.class;
 
+  /**
+   * Define the member-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
+   * types of members.
+   */
+  MemberAccessFlags[] memberAccess() default {};
+
+  /**
+   * Define the method-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
+   */
+  MethodAccessFlags[] methodAccess() default {};
+
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
+   */
   String methodReturnType() default "";
 
-  String[] methodParameters() default {""};
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
+   */
+  String[] methodParameters() default {"<default>"};
 
+  /**
+   * Define the field-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
+   */
+  FieldAccessFlags[] fieldAccess() default {};
+
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   */
   String fieldType() default "";
 }
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 a0dbcf4..7777fd8 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
@@ -1,6 +1,11 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
 package com.android.tools.r8.keepanno.annotations;
 
 import java.lang.annotation.ElementType;
@@ -11,31 +16,272 @@
 /**
  * A condition for a keep edge.
  *
- * <p>See KeepTarget for documentation on specifying an item pattern.
+ * <p>The condition denotes an item used as a precondition of a rule. An item can be:
+ *
+ * <ul>
+ *   <li>a pattern on classes;
+ *   <li>a pattern on methods; or
+ *   <li>a pattern on fields.
+ * </ul>
  */
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepCondition {
 
+  /**
+   * Define the class pattern by reference to a binding.
+   *
+   * <p>Mutually exclusive with the following other properties defining class:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class.
+   */
   String classFromBinding() default "";
 
+  /**
+   * Define the class-name pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   */
   String className() default "";
 
+  /**
+   * Define the class-name pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   */
   Class<?> classConstant() default Object.class;
 
+  /**
+   * 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:
+   *
+   * <ul>
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassName() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances of the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassNameExclusive() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstant() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstantExclusive() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes extending the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>This property is deprecated, use instanceOfClassName instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
   String extendsClassName() default "";
 
+  /**
+   * Define the instance-of pattern as classes extending the referenced Class constant.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>This property is deprecated, use instanceOfClassConstant instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
   Class<?> extendsClassConstant() default Object.class;
 
+  /**
+   * Define the member pattern in full by a reference to a binding.
+   *
+   * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
+   * is referenced this item is defined to be that item, including its class and member patterns.
+   */
   String memberFromBinding() default "";
 
+  /**
+   * Define the member-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
+   * types of members.
+   */
+  MemberAccessFlags[] memberAccess() default {};
+
+  /**
+   * Define the method-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
+   */
+  MethodAccessFlags[] methodAccess() default {};
+
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
+   */
   String methodReturnType() default "";
 
-  String[] methodParameters() default {""};
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
+   */
+  String[] methodParameters() default {"<default>"};
 
+  /**
+   * Define the field-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
+   */
+  FieldAccessFlags[] fieldAccess() default {};
+
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
index 24b9c4f..65454fe 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
@@ -1,6 +1,11 @@
 // 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;
@@ -21,37 +26,103 @@
 @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepForApi {
+
+  /** Optional description to document the reason for this annotation. */
   String description() default "";
 
-  /** Additional targets to be kept as part of the API surface. */
+  /**
+   * Additional targets to be kept as part of the API surface.
+   *
+   * <p>Defaults to no additional targets.
+   */
   KeepTarget[] additionalTargets() default {};
 
   /**
-   * The target kind to be kept.
+   * Specify the kind of this item pattern.
    *
-   * <p>Default kind is CLASS_AND_MEMBERS, meaning the annotated class and/or member is to be kept.
+   * <p>Default kind is CLASS_AND_MEMBERS , meaning the annotated class and/or member is to be kept.
    * When annotating a class this can be set to ONLY_CLASS to avoid patterns on any members. That
    * can be useful when the API members are themselves explicitly annotated.
    *
    * <p>It is not possible to use ONLY_CLASS if annotating a member. Also, it is never valid to use
-   * kind ONLY_MEMBERS as the API surface must keep the class if any member it to be accessible.
+   * kind ONLY_MEMBERS as the API surface must keep the class if any member is to be accessible.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
-  // Member patterns. See KeepTarget for documentation.
+  /**
+   * Define the member-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
+   * types of members.
+   */
   MemberAccessFlags[] memberAccess() default {};
 
+  /**
+   * Define the method-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
+   */
   MethodAccessFlags[] methodAccess() default {};
 
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
+   */
   String methodReturnType() default "";
 
-  String[] methodParameters() default {""};
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
+   */
+  String[] methodParameters() default {"<default>"};
 
+  /**
+   * Define the field-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
+   */
   FieldAccessFlags[] fieldAccess() default {};
 
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   */
   String fieldType() default "";
 }
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 c9626f2..59e7e7c 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
@@ -1,6 +1,11 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
 package com.android.tools.r8.keepanno.annotations;
 
 import java.lang.annotation.ElementType;
@@ -11,45 +16,67 @@
 /**
  * A target for a keep edge.
  *
- * <p>The target denotes a keep item along with options for what to keep:
+ * <p>The target denotes an item along with options for what to keep. An item can be:
  *
  * <ul>
  *   <li>a pattern on classes;
  *   <li>a pattern on methods; or
  *   <li>a pattern on fields.
  * </ul>
- *
- * <p>The structure of a target item is the same as for a condition item but has the additional keep
- * options.
  */
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepTarget {
 
+  /**
+   * Specify the kind of this item pattern.
+   *
+   * <p>Possible values are:
+   *
+   * <ul>
+   *   <li>ONLY_CLASS
+   *   <li>ONLY_MEMBERS
+   *   <li>CLASS_AND_MEMBERS
+   * </ul>
+   *
+   * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
+   * have member patterns the default is ONLY_MEMBERS
+   */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
   /**
    * Define the options that do not need to be preserved for the target.
    *
-   * <p>Mutually exclusive with `disallow`.
+   * <p>Mutually exclusive with the property `disallow` also defining options.
    *
-   * <p>If none are specified the default is "allow none" / "disallow all".
+   * <p>If nothing is specified for options the default is "allow none" / "disallow all".
    */
   KeepOption[] allow() default {};
 
   /**
    * Define the options that *must* be preserved for the target.
    *
-   * <p>Mutually exclusive with `allow`.
+   * <p>Mutually exclusive with the property `allow` also defining options.
    *
-   * <p>If none are specified the default is "allow none" / "disallow all".
+   * <p>If nothing is specified for options the default is "allow none" / "disallow all".
    */
   KeepOption[] disallow() default {};
 
   /**
-   * Define the class-name pattern by reference to a binding.
+   * Define the class pattern by reference to a binding.
    *
-   * <p>Mutually exclusive with `className` and `classConstant`.
+   * <p>Mutually exclusive with the following other properties defining class:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   * </ul>
    *
    * <p>If none are specified the default is to match any class.
    */
@@ -58,111 +85,237 @@
   /**
    * Define the class-name pattern by fully qualified class name.
    *
-   * <p>Mutually exclusive with `classFromBinding` and `classConstant`.
+   * <p>Mutually exclusive with the following other properties defining class-name:
    *
-   * <p>If none are specified the default is to match any class.
+   * <ul>
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
    */
   String className() default "";
 
   /**
    * Define the class-name pattern by reference to a Class constant.
    *
-   * <p>Mutually exclusive with `classFromBinding` and `className`.
+   * <p>Mutually exclusive with the following other properties defining class-name:
    *
-   * <p>If none are specified the default is to match any class.
+   * <ul>
+   *   <li>className
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
    */
   Class<?> classConstant() default Object.class;
 
   /**
-   * Define the extends pattern by fully qualified class name.
+   * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
-   * <p>Mutually exclusive with `extendsClassConstant`.
+   * <p>Mutually exclusive with the following other properties defining instance-of:
    *
-   * <p>If none are specified the default is to match any extends clause.
+   * <ul>
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassName() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances of the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  String instanceOfClassNameExclusive() default "";
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstant() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes that are instances the referenced Class constant.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>extendsClassName
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
+   */
+  Class<?> instanceOfClassConstantExclusive() default Object.class;
+
+  /**
+   * Define the instance-of pattern as classes extending the fully qualified class name.
+   *
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
+   *
+   * <p>This property is deprecated, use instanceOfClassName instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
    */
   String extendsClassName() default "";
 
   /**
-   * Define the extends pattern by Class constant.
+   * Define the instance-of pattern as classes extending the referenced Class constant.
    *
-   * <p>Mutually exclusive with `extendsClassName`.
+   * <p>The pattern is exclusive in that it does not match classes that are instances of the
+   * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>If none are specified the default is to match any extends clause.
+   * <p>This property is deprecated, use instanceOfClassConstant instead.
+   *
+   * <p>Mutually exclusive with the following other properties defining instance-of:
+   *
+   * <ul>
+   *   <li>instanceOfClassName
+   *   <li>instanceOfClassNameExclusive
+   *   <li>instanceOfClassConstant
+   *   <li>instanceOfClassConstantExclusive
+   *   <li>extendsClassName
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class instance.
    */
   Class<?> extendsClassConstant() default Object.class;
 
   /**
    * Define the member pattern in full by a reference to a binding.
    *
-   * <p>Mutually exclusive with all other pattern properties. When a member binding is referenced
-   * this item is defined to be that item, including its class and member patterns.
+   * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
+   * is referenced this item is defined to be that item, including its class and member patterns.
    */
   String memberFromBinding() default "";
 
   /**
-   * Define the member pattern by matching on access flags.
+   * Define the member-access pattern by matching on access flags.
    *
-   * <p>Mutually exclusive with all field and method patterns as use restricts the match to both
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
    */
   MemberAccessFlags[] memberAccess() default {};
 
   /**
-   * Define the method pattern by matching on access flags.
+   * Define the method-access pattern by matching on access flags.
    *
-   * <p>Mutually exclusive with any field properties.
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
    */
   MethodAccessFlags[] methodAccess() default {};
 
   /**
    * Define the method-name pattern by an exact method name.
    *
-   * <p>Mutually exclusive with any field properties.
+   * <p>Mutually exclusive with all field properties.
    *
-   * <p>If none and other properties define this as a method the default matches any method name.
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
    */
   String methodName() default "";
 
   /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
-   * <p>Mutually exclusive with any field properties.
+   * <p>Mutually exclusive with all field properties.
    *
-   * <p>If none and other properties define this as a method the default matches any return type.
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
    */
   String methodReturnType() default "";
 
   /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
-   * <p>Mutually exclusive with any field properties.
+   * <p>Mutually exclusive with all field properties.
    *
-   * <p>If none and other properties define this as a method the default matches any parameters.
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
    */
-  String[] methodParameters() default {""};
+  String[] methodParameters() default {"<default>"};
 
   /**
-   * Define the field pattern by matching on field access flags.
+   * Define the field-access pattern by matching on access flags.
    *
-   * <p>Mutually exclusive with any method properties.
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
   /**
    * Define the field-name pattern by an exact field name.
    *
-   * <p>Mutually exclusive with any method properties.
+   * <p>Mutually exclusive with all method properties.
    *
-   * <p>If none and other properties define this as a field the default matches any field name.
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
    */
   String fieldName() default "";
 
   /**
    * Define the field-type pattern by a fully qualified type.
    *
-   * <p>Mutually exclusive with any method properties.
+   * <p>Mutually exclusive with all method properties.
    *
-   * <p>If none and other properties define this as a field the default matches any field type.
+   * <p>If none, and other properties define this item as a field, the default matches any type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
index cd00e00..b1ad804 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -1,6 +1,11 @@
 // 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;
@@ -11,6 +16,12 @@
 /**
  * Annotation to mark a class, field or method as being accessed from native code via JNI.
  *
+ * <p>Note: Before using this annotation, consider if instead you can annotate the code that is
+ * doing reflection with {@link UsesReflection}. Annotating the reflecting code is generally more
+ * clear and maintainable, and it also naturally gives rise to edges that describe just the
+ * reflected aspects of the program. The {@link UsedByReflection} annotation is suitable for cases
+ * where the reflecting code is not under user control, or in migrating away from rules.
+ *
  * <p>When a class is annotated, member patterns can be used to define which members are to be kept.
  * When no member patterns are specified the default pattern is to match just the class.
  *
@@ -20,6 +31,8 @@
 @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface UsedByNative {
+
+  /** Optional description to document the reason for this annotation. */
   String description() default "";
 
   /**
@@ -29,11 +42,15 @@
    */
   KeepCondition[] preconditions() default {};
 
-  /** Additional targets to be kept in addition to the annotated class/members. */
+  /**
+   * Additional targets to be kept in addition to the annotated class/members.
+   *
+   * <p>Defaults to no additional targets.
+   */
   KeepTarget[] additionalTargets() default {};
 
   /**
-   * The target kind to be kept.
+   * Specify the kind of this item pattern.
    *
    * <p>When annotating a class without member patterns, the default kind is {@link
    * KeepItemKind#ONLY_CLASS}.
@@ -47,20 +64,80 @@
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
-  // Member patterns. See KeepTarget for documentation.
+  /**
+   * Define the member-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
+   * types of members.
+   */
   MemberAccessFlags[] memberAccess() default {};
 
+  /**
+   * Define the method-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
+   */
   MethodAccessFlags[] methodAccess() default {};
 
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
+   */
   String methodReturnType() default "";
 
-  String[] methodParameters() default {""};
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
+   */
+  String[] methodParameters() default {"<default>"};
 
+  /**
+   * Define the field-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
+   */
   FieldAccessFlags[] fieldAccess() default {};
 
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
index 3b065ca..27489cb 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -1,6 +1,11 @@
 // 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;
@@ -9,7 +14,7 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation to mark a class, field or method as being reflectively accessed.
+ * Annotation to mark a class, field or method as being accessed reflectively.
  *
  * <p>Note: Before using this annotation, consider if instead you can annotate the code that is
  * doing reflection with {@link UsesReflection}. Annotating the reflecting code is generally more
@@ -26,6 +31,8 @@
 @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface UsedByReflection {
+
+  /** Optional description to document the reason for this annotation. */
   String description() default "";
 
   /**
@@ -35,11 +42,15 @@
    */
   KeepCondition[] preconditions() default {};
 
-  /** Additional targets to be kept in addition to the annotated class/members. */
+  /**
+   * Additional targets to be kept in addition to the annotated class/members.
+   *
+   * <p>Defaults to no additional targets.
+   */
   KeepTarget[] additionalTargets() default {};
 
   /**
-   * The target kind to be kept.
+   * Specify the kind of this item pattern.
    *
    * <p>When annotating a class without member patterns, the default kind is {@link
    * KeepItemKind#ONLY_CLASS}.
@@ -53,20 +64,80 @@
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
-  // Member patterns. See KeepTarget for documentation.
+  /**
+   * Define the member-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field and method properties as use restricts the match to both
+   * types of members.
+   */
   MemberAccessFlags[] memberAccess() default {};
 
+  /**
+   * Define the method-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * method-access flags.
+   */
   MethodAccessFlags[] methodAccess() default {};
 
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any return
+   * type.
+   */
   String methodReturnType() default "";
 
-  String[] methodParameters() default {""};
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any
+   * parameters.
+   */
+  String[] methodParameters() default {"<default>"};
 
+  /**
+   * Define the field-access pattern by matching on access flags.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any
+   * field-access flags.
+   */
   FieldAccessFlags[] fieldAccess() default {};
 
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
index 0af2ec6..56a215d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
@@ -1,6 +1,11 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
 package com.android.tools.r8.keepanno.annotations;
 
 import java.lang.annotation.ElementType;
@@ -9,7 +14,7 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation to declare the reflective usages made by an item.
+ * Annotation to declare the reflective usages made by a class, method or field.
  *
  * <p>The annotation's 'value' is a list of targets to be kept if the annotated item is used. The
  * annotated item is a precondition for keeping any of the specified targets. Thus, if an annotated
@@ -19,14 +24,15 @@
  * <p>The annotation's 'additionalPreconditions' is optional and can specify additional conditions
  * that should be satisfied for the annotation to be in effect.
  *
- * <p>The translation of the @UsesReflection annotation into a @KeepEdge is as follows:
+ * <p>The translation of the {@link UsesReflection} annotation into a {@link KeepEdge} is as
+ * follows:
  *
  * <p>Assume the item of the annotation is denoted by 'CTX' and referred to as its context.
  *
  * <pre>
- * @UsesReflection(value = targets, [additionalPreconditions = preconditions])
- * ==>
- * @KeepEdge(
+ * &#64;UsesReflection(value = targets, [additionalPreconditions = preconditions])
+ * ==&gt;
+ * &#64;KeepEdge(
  *   consequences = targets,
  *   preconditions = {createConditionFromContext(CTX)} + preconditions
  * )
@@ -56,10 +62,17 @@
 @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface UsesReflection {
+
+  /** Optional description to document the reason for this annotation. */
   String description() default "";
 
+  /** Consequences that must be kept if the annotation is in effect. */
   KeepTarget[] value();
 
+  /**
+   * Additional preconditions for the annotation to be in effect.
+   *
+   * <p>Defaults to no additional preconditions.
+   */
   KeepCondition[] additionalPreconditions() default {};
-
 }
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 d3fc1ba..bc466c5 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
@@ -18,25 +18,28 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 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.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
 import com.android.tools.r8.keepanno.ast.KeepCheck;
 import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepClassItemReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 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.KeepExtendsPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
 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.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
@@ -51,7 +54,6 @@
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -81,6 +83,11 @@
     return declarations;
   }
 
+  private static KeepClassItemReference classReferenceFromName(String className) {
+    return KeepClassItemReference.fromClassNamePattern(
+        KeepQualifiedClassNamePattern.exact(className));
+  }
+
   private static class KeepEdgeClassVisitor extends ClassVisitor {
     private final Parent<KeepDeclaration> parent;
     private String className;
@@ -116,9 +123,9 @@
         return new KeepEdgeVisitor(parent::accept, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
-        KeepItemPattern classItem =
-            KeepItemPattern.builder()
-                .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+        KeepClassItemPattern classItem =
+            KeepClassItemPattern.builder()
+                .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
                 .build();
         return new UsesReflectionVisitor(parent::accept, this::setContext, classItem);
       }
@@ -176,7 +183,7 @@
       this.methodDescriptor = methodDescriptor;
     }
 
-    private KeepItemPattern createItemContext() {
+    private KeepMemberItemPattern createMethodItemContext() {
       String returnTypeDescriptor = Type.getReturnType(methodDescriptor).getDescriptor();
       Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
       KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
@@ -188,8 +195,8 @@
               ? KeepMethodReturnTypePattern.voidType()
               : KeepMethodReturnTypePattern.fromType(
                   KeepTypePattern.fromDescriptor(returnTypeDescriptor));
-      return KeepItemPattern.builder()
-          .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+      return KeepMemberItemPattern.builder()
+          .setClassReference(classReferenceFromName(className))
           .setMemberPattern(
               KeepMethodPattern.builder()
                   .setNamePattern(KeepMethodNamePattern.exact(methodName))
@@ -209,22 +216,23 @@
         return new KeepEdgeVisitor(parent::accept, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
-        return new UsesReflectionVisitor(parent::accept, this::setContext, createItemContext());
+        return new UsesReflectionVisitor(
+            parent::accept, this::setContext, createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
-        return new ForApiMemberVisitor(parent::accept, this::setContext, createItemContext());
+        return new ForApiMemberVisitor(parent::accept, this::setContext, createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionMemberVisitor(
-            descriptor, parent::accept, this::setContext, createItemContext());
+            descriptor, parent::accept, this::setContext, createMethodItemContext());
       }
       if (descriptor.equals(AnnotationConstants.CheckRemoved.DESCRIPTOR)) {
         return new CheckRemovedMemberVisitor(
             descriptor,
             parent::accept,
             this::setContext,
-            createItemContext(),
+            createMethodItemContext(),
             KeepCheckKind.REMOVED);
       }
       if (descriptor.equals(AnnotationConstants.CheckOptimizedOut.DESCRIPTOR)) {
@@ -232,7 +240,7 @@
             descriptor,
             parent::accept,
             this::setContext,
-            createItemContext(),
+            createMethodItemContext(),
             KeepCheckKind.OPTIMIZED_OUT);
       }
       return null;
@@ -259,11 +267,11 @@
       this.fieldDescriptor = fieldDescriptor;
     }
 
-    private KeepItemPattern createItemContext() {
+    private KeepMemberItemPattern createMemberItemContext() {
       KeepFieldTypePattern typePattern =
           KeepFieldTypePattern.fromType(KeepTypePattern.fromDescriptor(fieldDescriptor));
-      return KeepItemPattern.builder()
-          .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+      return KeepMemberItemPattern.builder()
+          .setClassReference(classReferenceFromName(className))
           .setMemberPattern(
               KeepFieldPattern.builder()
                   .setNamePattern(KeepFieldNamePattern.exact(fieldName))
@@ -287,15 +295,15 @@
         return new KeepEdgeVisitor(parent, this::setContext);
       }
       if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
-        return new UsesReflectionVisitor(parent, this::setContext, createItemContext());
+        return new UsesReflectionVisitor(parent, this::setContext, createMemberItemContext());
       }
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
-        return new ForApiMemberVisitor(parent, this::setContext, createItemContext());
+        return new ForApiMemberVisitor(parent, this::setContext, createMemberItemContext());
       }
       if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionMemberVisitor(
-            descriptor, parent, this::setContext, createItemContext());
+            descriptor, parent, this::setContext, createMemberItemContext());
       }
       return null;
     }
@@ -342,9 +350,9 @@
 
   private static class UserBindingsHelper {
     private final KeepBindings.Builder builder = KeepBindings.builder();
-    private final Map<String, BindingSymbol> userNames = new HashMap<>();
+    private final Map<String, KeepBindingSymbol> userNames = new HashMap<>();
 
-    public BindingSymbol resolveUserBinding(String name) {
+    public KeepBindingSymbol resolveUserBinding(String name) {
       return userNames.computeIfAbsent(name, builder::create);
     }
 
@@ -352,8 +360,8 @@
       builder.addBinding(resolveUserBinding(name), item);
     }
 
-    public BindingSymbol defineFreshBinding(String name, KeepItemPattern item) {
-      BindingSymbol symbol = builder.generateFreshSymbol(name);
+    public KeepBindingSymbol defineFreshBinding(String name, KeepItemPattern item) {
+      KeepBindingSymbol symbol = builder.generateFreshSymbol(name);
       builder.addBinding(symbol, item);
       return symbol;
     }
@@ -444,7 +452,7 @@
 
     @Override
     public String getAnnotationName() {
-      return ForApi.CLASS.getSimpleName();
+      return ForApi.SIMPLE_NAME;
     }
 
     @Override
@@ -483,17 +491,20 @@
         if (item.isBindingReference()) {
           throw new KeepEdgeException("@KeepForApi cannot reference bindings");
         }
-        KeepItemPattern itemPattern = item.asItemPattern();
-        String descriptor = AnnotationConstants.getDescriptorFromClassTypeName(className);
-        String itemDescriptor =
-            itemPattern.getClassReference().asClassNamePattern().getExactDescriptor();
+        KeepClassItemPattern classItemPattern = item.asClassItemPattern();
+        if (classItemPattern == null) {
+          assert item.isMemberItemReference();
+          classItemPattern = item.asMemberItemPattern().getClassReference().asClassItemPattern();
+        }
+        String descriptor = KeepEdgeReaderUtils.getDescriptorFromClassTypeName(className);
+        String itemDescriptor = classItemPattern.getClassNamePattern().getExactDescriptor();
         if (!descriptor.equals(itemDescriptor)) {
           throw new KeepEdgeException("@KeepForApi must reference its class context " + className);
         }
-        if (itemPattern.isMemberItemPattern() && items.size() == 1) {
+        if (classItemPattern.isMemberItemPattern() && items.size() == 1) {
             throw new KeepEdgeException("@KeepForApi kind must include its class");
         }
-        if (!itemPattern.getExtendsPattern().isAny()) {
+        if (!classItemPattern.getInstanceOfPattern().isAny()) {
           throw new KeepEdgeException("@KeepForApi cannot define an 'extends' pattern.");
         }
         consequences.addTarget(KeepTarget.builder().setItemReference(item).build());
@@ -522,27 +533,28 @@
     ForApiMemberVisitor(
         Parent<KeepEdge> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
-        KeepItemPattern context) {
+        KeepMemberItemPattern context) {
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
-      BindingSymbol contextBinding = bindingsHelper.defineFreshBinding("CONTEXT", context);
       // Create a binding for the context such that the class and member are shared.
+      KeepClassItemPattern classContext = context.getClassReference().asClassItemPattern();
+      KeepBindingSymbol bindingSymbol = bindingsHelper.defineFreshBinding("CONTEXT", classContext);
+      KeepClassItemReference classReference =
+          KeepBindingReference.forClass(bindingSymbol).toClassItemReference();
       consequences.addTarget(
           KeepTarget.builder()
               .setItemPattern(
-                  KeepItemPattern.builder()
-                      .setClassReference(KeepClassReference.fromBindingReference(contextBinding))
+                  KeepMemberItemPattern.builder()
+                      .copyFrom(context)
+                      .setClassReference(classReference)
                       .build())
               .build());
-      consequences.addTarget(
-          KeepTarget.builder()
-              .setItemReference(KeepItemReference.fromBindingReference(contextBinding))
-              .build());
+      consequences.addTarget(KeepTarget.builder().setItemReference(classReference).build());
     }
 
     @Override
     public String getAnnotationName() {
-      return ForApi.CLASS.getSimpleName();
+      return ForApi.SIMPLE_NAME;
     }
 
     @Override
@@ -658,9 +670,12 @@
           throw new KeepEdgeException("@" + getAnnotationName() + " cannot reference bindings");
         }
         KeepItemPattern itemPattern = item.asItemPattern();
-        String descriptor = AnnotationConstants.getDescriptorFromClassTypeName(className);
-        String itemDescriptor =
-            itemPattern.getClassReference().asClassNamePattern().getExactDescriptor();
+        KeepClassItemPattern holderPattern =
+            itemPattern.isClassItemPattern()
+                ? itemPattern.asClassItemPattern()
+                : itemPattern.asMemberItemPattern().getClassReference().asClassItemPattern();
+        String descriptor = KeepEdgeReaderUtils.getDescriptorFromClassTypeName(className);
+        String itemDescriptor = holderPattern.getClassNamePattern().getExactDescriptor();
         if (!descriptor.equals(itemDescriptor)) {
           throw new KeepEdgeException(
               "@" + getAnnotationName() + " must reference its class context " + className);
@@ -668,7 +683,7 @@
         if (itemPattern.isMemberItemPattern() && items.size() == 1) {
           throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its class");
         }
-        if (!itemPattern.getExtendsPattern().isAny()) {
+        if (!holderPattern.getInstanceOfPattern().isAny()) {
           throw new KeepEdgeException(
               "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
         }
@@ -730,9 +745,6 @@
         super.visitEnum(name, descriptor, value);
       }
       switch (value) {
-        case Kind.DEFAULT:
-          // The default value is obtained by not assigning a kind (e.g., null in the builder).
-          break;
         case Kind.ONLY_CLASS:
         case Kind.ONLY_MEMBERS:
         case Kind.CLASS_AND_MEMBERS:
@@ -766,14 +778,10 @@
         throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
       }
       assert context.isMemberItemPattern();
+      KeepMemberItemPattern memberContext = context.asMemberItemPattern();
       if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
         consequences.addTarget(
-            KeepTarget.builder()
-                .setItemPattern(
-                    KeepItemPattern.builder()
-                        .setClassReference(context.getClassReference())
-                        .build())
-                .build());
+            KeepTarget.builder().setItemReference(memberContext.getClassReference()).build());
       }
       consequences.addTarget(KeepTarget.builder().setItemPattern(context).build());
       parent.accept(
@@ -803,7 +811,7 @@
 
     @Override
     public String getAnnotationName() {
-      return UsesReflection.CLASS.getSimpleName();
+      return UsesReflection.SIMPLE_NAME;
     }
 
     @Override
@@ -1127,69 +1135,160 @@
     }
   }
 
-  private static class ClassDeclaration extends SingleDeclaration<KeepClassReference> {
+  private static class ClassNameDeclaration
+      extends SingleDeclaration<KeepQualifiedClassNamePattern> {
+
+    @Override
+    String kind() {
+      return "class-name";
+    }
+
+    @Override
+    KeepQualifiedClassNamePattern getDefaultValue() {
+      return KeepQualifiedClassNamePattern.any();
+    }
+
+    @Override
+    KeepQualifiedClassNamePattern parse(String name, Object value) {
+      if (name.equals(Item.classConstant) && value instanceof Type) {
+        return KeepQualifiedClassNamePattern.exact(((Type) value).getClassName());
+      }
+      if (name.equals(Item.className) && value instanceof String) {
+        return KeepQualifiedClassNamePattern.exact(((String) value));
+      }
+      return null;
+    }
+  }
+
+  private static class InstanceOfDeclaration extends SingleDeclaration<KeepInstanceOfPattern> {
+
+    @Override
+    String kind() {
+      return "instance-of";
+    }
+
+    @Override
+    KeepInstanceOfPattern getDefaultValue() {
+      return KeepInstanceOfPattern.any();
+    }
+
+    @Override
+    KeepInstanceOfPattern parse(String name, Object value) {
+      if (name.equals(Item.instanceOfClassConstant) && value instanceof Type) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()))
+            .build();
+      }
+      if (name.equals(Item.instanceOfClassName) && value instanceof String) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((String) value)))
+            .build();
+      }
+      if (name.equals(Item.instanceOfClassConstantExclusive) && value instanceof Type) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()))
+            .setInclusive(false)
+            .build();
+      }
+      if (name.equals(Item.instanceOfClassNameExclusive) && value instanceof String) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((String) value)))
+            .setInclusive(false)
+            .build();
+      }
+      if (name.equals(Item.extendsClassConstant) && value instanceof Type) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()))
+            .setInclusive(false)
+            .build();
+      }
+      if (name.equals(Item.extendsClassName) && value instanceof String) {
+        return KeepInstanceOfPattern.builder()
+            .classPattern(KeepQualifiedClassNamePattern.exact(((String) value)))
+            .setInclusive(false)
+            .build();
+      }
+      return null;
+    }
+  }
+
+  private static class ClassDeclaration extends Declaration<KeepClassItemReference> {
 
     private final Supplier<UserBindingsHelper> getBindingsHelper;
 
+    private KeepClassItemReference boundClassItemReference = null;
+    private final ClassNameDeclaration classNameDeclaration = new ClassNameDeclaration();
+    private final InstanceOfDeclaration instanceOfDeclaration = new InstanceOfDeclaration();
+
     public ClassDeclaration(Supplier<UserBindingsHelper> getBindingsHelper) {
       this.getBindingsHelper = getBindingsHelper;
     }
 
+    private boolean isBindingReferenceDefined() {
+      return boundClassItemReference != null;
+    }
+
+    private boolean classPatternsAreDefined() {
+      return !classNameDeclaration.isDefault() || !instanceOfDeclaration.isDefault();
+    }
+
+    private void checkAllowedDefinitions() {
+      if (isBindingReferenceDefined() && classPatternsAreDefined()) {
+        throw new KeepEdgeException(
+            "Cannot reference a class binding and class patterns for a single class item");
+      }
+    }
+
     @Override
     String kind() {
       return "class";
     }
 
-    KeepClassReference wrap(KeepQualifiedClassNamePattern namePattern) {
-      return KeepClassReference.fromClassNamePattern(namePattern);
+    @Override
+    boolean isDefault() {
+      return !isBindingReferenceDefined() && !classPatternsAreDefined();
     }
 
     @Override
-    KeepClassReference getDefaultValue() {
-      return wrap(KeepQualifiedClassNamePattern.any());
+    KeepClassItemReference getValue() {
+      if (isBindingReferenceDefined()) {
+        return boundClassItemReference;
+      }
+      if (classPatternsAreDefined()) {
+        return KeepClassItemPattern.builder()
+            .setClassNamePattern(classNameDeclaration.getValue())
+            .setInstanceOfPattern(instanceOfDeclaration.getValue())
+            .build()
+            .toClassItemReference();
+      }
+      assert isDefault();
+      return KeepClassItemPattern.any().toClassItemReference();
+    }
+
+    public void setBindingReference(KeepClassItemReference bindingReference) {
+      if (isBindingReferenceDefined()) {
+        throw new KeepEdgeException(
+            "Cannot reference multiple class bindings for a single class item");
+      }
+      this.boundClassItemReference = bindingReference;
     }
 
     @Override
-    KeepClassReference parse(String name, Object value) {
+    boolean tryParse(String name, Object value) {
       if (name.equals(Item.classFromBinding) && value instanceof String) {
-        BindingSymbol symbol = getBindingsHelper.get().resolveUserBinding((String) value);
-        return KeepClassReference.fromBindingReference(symbol);
+        KeepBindingSymbol symbol = getBindingsHelper.get().resolveUserBinding((String) value);
+        setBindingReference(KeepBindingReference.forClass(symbol).toClassItemReference());
+        return true;
       }
-      if (name.equals(Item.classConstant) && value instanceof Type) {
-        return wrap(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()));
+      if (classNameDeclaration.tryParse(name, value)) {
+        checkAllowedDefinitions();
+        return true;
       }
-      if (name.equals(Item.className) && value instanceof String) {
-        return wrap(KeepQualifiedClassNamePattern.exact(((String) value)));
+      if (instanceOfDeclaration.tryParse(name, value)) {
+        checkAllowedDefinitions();
+        return true;
       }
-      return null;
-    }
-  }
-
-  private static class ExtendsDeclaration extends SingleDeclaration<KeepExtendsPattern> {
-
-    @Override
-    String kind() {
-      return "extends";
-    }
-
-    @Override
-    KeepExtendsPattern getDefaultValue() {
-      return KeepExtendsPattern.any();
-    }
-
-    @Override
-    KeepExtendsPattern parse(String name, Object value) {
-      if (name.equals(Item.extendsClassConstant) && value instanceof Type) {
-        return KeepExtendsPattern.builder()
-            .classPattern(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()))
-            .build();
-      }
-      if (name.equals(Item.extendsClassName) && value instanceof String) {
-        return KeepExtendsPattern.builder()
-            .classPattern(KeepQualifiedClassNamePattern.exact(((String) value)))
-            .build();
-      }
-      return null;
+      return false;
     }
   }
 
@@ -1230,18 +1329,12 @@
     @Override
     boolean tryParse(String name, Object value) {
       if (name.equals(Item.methodName) && value instanceof String) {
-        String methodName = (String) value;
-        if (!Item.methodNameDefaultValue.equals(methodName)) {
-          getBuilder().setNamePattern(KeepMethodNamePattern.exact(methodName));
-        }
+        getBuilder().setNamePattern(KeepMethodNamePattern.exact((String) value));
         return true;
       }
       if (name.equals(Item.methodReturnType) && value instanceof String) {
-        String returnType = (String) value;
-        if (!Item.methodReturnTypeDefaultValue.equals(returnType)) {
-          getBuilder()
-              .setReturnTypePattern(KeepEdgeReaderUtils.methodReturnTypeFromString(returnType));
-        }
+        getBuilder()
+            .setReturnTypePattern(KeepEdgeReaderUtils.methodReturnTypeFromString((String) value));
         return true;
       }
       return false;
@@ -1257,9 +1350,6 @@
         return new StringArrayVisitor(
             annotationName,
             params -> {
-              if (Arrays.asList(Item.methodParametersDefaultValue).equals(params)) {
-                return;
-              }
               KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
               for (String param : params) {
                 builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
@@ -1309,20 +1399,14 @@
     @Override
     boolean tryParse(String name, Object value) {
       if (name.equals(Item.fieldName) && value instanceof String) {
-        String fieldName = (String) value;
-        if (!Item.fieldNameDefaultValue.equals(fieldName)) {
-          getBuilder().setNamePattern(KeepFieldNamePattern.exact(fieldName));
-        }
+        getBuilder().setNamePattern(KeepFieldNamePattern.exact((String) value));
         return true;
       }
       if (name.equals(Item.fieldType) && value instanceof String) {
-        String fieldType = (String) value;
-        if (!Item.fieldTypeDefaultValue.equals(fieldType)) {
-          getBuilder()
-              .setTypePattern(
-                  KeepFieldTypePattern.fromType(
-                      KeepEdgeReaderUtils.typePatternFromString(fieldType)));
-        }
+        getBuilder()
+            .setTypePattern(
+                KeepFieldTypePattern.fromType(
+                    KeepEdgeReaderUtils.typePatternFromString((String) value)));
         return true;
       }
       return false;
@@ -1406,7 +1490,6 @@
     private String memberBindingReference = null;
     private String kind = null;
     private final ClassDeclaration classDeclaration = new ClassDeclaration(this::getBindingsHelper);
-    private final ExtendsDeclaration extendsDeclaration = new ExtendsDeclaration();
     private final MemberDeclaration memberDeclaration;
 
     public abstract UserBindingsHelper getBindingsHelper();
@@ -1426,28 +1509,20 @@
         // If kind is set then visitEnd ensures that this cannot be a binding reference.
         assert !itemReference.isBindingReference();
         KeepItemPattern itemPattern = itemReference.asItemPattern();
-        KeepItemPattern classPattern;
-        KeepItemPattern memberPattern;
+        KeepClassItemReference classReference;
+        KeepMemberItemPattern memberPattern;
         if (itemPattern.isClassItemPattern()) {
-          classPattern = itemPattern;
+          classReference = itemPattern.asClassItemPattern().toClassItemReference();
           memberPattern =
-              KeepItemPattern.builder()
-                  .copyFrom(itemPattern)
+              KeepMemberItemPattern.builder()
+                  .setClassReference(classReference)
                   .setMemberPattern(KeepMemberPattern.allMembers())
                   .build();
         } else {
-          memberPattern = itemPattern;
-          classPattern =
-              KeepItemPattern.builder()
-                  .copyFrom(itemPattern)
-                  .setMemberPattern(KeepMemberPattern.none())
-                  .build();
+          memberPattern = itemPattern.asMemberItemPattern();
+          classReference = memberPattern.getClassReference();
         }
-        assert classPattern.isClassItemPattern();
-        assert memberPattern.isMemberItemPattern();
-        return ImmutableList.of(
-            KeepItemReference.fromItemPattern(classPattern),
-            KeepItemReference.fromItemPattern(memberPattern));
+        return ImmutableList.of(classReference, memberPattern.toItemReference());
       } else {
         return Collections.singletonList(itemReference);
       }
@@ -1464,31 +1539,32 @@
       assert kind != null;
       if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
         KeepItemPattern itemPattern = itemReference.asItemPattern();
-        KeepMemberPattern memberPattern;
-        KeepItemPattern classPattern;
+        // Ensure we have a member item linked to the correct class.
+        KeepMemberItemPattern memberItemPattern;
         if (itemPattern.isClassItemPattern()) {
-          memberPattern = KeepMemberPattern.allMembers();
-          classPattern = itemPattern;
+          memberItemPattern =
+              KeepMemberItemPattern.builder()
+                  .setClassReference(itemPattern.asClassItemPattern().toClassItemReference())
+                  .build();
         } else {
-          memberPattern = itemPattern.getMemberPattern();
-          classPattern =
-              KeepItemPattern.builder()
-                  .copyFrom(itemPattern)
-                  .setMemberPattern(KeepMemberPattern.none())
+          memberItemPattern = itemPattern.asMemberItemPattern();
+        }
+        // If the class is not a binding, introduce the binding and rewrite the member.
+        KeepClassItemReference classItemReference = memberItemPattern.getClassReference();
+        if (classItemReference.isClassItemPattern()) {
+          KeepClassItemPattern classItemPattern = classItemReference.asClassItemPattern();
+          KeepBindingSymbol symbol =
+              getBindingsHelper().defineFreshBinding("CLASS", classItemPattern);
+          classItemReference = KeepBindingReference.forClass(symbol).toClassItemReference();
+          memberItemPattern =
+              KeepMemberItemPattern.builder()
+                  .copyFrom(memberItemPattern)
+                  .setClassReference(classItemReference)
                   .build();
         }
-        BindingSymbol symbol = getBindingsHelper().defineFreshBinding("CLASS", classPattern);
-        KeepItemPattern memberItemPattern =
-            KeepItemPattern.builder()
-                .copyFrom(itemPattern)
-                .setClassReference(KeepClassReference.fromBindingReference(symbol))
-                .setMemberPattern(memberPattern)
-                .build();
-        assert classPattern.isClassItemPattern();
-        assert memberItemPattern.isMemberItemPattern();
-        return ImmutableList.of(
-            KeepItemReference.fromItemPattern(classPattern),
-            KeepItemReference.fromItemPattern(memberItemPattern));
+        assert classItemReference.isBindingReference();
+        assert memberItemPattern.getClassReference().equals(classItemReference);
+        return ImmutableList.of(classItemReference, memberItemPattern.toItemReference());
       } else {
         return Collections.singletonList(itemReference);
       }
@@ -1515,9 +1591,6 @@
         super.visitEnum(name, descriptor, value);
       }
       switch (value) {
-        case Kind.DEFAULT:
-          // The default value is obtained by not assigning a kind (e.g., null in the builder).
-          break;
         case Kind.ONLY_CLASS:
         case Kind.ONLY_MEMBERS:
         case Kind.CLASS_AND_MEMBERS:
@@ -1535,7 +1608,6 @@
         return;
       }
       if (classDeclaration.tryParse(name, value)
-          || extendsDeclaration.tryParse(name, value)
           || memberDeclaration.tryParse(name, value)) {
         return;
       }
@@ -1554,32 +1626,33 @@
     @Override
     public void visitEnd() {
       if (memberBindingReference != null) {
-        if (!classDeclaration.getValue().equals(classDeclaration.getDefaultValue())
+        if (!classDeclaration.isDefault()
             || !memberDeclaration.getValue().isNone()
-            || !extendsDeclaration.getValue().isAny()
             || kind != null) {
           throw new KeepEdgeException(
               "Cannot define an item explicitly and via a member-binding reference");
         }
-        BindingSymbol symbol = getBindingsHelper().resolveUserBinding(memberBindingReference);
-        itemReference = KeepItemReference.fromBindingReference(symbol);
+        KeepBindingSymbol symbol = getBindingsHelper().resolveUserBinding(memberBindingReference);
+        itemReference = KeepBindingReference.forMember(symbol).toItemReference();
       } else {
         KeepMemberPattern memberPattern = memberDeclaration.getValue();
         // If the kind is not set (default) then the content of the members determines the kind.
         if (kind == null) {
           kind = memberPattern.isNone() ? Kind.ONLY_CLASS : Kind.ONLY_MEMBERS;
         }
-        // If the kind is a member kind and no member pattern is set then set members to all.
-        if (!kind.equals(Kind.ONLY_CLASS) && memberPattern.isNone()) {
-          memberPattern = KeepMemberPattern.allMembers();
+
+        KeepClassItemReference classReference = classDeclaration.getValue();
+        if (kind.equals(Kind.ONLY_CLASS)) {
+          itemReference = classReference;
+        } else {
+          KeepItemPattern itemPattern =
+              KeepMemberItemPattern.builder()
+                  .setClassReference(classReference)
+                  .setMemberPattern(
+                      memberPattern.isNone() ? KeepMemberPattern.allMembers() : memberPattern)
+                  .build();
+          itemReference = itemPattern.toItemReference();
         }
-        itemReference =
-            KeepItemReference.fromItemPattern(
-                KeepItemPattern.builder()
-                    .setClassReference(classDeclaration.getValue())
-                    .setExtendsPattern(extendsDeclaration.getValue())
-                    .setMemberPattern(memberPattern)
-                    .build());
       }
     }
   }
@@ -1600,7 +1673,7 @@
 
     @Override
     public String getAnnotationName() {
-      return Binding.CLASS.getSimpleName();
+      return Binding.SIMPLE_NAME;
     }
 
     @Override
@@ -1724,7 +1797,7 @@
 
     @Override
     public String getAnnotationName() {
-      return Target.CLASS.getSimpleName();
+      return Target.SIMPLE_NAME;
     }
 
     @Override
@@ -1762,7 +1835,7 @@
 
     @Override
     public String getAnnotationName() {
-      return Condition.CLASS.getSimpleName();
+      return Condition.SIMPLE_NAME;
     }
 
     @Override
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 e18bf1e..7b0cf28 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
@@ -15,6 +15,14 @@
  */
 public class KeepEdgeReaderUtils {
 
+  public static String getBinaryNameFromClassTypeName(String classTypeName) {
+    return classTypeName.replace('.', '/');
+  }
+
+  public static String getDescriptorFromClassTypeName(String classTypeName) {
+    return "L" + getBinaryNameFromClassTypeName(classTypeName) + ";";
+  }
+
   public static KeepTypePattern typePatternFromString(String string) {
     if (string.equals("<any>")) {
       return KeepTypePattern.any();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index ec3fb30..568e6fe 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -8,13 +8,14 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern.KeepFieldNameExactPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern.KeepMethodNameExactPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
@@ -138,21 +139,34 @@
   }
 
   private void writeItem(AnnotationVisitor itemVisitor, KeepItemPattern item) {
-    KeepClassReference classReference = item.getClassReference();
-    if (classReference.isBindingReference()) {
-      throw new Unimplemented();
+    if (item.isClassItemPattern()) {
+      writeClassItem(item.asClassItemPattern(), itemVisitor);
+    } else {
+      writeMemberItem(item.asMemberItemPattern(), itemVisitor);
     }
-    KeepQualifiedClassNamePattern namePattern = classReference.asClassNamePattern();
+  }
+
+  private void writeClassItem(
+      KeepClassItemPattern classItemPattern, AnnotationVisitor itemVisitor) {
+    KeepQualifiedClassNamePattern namePattern = classItemPattern.getClassNamePattern();
     if (namePattern.isExact()) {
       Type typeConstant = Type.getType(namePattern.getExactDescriptor());
       itemVisitor.visit(AnnotationConstants.Item.classConstant, typeConstant);
     } else {
       throw new Unimplemented();
     }
-    if (!item.getExtendsPattern().isAny()) {
+    if (!classItemPattern.getInstanceOfPattern().isAny()) {
       throw new Unimplemented();
     }
-    writeMember(item.getMemberPattern(), itemVisitor);
+  }
+
+  private void writeMemberItem(
+      KeepMemberItemPattern memberItemPattern, AnnotationVisitor itemVisitor) {
+    if (memberItemPattern.getClassReference().isBindingReference()) {
+      throw new Unimplemented();
+    }
+    writeClassItem(memberItemPattern.getClassReference().asClassItemPattern(), itemVisitor);
+    writeMember(memberItemPattern.getMemberPattern(), itemVisitor);
     itemVisitor.visitEnd();
   }
 
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 f6c6a74..7ca4306 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
@@ -1,18 +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.ast;
 
-import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
-import com.android.tools.r8.keepanno.annotations.KeepBinding;
-import com.android.tools.r8.keepanno.annotations.KeepCondition;
-import com.android.tools.r8.keepanno.annotations.KeepEdge;
-import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.keepanno.annotations.KeepItemKind;
-import com.android.tools.r8.keepanno.annotations.KeepOption;
-import com.android.tools.r8.keepanno.annotations.KeepTarget;
-import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
-import com.android.tools.r8.keepanno.annotations.MethodAccessFlags;
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.keepanno.ast;
 
 /**
  * Utility class for referencing the various keep annotations and their structure.
@@ -21,32 +15,9 @@
  * annotations which overlap in name with the actual semantic AST types.
  */
 public final class AnnotationConstants {
-
-  public static String getDescriptor(Class<?> clazz) {
-    return getDescriptorFromClassTypeName(clazz.getTypeName());
-  }
-
-  public static String getBinaryNameFromClassTypeName(String classTypeName) {
-    return classTypeName.replace('.', '/');
-  }
-
-  public static String getDescriptorFromClassTypeName(String classTypeName) {
-    return "L" + getBinaryNameFromClassTypeName(classTypeName) + ";";
-  }
-
-  public static boolean isKeepAnnotation(String descriptor, boolean visible) {
-    if (visible) {
-      return false;
-    }
-    return descriptor.equals(Edge.DESCRIPTOR)
-        || descriptor.equals(UsesReflection.DESCRIPTOR)
-        || descriptor.equals(Condition.DESCRIPTOR)
-        || descriptor.equals(Target.DESCRIPTOR);
-  }
-
   public static final class Edge {
-    public static final Class<KeepEdge> CLASS = KeepEdge.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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";
     public static final String preconditions = "preconditions";
@@ -54,158 +25,134 @@
   }
 
   public static final class ForApi {
-    public static final Class<KeepForApi> CLASS = KeepForApi.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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";
     public static final String additionalTargets = "additionalTargets";
     public static final String memberAccess = "memberAccess";
   }
 
   public static final class UsesReflection {
-    public static final Class<com.android.tools.r8.keepanno.annotations.UsesReflection> CLASS =
-        com.android.tools.r8.keepanno.annotations.UsesReflection.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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";
     public static final String value = "value";
     public static final String additionalPreconditions = "additionalPreconditions";
   }
 
   public static final class UsedByReflection {
-    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByReflection> CLASS =
-        com.android.tools.r8.keepanno.annotations.UsedByReflection.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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";
     public static final String preconditions = "preconditions";
     public static final String additionalTargets = "additionalTargets";
-    public static final String memberAccess = "memberAccess";
   }
 
   public static final class UsedByNative {
-    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByNative> CLASS =
-        com.android.tools.r8.keepanno.annotations.UsedByNative.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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 Class<com.android.tools.r8.keepanno.annotations.CheckRemoved> CLASS =
-        com.android.tools.r8.keepanno.annotations.CheckRemoved.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-    public static final String description = "description";
+    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 Class<com.android.tools.r8.keepanno.annotations.CheckOptimizedOut> CLASS =
-        com.android.tools.r8.keepanno.annotations.CheckOptimizedOut.class;
-
-    @SuppressWarnings("MutablePublicArray")
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
-    public static final String description = "description";
+    public static final String SIMPLE_NAME = "CheckOptimizedOut";
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/CheckOptimizedOut;";
   }
 
-  // Implicit hidden item which is "super type" of Condition and Target.
+  /** Item properties common to binding items, conditions and targets. */
   public static final class Item {
     public static final String classFromBinding = "classFromBinding";
     public static final String memberFromBinding = "memberFromBinding";
-
     public static final String className = "className";
     public static final String classConstant = "classConstant";
-
+    public static final String instanceOfClassName = "instanceOfClassName";
+    public static final String instanceOfClassNameExclusive = "instanceOfClassNameExclusive";
+    public static final String instanceOfClassConstant = "instanceOfClassConstant";
+    public static final String instanceOfClassConstantExclusive =
+        "instanceOfClassConstantExclusive";
     public static final String extendsClassName = "extendsClassName";
     public static final String extendsClassConstant = "extendsClassConstant";
-
     public static final String memberAccess = "memberAccess";
-
     public static final String methodAccess = "methodAccess";
     public static final String methodName = "methodName";
     public static final String methodReturnType = "methodReturnType";
     public static final String methodParameters = "methodParameters";
-
     public static final String fieldAccess = "fieldAccess";
     public static final String fieldName = "fieldName";
     public static final String fieldType = "fieldType";
-
-    // Default values for the optional entries. The defaults should be chosen such that they do
-    // not coincide with any actual valid value. E.g., the empty string in place of a name or type.
-    // These must be 1:1 with the value defined on the actual annotation definition.
-    public static final String classNameDefault = "";
-    public static final Class<?> classConstantDefault = Object.class;
-
-    public static final String extendsClassNameDefault = "";
-    public static final Class<?> extendsClassConstantDefault = Object.class;
-
-    public static final String methodNameDefaultValue = "";
-    public static final String methodReturnTypeDefaultValue = "";
-
-    @SuppressWarnings("MutablePublicArray")
-    public static final String[] methodParametersDefaultValue = new String[] {""};
-
-    public static final String fieldNameDefaultValue = "";
-    public static final String fieldTypeDefaultValue = "";
   }
 
   public static final class Binding {
-    public static final Class<KeepBinding> CLASS = KeepBinding.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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 Class<KeepCondition> CLASS = KeepCondition.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    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 Class<KeepTarget> CLASS = KeepTarget.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
+    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";
     public static final String allow = "allow";
     public static final String disallow = "disallow";
   }
 
   public static final class Kind {
-    public static final Class<KeepItemKind> CLASS = KeepItemKind.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
-    public static final String DEFAULT = "DEFAULT";
+    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";
     public static final String ONLY_MEMBERS = "ONLY_MEMBERS";
     public static final String CLASS_AND_MEMBERS = "CLASS_AND_MEMBERS";
   }
 
   public static final class Option {
-    public static final Class<KeepOption> CLASS = KeepOption.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
+    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";
-    public static final String OBFUSCATION = "OBFUSCATION";
     public static final String OPTIMIZATION = "OPTIMIZATION";
+    public static final String OBFUSCATION = "OBFUSCATION";
     public static final String ACCESS_MODIFICATION = "ACCESS_MODIFICATION";
     public static final String ANNOTATION_REMOVAL = "ANNOTATION_REMOVAL";
   }
 
   public static final class MemberAccess {
-    public static final Class<MemberAccessFlags> CLASS = MemberAccessFlags.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
+    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_";
-
     public static final String PUBLIC = "PUBLIC";
     public static final String PROTECTED = "PROTECTED";
     public static final String PACKAGE_PRIVATE = "PACKAGE_PRIVATE";
     public static final String PRIVATE = "PRIVATE";
-
     public static final String STATIC = "STATIC";
     public static final String FINAL = "FINAL";
     public static final String SYNTHETIC = "SYNTHETIC";
   }
 
   public static final class MethodAccess {
-    public static final Class<MethodAccessFlags> CLASS = MethodAccessFlags.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
+    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";
     public static final String BRIDGE = "BRIDGE";
     public static final String NATIVE = "NATIVE";
@@ -214,9 +161,9 @@
   }
 
   public static final class FieldAccess {
-    public static final Class<FieldAccessFlags> CLASS = FieldAccessFlags.class;
-    public static final String DESCRIPTOR = getDescriptor(CLASS);
-
+    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";
     public static final String TRANSIENT = "TRANSIENT";
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindingReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindingReference.java
new file mode 100644
index 0000000..6097e39
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindingReference.java
@@ -0,0 +1,55 @@
+// 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 com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+
+public abstract class KeepBindingReference {
+
+  public static KeepClassBindingReference forClass(KeepBindingSymbol name) {
+    return new KeepClassBindingReference(name);
+  }
+
+  public static KeepMemberBindingReference forMember(KeepBindingSymbol name) {
+    return new KeepMemberBindingReference(name);
+  }
+
+  public static KeepBindingReference forItem(KeepBindingSymbol name, KeepItemPattern item) {
+    return item.isClassItemPattern() ? forClass(name) : forMember(name);
+  }
+
+  private final KeepBindingSymbol name;
+
+  KeepBindingReference(KeepBindingSymbol name) {
+    this.name = name;
+  }
+
+  public abstract KeepItemReference toItemReference();
+
+  public KeepBindingSymbol getName() {
+    return name;
+  }
+
+  public final boolean isClassType() {
+    return asClassBindingReference() != null;
+  }
+
+  public final boolean isMemberType() {
+    return asMemberBindingReference() != null;
+  }
+
+  public KeepClassBindingReference asClassBindingReference() {
+    return null;
+  }
+
+  public KeepMemberBindingReference asMemberBindingReference() {
+    return null;
+  }
+
+  @Override
+  public String toString() {
+    return name.toString();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java
index a3ba447..8b759ee 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java
@@ -18,9 +18,9 @@
 
   private static final KeepBindings NONE_INSTANCE = new KeepBindings(Collections.emptyMap());
 
-  private final Map<BindingSymbol, Binding> bindings;
+  private final Map<KeepBindingSymbol, Binding> bindings;
 
-  private KeepBindings(Map<BindingSymbol, Binding> bindings) {
+  private KeepBindings(Map<KeepBindingSymbol, Binding> bindings) {
     assert bindings != null;
     this.bindings = bindings;
   }
@@ -29,7 +29,11 @@
     return NONE_INSTANCE;
   }
 
-  public Binding get(BindingSymbol bindingReference) {
+  public Binding get(KeepBindingReference bindingReference) {
+    return bindings.get(bindingReference.getName());
+  }
+
+  public Binding get(KeepBindingSymbol bindingReference) {
     return bindings.get(bindingReference);
   }
 
@@ -41,7 +45,7 @@
     return bindings.isEmpty();
   }
 
-  public void forEach(BiConsumer<BindingSymbol, KeepItemPattern> fn) {
+  public void forEach(BiConsumer<KeepBindingSymbol, KeepItemPattern> fn) {
     bindings.forEach((name, binding) -> fn.accept(name, binding.getItem()));
   }
 
@@ -125,11 +129,11 @@
     }
   }
 
-  public static class BindingSymbol {
+  public static class KeepBindingSymbol {
     private final String hint;
     private String suffix = "";
 
-    public BindingSymbol(String hint) {
+    public KeepBindingSymbol(String hint) {
       this.hint = hint;
     }
 
@@ -155,24 +159,24 @@
 
   public static class Builder {
 
-    private final Map<String, BindingSymbol> reserved = new HashMap<>();
-    private final Map<BindingSymbol, KeepItemPattern> bindings = new IdentityHashMap<>();
+    private final Map<String, KeepBindingSymbol> reserved = new HashMap<>();
+    private final Map<KeepBindingSymbol, KeepItemPattern> bindings = new IdentityHashMap<>();
 
-    public BindingSymbol generateFreshSymbol(String hint) {
+    public KeepBindingSymbol generateFreshSymbol(String hint) {
       // Allocate a fresh non-forgeable symbol. The actual name is chosen at build time.
-      return new BindingSymbol(hint);
+      return new KeepBindingSymbol(hint);
     }
 
-    public BindingSymbol create(String name) {
-      BindingSymbol symbol = new BindingSymbol(name);
-      BindingSymbol old = reserved.put(name, symbol);
+    public KeepBindingSymbol create(String name) {
+      KeepBindingSymbol symbol = new KeepBindingSymbol(name);
+      KeepBindingSymbol old = reserved.put(name, symbol);
       if (old != null) {
         throw new KeepEdgeException("Multiple bindings with name '" + name + "'");
       }
       return symbol;
     }
 
-    public Builder addBinding(BindingSymbol symbol, KeepItemPattern itemPattern) {
+    public Builder addBinding(KeepBindingSymbol symbol, KeepItemPattern itemPattern) {
       if (symbol == null || itemPattern == null) {
         throw new KeepEdgeException("Invalid binding of '" + symbol + "'");
       }
@@ -183,25 +187,17 @@
       return this;
     }
 
-    public BindingSymbol getClassBinding(BindingSymbol bindingSymbol) {
-      KeepItemPattern pattern = bindings.get(bindingSymbol);
-      if (pattern.isClassItemPattern()) {
-        return bindingSymbol;
-      }
-      return pattern.getClassReference().asBindingReference();
-    }
-
     @SuppressWarnings("ReferenceEquality")
     public KeepBindings build() {
       if (bindings.isEmpty()) {
         return NONE_INSTANCE;
       }
-      Map<BindingSymbol, Binding> definitions = new HashMap<>(bindings.size());
-      for (BindingSymbol symbol : bindings.keySet()) {
+      Map<KeepBindingSymbol, Binding> definitions = new HashMap<>(bindings.size());
+      for (KeepBindingSymbol symbol : bindings.keySet()) {
         // The reserved symbols are a subset of all symbols. Those that are not yet reserved denote
         // symbols that must be "unique" in the set of symbols, but that do not have a specific
         // name. Now that all symbols are known we can give each of these a unique name.
-        BindingSymbol defined = reserved.get(symbol.toString());
+        KeepBindingSymbol defined = reserved.get(symbol.toString());
         if (defined != symbol) {
           // For each undefined symbol we try to use the "hint" as its name, if the name is already
           // reserved for another symbol then we search for the first non-reserved name with an
@@ -218,18 +214,20 @@
       return new KeepBindings(definitions);
     }
 
-    private Binding verifyAndCreateBinding(BindingSymbol bindingDefinitionSymbol) {
+    private Binding verifyAndCreateBinding(KeepBindingSymbol bindingDefinitionSymbol) {
       KeepItemPattern pattern = bindings.get(bindingDefinitionSymbol);
-      for (BindingSymbol bindingReference : pattern.getBindingReferences()) {
+      for (KeepBindingReference bindingReference : pattern.getBindingReferences()) {
         // Currently, it is not possible to define mutually recursive items, so we only need
         // to check against self.
-        if (bindingReference.equals(bindingDefinitionSymbol)) {
+        if (bindingReference.getName().equals(bindingDefinitionSymbol)) {
           throw new KeepEdgeException("Recursive binding for name '" + bindingReference + "'");
         }
-        if (!bindings.containsKey(bindingReference)) {
+        if (!bindings.containsKey(bindingReference.getName())) {
           throw new KeepEdgeException(
-              "Undefined binding for name '"
-                  + bindingReference
+              "Undefined binding for binding '"
+                  + bindingReference.getName()
+                  + "' or type '"
+                  + (bindingReference.isClassType() ? "class" : "member")
                   + "' referenced in binding of '"
                   + bindingDefinitionSymbol
                   + "'");
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassBindingReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassBindingReference.java
new file mode 100644
index 0000000..19fc080
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassBindingReference.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.ast;
+
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+
+public final class KeepClassBindingReference extends KeepBindingReference {
+
+  KeepClassBindingReference(KeepBindingSymbol name) {
+    super(name);
+  }
+
+  @Override
+  public KeepClassBindingReference asClassBindingReference() {
+    return this;
+  }
+
+  public KeepClassItemReference toClassItemReference() {
+    return KeepClassItemReference.fromBindingReference(this);
+  }
+
+  @Override
+  public KeepItemReference toItemReference() {
+    return toClassItemReference();
+  }
+
+  @Override
+  public String toString() {
+    return "class-ref(" + super.toString() + ")";
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
new file mode 100644
index 0000000..5c63faf
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.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.keepanno.ast;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+
+public class KeepClassItemPattern extends KeepItemPattern {
+
+  public static KeepClassItemPattern any() {
+    return builder().build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
+    private KeepInstanceOfPattern instanceOfPattern = KeepInstanceOfPattern.any();
+
+    private Builder() {}
+
+    public Builder copyFrom(KeepClassItemPattern pattern) {
+      return setClassNamePattern(pattern.getClassNamePattern())
+          .setInstanceOfPattern(pattern.getInstanceOfPattern());
+    }
+
+    public Builder setClassNamePattern(KeepQualifiedClassNamePattern classNamePattern) {
+      this.classNamePattern = classNamePattern;
+      return this;
+    }
+
+    public Builder setInstanceOfPattern(KeepInstanceOfPattern instanceOfPattern) {
+      this.instanceOfPattern = instanceOfPattern;
+      return this;
+    }
+
+    public KeepClassItemPattern build() {
+      return new KeepClassItemPattern(classNamePattern, instanceOfPattern);
+    }
+  }
+
+  private final KeepQualifiedClassNamePattern classNamePattern;
+  private final KeepInstanceOfPattern instanceOfPattern;
+
+  public KeepClassItemPattern(
+      KeepQualifiedClassNamePattern classNamePattern, KeepInstanceOfPattern instanceOfPattern) {
+    assert classNamePattern != null;
+    assert instanceOfPattern != null;
+    this.classNamePattern = classNamePattern;
+    this.instanceOfPattern = instanceOfPattern;
+  }
+
+  @Override
+  public KeepClassItemPattern asClassItemPattern() {
+    return this;
+  }
+
+  @Override
+  public KeepItemReference toItemReference() {
+    return toClassItemReference();
+  }
+
+  public final KeepClassItemReference toClassItemReference() {
+    return KeepClassItemReference.fromClassItemPattern(this);
+  }
+
+  @Override
+  public Collection<KeepBindingReference> getBindingReferences() {
+    return Collections.emptyList();
+  }
+
+  public KeepQualifiedClassNamePattern getClassNamePattern() {
+    return classNamePattern;
+  }
+
+  public KeepInstanceOfPattern getInstanceOfPattern() {
+    return instanceOfPattern;
+  }
+
+  public boolean isAny() {
+    return classNamePattern.isAny() && instanceOfPattern.isAny();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof KeepClassItemPattern)) {
+      return false;
+    }
+    KeepClassItemPattern that = (KeepClassItemPattern) obj;
+    return classNamePattern.equals(that.classNamePattern)
+        && instanceOfPattern.equals(that.instanceOfPattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(classNamePattern, instanceOfPattern);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepClassItemPattern"
+        + "{ class="
+        + classNamePattern
+        + ", instance-of="
+        + instanceOfPattern
+        + '}';
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java
new file mode 100644
index 0000000..5a3c7d5
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java
@@ -0,0 +1,119 @@
+// 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 java.util.Collection;
+import java.util.Collections;
+
+public abstract class KeepClassItemReference extends KeepItemReference {
+
+  public static KeepClassItemReference fromBindingReference(
+      KeepClassBindingReference bindingReference) {
+    return new ClassBinding(bindingReference);
+  }
+
+  public static KeepClassItemReference fromClassItemPattern(KeepClassItemPattern classItemPattern) {
+    return new ClassItem(classItemPattern);
+  }
+
+  public static KeepClassItemReference fromClassNamePattern(
+      KeepQualifiedClassNamePattern classNamePattern) {
+    return new ClassItem(
+        KeepClassItemPattern.builder().setClassNamePattern(classNamePattern).build());
+  }
+
+  @Override
+  public final KeepClassItemReference asClassItemReference() {
+    return this;
+  }
+
+  public abstract Collection<KeepBindingReference> getBindingReferences();
+
+  private static class ClassBinding extends KeepClassItemReference {
+    private final KeepClassBindingReference bindingReference;
+
+    private ClassBinding(KeepClassBindingReference bindingReference) {
+      assert bindingReference != null;
+      this.bindingReference = bindingReference;
+    }
+
+    @Override
+    public KeepClassBindingReference asBindingReference() {
+      return bindingReference;
+    }
+
+    @Override
+    public Collection<KeepBindingReference> getBindingReferences() {
+      return Collections.singletonList(bindingReference);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ClassBinding)) {
+        return false;
+      }
+      ClassBinding that = (ClassBinding) o;
+      return bindingReference.equals(that.bindingReference);
+    }
+
+    @Override
+    public int hashCode() {
+      return bindingReference.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return bindingReference.toString();
+    }
+  }
+
+  private static class ClassItem extends KeepClassItemReference {
+    private final KeepClassItemPattern classItemPattern;
+
+    private ClassItem(KeepClassItemPattern classItemPattern) {
+      assert classItemPattern != null;
+      this.classItemPattern = classItemPattern;
+    }
+
+    @Override
+    public KeepItemPattern asItemPattern() {
+      return classItemPattern;
+    }
+
+    @Override
+    public KeepClassItemPattern asClassItemPattern() {
+      return classItemPattern;
+    }
+
+    @Override
+    public Collection<KeepBindingReference> getBindingReferences() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ClassItem)) {
+        return false;
+      }
+      ClassItem someItem = (ClassItem) o;
+      return classItemPattern.equals(someItem.classItemPattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return classItemPattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return classItemPattern.toString();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java
deleted file mode 100644
index 4f0f3f3..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.ast;
-
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.function.Predicate;
-
-public abstract class KeepClassReference {
-
-  public static KeepClassReference fromBindingReference(BindingSymbol bindingReference) {
-    return new BindingReference(bindingReference);
-  }
-
-  public static KeepClassReference fromClassNamePattern(
-      KeepQualifiedClassNamePattern classNamePattern) {
-    return new SomeItem(classNamePattern);
-  }
-
-  public boolean isBindingReference() {
-    return asBindingReference() != null;
-  }
-
-  public boolean isClassNamePattern() {
-    return asClassNamePattern() != null;
-  }
-
-  public BindingSymbol asBindingReference() {
-    return null;
-  }
-
-  public KeepQualifiedClassNamePattern asClassNamePattern() {
-    return null;
-  }
-
-  public abstract Collection<BindingSymbol> getBindingReferences();
-
-  public boolean isAny(Predicate<BindingSymbol> onReference) {
-    return isBindingReference()
-        ? onReference.test(asBindingReference())
-        : asClassNamePattern().isAny();
-  }
-
-  private static class BindingReference extends KeepClassReference {
-    private final BindingSymbol bindingReference;
-
-    private BindingReference(BindingSymbol bindingReference) {
-      assert bindingReference != null;
-      this.bindingReference = bindingReference;
-    }
-
-    @Override
-    public BindingSymbol asBindingReference() {
-      return bindingReference;
-    }
-
-    @Override
-    public Collection<BindingSymbol> getBindingReferences() {
-      return Collections.singletonList(bindingReference);
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      BindingReference that = (BindingReference) o;
-      return bindingReference.equals(that.bindingReference);
-    }
-
-    @Override
-    public int hashCode() {
-      return bindingReference.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return bindingReference.toString();
-    }
-  }
-
-  private static class SomeItem extends KeepClassReference {
-    private final KeepQualifiedClassNamePattern classNamePattern;
-
-    private SomeItem(KeepQualifiedClassNamePattern classNamePattern) {
-      assert classNamePattern != null;
-      this.classNamePattern = classNamePattern;
-    }
-
-    @Override
-    public KeepQualifiedClassNamePattern asClassNamePattern() {
-      return classNamePattern;
-    }
-
-    @Override
-    public Collection<BindingSymbol> getBindingReferences() {
-      return Collections.emptyList();
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      SomeItem someItem = (SomeItem) o;
-      return classNamePattern.equals(someItem.classNamePattern);
-    }
-
-    @Override
-    public int hashCode() {
-      return classNamePattern.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return classNamePattern.toString();
-    }
-  }
-}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
index de4b742..e9f7be2 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
@@ -29,7 +29,7 @@
     }
 
     public Builder setItemPattern(KeepItemPattern itemPattern) {
-      return setItemReference(KeepItemReference.fromItemPattern(itemPattern));
+      return setItemReference(itemPattern.toItemReference());
     }
 
     public KeepCondition build() {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index 3b11a28..34dcab5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -22,9 +22,11 @@
  *   CONTEXT ::= class-descriptor | method-descriptor | field-descriptor
  *   DESCRIPTION ::= string-content
  *
- *   BINDINGS ::= (BINDING_NAME = ITEM_PATTERN)*
- *   BINDING_NAME ::= string-content
- *   BINDING_REFERENCE ::= BINDING_NAME
+ *   BINDINGS ::= (BINDING_SYMBOL = ITEM_PATTERN)*
+ *   BINDING_SYMBOL ::= string-content
+ *   BINDING_REFERENCE ::= CLASS_BINDING_REFERENCE | MEMBER_BINDING_REFERENCE
+ *   CLASS_BINDING_REFERENCE ::= class BINDING_SYMBOL
+ *   MEMBER_BINDING_REFERENCE ::= member BINDING_SYMBOL
  *
  *   PRECONDITIONS ::= always | CONDITION+
  *   CONDITION ::= ITEM_REFERENCE
@@ -34,10 +36,13 @@
  *   OPTIONS ::= keep-all | OPTION+
  *   OPTION ::= shrinking | optimizing | obfuscating | access-modification | annotation-removal
  *
- *   ITEM_REFERENCE  ::= BINDING_REFERENCE | ITEM_PATTERN
- *   CLASS_REFERENCE ::= BINDING_REFERENCE | QUALIFIED_CLASS_NAME_PATTERN
+ *   ITEM_REFERENCE  ::= CLASS_ITEM_REFERENCE | MEMBER_ITEM_REFERENCE
+ *   CLASS_ITEM_REFERENCE ::= CLASS_BINDING_REFERENCE | CLASS_ITEM_PATTERN
+ *   MEMBER_ITEM_REFERENCE ::= MEMBER_BINDING_REFERENCE | MEMBER_ITEM_PATTERN
  *
- *   ITEM_PATTERN ::= class CLASS_REFERENCE extends EXTENDS_PATTERN { MEMBER_PATTERN }
+ *   ITEM_PATTERN ::= CLASS_ITEM_PATTERN | MEMBER_ITEM_PATTERN
+ *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN instance-of INSTANCE_OF_PATTERN
+ *   MEMBER_ITEM_PATTERN ::= CLASS_ITEM_REFERENCE { MEMBER_PATTERN }
  *
  *   TYPE_PATTERN ::= any | exact type-descriptor
  *   PACKAGE_PATTERN ::= any | exact package-name
@@ -45,11 +50,12 @@
  *   QUALIFIED_CLASS_NAME_PATTERN
  *     ::= any
  *       | PACKAGE_PATTERN UNQUALIFIED_CLASS_NAME_PATTERN
- *       | BINDING_REFERENCE
  *
  *   UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
  *
- *   EXTENDS_PATTERN ::= any | QUALIFIED_CLASS_NAME_PATTERN
+ *   INSTANCE_OF_PATTERN ::= INSTANCE_OF_PATTERN_INCLUSIVE | INSTANCE_OF_PATTERN_EXCLUSIVE
+ *   INSTANCE_OF_PATTERN_INCLUSIVE ::= QUALIFIED_CLASS_NAME_PATTERN
+ *   INSTANCE_OF_PATTERN_EXCLUSIVE ::= QUALIFIED_CLASS_NAME_PATTERN
  *
  *   MEMBER_PATTERN ::= none | all | FIELD_PATTERN | METHOD_PATTERN
  *
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
deleted file mode 100644
index c6b3db3..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.ast;
-
-/** Pattern for matching the "extends" or "implements" clause of a class. */
-public abstract class KeepExtendsPattern {
-
-  public static KeepExtendsPattern any() {
-    return Some.getAnyInstance();
-  }
-
-  public static class Builder {
-
-    private KeepExtendsPattern pattern = KeepExtendsPattern.any();
-
-    private Builder() {}
-
-    public Builder classPattern(KeepQualifiedClassNamePattern pattern) {
-      this.pattern = new Some(pattern);
-      return this;
-    }
-
-    public KeepExtendsPattern build() {
-      return pattern;
-    }
-  }
-
-  private static class Some extends KeepExtendsPattern {
-
-    private static final KeepExtendsPattern ANY_INSTANCE =
-        new Some(KeepQualifiedClassNamePattern.any());
-
-    private static KeepExtendsPattern getAnyInstance() {
-      return ANY_INSTANCE;
-    }
-
-    private final KeepQualifiedClassNamePattern pattern;
-
-    public Some(KeepQualifiedClassNamePattern pattern) {
-      assert pattern != null;
-      this.pattern = pattern;
-    }
-
-    @Override
-    public boolean isAny() {
-      return pattern.isAny();
-    }
-
-    @Override
-    public KeepQualifiedClassNamePattern asClassNamePattern() {
-      return pattern;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      Some that = (Some) o;
-      return pattern.equals(that.pattern);
-    }
-
-    @Override
-    public int hashCode() {
-      return pattern.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return pattern.toString();
-    }
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  private KeepExtendsPattern() {}
-
-  public abstract boolean isAny();
-
-  public abstract KeepQualifiedClassNamePattern asClassNamePattern();
-}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
new file mode 100644
index 0000000..79e2f43
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
@@ -0,0 +1,115 @@
+// 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;
+
+/** Pattern for matching the instance-of properties of a class. */
+public abstract class KeepInstanceOfPattern {
+
+  public static KeepInstanceOfPattern any() {
+    return Some.getAnyInstance();
+  }
+
+  public static class Builder {
+
+    private KeepQualifiedClassNamePattern namePattern = KeepQualifiedClassNamePattern.any();
+    private boolean isInclusive = true;
+
+    private Builder() {}
+
+    public Builder classPattern(KeepQualifiedClassNamePattern namePattern) {
+      this.namePattern = namePattern;
+      return this;
+    }
+
+    public Builder setInclusive(boolean isInclusive) {
+      this.isInclusive = isInclusive;
+      return this;
+    }
+
+    public KeepInstanceOfPattern build() {
+      if (namePattern.isAny()) {
+        if (!isInclusive) {
+          throw new KeepEdgeException(
+              "Invalid instance-of pattern matching any class exclusive. "
+                  + "This pattern matches nothing.");
+        }
+        return any();
+      }
+      return new Some(namePattern, isInclusive);
+    }
+  }
+
+  private static class Some extends KeepInstanceOfPattern {
+
+    private static final KeepInstanceOfPattern ANY_INSTANCE =
+        new Some(KeepQualifiedClassNamePattern.any(), true);
+
+    private static KeepInstanceOfPattern getAnyInstance() {
+      return ANY_INSTANCE;
+    }
+
+    private final KeepQualifiedClassNamePattern namePattern;
+    private final boolean isInclusive;
+
+    public Some(KeepQualifiedClassNamePattern namePattern, boolean isInclusive) {
+      assert namePattern != null;
+      this.namePattern = namePattern;
+      this.isInclusive = isInclusive;
+    }
+
+    @Override
+    public boolean isAny() {
+      return namePattern.isAny();
+    }
+
+    @Override
+    public boolean isInclusive() {
+      return isInclusive;
+    }
+
+    @Override
+    public KeepQualifiedClassNamePattern getClassNamePattern() {
+      return namePattern;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof Some)) {
+        return false;
+      }
+      Some that = (Some) o;
+      return namePattern.equals(that.namePattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return namePattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      String nameString = namePattern.toString();
+      return isInclusive ? nameString : ("excl(" + nameString + ")");
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private KeepInstanceOfPattern() {}
+
+  public abstract boolean isAny();
+
+  public abstract KeepQualifiedClassNamePattern getClassNamePattern();
+
+  public abstract boolean isInclusive();
+
+  public final boolean isExclusive() {
+    return !isInclusive();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
index 7634c68..c88c7c4 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
@@ -3,9 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
 import java.util.Collection;
-import java.util.Objects;
 
 /**
  * A pattern for matching items in the program.
@@ -17,135 +15,34 @@
  * classes or it is a pattern on members. The distinction is defined by having a "none" member
  * pattern.
  */
-public class KeepItemPattern {
+public abstract class KeepItemPattern {
 
   public static KeepItemPattern anyClass() {
-    return builder().setMemberPattern(KeepMemberPattern.none()).build();
+    return KeepClassItemPattern.any();
   }
 
   public static KeepItemPattern anyMember() {
-    return builder().setMemberPattern(KeepMemberPattern.allMembers()).build();
-  }
-
-  public static Builder builder() {
-    return new Builder();
+    return KeepMemberItemPattern.any();
   }
 
   public boolean isClassItemPattern() {
-    return memberPattern.isNone();
+    return asClassItemPattern() != null;
   }
 
   public boolean isMemberItemPattern() {
-    return !isClassItemPattern();
+    return asMemberItemPattern() != null;
   }
 
-  public static class Builder {
-
-    private KeepClassReference classReference =
-        KeepClassReference.fromClassNamePattern(KeepQualifiedClassNamePattern.any());
-    private KeepExtendsPattern extendsPattern = KeepExtendsPattern.any();
-    private KeepMemberPattern memberPattern = KeepMemberPattern.none();
-
-    private Builder() {}
-
-    public Builder copyFrom(KeepItemPattern pattern) {
-      return setClassReference(pattern.getClassReference())
-          .setExtendsPattern(pattern.getExtendsPattern())
-          .setMemberPattern(pattern.getMemberPattern());
-    }
-
-    public Builder setClassReference(KeepClassReference classReference) {
-      this.classReference = classReference;
-      return this;
-    }
-
-    public Builder setClassPattern(KeepQualifiedClassNamePattern qualifiedClassNamePattern) {
-      return setClassReference(KeepClassReference.fromClassNamePattern(qualifiedClassNamePattern));
-    }
-
-    public Builder setExtendsPattern(KeepExtendsPattern extendsPattern) {
-      this.extendsPattern = extendsPattern;
-      return this;
-    }
-
-    public Builder setMemberPattern(KeepMemberPattern memberPattern) {
-      this.memberPattern = memberPattern;
-      return this;
-    }
-
-    public KeepItemPattern build() {
-      return new KeepItemPattern(classReference, extendsPattern, memberPattern);
-    }
+  public KeepClassItemPattern asClassItemPattern() {
+    return null;
   }
 
-  private final KeepClassReference classReference;
-  private final KeepExtendsPattern extendsPattern;
-  private final KeepMemberPattern memberPattern;
-  // TODO: class annotations
-
-  private KeepItemPattern(
-      KeepClassReference classReference,
-      KeepExtendsPattern extendsPattern,
-      KeepMemberPattern memberPattern) {
-    assert classReference != null;
-    assert extendsPattern != null;
-    assert memberPattern != null;
-    this.classReference = classReference;
-    this.extendsPattern = extendsPattern;
-    this.memberPattern = memberPattern;
+  public KeepMemberItemPattern asMemberItemPattern() {
+    return null;
   }
 
-  public KeepClassReference getClassReference() {
-    return classReference;
-  }
+  public abstract Collection<KeepBindingReference> getBindingReferences();
 
-  public KeepExtendsPattern getExtendsPattern() {
-    return extendsPattern;
-  }
-
-  public KeepMemberPattern getMemberPattern() {
-    return memberPattern;
-  }
-
-  public Collection<BindingSymbol> getBindingReferences() {
-    return classReference.getBindingReferences();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (this == obj) {
-      return true;
-    }
-    if (!(obj instanceof KeepItemPattern)) {
-      return false;
-    }
-    KeepItemPattern that = (KeepItemPattern) obj;
-    return classReference.equals(that.classReference)
-        && extendsPattern.equals(that.extendsPattern)
-        && memberPattern.equals(that.memberPattern);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(classReference, extendsPattern, memberPattern);
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    if (isClassItemPattern()) {
-      builder.append("KeepClassPattern");
-    } else {
-      assert isMemberItemPattern();
-      builder.append("KeepMemberPattern");
-    }
-    builder.append("{ class=").append(classReference);
-    if (!extendsPattern.isAny()) {
-      builder.append(", extends=").append(extendsPattern);
-    }
-    if (!memberPattern.isNone()) {
-      builder.append(", members=").append(memberPattern);
-    }
-    return builder.append('}').toString();
-  }
+  public abstract KeepItemReference toItemReference();
 }
+
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java
index 21bb63c..2883d5c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java
@@ -3,27 +3,50 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
-
+/**
+ * A reference to an item pattern.
+ *
+ * <p>A reference can either be a binding-reference to an item pattern or the item pattern itself.
+ */
 public abstract class KeepItemReference {
 
-  public static KeepItemReference fromBindingReference(BindingSymbol bindingReference) {
-    return new BindingReference(bindingReference);
+  public final boolean isClassItemReference() {
+    return asClassItemReference() != null;
   }
 
-  public static KeepItemReference fromItemPattern(KeepItemPattern itemPattern) {
-    return new SomeItem(itemPattern);
+  public final boolean isMemberItemReference() {
+    return asMemberItemReference() != null;
   }
 
-  public boolean isBindingReference() {
+  public KeepClassItemReference asClassItemReference() {
+    return null;
+  }
+
+  public KeepMemberItemReference asMemberItemReference() {
+    return null;
+  }
+
+  // Helpers below.
+
+  /* Returns true if the reference is a binding to a class or member. */
+  public final boolean isBindingReference() {
     return asBindingReference() != null;
   }
 
-  public boolean isItemPattern() {
+  /* Returns true if the reference is an item pattern for a class or member. */
+  public final boolean isItemPattern() {
     return asItemPattern() != null;
   }
 
-  public BindingSymbol asBindingReference() {
+  public final boolean isClassItemPattern() {
+    return asClassItemPattern() != null;
+  }
+
+  public final boolean isMemberItemPattern() {
+    return asMemberItemPattern() != null;
+  }
+
+  public KeepBindingReference asBindingReference() {
     return null;
   }
 
@@ -31,89 +54,11 @@
     return null;
   }
 
-  public abstract KeepItemPattern lookupItemPattern(KeepBindings bindings);
-
-  private static class BindingReference extends KeepItemReference {
-    private final BindingSymbol bindingReference;
-
-    private BindingReference(BindingSymbol bindingReference) {
-      assert bindingReference != null;
-      this.bindingReference = bindingReference;
-    }
-
-    @Override
-    public BindingSymbol asBindingReference() {
-      return bindingReference;
-    }
-
-    @Override
-    public KeepItemPattern lookupItemPattern(KeepBindings bindings) {
-      return bindings.get(bindingReference).getItem();
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      BindingReference that = (BindingReference) o;
-      return bindingReference.equals(that.bindingReference);
-    }
-
-    @Override
-    public int hashCode() {
-      return bindingReference.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return "reference='" + bindingReference + "'";
-    }
+  public KeepClassItemPattern asClassItemPattern() {
+    return null;
   }
 
-  private static class SomeItem extends KeepItemReference {
-    private final KeepItemPattern itemPattern;
-
-    private SomeItem(KeepItemPattern itemPattern) {
-      assert itemPattern != null;
-      this.itemPattern = itemPattern;
-    }
-
-    @Override
-    public KeepItemPattern asItemPattern() {
-      return itemPattern;
-    }
-
-    @Override
-    public KeepItemPattern lookupItemPattern(KeepBindings bindings) {
-      return asItemPattern();
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      SomeItem someItem = (SomeItem) o;
-      return itemPattern.equals(someItem.itemPattern);
-    }
-
-    @Override
-    public int hashCode() {
-      return itemPattern.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return itemPattern.toString();
-    }
+  public KeepMemberItemPattern asMemberItemPattern() {
+    return null;
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberBindingReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberBindingReference.java
new file mode 100644
index 0000000..443ea55
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberBindingReference.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.keepanno.ast;
+
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+
+public final class KeepMemberBindingReference extends KeepBindingReference {
+
+  KeepMemberBindingReference(KeepBindingSymbol name) {
+    super(name);
+  }
+
+  @Override
+  public KeepMemberBindingReference asMemberBindingReference() {
+    return this;
+  }
+
+  @Override
+  public KeepItemReference toItemReference() {
+    return KeepMemberItemReference.fromBindingReference(this);
+  }
+
+  @Override
+  public String toString() {
+    return "member-ref(" + super.toString() + ")";
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemPattern.java
new file mode 100644
index 0000000..5184528
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemPattern.java
@@ -0,0 +1,107 @@
+// 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 java.util.Collection;
+import java.util.Objects;
+
+public class KeepMemberItemPattern extends KeepItemPattern {
+
+  public static KeepMemberItemPattern any() {
+    return builder().build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private KeepClassItemReference classReference =
+        KeepClassItemPattern.any().toClassItemReference();
+    private KeepMemberPattern memberPattern = KeepMemberPattern.allMembers();
+
+    private Builder() {}
+
+    public Builder copyFrom(KeepMemberItemPattern pattern) {
+      return setClassReference(pattern.getClassReference())
+          .setMemberPattern(pattern.getMemberPattern());
+    }
+
+    public Builder setClassReference(KeepClassItemReference classReference) {
+      this.classReference = classReference;
+      return this;
+    }
+
+    public Builder setMemberPattern(KeepMemberPattern memberPattern) {
+      this.memberPattern = memberPattern;
+      return this;
+    }
+
+    public KeepMemberItemPattern build() {
+      return new KeepMemberItemPattern(classReference, memberPattern);
+    }
+  }
+
+  private final KeepClassItemReference classReference;
+  private final KeepMemberPattern memberPattern;
+
+  private KeepMemberItemPattern(
+      KeepClassItemReference classReference, KeepMemberPattern memberPattern) {
+    assert classReference != null;
+    assert memberPattern != null;
+    this.classReference = classReference;
+    this.memberPattern = memberPattern;
+  }
+
+  @Override
+  public KeepMemberItemPattern asMemberItemPattern() {
+    return this;
+  }
+
+  @Override
+  public KeepItemReference toItemReference() {
+    return KeepMemberItemReference.fromMemberItemPattern(this);
+  }
+
+  public KeepClassItemReference getClassReference() {
+    return classReference;
+  }
+
+  public KeepMemberPattern getMemberPattern() {
+    return memberPattern;
+  }
+
+  public Collection<KeepBindingReference> getBindingReferences() {
+    return classReference.getBindingReferences();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof KeepMemberItemPattern)) {
+      return false;
+    }
+    KeepMemberItemPattern that = (KeepMemberItemPattern) obj;
+    return classReference.equals(that.classReference) && memberPattern.equals(that.memberPattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(classReference, memberPattern);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepMemberItemPattern"
+        + "{ class="
+        + classReference
+        + ", members="
+        + memberPattern
+        + '}';
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemReference.java
new file mode 100644
index 0000000..d2ab55d
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberItemReference.java
@@ -0,0 +1,65 @@
+// 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 abstract class KeepMemberItemReference extends KeepItemReference {
+
+  public static KeepMemberItemReference fromBindingReference(
+      KeepMemberBindingReference bindingReference) {
+    return new MemberBinding(bindingReference);
+  }
+
+  public static KeepMemberItemReference fromMemberItemPattern(KeepMemberItemPattern itemPattern) {
+    return new MemberItem(itemPattern);
+  }
+
+  @Override
+  public final KeepMemberItemReference asMemberItemReference() {
+    return this;
+  }
+
+  private static final class MemberBinding extends KeepMemberItemReference {
+
+    private final KeepMemberBindingReference bindingReference;
+
+    private MemberBinding(KeepMemberBindingReference bindingReference) {
+      this.bindingReference = bindingReference;
+    }
+
+    @Override
+    public KeepBindingReference asBindingReference() {
+      return bindingReference;
+    }
+
+    @Override
+    public String toString() {
+      return bindingReference.toString();
+    }
+  }
+
+  private static final class MemberItem extends KeepMemberItemReference {
+
+    private final KeepMemberItemPattern itemPattern;
+
+    public MemberItem(KeepMemberItemPattern itemPattern) {
+      this.itemPattern = itemPattern;
+    }
+
+    @Override
+    public KeepItemPattern asItemPattern() {
+      return itemPattern;
+    }
+
+    @Override
+    public KeepMemberItemPattern asMemberItemPattern() {
+      return itemPattern;
+    }
+
+    @Override
+    public String toString() {
+      return itemPattern.toString();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
index c67fd9a..aec5fbf 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
@@ -6,6 +6,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 public abstract class KeepMethodParametersPattern {
 
@@ -88,6 +89,13 @@
     public int hashCode() {
       return parameterPatterns.hashCode();
     }
+
+    @Override
+    public String toString() {
+      return "("
+          + parameterPatterns.stream().map(Object::toString).collect(Collectors.joining(", "))
+          + ")";
+    }
   }
 
   private static class Any extends KeepMethodParametersPattern {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
index 4d450a2..1d8b32b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
@@ -20,7 +20,7 @@
     }
 
     public Builder setItemPattern(KeepItemPattern itemPattern) {
-      return setItemReference(KeepItemReference.fromItemPattern(itemPattern));
+      return setItemReference(itemPattern.toItemReference());
     }
 
     public Builder setOptions(KeepOptions options) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
index 9bd2c09..baf9f6c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
@@ -3,16 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.keeprules;
 
+import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepClassItemReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Normalize a keep edge with respect to its bindings. This will systematically introduce a binding
@@ -31,6 +35,7 @@
 
   private final KeepEdge edge;
 
+  private final Map<KeepBindingSymbol, KeepItemPattern> normalizedUserBindings = new HashMap<>();
   private final KeepBindings.Builder bindingsBuilder = KeepBindings.builder();
   private final KeepPreconditions.Builder preconditionsBuilder = KeepPreconditions.builder();
   private final KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
@@ -43,7 +48,9 @@
     edge.getBindings()
         .forEach(
             (name, pattern) -> {
-              bindingsBuilder.addBinding(name, normalizeItemPattern(pattern));
+              KeepItemPattern normalizedItem = normalizeItemPattern(pattern);
+              bindingsBuilder.addBinding(name, normalizedItem);
+              normalizedUserBindings.put(name, normalizedItem);
             });
     // TODO(b/248408342): Normalize the preconditions by identifying vacuously true conditions.
     edge.getPreconditions()
@@ -51,7 +58,7 @@
             condition ->
                 preconditionsBuilder.addCondition(
                     KeepCondition.builder()
-                        .setItemReference(normalizeItem(condition.getItem()))
+                        .setItemReference(normalizeItemReference(condition.getItem()))
                         .build()));
     edge.getConsequences()
         .forEachTarget(
@@ -59,7 +66,7 @@
               consequencesBuilder.addTarget(
                   KeepTarget.builder()
                       .setOptions(target.getOptions())
-                      .setItemReference(normalizeItem(target.getItem()))
+                      .setItemReference(normalizeItemReference(target.getItem()))
                       .build());
             });
     return KeepEdge.builder()
@@ -70,58 +77,55 @@
         .build();
   }
 
-  private KeepItemReference normalizeItem(KeepItemReference item) {
+  private KeepBindingSymbol synthesizeFreshBindingSymbol(KeepItemPattern item) {
+    KeepBindingSymbol bindingName = bindingsBuilder.generateFreshSymbol(syntheticBindingPrefix);
+    bindingsBuilder.addBinding(bindingName, item);
+    return bindingName;
+  }
+
+  private KeepBindingReference synthesizeFreshBindingReference(KeepItemPattern item) {
+    KeepBindingSymbol bindingName = synthesizeFreshBindingSymbol(item);
+    return KeepBindingReference.forItem(bindingName, item);
+  }
+
+  private KeepItemReference normalizeItemReference(KeepItemReference item) {
     if (item.isBindingReference()) {
+      KeepBindingReference bindingReference = item.asBindingReference();
+      if (bindingReference.isClassType()) {
+        // A class-type reference is allowed to reference a member-typed binding.
+        // In this case, the normalized reference is to the class of the member.
+        KeepItemPattern boundItemPattern = normalizedUserBindings.get(bindingReference.getName());
+        if (boundItemPattern.isMemberItemPattern()) {
+          return boundItemPattern.asMemberItemPattern().getClassReference();
+        }
+      }
       return item;
     }
-    KeepItemPattern itemPattern = item.asItemPattern();
-    if (itemPattern.isClassItemPattern() && itemPattern.getClassReference().isBindingReference()) {
-      BindingSymbol classBinding =
-          bindingsBuilder.getClassBinding(itemPattern.getClassReference().asBindingReference());
-      return KeepItemReference.fromBindingReference(classBinding);
-    }
-    KeepItemPattern newItemPattern = normalizeItemPattern(itemPattern);
-    BindingSymbol bindingName = bindingsBuilder.generateFreshSymbol(syntheticBindingPrefix);
-    bindingsBuilder.addBinding(bindingName, newItemPattern);
-    return KeepItemReference.fromBindingReference(bindingName);
+    KeepItemPattern newItemPattern = normalizeItemPattern(item.asItemPattern());
+    return synthesizeFreshBindingReference(newItemPattern).toItemReference();
   }
 
   private KeepItemPattern normalizeItemPattern(KeepItemPattern pattern) {
-    // If the pattern is just a class pattern it is in normal form.
     if (pattern.isClassItemPattern()) {
+      // If the pattern is just a class pattern it is in normal form.
       return pattern;
     }
-    KeepClassReference bindingReference = bindingForClassItem(pattern);
-    return getMemberItemPattern(pattern, bindingReference);
+    return normalizeMemberItemPattern(pattern.asMemberItemPattern());
   }
 
-  private KeepClassReference bindingForClassItem(KeepItemPattern pattern) {
-    KeepClassReference classReference = pattern.getClassReference();
+  private KeepMemberItemPattern normalizeMemberItemPattern(
+      KeepMemberItemPattern memberItemPattern) {
+    KeepClassItemReference classReference = memberItemPattern.getClassReference();
     if (classReference.isBindingReference()) {
       // If the class is already defined via a binding then no need to introduce a new one and
-      // change the item.
-      return classReference;
+      // change the member item pattern.
+      return memberItemPattern;
     }
-    BindingSymbol bindingName = bindingsBuilder.generateFreshSymbol(syntheticBindingPrefix);
-    KeepClassReference bindingReference = KeepClassReference.fromBindingReference(bindingName);
-    KeepItemPattern newClassPattern = getClassItemPattern(pattern);
-    bindingsBuilder.addBinding(bindingName, newClassPattern);
-    return bindingReference;
-  }
-
-  public static KeepItemPattern getClassItemPattern(KeepItemPattern fromPattern) {
-    return KeepItemPattern.builder()
-        .setClassReference(fromPattern.getClassReference())
-        .setExtendsPattern(fromPattern.getExtendsPattern())
-        .build();
-  }
-
-  private KeepItemPattern getMemberItemPattern(
-      KeepItemPattern fromPattern, KeepClassReference classReference) {
-    assert fromPattern.isMemberItemPattern();
-    return KeepItemPattern.builder()
-        .setClassReference(classReference)
-        .setMemberPattern(fromPattern.getMemberPattern())
+    KeepBindingSymbol bindingName =
+        synthesizeFreshBindingSymbol(classReference.asClassItemPattern());
+    return KeepMemberItemPattern.builder()
+        .copyFrom(memberItemPattern)
+        .setClassReference(KeepBindingReference.forClass(bindingName).toClassItemReference())
         .build();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 5ad2ae3..41f593e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -3,10 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.keeprules;
 
+import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
 import com.android.tools.r8.keepanno.ast.KeepCheck;
 import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
@@ -14,8 +16,10 @@
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
+import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
@@ -71,19 +75,21 @@
     boolean isRemovedPattern = check.getKind() == KeepCheckKind.REMOVED;
     List<PgRule> rules = new ArrayList<>(isRemovedPattern ? 2 : 1);
     Holder holder;
-    Map<BindingSymbol, KeepMemberPattern> memberPatterns;
-    List<BindingSymbol> targetMembers;
+    Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns;
+    List<KeepBindingSymbol> targetMembers;
     KeepBindings.Builder builder = KeepBindings.builder();
-    BindingSymbol symbol = builder.generateFreshSymbol("CLASS");
+    KeepBindingSymbol symbol = builder.generateFreshSymbol("CLASS");
     if (itemPattern.isClassItemPattern()) {
-      builder.addBinding(symbol, check.getItemPattern());
+      builder.addBinding(symbol, itemPattern);
       memberPatterns = Collections.emptyMap();
       targetMembers = Collections.emptyList();
     } else {
-      builder.addBinding(symbol, KeepEdgeNormalizer.getClassItemPattern(check.getItemPattern()));
-      KeepMemberPattern memberPattern = itemPattern.getMemberPattern();
+      KeepMemberItemPattern memberItemPattern = itemPattern.asMemberItemPattern();
+      assert memberItemPattern.getClassReference().isClassItemPattern();
+      builder.addBinding(symbol, memberItemPattern.getClassReference().asClassItemPattern());
+      KeepMemberPattern memberPattern = memberItemPattern.getMemberPattern();
       // This does not actually allocate a binding as the mapping is maintained in 'memberPatterns'.
-      BindingSymbol memberSymbol = new BindingSymbol("MEMBERS");
+      KeepBindingSymbol memberSymbol = new KeepBindingSymbol("MEMBERS");
       memberPatterns = Collections.singletonMap(memberSymbol, memberPattern);
       targetMembers = Collections.singletonList(memberSymbol);
     }
@@ -104,7 +110,7 @@
       if (itemPattern.isClassItemPattern()) {
         // A check removal on a class means that the entire class is removed, thus soft-pin the
         // class and *all* of its members.
-        BindingSymbol memberSymbol = new BindingSymbol("MEMBERS");
+        KeepBindingSymbol memberSymbol = new KeepBindingSymbol("MEMBERS");
         rules.add(
             new PgUnconditionalRule(
                 check.getMetaInfo(),
@@ -129,36 +135,86 @@
     return rules;
   }
 
-  /**
-   * Utility to package up a class binding with its name and item pattern.
-   *
-   * <p>This is useful as the normalizer will have introduced class reference indirections so a
-   * given item may need to.
-   */
+  /** Utility to package up a class binding with its name and item pattern. */
   public static class Holder {
-    final KeepItemPattern itemPattern;
-    final KeepQualifiedClassNamePattern namePattern;
+    private final KeepClassItemPattern itemPattern;
 
-    static Holder create(BindingSymbol bindingName, KeepBindings bindings) {
-      KeepItemPattern itemPattern = bindings.get(bindingName).getItem();
-      assert itemPattern.isClassItemPattern();
-      KeepQualifiedClassNamePattern namePattern = getClassNamePattern(itemPattern, bindings);
-      return new Holder(itemPattern, namePattern);
+    static Holder create(KeepBindingSymbol bindingName, KeepBindings bindings) {
+      KeepClassItemPattern itemPattern = bindings.get(bindingName).getItem().asClassItemPattern();
+      return new Holder(itemPattern);
     }
 
-    private Holder(KeepItemPattern itemPattern, KeepQualifiedClassNamePattern namePattern) {
+    private Holder(KeepClassItemPattern itemPattern) {
+      assert itemPattern != null;
       this.itemPattern = itemPattern;
-      this.namePattern = namePattern;
+    }
+
+    public KeepClassItemPattern getClassItemPattern() {
+      return itemPattern;
+    }
+
+    public KeepQualifiedClassNamePattern getNamePattern() {
+      return getClassItemPattern().getClassNamePattern();
+    }
+
+    public void onTargetHolders(Consumer<Holder> fn) {
+      KeepInstanceOfPattern instanceOfPattern = itemPattern.getInstanceOfPattern();
+      if (instanceOfPattern.isAny()) {
+        // An any-pattern does not give rise to 'extends' and maps as is.
+        fn.accept(this);
+        return;
+      }
+      if (instanceOfPattern.isExclusive()) {
+        // An exclusive-pattern maps to the "extends" clause as is.
+        fn.accept(this);
+        return;
+      }
+      if (getNamePattern().isExact()) {
+        // This case is a pattern of "Foo instance-of Bar" and only makes sense if Foo==Bar.
+        // In any case we can conservatively cover this case by ignoring the instance-of clause.
+        Holder holderWithoutExtends =
+            new Holder(
+                KeepClassItemPattern.builder()
+                    .copyFrom(itemPattern)
+                    .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                    .build());
+        fn.accept(holderWithoutExtends);
+        return;
+      }
+      if (getNamePattern().isAny()) {
+        // This case is a pattern of "* instance-of Bar" and we match that as two rules, one of
+        // which is just the rule on the instance-of moved to the class name.
+        Holder holderWithInstanceOfAsName =
+            new Holder(
+                KeepClassItemPattern.builder()
+                    .copyFrom(itemPattern)
+                    .setClassNamePattern(instanceOfPattern.getClassNamePattern())
+                    .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                    .build());
+        fn.accept(this);
+        fn.accept(holderWithInstanceOfAsName);
+        return;
+      }
+      // The remaining case is the general "*Foo* instance-of *Bar*" case. Here it unfolds to two
+      // cases matching anything of the form "*Foo*" and the other being the exclusive extends.
+      Holder holderWithNoInstanceOf =
+          new Holder(
+              KeepClassItemPattern.builder()
+                  .copyFrom(itemPattern)
+                  .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                  .build());
+      fn.accept(this);
+      fn.accept(holderWithNoInstanceOf);
     }
   }
 
   private static class BindingUsers {
 
     final Holder holder;
-    final Set<BindingSymbol> conditionRefs = new HashSet<>();
-    final Map<KeepOptions, Set<BindingSymbol>> targetRefs = new HashMap<>();
+    final Set<KeepBindingSymbol> conditionRefs = new HashSet<>();
+    final Map<KeepOptions, Set<KeepBindingSymbol>> targetRefs = new HashMap<>();
 
-    static BindingUsers create(BindingSymbol bindingName, KeepBindings bindings) {
+    static BindingUsers create(KeepBindingSymbol bindingName, KeepBindings bindings) {
       return new BindingUsers(Holder.create(bindingName, bindings));
     }
 
@@ -168,14 +224,14 @@
 
     public void addCondition(KeepCondition condition) {
       assert condition.getItem().isBindingReference();
-      conditionRefs.add(condition.getItem().asBindingReference());
+      conditionRefs.add(condition.getItem().asBindingReference().getName());
     }
 
     public void addTarget(KeepTarget target) {
       assert target.getItem().isBindingReference();
       targetRefs
           .computeIfAbsent(target.getOptions(), k -> new HashSet<>())
-          .add(target.getItem().asBindingReference());
+          .add(target.getItem().asBindingReference().getName());
     }
   }
 
@@ -186,11 +242,11 @@
     // First step after normalizing is to group up all conditions and targets on their target class.
     // Here we use the normalized binding as the notion of identity on a class.
     KeepBindings bindings = edge.getBindings();
-    Map<BindingSymbol, BindingUsers> bindingUsers = new HashMap<>();
+    Map<KeepBindingSymbol, BindingUsers> bindingUsers = new HashMap<>();
     edge.getPreconditions()
         .forEach(
             condition -> {
-              BindingSymbol classReference =
+              KeepBindingSymbol classReference =
                   getClassItemBindingReference(condition.getItem(), bindings);
               assert classReference != null;
               bindingUsers
@@ -200,7 +256,7 @@
     edge.getConsequences()
         .forEachTarget(
             target -> {
-              BindingSymbol classReference =
+              KeepBindingSymbol classReference =
                   getClassItemBindingReference(target.getItem(), bindings);
               assert classReference != null;
               bindingUsers
@@ -263,16 +319,18 @@
     return rules;
   }
 
-  private static List<BindingSymbol> computeConditions(
-      Set<BindingSymbol> conditions,
+  private static List<KeepBindingSymbol> computeConditions(
+      Set<KeepBindingSymbol> conditions,
       KeepBindings bindings,
-      Map<BindingSymbol, KeepMemberPattern> memberPatterns) {
-    List<BindingSymbol> conditionMembers = new ArrayList<>();
+      Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns) {
+    List<KeepBindingSymbol> conditionMembers = new ArrayList<>();
     conditions.forEach(
         conditionReference -> {
           KeepItemPattern item = bindings.get(conditionReference).getItem();
           if (!item.isClassItemPattern()) {
-            KeepMemberPattern old = memberPatterns.put(conditionReference, item.getMemberPattern());
+            KeepMemberItemPattern memberItemPattern = item.asMemberItemPattern();
+            KeepMemberPattern old =
+                memberPatterns.put(conditionReference, memberItemPattern.getMemberPattern());
             conditionMembers.add(conditionReference);
             assert old == null;
           }
@@ -283,31 +341,37 @@
   @FunctionalInterface
   private interface OnTargetCallback {
     void accept(
-        Map<BindingSymbol, KeepMemberPattern> memberPatterns,
-        List<BindingSymbol> memberTargets,
+        Holder targetHolder,
+        Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
+        List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind);
   }
 
   private static void computeTargets(
-      Set<BindingSymbol> targets,
+      Holder targetHolder,
+      Set<KeepBindingSymbol> targets,
       KeepBindings bindings,
-      Map<BindingSymbol, KeepMemberPattern> memberPatterns,
+      Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
       OnTargetCallback callback) {
     TargetKeepKind keepKind = TargetKeepKind.JUST_MEMBERS;
-    List<BindingSymbol> targetMembers = new ArrayList<>();
-    for (BindingSymbol targetReference : targets) {
+    List<KeepBindingSymbol> targetMembers = new ArrayList<>();
+    for (KeepBindingSymbol targetReference : targets) {
       KeepItemPattern item = bindings.get(targetReference).getItem();
       if (item.isClassItemPattern()) {
         keepKind = TargetKeepKind.CLASS_AND_MEMBERS;
       } else {
-        memberPatterns.putIfAbsent(targetReference, item.getMemberPattern());
+        KeepMemberItemPattern memberItemPattern = item.asMemberItemPattern();
+        memberPatterns.putIfAbsent(targetReference, memberItemPattern.getMemberPattern());
         targetMembers.add(targetReference);
       }
     }
     if (targetMembers.isEmpty()) {
       keepKind = TargetKeepKind.CLASS_OR_MEMBERS;
     }
-    callback.accept(memberPatterns, targetMembers, keepKind);
+    final TargetKeepKind finalKeepKind = keepKind;
+    targetHolder.onTargetHolders(
+        newTargetHolder ->
+            callback.accept(newTargetHolder, memberPatterns, targetMembers, finalKeepKind));
   }
 
   private static void createUnconditionalRules(
@@ -316,18 +380,19 @@
       KeepEdgeMetaInfo metaInfo,
       KeepBindings bindings,
       KeepOptions options,
-      Set<BindingSymbol> targets) {
+      Set<KeepBindingSymbol> targets) {
     computeTargets(
+        holder,
         targets,
         bindings,
         new HashMap<>(),
-        (memberPatterns, targetMembers, targetKeepKind) -> {
+        (targetHolder, memberPatterns, targetMembers, targetKeepKind) -> {
           if (targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS)) {
             // Members dependent on the class, so they go to the implicitly dependent rule.
             rules.add(
                 new PgDependentMembersRule(
                     metaInfo,
-                    holder,
+                    targetHolder,
                     options,
                     memberPatterns,
                     Collections.emptyList(),
@@ -336,7 +401,12 @@
           } else {
             rules.add(
                 new PgUnconditionalRule(
-                    metaInfo, holder, options, memberPatterns, targetMembers, targetKeepKind));
+                    metaInfo,
+                    targetHolder,
+                    options,
+                    memberPatterns,
+                    targetMembers,
+                    targetKeepKind));
           }
         });
   }
@@ -348,29 +418,31 @@
       Holder targetHolder,
       KeepBindings bindings,
       KeepOptions options,
-      Set<BindingSymbol> conditions,
-      Set<BindingSymbol> targets) {
-    if (conditionHolder.namePattern.isExact()
-        && conditionHolder.itemPattern.equals(targetHolder.itemPattern)) {
+      Set<KeepBindingSymbol> conditions,
+      Set<KeepBindingSymbol> targets) {
+    if (conditionHolder.getNamePattern().isExact()
+        && conditionHolder.getClassItemPattern().equals(targetHolder.getClassItemPattern())) {
       // If the targets are conditional on its holder, the rule can be simplified as a dependent
       // rule. Note that this is only valid on an *exact* class matching as otherwise any
       // wildcard is allowed to be matched independently on the left and right of the edge.
       createDependentRules(rules, targetHolder, metaInfo, bindings, options, conditions, targets);
       return;
     }
-    Map<BindingSymbol, KeepMemberPattern> memberPatterns = new HashMap<>();
-    List<BindingSymbol> conditionMembers = computeConditions(conditions, bindings, memberPatterns);
+    Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns = new HashMap<>();
+    List<KeepBindingSymbol> conditionMembers =
+        computeConditions(conditions, bindings, memberPatterns);
     computeTargets(
+        targetHolder,
         targets,
         bindings,
         memberPatterns,
-        (ignore, targetMembers, targetKeepKind) ->
+        (newTargetHolder, ignore, targetMembers, targetKeepKind) ->
             rules.add(
                 new PgConditionalRule(
                     metaInfo,
                     options,
                     conditionHolder,
-                    targetHolder,
+                    newTargetHolder,
                     memberPatterns,
                     conditionMembers,
                     targetMembers,
@@ -379,27 +451,29 @@
 
   private static void createDependentRules(
       List<PgRule> rules,
-      Holder holder,
+      Holder initialHolder,
       KeepEdgeMetaInfo metaInfo,
       KeepBindings bindings,
       KeepOptions options,
-      Set<BindingSymbol> conditions,
-      Set<BindingSymbol> targets) {
-    Map<BindingSymbol, KeepMemberPattern> memberPatterns = new HashMap<>();
-    List<BindingSymbol> conditionMembers = computeConditions(conditions, bindings, memberPatterns);
+      Set<KeepBindingSymbol> conditions,
+      Set<KeepBindingSymbol> targets) {
+    Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns = new HashMap<>();
+    List<KeepBindingSymbol> conditionMembers =
+        computeConditions(conditions, bindings, memberPatterns);
     computeTargets(
+        initialHolder,
         targets,
         bindings,
         memberPatterns,
-        (ignore, targetMembers, targetKeepKind) -> {
-          List<BindingSymbol> nonAllMemberTargets = new ArrayList<>(targetMembers.size());
-          for (BindingSymbol targetMember : targetMembers) {
+        (holder, ignore, targetMembers, targetKeepKind) -> {
+          List<KeepBindingSymbol> nonAllMemberTargets = new ArrayList<>(targetMembers.size());
+          for (KeepBindingSymbol targetMember : targetMembers) {
             KeepMemberPattern memberPattern = memberPatterns.get(targetMember);
             if (memberPattern.isGeneralMember() && conditionMembers.contains(targetMember)) {
               // This pattern is on "members in general" and it is bound by a condition.
               // Since backrefs can't reference a *-member we split this target in two, one for
               // fields and one for methods.
-              HashMap<BindingSymbol, KeepMemberPattern> copyWithMethod =
+              HashMap<KeepBindingSymbol, KeepMemberPattern> copyWithMethod =
                   new HashMap<>(memberPatterns);
               copyWithMethod.put(targetMember, copyMethodFromMember(memberPattern));
               rules.add(
@@ -411,7 +485,7 @@
                       conditionMembers,
                       Collections.singletonList(targetMember),
                       targetKeepKind));
-              HashMap<BindingSymbol, KeepMemberPattern> copyWithField =
+              HashMap<KeepBindingSymbol, KeepMemberPattern> copyWithField =
                   new HashMap<>(memberPatterns);
               copyWithField.put(targetMember, copyFieldFromMember(memberPattern));
               rules.add(
@@ -454,18 +528,10 @@
     return KeepFieldPattern.builder().setAccessPattern(accessPattern).build();
   }
 
-  private static KeepQualifiedClassNamePattern getClassNamePattern(
-      KeepItemPattern itemPattern, KeepBindings bindings) {
-    return itemPattern.getClassReference().isClassNamePattern()
-        ? itemPattern.getClassReference().asClassNamePattern()
-        : getClassNamePattern(
-            bindings.get(itemPattern.getClassReference().asBindingReference()).getItem(), bindings);
-  }
-
-  private static BindingSymbol getClassItemBindingReference(
+  private static KeepBindingSymbol getClassItemBindingReference(
       KeepItemReference itemReference, KeepBindings bindings) {
-    BindingSymbol classReference = null;
-    for (BindingSymbol reference : getTransitiveBindingReferences(itemReference, bindings)) {
+    KeepBindingSymbol classReference = null;
+    for (KeepBindingSymbol reference : getTransitiveBindingReferences(itemReference, bindings)) {
       if (bindings.get(reference).getItem().isClassItemPattern()) {
         if (classReference != null) {
           throw new KeepEdgeException("Unexpected reference to multiple class bindings");
@@ -476,30 +542,29 @@
     return classReference;
   }
 
-  private static Set<BindingSymbol> getTransitiveBindingReferences(
+  private static Set<KeepBindingSymbol> getTransitiveBindingReferences(
       KeepItemReference itemReference, KeepBindings bindings) {
-    Set<BindingSymbol> references = new HashSet<>(2);
-    Deque<BindingSymbol> worklist = new ArrayDeque<>();
+    Set<KeepBindingSymbol> symbols = new HashSet<>(2);
+    Deque<KeepBindingReference> worklist = new ArrayDeque<>();
     worklist.addAll(getBindingReference(itemReference));
     while (!worklist.isEmpty()) {
-      BindingSymbol bindingReference = worklist.pop();
-      if (references.add(bindingReference)) {
+      KeepBindingReference bindingReference = worklist.pop();
+      if (symbols.add(bindingReference.getName())) {
         worklist.addAll(getBindingReference(bindings.get(bindingReference).getItem()));
       }
     }
-    return references;
+    return symbols;
   }
 
-  private static Collection<BindingSymbol> getBindingReference(KeepItemReference itemReference) {
+  private static Collection<KeepBindingReference> getBindingReference(
+      KeepItemReference itemReference) {
     if (itemReference.isBindingReference()) {
       return Collections.singletonList(itemReference.asBindingReference());
     }
     return getBindingReference(itemReference.asItemPattern());
   }
 
-  private static Collection<BindingSymbol> getBindingReference(KeepItemPattern itemPattern) {
-    return itemPattern.getClassReference().isBindingReference()
-        ? Collections.singletonList(itemPattern.getClassReference().asBindingReference())
-        : Collections.emptyList();
+  private static Collection<KeepBindingReference> getBindingReference(KeepItemPattern itemPattern) {
+    return itemPattern.getBindingReferences();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
index 5d55174..96e4bde 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
@@ -10,11 +10,10 @@
 import static com.android.tools.r8.keepanno.keeprules.RulePrintingUtils.printClassHeader;
 import static com.android.tools.r8.keepanno.keeprules.RulePrintingUtils.printMemberClause;
 
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
-import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
@@ -96,14 +95,10 @@
   }
 
   // Helper to print the class-name pattern in a class-item.
-  // The item is assumed to either be a binding (where the binding is a class with
-  // the supplied class-name pattern), or a class-item that has the class-name pattern itself (e.g.,
-  // without a binding indirection).
-  public static BiConsumer<StringBuilder, KeepClassReference> classReferencePrinter(
+  public static BiConsumer<StringBuilder, KeepQualifiedClassNamePattern> classNamePrinter(
       KeepQualifiedClassNamePattern classNamePattern) {
-    return (builder, classReference) -> {
-      assert classReference.isBindingReference()
-          || classReference.asClassNamePattern().equals(classNamePattern);
+    return (builder, className) -> {
+      assert className.equals(classNamePattern);
       RulePrintingUtils.printClassName(
           classNamePattern, RulePrinter.withoutBackReferences(builder));
     };
@@ -123,10 +118,10 @@
     if (hasCondition()) {
       builder.append(RulePrintingUtils.IF).append(' ');
       printConditionHolder(builder);
-      List<BindingSymbol> members = getConditionMembers();
+      List<KeepBindingSymbol> members = getConditionMembers();
       if (!members.isEmpty()) {
         builder.append(" {");
-        for (BindingSymbol member : members) {
+        for (KeepBindingSymbol member : members) {
           builder.append(' ');
           printConditionMember(builder, member);
         }
@@ -141,10 +136,10 @@
     printKeepOptions(builder);
     builder.append(' ');
     printTargetHolder(builder);
-    List<BindingSymbol> members = getTargetMembers();
+    List<KeepBindingSymbol> members = getTargetMembers();
     if (!members.isEmpty()) {
       builder.append(" {");
-      for (BindingSymbol member : members) {
+      for (KeepBindingSymbol member : members) {
         builder.append(' ');
         printTargetMember(builder, member);
       }
@@ -156,25 +151,25 @@
     return false;
   }
 
-  List<BindingSymbol> getConditionMembers() {
+  List<KeepBindingSymbol> getConditionMembers() {
     throw new KeepEdgeException("Unreachable");
   }
 
   abstract String getConsequenceKeepType();
 
-  abstract List<BindingSymbol> getTargetMembers();
+  abstract List<KeepBindingSymbol> getTargetMembers();
 
   void printConditionHolder(StringBuilder builder) {
     throw new KeepEdgeException("Unreachable");
   }
 
-  void printConditionMember(StringBuilder builder, BindingSymbol member) {
+  void printConditionMember(StringBuilder builder, KeepBindingSymbol member) {
     throw new KeepEdgeException("Unreachable");
   }
 
   abstract void printTargetHolder(StringBuilder builder);
 
-  abstract void printTargetMember(StringBuilder builder, BindingSymbol member);
+  abstract void printTargetMember(StringBuilder builder, KeepBindingSymbol member);
 
   /**
    * Representation of an unconditional rule to keep a class and methods.
@@ -187,22 +182,22 @@
    */
   static class PgUnconditionalRule extends PgRule {
     private final KeepQualifiedClassNamePattern holderNamePattern;
-    private final KeepItemPattern holderPattern;
+    private final KeepClassItemPattern holderPattern;
     private final TargetKeepKind targetKeepKind;
-    private final List<BindingSymbol> targetMembers;
-    private final Map<BindingSymbol, KeepMemberPattern> memberPatterns;
+    private final List<KeepBindingSymbol> targetMembers;
+    private final Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns;
 
     public PgUnconditionalRule(
         KeepEdgeMetaInfo metaInfo,
         Holder holder,
         KeepOptions options,
-        Map<BindingSymbol, KeepMemberPattern> memberPatterns,
-        List<BindingSymbol> targetMembers,
+        Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
+        List<KeepBindingSymbol> targetMembers,
         TargetKeepKind targetKeepKind) {
       super(metaInfo, options);
       assert !targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS);
-      this.holderNamePattern = holder.namePattern;
-      this.holderPattern = holder.itemPattern;
+      this.holderNamePattern = holder.getNamePattern();
+      this.holderPattern = holder.getClassItemPattern();
       this.targetKeepKind = targetKeepKind;
       this.memberPatterns = memberPatterns;
       this.targetMembers = targetMembers;
@@ -214,20 +209,20 @@
     }
 
     @Override
-    List<BindingSymbol> getTargetMembers() {
+    List<KeepBindingSymbol> getTargetMembers() {
       return targetMembers;
     }
 
     @Override
     void printTargetHolder(StringBuilder builder) {
-      printClassHeader(builder, holderPattern, classReferencePrinter(holderNamePattern));
+      printClassHeader(builder, holderPattern, classNamePrinter(holderNamePattern));
       if (getTargetMembers().isEmpty()) {
         printNonEmptyMembersPatternAsDefaultInitWorkaround(builder, targetKeepKind);
       }
     }
 
     @Override
-    void printTargetMember(StringBuilder builder, BindingSymbol memberReference) {
+    void printTargetMember(StringBuilder builder, KeepBindingSymbol memberReference) {
       KeepMemberPattern memberPattern = memberPatterns.get(memberReference);
       printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
@@ -245,11 +240,11 @@
    */
   static class PgConditionalRule extends PgRule {
 
-    final KeepItemPattern classCondition;
-    final KeepItemPattern classTarget;
-    final Map<BindingSymbol, KeepMemberPattern> memberPatterns;
-    final List<BindingSymbol> memberConditions;
-    private final List<BindingSymbol> memberTargets;
+    final KeepClassItemPattern classCondition;
+    final KeepClassItemPattern classTarget;
+    final Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns;
+    final List<KeepBindingSymbol> memberConditions;
+    private final List<KeepBindingSymbol> memberTargets;
     private final TargetKeepKind keepKind;
 
     public PgConditionalRule(
@@ -257,13 +252,13 @@
         KeepOptions options,
         Holder classCondition,
         Holder classTarget,
-        Map<BindingSymbol, KeepMemberPattern> memberPatterns,
-        List<BindingSymbol> memberConditions,
-        List<BindingSymbol> memberTargets,
+        Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
+        List<KeepBindingSymbol> memberConditions,
+        List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind) {
       super(metaInfo, options);
-      this.classCondition = classCondition.itemPattern;
-      this.classTarget = classTarget.itemPattern;
+      this.classCondition = classCondition.getClassItemPattern();
+      this.classTarget = classTarget.getClassItemPattern();
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
       this.memberTargets = memberTargets;
@@ -276,7 +271,7 @@
     }
 
     @Override
-    List<BindingSymbol> getConditionMembers() {
+    List<KeepBindingSymbol> getConditionMembers() {
       return memberConditions;
     }
 
@@ -286,7 +281,7 @@
     }
 
     @Override
-    void printConditionMember(StringBuilder builder, BindingSymbol member) {
+    void printConditionMember(StringBuilder builder, KeepBindingSymbol member) {
       KeepMemberPattern memberPattern = memberPatterns.get(member);
       printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
@@ -305,19 +300,18 @@
     }
 
     @Override
-    List<BindingSymbol> getTargetMembers() {
+    List<KeepBindingSymbol> getTargetMembers() {
       return memberTargets;
     }
 
     @Override
-    void printTargetMember(StringBuilder builder, BindingSymbol member) {
+    void printTargetMember(StringBuilder builder, KeepBindingSymbol member) {
       KeepMemberPattern memberPattern = memberPatterns.get(member);
       printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
 
-    private void printClassName(StringBuilder builder, KeepClassReference clazz) {
-      RulePrintingUtils.printClassName(
-          clazz.asClassNamePattern(), RulePrinter.withoutBackReferences(builder));
+    private void printClassName(StringBuilder builder, KeepQualifiedClassNamePattern clazzName) {
+      RulePrintingUtils.printClassName(clazzName, RulePrinter.withoutBackReferences(builder));
     }
   }
 
@@ -338,27 +332,27 @@
   static class PgDependentMembersRule extends PgRule {
 
     private final KeepQualifiedClassNamePattern holderNamePattern;
-    private final KeepItemPattern holderPattern;
-    private final Map<BindingSymbol, KeepMemberPattern> memberPatterns;
-    private final List<BindingSymbol> memberConditions;
-    private final List<BindingSymbol> memberTargets;
+    private final KeepClassItemPattern holderPattern;
+    private final Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns;
+    private final List<KeepBindingSymbol> memberConditions;
+    private final List<KeepBindingSymbol> memberTargets;
     private final TargetKeepKind keepKind;
 
     private int nextBackReferenceNumber = 1;
     private String holderBackReferencePattern = null;
-    private final Map<BindingSymbol, String> membersBackReferencePatterns = new HashMap<>();
+    private final Map<KeepBindingSymbol, String> membersBackReferencePatterns = new HashMap<>();
 
     public PgDependentMembersRule(
         KeepEdgeMetaInfo metaInfo,
         Holder holder,
         KeepOptions options,
-        Map<BindingSymbol, KeepMemberPattern> memberPatterns,
-        List<BindingSymbol> memberConditions,
-        List<BindingSymbol> memberTargets,
+        Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
+        List<KeepBindingSymbol> memberConditions,
+        List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind) {
       super(metaInfo, options);
-      this.holderNamePattern = holder.namePattern;
-      this.holderPattern = holder.itemPattern;
+      this.holderNamePattern = holder.getNamePattern();
+      this.holderPattern = holder.getClassItemPattern();
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
       this.memberTargets = memberTargets;
@@ -384,12 +378,12 @@
     }
 
     @Override
-    List<BindingSymbol> getConditionMembers() {
+    List<KeepBindingSymbol> getConditionMembers() {
       return memberConditions;
     }
 
     @Override
-    List<BindingSymbol> getTargetMembers() {
+    List<KeepBindingSymbol> getTargetMembers() {
       return memberTargets;
     }
 
@@ -407,7 +401,7 @@
     }
 
     @Override
-    void printConditionMember(StringBuilder builder, BindingSymbol member) {
+    void printConditionMember(StringBuilder builder, KeepBindingSymbol member) {
       KeepMemberPattern memberPattern = memberPatterns.get(member);
       BackReferencePrinter printer =
           RulePrinter.withBackReferences(builder, this::getNextBackReferenceNumber);
@@ -420,9 +414,8 @@
       printClassHeader(
           builder,
           holderPattern,
-          (b, reference) -> {
-            assert reference.isBindingReference()
-                || reference.asClassNamePattern().equals(holderNamePattern);
+          (b, className) -> {
+            assert className.equals(holderNamePattern);
             if (hasCondition()) {
               b.append(holderBackReferencePattern);
             } else {
@@ -437,7 +430,7 @@
     }
 
     @Override
-    void printTargetMember(StringBuilder builder, BindingSymbol member) {
+    void printTargetMember(StringBuilder builder, KeepBindingSymbol member) {
       if (hasCondition()) {
         String backref = membersBackReferencePatterns.get(member);
         if (backref != null) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index 604fe5b..3669002 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -4,14 +4,13 @@
 package com.android.tools.r8.keepanno.keeprules;
 
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
-import com.android.tools.r8.keepanno.ast.KeepExtendsPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
-import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
@@ -90,15 +89,15 @@
 
   public static StringBuilder printClassHeader(
       StringBuilder builder,
-      KeepItemPattern classPattern,
-      BiConsumer<StringBuilder, KeepClassReference> printClassReference) {
+      KeepClassItemPattern classPattern,
+      BiConsumer<StringBuilder, KeepQualifiedClassNamePattern> printClassName) {
     builder.append("class ");
-    printClassReference.accept(builder, classPattern.getClassReference());
-    KeepExtendsPattern extendsPattern = classPattern.getExtendsPattern();
+    printClassName.accept(builder, classPattern.getClassNamePattern());
+    KeepInstanceOfPattern extendsPattern = classPattern.getInstanceOfPattern();
     if (!extendsPattern.isAny()) {
       builder.append(" extends ");
       printClassName(
-          extendsPattern.asClassNamePattern(), RulePrinter.withoutBackReferences(builder));
+          extendsPattern.getClassNamePattern(), RulePrinter.withoutBackReferences(builder));
     }
     return builder;
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
deleted file mode 100644
index 1b8d1a6..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
+++ /dev/null
@@ -1,334 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.processor;
-
-import static org.objectweb.asm.Opcodes.ACC_FINAL;
-import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
-import static org.objectweb.asm.Opcodes.ACC_SUPER;
-
-import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
-import com.android.tools.r8.keepanno.ast.KeepCondition;
-import com.android.tools.r8.keepanno.ast.KeepConsequences;
-import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.ast.KeepEdge.Builder;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
-import com.android.tools.r8.keepanno.ast.KeepItemPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
-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 java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.function.Consumer;
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Filer;
-import javax.annotation.processing.RoundEnvironment;
-import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
-import javax.lang.model.SourceVersion;
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.AnnotationValue;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ExecutableElement;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.DeclaredType;
-import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.SimpleAnnotationValueVisitor7;
-import javax.lang.model.util.SimpleTypeVisitor7;
-import javax.tools.Diagnostic.Kind;
-import javax.tools.JavaFileObject;
-import org.objectweb.asm.ClassWriter;
-
-@SupportedAnnotationTypes("com.android.tools.r8.keepanno.annotations.*")
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
-public class KeepEdgeProcessor extends AbstractProcessor {
-
-  public static String getClassTypeNameForSynthesizedEdges(String classTypeName) {
-    return classTypeName + "$$KeepEdges";
-  }
-
-  @Override
-  @SuppressWarnings("DoNotClaimAnnotations")
-  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
-    Map<String, List<KeepEdge>> collectedEdges = new HashMap<>();
-    for (TypeElement annotation : annotations) {
-      for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
-        KeepEdge edge = processKeepEdge(element);
-        if (edge != null) {
-          TypeElement enclosingType = getEnclosingTypeElement(element);
-          String enclosingTypeName = enclosingType.getQualifiedName().toString();
-          collectedEdges.computeIfAbsent(enclosingTypeName, k -> new ArrayList<>()).add(edge);
-        }
-      }
-    }
-    for (Entry<String, List<KeepEdge>> entry : collectedEdges.entrySet()) {
-      String enclosingTypeName = entry.getKey();
-      String edgeTargetClass = getClassTypeNameForSynthesizedEdges(enclosingTypeName);
-      byte[] writtenEdge = writeEdges(entry.getValue(), edgeTargetClass);
-      Filer filer = processingEnv.getFiler();
-      try {
-        JavaFileObject classFile = filer.createClassFile(edgeTargetClass);
-        classFile.openOutputStream().write(writtenEdge);
-      } catch (IOException e) {
-        error(e.getMessage());
-      }
-    }
-    return true;
-  }
-
-  private static byte[] writeEdges(List<KeepEdge> edges, String classTypeName) {
-    String classBinaryName = AnnotationConstants.getBinaryNameFromClassTypeName(classTypeName);
-    ClassWriter classWriter = new ClassWriter(0);
-    classWriter.visit(
-        KeepEdgeReader.ASM_VERSION,
-        ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
-        classBinaryName,
-        null,
-        "java/lang/Object",
-        null);
-    classWriter.visitSource("SynthesizedKeepEdge", null);
-    for (KeepEdge edge : edges) {
-      KeepEdgeWriter.writeEdge(edge, classWriter);
-    }
-    classWriter.visitEnd();
-    return classWriter.toByteArray();
-  }
-
-  @SuppressWarnings("BadImport")
-  private KeepEdge processKeepEdge(Element element) {
-    AnnotationMirror mirror = getAnnotationMirror(element, AnnotationConstants.Edge.CLASS);
-    if (mirror == null) {
-      return null;
-    }
-    Builder edgeBuilder = KeepEdge.builder();
-    processPreconditions(edgeBuilder, mirror);
-    processConsequences(edgeBuilder, mirror);
-    return edgeBuilder.build();
-  }
-
-  @SuppressWarnings("BadImport")
-  private void processPreconditions(Builder edgeBuilder, AnnotationMirror mirror) {
-    AnnotationValue preconditions = getAnnotationValue(mirror, Edge.preconditions);
-    if (preconditions == null) {
-      return;
-    }
-    KeepPreconditions.Builder preconditionsBuilder = KeepPreconditions.builder();
-    new AnnotationListValueVisitor(
-            value -> {
-              KeepCondition.Builder conditionBuilder = KeepCondition.builder();
-              processCondition(conditionBuilder, AnnotationMirrorValueVisitor.getMirror(value));
-              preconditionsBuilder.addCondition(conditionBuilder.build());
-            })
-        .onValue(preconditions);
-    edgeBuilder.setPreconditions(preconditionsBuilder.build());
-  }
-
-  @SuppressWarnings("BadImport")
-  private void processConsequences(Builder edgeBuilder, AnnotationMirror mirror) {
-    AnnotationValue consequences = getAnnotationValue(mirror, Edge.consequences);
-    if (consequences == null) {
-      return;
-    }
-    KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
-    new AnnotationListValueVisitor(
-            value -> {
-              KeepTarget.Builder targetBuilder = KeepTarget.builder();
-              processTarget(targetBuilder, AnnotationMirrorValueVisitor.getMirror(value));
-              consequencesBuilder.addTarget(targetBuilder.build());
-            })
-        .onValue(consequences);
-    edgeBuilder.setConsequences(consequencesBuilder.build());
-  }
-
-  private String getTypeNameForClassConstantElement(DeclaredType type) {
-    // The processor API does not expose the descriptor or typename, so we need to depend on the
-    // sun.tools internals to extract it. If not, this code will not work for inner classes as
-    // we cannot recover the $ separator.
-    try {
-      Object tsym = type.getClass().getField("tsym").get(type);
-      Object flatname = tsym.getClass().getField("flatname").get(tsym);
-      return flatname.toString();
-    } catch (NoSuchFieldException | IllegalAccessException e) {
-      throw new KeepEdgeException("Unable to obtain the class type name for: " + type);
-    }
-  }
-
-  private void processCondition(KeepCondition.Builder builder, AnnotationMirror mirror) {
-    KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
-    processItem(itemBuilder, mirror);
-    builder.setItemPattern(itemBuilder.build());
-  }
-
-  private void processTarget(KeepTarget.Builder builder, AnnotationMirror mirror) {
-    KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
-    processItem(itemBuilder, mirror);
-    builder.setItemPattern(itemBuilder.build());
-  }
-
-  private void processItem(KeepItemPattern.Builder builder, AnnotationMirror mirror) {
-    AnnotationValue classConstantValue = getAnnotationValue(mirror, Item.classConstant);
-    if (classConstantValue != null) {
-      DeclaredType type = AnnotationClassValueVisitor.getType(classConstantValue);
-      String typeName = getTypeNameForClassConstantElement(type);
-      builder.setClassPattern(KeepQualifiedClassNamePattern.exact(typeName));
-    }
-    AnnotationValue methodNameValue = getAnnotationValue(mirror, Item.methodName);
-    AnnotationValue fieldNameValue = getAnnotationValue(mirror, Item.fieldName);
-    if (methodNameValue != null && fieldNameValue != null) {
-      throw new KeepEdgeException("Cannot define both a method and a field name pattern");
-    }
-    if (methodNameValue != null) {
-      String methodName = AnnotationStringValueVisitor.getString(methodNameValue);
-      builder.setMemberPattern(
-          KeepMethodPattern.builder()
-              .setNamePattern(KeepMethodNamePattern.exact(methodName))
-              .build());
-    } else if (fieldNameValue != null) {
-      String fieldName = AnnotationStringValueVisitor.getString(fieldNameValue);
-      builder.setMemberPattern(
-          KeepFieldPattern.builder().setNamePattern(KeepFieldNamePattern.exact(fieldName)).build());
-    }
-  }
-
-  private void error(String message) {
-    processingEnv.getMessager().printMessage(Kind.ERROR, message);
-  }
-
-  private static TypeElement getEnclosingTypeElement(Element element) {
-    while (true) {
-      if (element == null || element instanceof TypeElement) {
-        return (TypeElement) element;
-      }
-      element = element.getEnclosingElement();
-    }
-  }
-
-  private static AnnotationMirror getAnnotationMirror(Element element, Class<?> clazz) {
-    String clazzName = clazz.getName();
-    for (AnnotationMirror m : element.getAnnotationMirrors()) {
-      if (m.getAnnotationType().toString().equals(clazzName)) {
-        return m;
-      }
-    }
-    return null;
-  }
-
-  private static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String key) {
-    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
-        annotationMirror.getElementValues().entrySet()) {
-      if (entry.getKey().getSimpleName().toString().equals(key)) {
-        return entry.getValue();
-      }
-    }
-    return null;
-  }
-
-  /// Annotation Visitors
-
-  private abstract static class AnnotationValueVisitorBase<T>
-      extends SimpleAnnotationValueVisitor7<T, Object> {
-    @Override
-    protected T defaultAction(Object o1, Object o2) {
-      throw new IllegalStateException();
-    }
-
-    public T onValue(AnnotationValue value) {
-      return value.accept(this, null);
-    }
-  }
-
-  private static class AnnotationListValueVisitor
-      extends AnnotationValueVisitorBase<AnnotationListValueVisitor> {
-
-    private final Consumer<AnnotationValue> fn;
-
-    public AnnotationListValueVisitor(Consumer<AnnotationValue> fn) {
-      this.fn = fn;
-    }
-
-    @Override
-    public AnnotationListValueVisitor visitArray(
-        List<? extends AnnotationValue> values, Object ignore) {
-      values.forEach(fn);
-      return this;
-    }
-  }
-
-  private static class AnnotationMirrorValueVisitor
-      extends AnnotationValueVisitorBase<AnnotationMirrorValueVisitor> {
-
-    private AnnotationMirror mirror = null;
-
-    public static AnnotationMirror getMirror(AnnotationValue value) {
-      return new AnnotationMirrorValueVisitor().onValue(value).mirror;
-    }
-
-    @Override
-    public AnnotationMirrorValueVisitor visitAnnotation(AnnotationMirror mirror, Object o) {
-      this.mirror = mirror;
-      return this;
-    }
-  }
-
-  private static class AnnotationStringValueVisitor
-      extends AnnotationValueVisitorBase<AnnotationStringValueVisitor> {
-    private String string;
-
-    public static String getString(AnnotationValue value) {
-      return new AnnotationStringValueVisitor().onValue(value).string;
-    }
-
-    @Override
-    public AnnotationStringValueVisitor visitString(String string, Object ignore) {
-      this.string = string;
-      return this;
-    }
-  }
-
-  private static class AnnotationClassValueVisitor
-      extends AnnotationValueVisitorBase<AnnotationClassValueVisitor> {
-    private DeclaredType type = null;
-
-    public static DeclaredType getType(AnnotationValue value) {
-      return new AnnotationClassValueVisitor().onValue(value).type;
-    }
-
-    @Override
-    public AnnotationClassValueVisitor visitType(TypeMirror t, Object ignore) {
-      ClassTypeVisitor classTypeVisitor = new ClassTypeVisitor();
-      t.accept(classTypeVisitor, null);
-      type = classTypeVisitor.type;
-      return this;
-    }
-  }
-
-  private static class TypeVisitorBase<T> extends SimpleTypeVisitor7<T, Object> {
-    @Override
-    protected T defaultAction(TypeMirror typeMirror, Object ignore) {
-      throw new IllegalStateException();
-    }
-  }
-
-  private static class ClassTypeVisitor extends TypeVisitorBase<ClassTypeVisitor> {
-    private DeclaredType type = null;
-
-    @Override
-    public ClassTypeVisitor visitDeclared(DeclaredType t, Object ignore) {
-      this.type = t;
-      return this;
-    }
-  }
-}
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index df0df7b..3d23d9f 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.0.4",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.1.0",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
@@ -7,6 +7,12 @@
   "common_flags": [
     {
       "api_level_below_or_equal": 10000,
+      "amend_library_method": [
+        "public java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
+      ]
+    },
+    {
+      "api_level_below_or_equal": 10000,
       "api_level_greater_or_equal": 26,
       "rewrite_prefix": {
         "java.time.DesugarLocalDate": "j$.time.DesugarLocalDate",
@@ -57,6 +63,14 @@
       "rewrite_prefix": {
         "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit"
       },
+      "emulate_interface": {
+        "java.util.Collection": {
+          "rewrittenType": "j$.util.Collection",
+          "emulatedMethods": [
+            "java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
+          ]
+        }
+      },
       "retarget_method": {
         "java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit#of(java.time.temporal.ChronoUnit)": "java.util.concurrent.DesugarTimeUnit",
         "java.time.temporal.ChronoUnit java.util.concurrent.TimeUnit#toChronoUnit()": "java.util.concurrent.DesugarTimeUnit",
@@ -360,12 +374,6 @@
       ]
     },
     {
-      "api_level_below_or_equal": 33,
-      "amend_library_method": [
-        "public java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
-      ]
-    },
-    {
       "api_level_below_or_equal": 32,
       "api_level_greater_or_equal": 26,
       "covariant_retarget_method": {
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
index d69ed9a..7303240 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.0.4",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.1.0",
   "configuration_format_version": 101,
   "required_compilation_api_level": 24,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index 29e4e26..0f81af4 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.0.4",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.1.0",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
@@ -8,6 +8,7 @@
     {
       "api_level_below_or_equal": 10000,
       "amend_library_method": [
+        "public java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)",
         "public static java.nio.file.Path java.nio.file.Path#of(java.lang.String, java.lang.String[])",
         "public static java.nio.file.Path java.nio.file.Path#of(java.net.URI)"
       ]
@@ -76,6 +77,14 @@
         "java.io.DesugarInputStream": "j$.io.DesugarInputStream",
         "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit"
       },
+      "emulate_interface": {
+        "java.util.Collection": {
+          "rewrittenType": "j$.util.Collection",
+          "emulatedMethods": [
+            "java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
+          ]
+        }
+      },
       "retarget_method": {
         "java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit#of(java.time.temporal.ChronoUnit)": "java.util.concurrent.DesugarTimeUnit",
         "java.time.temporal.ChronoUnit java.util.concurrent.TimeUnit#toChronoUnit()": "java.util.concurrent.DesugarTimeUnit",
@@ -532,12 +541,6 @@
       }
     },
     {
-      "api_level_below_or_equal": 33,
-      "amend_library_method": [
-        "public java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
-      ]
-    },
-    {
       "api_level_below_or_equal": 32,
       "api_level_greater_or_equal": 26,
       "covariant_retarget_method": {
@@ -588,7 +591,9 @@
       "api_level_below_or_equal": 10000,
       "api_level_greater_or_equal": 26,
       "rewrite_prefix": {
-        "sun.nio.cs.UTF_8": "j$.sun.nio.cs.UTF_8"
+        "sun.nio.cs.UTF_8": "j$.sun.nio.cs.UTF_8",
+        "sun.nio.cs.Unicode": "j$.sun.nio.cs.Unicode",
+        "sun.nio.cs.HistoricallyNamedCharset": "j$.sun.nio.cs.HistoricallyNamedCharset"
       },
       "retarget_static_field": {
         "sun.nio.cs.UTF_8 sun.nio.cs.UTF_8#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#UTF_8"
diff --git a/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java b/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
index 3084ee0..186856b 100644
--- a/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
+++ b/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
@@ -9,23 +9,18 @@
 // This class is used by the Android Studio Gradle plugin and is thus part of the R8 API.
 @KeepForApi
 public class CompatProguardCommandBuilder extends R8Command.Builder {
+
   public CompatProguardCommandBuilder() {
     this(true);
   }
 
+  public CompatProguardCommandBuilder(boolean forceProguardCompatibility) {
+    setProguardCompatibility(forceProguardCompatibility);
+  }
+
   public CompatProguardCommandBuilder(
       boolean forceProguardCompatibility, DiagnosticsHandler diagnosticsHandler) {
     super(diagnosticsHandler);
     setProguardCompatibility(forceProguardCompatibility);
   }
-
-  public CompatProguardCommandBuilder(boolean forceProguardCompatibility) {
-    this(forceProguardCompatibility, false);
-  }
-
-  public CompatProguardCommandBuilder(
-      boolean forceProguardCompatibility, boolean disableVerticalClassMerging) {
-    setProguardCompatibility(forceProguardCompatibility);
-    setDisableVerticalClassMerging(disableVerticalClassMerging);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 211dfb5..2250d81 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -755,7 +755,7 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
-    assert !internal.enableVerticalClassMerging;
+    assert internal.getVerticalClassMergerOptions().isDisabled();
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 97e6cb2..08b1657 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -32,7 +32,7 @@
 public class FeatureSplit {
 
   public static final FeatureSplit BASE =
-      new FeatureSplit(null, null) {
+      new FeatureSplit(null, null, null, null) {
         @Override
         public boolean isBase() {
           return true;
@@ -40,7 +40,7 @@
       };
 
   public static final FeatureSplit BASE_STARTUP =
-      new FeatureSplit(null, null) {
+      new FeatureSplit(null, null, null, null) {
         @Override
         public boolean isBase() {
           return true;
@@ -52,13 +52,20 @@
         }
       };
 
-  private final ProgramConsumer programConsumer;
+  private ProgramConsumer programConsumer;
   private final List<ProgramResourceProvider> programResourceProviders;
+  private final AndroidResourceProvider androidResourceProvider;
+  private final AndroidResourceConsumer androidResourceConsumer;
 
   private FeatureSplit(
-      ProgramConsumer programConsumer, List<ProgramResourceProvider> programResourceProviders) {
+      ProgramConsumer programConsumer,
+      List<ProgramResourceProvider> programResourceProviders,
+      AndroidResourceProvider androidResourceProvider,
+      AndroidResourceConsumer androidResourceConsumer) {
     this.programConsumer = programConsumer;
     this.programResourceProviders = programResourceProviders;
+    this.androidResourceProvider = androidResourceProvider;
+    this.androidResourceConsumer = androidResourceConsumer;
   }
 
   public boolean isBase() {
@@ -69,6 +76,10 @@
     return false;
   }
 
+  void internalSetProgramConsumer(ProgramConsumer consumer) {
+    this.programConsumer = consumer;
+  }
+
   public List<ProgramResourceProvider> getProgramResourceProviders() {
     return programResourceProviders;
   }
@@ -81,6 +92,14 @@
     return new Builder(handler);
   }
 
+  public AndroidResourceProvider getAndroidResourceProvider() {
+    return androidResourceProvider;
+  }
+
+  public AndroidResourceConsumer getAndroidResourceConsumer() {
+    return androidResourceConsumer;
+  }
+
   /**
    * Builder for constructing a FeatureSplit.
    *
@@ -90,10 +109,13 @@
   public static class Builder {
     private ProgramConsumer programConsumer;
     private final List<ProgramResourceProvider> programResourceProviders = new ArrayList<>();
+    private AndroidResourceProvider androidResourceProvider;
+    private AndroidResourceConsumer androidResourceConsumer;
 
     @SuppressWarnings("UnusedVariable")
     private final DiagnosticsHandler handler;
 
+
     private Builder(DiagnosticsHandler handler) {
       this.handler = handler;
     }
@@ -121,9 +143,23 @@
       return this;
     }
 
+    public Builder setAndroidResourceProvider(AndroidResourceProvider androidResourceProvider) {
+      this.androidResourceProvider = androidResourceProvider;
+      return this;
+    }
+
+    public Builder setAndroidResourceConsumer(AndroidResourceConsumer androidResourceConsumer) {
+      this.androidResourceConsumer = androidResourceConsumer;
+      return this;
+    }
+
     /** Build and return the {@link FeatureSplit} */
     public FeatureSplit build() {
-      return new FeatureSplit(programConsumer, programResourceProviders);
+      return new FeatureSplit(
+          programConsumer,
+          programResourceProviders,
+          androidResourceProvider,
+          androidResourceConsumer);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 664eae4..726013e 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.dump.DumpOptions;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.Inspector;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
@@ -22,7 +21,6 @@
 import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
-import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Reporter;
@@ -197,16 +195,12 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
-    assert !internal.enableVerticalClassMerging;
+    assert internal.getVerticalClassMergerOptions().isDisabled();
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
 
-    HorizontalClassMergerOptions horizontalClassMergerOptions =
-        internal.horizontalClassMergerOptions();
-    horizontalClassMergerOptions.disable();
-    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.INITIAL);
-    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.FINAL);
+    internal.horizontalClassMergerOptions().disable();
 
     assert internal.desugarState == DesugarState.ON;
     assert internal.enableInheritanceClassInDexDistributor;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index d0264fc..6e8edf7 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -34,7 +34,6 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.graph.lens.AppliedGraphLens;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
@@ -69,7 +68,7 @@
 import com.android.tools.r8.naming.RecordInvokeDynamicInvokeCustomRewriter;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
-import com.android.tools.r8.optimize.LegacyAccessModifier;
+import com.android.tools.r8.optimize.BridgeHoistingToSharedSyntheticSuperClass;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
@@ -105,8 +104,6 @@
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.shaking.TreePrunerConfiguration;
-import com.android.tools.r8.shaking.VerticalClassMerger;
-import com.android.tools.r8.shaking.VerticalClassMergerGraphLens;
 import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
 import com.android.tools.r8.synthesis.SyntheticFinalization;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -114,13 +111,13 @@
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.SelfRetraceTest;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.verticalclassmerging.VerticalClassMerger;
 import com.google.common.collect.Iterables;
 import com.google.common.io.ByteStreams;
 import java.io.ByteArrayOutputStream;
@@ -131,7 +128,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Supplier;
@@ -263,7 +262,7 @@
     if (options.quiet) {
       System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
     }
-    if (this.getClass().desiredAssertionStatus()) {
+    if (this.getClass().desiredAssertionStatus() && !options.quiet) {
       options.reporter.info(
           new StringDiagnostic(
               "Running R8 version " + Version.LABEL + " with assertions enabled."));
@@ -347,7 +346,7 @@
       timing.end();
       timing.begin("Strip unused code");
       timing.begin("Before enqueuer");
-      RuntimeTypeCheckInfo.Builder classMergingEnqueuerExtensionBuilder =
+      RuntimeTypeCheckInfo.Builder initialRuntimeTypeCheckInfoBuilder =
           new RuntimeTypeCheckInfo.Builder(appView);
       List<ProguardConfigurationRule> synthesizedProguardRules;
       try {
@@ -391,7 +390,7 @@
                 appView,
                 profileCollectionAdditions,
                 subtypingInfo,
-                classMergingEnqueuerExtensionBuilder);
+                initialRuntimeTypeCheckInfoBuilder);
         timing.end();
         timing.begin("After enqueuer");
         assert appView.rootSet().verifyKeptFieldsAreAccessedAndLive(appViewWithLiveness);
@@ -480,9 +479,6 @@
       // to clear the cache, so that we will recompute the type lattice elements.
       appView.dexItemFactory().clearTypeElementsCache();
 
-      // TODO(b/132677331): Remove legacy access modifier.
-      LegacyAccessModifier.run(appViewWithLiveness, executorService, timing);
-
       // This pass attempts to reduce the number of nests and nest size to allow further passes, and
       // should therefore be run after the publicizer.
       new NestReducer(appViewWithLiveness).run(executorService, timing);
@@ -498,43 +494,16 @@
           .setMustRetargetInvokesToTargetMethod()
           .run(executorService, timing);
 
-      boolean isKotlinLibraryCompilationWithInlinePassThrough =
-          options.enableCfByteCodePassThrough && appView.hasCfByteCodePassThroughMethods();
-
-      RuntimeTypeCheckInfo runtimeTypeCheckInfo =
-          classMergingEnqueuerExtensionBuilder.build(appView.graphLens());
-      classMergingEnqueuerExtensionBuilder = null;
+      BridgeHoistingToSharedSyntheticSuperClass.run(appViewWithLiveness, executorService, timing);
 
       assert ArtProfileCompletenessChecker.verify(appView);
 
-      if (!isKotlinLibraryCompilationWithInlinePassThrough
-          && options.getProguardConfiguration().isOptimizing()) {
-        if (options.enableVerticalClassMerging) {
-          timing.begin("VerticalClassMerger");
-          VerticalClassMergerGraphLens lens =
-              new VerticalClassMerger(
-                      getDirectApp(appViewWithLiveness),
-                      appViewWithLiveness,
-                      executorService,
-                      timing)
-                  .run();
-          if (lens != null) {
-            runtimeTypeCheckInfo = runtimeTypeCheckInfo.rewriteWithLens(lens);
-          }
-          timing.end();
-        } else {
-          appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
-        }
-        assert appView.verticallyMergedClasses() != null;
-
-        assert ArtProfileCompletenessChecker.verify(appView);
-
-        HorizontalClassMerger.createForInitialClassMerging(appViewWithLiveness)
-            .runIfNecessary(executorService, timing, runtimeTypeCheckInfo);
-      }
-      appViewWithLiveness
-          .appInfo()
-          .notifyHorizontalClassMergerFinished(HorizontalClassMerger.Mode.INITIAL);
+      VerticalClassMerger.runIfNecessary(appViewWithLiveness, executorService, timing);
+      HorizontalClassMerger.createForInitialClassMerging(appViewWithLiveness)
+          .runIfNecessary(
+              executorService,
+              timing,
+              initialRuntimeTypeCheckInfoBuilder.build(appView.graphLens()));
 
       // TODO(b/225838009): Horizontal merging currently assumes pre-phase CF conversion.
       appView.testing().enterLirSupportedPhase(appView, executorService);
@@ -571,10 +540,12 @@
       // At this point all code has been mapped according to the graph lens. We cannot remove the
       // graph lens entirely, though, since it is needed for mapping all field and method signatures
       // back to the original program.
-      timing.begin("AppliedGraphLens construction");
-      appView.setGraphLens(new AppliedGraphLens(appView));
+      timing.time(
+          "AppliedGraphLens construction",
+          () -> appView.setGraphLens(new AppliedGraphLens(appView)));
       timing.end();
-      timing.end();
+
+      RuntimeTypeCheckInfo.Builder finalRuntimeTypeCheckInfoBuilder = null;
       if (options.shouldRerunEnqueuer()) {
         timing.begin("Post optimization code stripping");
         try {
@@ -596,8 +567,8 @@
                   keptGraphConsumer,
                   prunedTypes);
           if (options.isClassMergingExtensionRequired(enqueuer.getMode())) {
-            classMergingEnqueuerExtensionBuilder = new RuntimeTypeCheckInfo.Builder(appView);
-            classMergingEnqueuerExtensionBuilder.attach(enqueuer);
+            finalRuntimeTypeCheckInfoBuilder = new RuntimeTypeCheckInfo.Builder(appView);
+            finalRuntimeTypeCheckInfoBuilder.attach(enqueuer);
           }
           EnqueuerResult enqueuerResult =
               enqueuer.traceApplication(appView.rootSet(), executorService, timing);
@@ -753,10 +724,10 @@
       timing.end();
 
       // Perform repackaging.
-      if (options.isRepackagingEnabled()) {
-        new Repackaging(appView.withLiveness()).run(executorService, timing);
-      }
       if (appView.hasLiveness()) {
+        if (options.isRepackagingEnabled()) {
+          new Repackaging(appView.withLiveness()).run(executorService, timing);
+        }
         assert Repackaging.verifyIdentityRepackaging(appView.withLiveness(), executorService);
       }
 
@@ -773,10 +744,9 @@
           .runIfNecessary(
               executorService,
               timing,
-              classMergingEnqueuerExtensionBuilder != null
-                  ? classMergingEnqueuerExtensionBuilder.build(appView.graphLens())
+              finalRuntimeTypeCheckInfoBuilder != null
+                  ? finalRuntimeTypeCheckInfoBuilder.build(appView.graphLens())
                   : null);
-      appView.appInfo().notifyHorizontalClassMergerFinished(HorizontalClassMerger.Mode.FINAL);
 
       // Perform minification.
       if (options.getProguardConfiguration().hasApplyMappingFile()) {
@@ -788,7 +758,7 @@
         appView.clearApplyMappingSeedMapper();
       } else if (options.isMinifying()) {
         timing.begin("Minification");
-        appView.setNamingLens(new Minifier(appView.withLiveness()).run(executorService, timing));
+        new Minifier(appView.withLiveness()).run(executorService, timing);
         timing.end();
       } else {
         timing.begin("MinifyIdentifiers");
@@ -854,20 +824,24 @@
 
       new DesugaredLibraryKeepRuleGenerator(appView).runIfNecessary(timing);
 
-      List<Pair<Integer, byte[]>> dexFileContent = new ArrayList<>();
-      if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
+      Map<String, byte[]> dexFileContent = new ConcurrentHashMap<>();
+      if (options.androidResourceProvider != null
+          && options.androidResourceConsumer != null
+          // We trace the dex directly in the enqueuer.
+          && !options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
         options.programConsumer =
-            new ForwardingConsumer((DexIndexedConsumer) options.programConsumer) {
-              @Override
-              public void accept(
-                  int fileIndex,
-                  ByteDataView data,
-                  Set<String> descriptors,
-                  DiagnosticsHandler handler) {
-                dexFileContent.add(new Pair<>(fileIndex, data.copyByteData()));
-                super.accept(fileIndex, data, descriptors, handler);
-              }
-            };
+            wrapConsumerStoreBytesInList(
+                dexFileContent, (DexIndexedConsumer) options.programConsumer, "base");
+        if (options.featureSplitConfiguration != null) {
+          int featureIndex = 0;
+          for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+            featureSplit.internalSetProgramConsumer(
+                wrapConsumerStoreBytesInList(
+                    dexFileContent,
+                    (DexIndexedConsumer) featureSplit.getProgramConsumer(),
+                    "feature" + featureIndex));
+          }
+        }
       }
 
       assert appView.verifyMovedMethodsHaveOriginalMethodPosition();
@@ -877,7 +851,7 @@
       writeApplication(appView, inputApp, executorService);
 
       if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
-        shrinkResources(dexFileContent);
+        shrinkResources(dexFileContent, appView);
       }
       assert appView.getDontWarnConfiguration().validate(options);
 
@@ -894,70 +868,142 @@
     }
   }
 
-  private void shrinkResources(List<Pair<Integer, byte[]>> dexFileContent) {
+  private static ForwardingConsumer wrapConsumerStoreBytesInList(
+      Map<String, byte[]> dexFileContent,
+      DexIndexedConsumer programConsumer,
+      String classesPrefix) {
+
+    return new ForwardingConsumer(programConsumer) {
+      @Override
+      public void accept(
+          int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+        dexFileContent.put(classesPrefix + "_classes" + fileIndex + ".dex", data.copyByteData());
+        super.accept(fileIndex, data, descriptors, handler);
+      }
+    };
+  }
+
+  private void shrinkResources(
+      Map<String, byte[]> dexFileContent, AppView<AppInfoWithClassHierarchy> appView) {
     LegacyResourceShrinker.Builder resourceShrinkerBuilder = LegacyResourceShrinker.builder();
     Reporter reporter = options.reporter;
-    dexFileContent.forEach(p -> resourceShrinkerBuilder.addDexInput(p.getFirst(), p.getSecond()));
+    dexFileContent.forEach(resourceShrinkerBuilder::addDexInput);
     try {
-      Collection<AndroidResourceInput> androidResources =
-          options.androidResourceProvider.getAndroidResources();
-      for (AndroidResourceInput androidResource : androidResources) {
-        try {
-          byte[] bytes = androidResource.getByteStream().readAllBytes();
-          Path path = Paths.get(androidResource.getPath().location());
-          switch (androidResource.getKind()) {
-            case MANIFEST:
-              resourceShrinkerBuilder.setManifest(path, bytes);
-              break;
-            case RES_FOLDER_FILE:
-              resourceShrinkerBuilder.addResFolderInput(path, bytes);
-              break;
-            case RESOURCE_TABLE:
-              resourceShrinkerBuilder.setResourceTable(path, bytes);
-              break;
-            case XML_FILE:
-              resourceShrinkerBuilder.addXmlInput(path, bytes);
-              break;
-            case UNKNOWN:
-              break;
+      addResourcesToBuilder(
+          resourceShrinkerBuilder, reporter, options.androidResourceProvider, null);
+      if (options.featureSplitConfiguration != null) {
+        for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+          if (featureSplit.getAndroidResourceProvider() != null) {
+            addResourcesToBuilder(
+                resourceShrinkerBuilder,
+                reporter,
+                featureSplit.getAndroidResourceProvider(),
+                featureSplit);
           }
-        } catch (IOException e) {
-          reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
         }
       }
 
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
-      ShrinkerResult shrinkerResult = shrinker.run();
-      AndroidResourceConsumer androidResourceConsumer = options.androidResourceConsumer;
+      ShrinkerResult shrinkerResult;
+      if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
+        shrinkerResult =
+            shrinker.shrinkModel(appView.getResourceShrinkerState().getR8ResourceShrinkerModel());
+      } else {
+        shrinkerResult = shrinker.run();
+      }
       Set<String> toKeep = shrinkerResult.getResFolderEntriesToKeep();
-      for (AndroidResourceInput androidResource : androidResources) {
-        switch (androidResource.getKind()) {
-          case MANIFEST:
-          case UNKNOWN:
-            androidResourceConsumer.accept(
-                new R8PassThroughAndroidResource(androidResource, reporter), reporter);
-            break;
-          case RESOURCE_TABLE:
-            androidResourceConsumer.accept(
-                new R8AndroidResourceWithData(
-                    androidResource, reporter, shrinkerResult.getResourceTableInProtoFormat()),
-                reporter);
-            break;
-          case RES_FOLDER_FILE:
-          case XML_FILE:
-            if (toKeep.contains(androidResource.getPath().location())) {
-              androidResourceConsumer.accept(
-                  new R8PassThroughAndroidResource(androidResource, reporter), reporter);
-            }
-            break;
+      writeResourcesToConsumer(
+          reporter,
+          shrinkerResult,
+          toKeep,
+          options.androidResourceProvider,
+          options.androidResourceConsumer,
+          null);
+      if (options.featureSplitConfiguration != null) {
+        for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+          if (featureSplit.getAndroidResourceProvider() != null) {
+            writeResourcesToConsumer(
+                reporter,
+                shrinkerResult,
+                toKeep,
+                featureSplit.getAndroidResourceProvider(),
+                featureSplit.getAndroidResourceConsumer(),
+                featureSplit);
+          }
         }
       }
-      androidResourceConsumer.finished(reporter);
     } catch (ParserConfigurationException | SAXException | ResourceException | IOException e) {
       reporter.error(new ExceptionDiagnostic(e));
     }
   }
 
+  private static void writeResourcesToConsumer(
+      Reporter reporter,
+      ShrinkerResult shrinkerResult,
+      Set<String> toKeep,
+      AndroidResourceProvider androidResourceProvider,
+      AndroidResourceConsumer androidResourceConsumer,
+      FeatureSplit featureSplit)
+      throws ResourceException {
+    for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+      switch (androidResource.getKind()) {
+        case MANIFEST:
+        case UNKNOWN:
+          androidResourceConsumer.accept(
+              new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+          break;
+        case RESOURCE_TABLE:
+          androidResourceConsumer.accept(
+              new R8AndroidResourceWithData(
+                  androidResource,
+                  reporter,
+                  shrinkerResult.getResourceTableInProtoFormat(featureSplit)),
+              reporter);
+          break;
+        case RES_FOLDER_FILE:
+        case XML_FILE:
+          if (toKeep.contains(androidResource.getPath().location())) {
+            androidResourceConsumer.accept(
+                new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+          }
+          break;
+      }
+    }
+    androidResourceConsumer.finished(reporter);
+  }
+
+  private static void addResourcesToBuilder(
+      LegacyResourceShrinker.Builder resourceShrinkerBuilder,
+      Reporter reporter,
+      AndroidResourceProvider androidResourceProvider,
+      FeatureSplit featureSplit)
+      throws ResourceException {
+    for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+      try {
+        byte[] bytes = androidResource.getByteStream().readAllBytes();
+        Path path = Paths.get(androidResource.getPath().location());
+        switch (androidResource.getKind()) {
+          case MANIFEST:
+            resourceShrinkerBuilder.addManifest(path, bytes);
+            break;
+          case RES_FOLDER_FILE:
+            resourceShrinkerBuilder.addResFolderInput(path, bytes);
+            break;
+          case RESOURCE_TABLE:
+            resourceShrinkerBuilder.addResourceTable(path, bytes, featureSplit);
+            break;
+          case XML_FILE:
+            resourceShrinkerBuilder.addXmlInput(path, bytes);
+            break;
+          case UNKNOWN:
+            break;
+        }
+      } catch (IOException e) {
+        reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+      }
+    }
+  }
+
   private static boolean allReferencesAssignedApiLevel(
       AppView<? extends AppInfoWithClassHierarchy> appView) {
     if (!appView.options().apiModelingOptions().isCheckAllApiReferencesAreSet()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index b4ac86e..09a8ceb 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -123,7 +123,6 @@
     private final List<ProguardConfigurationSource> proguardConfigs = new ArrayList<>();
     private boolean disableTreeShaking = false;
     private boolean disableMinification = false;
-    private boolean disableVerticalClassMerging = false;
     private boolean forceProguardCompatibility = false;
     private Optional<Boolean> includeDataResources = Optional.empty();
     private StringConsumer proguardUsageConsumer = null;
@@ -140,6 +139,8 @@
     private SemanticVersion fakeCompilerVersion = null;
     private AndroidResourceProvider androidResourceProvider = null;
     private AndroidResourceConsumer androidResourceConsumer = null;
+    private ResourceShrinkerConfiguration resourceShrinkerConfiguration =
+        ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
 
     private final ProguardConfigurationParserOptions.Builder parserOptionsBuilder =
         ProguardConfigurationParserOptions.builder().readEnvironment();
@@ -168,10 +169,6 @@
 
     // Internal
 
-    void setDisableVerticalClassMerging(boolean disableVerticalClassMerging) {
-      this.disableVerticalClassMerging = disableVerticalClassMerging;
-    }
-
     @Override
     Builder self() {
       return this;
@@ -539,6 +536,19 @@
       return this;
     }
 
+    /**
+     * API for configuring resource shrinking.
+     *
+     * <p>Set the configuration properties on the provided builder.
+     */
+    public Builder setResourceShrinkerConfiguration(
+        Function<ResourceShrinkerConfiguration.Builder, ResourceShrinkerConfiguration>
+            configurationBuilder) {
+      this.resourceShrinkerConfiguration =
+          configurationBuilder.apply(ResourceShrinkerConfiguration.builder(getReporter()));
+      return this;
+    }
+
     @Override
     void validate() {
       if (isPrintHelp()) {
@@ -566,8 +576,8 @@
         }
       }
       for (FeatureSplit featureSplit : featureSplits) {
-        assert featureSplit.getProgramConsumer() instanceof DexIndexedConsumer;
-        if (!(getProgramConsumer() instanceof DexIndexedConsumer)) {
+        verifyResourceSplitOrProgramSplit(featureSplit);
+        if (getProgramConsumer() != null && !(getProgramConsumer() instanceof DexIndexedConsumer)) {
           reporter.error("R8 does not support class file output when using feature splits");
         }
       }
@@ -587,6 +597,11 @@
       super.validate();
     }
 
+    private static void verifyResourceSplitOrProgramSplit(FeatureSplit featureSplit) {
+      assert featureSplit.getProgramConsumer() instanceof DexIndexedConsumer
+          || featureSplit.getAndroidResourceProvider() != null;
+    }
+
     @Override
     R8Command makeCommand() {
       // If printing versions ignore everything else.
@@ -663,7 +678,6 @@
               desugaring,
               configuration.isShrinking(),
               configuration.isObfuscating(),
-              disableVerticalClassMerging,
               forceProguardCompatibility,
               includeDataResources,
               proguardMapConsumer,
@@ -694,7 +708,8 @@
               getClassConflictResolver(),
               getCancelCompilationChecker(),
               androidResourceProvider,
-              androidResourceConsumer);
+              androidResourceConsumer,
+              resourceShrinkerConfiguration);
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -867,7 +882,6 @@
   private final ProguardConfiguration proguardConfiguration;
   private final boolean enableTreeShaking;
   private final boolean enableMinification;
-  private final boolean disableVerticalClassMerging;
   private final boolean forceProguardCompatibility;
   private final Optional<Boolean> includeDataResources;
   private final StringConsumer proguardMapConsumer;
@@ -885,6 +899,7 @@
   private final boolean enableMissingLibraryApiModeling;
   private final AndroidResourceProvider androidResourceProvider;
   private final AndroidResourceConsumer androidResourceConsumer;
+  private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -950,7 +965,6 @@
       DesugarState enableDesugaring,
       boolean enableTreeShaking,
       boolean enableMinification,
-      boolean disableVerticalClassMerging,
       boolean forceProguardCompatibility,
       Optional<Boolean> includeDataResources,
       StringConsumer proguardMapConsumer,
@@ -981,7 +995,8 @@
       ClassConflictResolver classConflictResolver,
       CancelCompilationChecker cancelCompilationChecker,
       AndroidResourceProvider androidResourceProvider,
-      AndroidResourceConsumer androidResourceConsumer) {
+      AndroidResourceConsumer androidResourceConsumer,
+      ResourceShrinkerConfiguration resourceShrinkerConfiguration) {
     super(
         inputApp,
         mode,
@@ -1010,7 +1025,6 @@
     this.proguardConfiguration = proguardConfiguration;
     this.enableTreeShaking = enableTreeShaking;
     this.enableMinification = enableMinification;
-    this.disableVerticalClassMerging = disableVerticalClassMerging;
     this.forceProguardCompatibility = forceProguardCompatibility;
     this.includeDataResources = includeDataResources;
     this.proguardMapConsumer = proguardMapConsumer;
@@ -1028,6 +1042,7 @@
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
     this.androidResourceProvider = androidResourceProvider;
     this.androidResourceConsumer = androidResourceConsumer;
+    this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -1036,7 +1051,6 @@
     proguardConfiguration = null;
     enableTreeShaking = false;
     enableMinification = false;
-    disableVerticalClassMerging = false;
     forceProguardCompatibility = false;
     includeDataResources = null;
     proguardMapConsumer = null;
@@ -1054,6 +1068,7 @@
     enableMissingLibraryApiModeling = false;
     androidResourceProvider = null;
     androidResourceConsumer = null;
+    resourceShrinkerConfiguration = null;
   }
 
   public DexItemFactory getDexItemFactory() {
@@ -1108,13 +1123,11 @@
     assert internal.isOptimizing() || horizontalClassMergerOptions.isRestrictedToSynthetics();
 
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
-    assert internal.enableVerticalClassMerging || !internal.isOptimizing();
 
     if (!internal.isShrinking()) {
       // If R8 is not shrinking, there is no point in running various optimizations since the
       // optimized classes will still remain in the program (the application size could increase).
       internal.enableEnumUnboxing = false;
-      internal.enableVerticalClassMerging = false;
     }
 
     // Amend the proguard-map consumer with options from the proguard configuration.
@@ -1193,9 +1206,6 @@
     // EXPERIMENTAL flags.
     assert !internal.forceProguardCompatibility;
     internal.forceProguardCompatibility = forceProguardCompatibility;
-    if (disableVerticalClassMerging) {
-      internal.enableVerticalClassMerging = false;
-    }
 
     internal.enableInheritanceClassInDexDistributor = isOptimizeMultidexForLinearAlloc();
 
@@ -1230,6 +1240,7 @@
 
     internal.androidResourceProvider = androidResourceProvider;
     internal.androidResourceConsumer = androidResourceConsumer;
+    internal.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
 
     if (!DETERMINISTIC_DEBUGGING) {
       assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
new file mode 100644
index 0000000..1a5b781
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
@@ -0,0 +1,103 @@
+// 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;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+/**
+ * Resource shrinker configuration. Allows building an immutable structure of resource shrinker
+ * settings.
+ *
+ * <p>A {@link ResourceShrinkerConfiguration} can be added to a {@link R8Command} change the way
+ * resource shrinking is performed.
+ *
+ * <p>To build a {@link ResourceShrinkerConfiguration} use the {@link
+ * ResourceShrinkerConfiguration.Builder} class, available through the {@link R8Command.Builder}.
+ * For example:
+ *
+ * <pre>
+ *   R8Command command = R8Command.builder()
+ *     .addProgramFiles(path1, path2)
+ *     .setMode(CompilationMode.RELEASE)
+ *     .setProgramConsumer(programConsumer)
+ *     .setResourceShrinkerConfiguration(builder -> builder
+ *         .enableOptimizedShrinkingWithR8()
+ *         .build())
+ *     .build();
+ * </pre>
+ */
+@KeepForApi
+public class ResourceShrinkerConfiguration {
+  public static ResourceShrinkerConfiguration DEFAULT_CONFIGURATION =
+      new ResourceShrinkerConfiguration(false, true);
+
+  private final boolean optimizedShrinking;
+  private final boolean preciseShrinking;
+
+  private ResourceShrinkerConfiguration(boolean optimizedShrinking, boolean preciseShrinking) {
+    this.optimizedShrinking = optimizedShrinking;
+    this.preciseShrinking = preciseShrinking;
+  }
+
+  public static Builder builder(DiagnosticsHandler handler) {
+    return new Builder();
+  }
+
+  public boolean isOptimizedShrinking() {
+    return optimizedShrinking;
+  }
+
+  public boolean isPreciseShrinking() {
+    return preciseShrinking;
+  }
+
+  /**
+   * Builder for constructing a ResourceShrinkerConfiguration.
+   *
+   * <p>A builder is obtained by calling setResourceShrinkerConfiguration on a {@link
+   * R8Command.Builder}.
+   */
+  @KeepForApi
+  public static class Builder {
+
+    private boolean optimizedShrinking = false;
+    private boolean preciseShrinking = true;
+
+    private Builder() {}
+
+    /**
+     * Enable R8 based resource shrinking.
+     *
+     * <p>If this is not set, r8 will use resource shrinking legacy mode where resource shrinking is
+     * done after code has been generated. This is consistent with a setup where resource shrinking
+     * is run seperately from R8.
+     *
+     * <p>Setting this option allows R8 to shrink resources as part of its normal compilation,
+     * tracing resources throughout the pipeline.
+     */
+    public Builder enableOptimizedShrinkingWithR8() {
+      assert preciseShrinking;
+      this.optimizedShrinking = true;
+      return this;
+    }
+
+    /**
+     * Disable precise shrinking.
+     *
+     * <p>The resource table will not be rewritten. Unused entries in the res folder will be
+     * replaced by small dummy files.
+     */
+    @Deprecated
+    public Builder disablePreciseShrinking() {
+      assert !optimizedShrinking;
+      this.preciseShrinking = false;
+      return this;
+    }
+
+    /** Build and return the {@link ResourceShrinkerConfiguration} */
+    public ResourceShrinkerConfiguration build() {
+      return new ResourceShrinkerConfiguration(optimizedShrinking, preciseShrinking);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java b/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
index e49137e..5a2fa56 100644
--- a/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
+++ b/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
@@ -52,9 +52,6 @@
     public final List<String> proguardConfig;
     public boolean printHelpAndExit;
 
-    // Flags to disable experimental features.
-    public boolean disableVerticalClassMerging;
-
     CompatProguardOptions(
         List<String> proguardConfig,
         String output,
@@ -67,8 +64,7 @@
         MapIdProvider mapIdProvider,
         SourceFileProvider sourceFileProvider,
         String depsFileOutput,
-        boolean printHelpAndExit,
-        boolean disableVerticalClassMerging) {
+        boolean printHelpAndExit) {
       this.output = output;
       this.mode = mode;
       this.minApi = minApi;
@@ -81,7 +77,6 @@
       this.sourceFileProvider = sourceFileProvider;
       this.depsFileOutput = depsFileOutput;
       this.printHelpAndExit = printHelpAndExit;
-      this.disableVerticalClassMerging = disableVerticalClassMerging;
     }
 
     public static CompatProguardOptions parse(String[] args) {
@@ -98,8 +93,6 @@
       MapIdProvider mapIdProvider = null;
       SourceFileProvider sourceFileProvider = null;
       String depsFileOutput = null;
-      // Flags to disable experimental features.
-      boolean disableVerticalClassMerging = false;
 
       ImmutableList.Builder<String> builder = ImmutableList.builder();
       if (args.length > 0) {
@@ -139,8 +132,6 @@
               sourceFileProvider = SourceFileTemplateProvider.create(args[++i], handler);
             } else if (arg.equals("--deps-file")) {
               depsFileOutput = args[++i];
-            } else if (arg.equals("--no-vertical-class-merging")) {
-              disableVerticalClassMerging = true;
             } else if (arg.equals("--core-library")
                 || arg.equals("--minimal-main-dex")
                 || arg.equals("--no-locals")) {
@@ -177,8 +168,7 @@
           mapIdProvider,
           sourceFileProvider,
           depsFileOutput,
-          printHelpAndExit,
-          disableVerticalClassMerging);
+          printHelpAndExit);
     }
 
     public static void print() {
@@ -218,8 +208,7 @@
       return;
     }
     CompatProguardCommandBuilder builder =
-        new CompatProguardCommandBuilder(
-            options.forceProguardCompatibility, options.disableVerticalClassMerging);
+        new CompatProguardCommandBuilder(options.forceProguardCompatibility);
     builder
         .setOutput(Paths.get(options.output), OutputMode.DexIndexed, options.includeDataResources)
         .addProguardConfiguration(options.proguardConfig, CommandLineOrigin.INSTANCE)
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 8232b0c..47f7ed6 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -169,7 +169,7 @@
     Diagnostic message = new StringDiagnostic("Dumped compilation inputs to: " + dumpOutput);
     if (dumpInputFlags.shouldFailCompilation()) {
       throw options.reporter.fatalError(message);
-    } else {
+    } else if (dumpInputFlags.shouldLogDumpInfoMessage()) {
       options.reporter.info(message);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index e6c4db1..105ea1c 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -836,7 +836,7 @@
               options.itemFactory));
     }
 
-    if (options.emitNestAnnotationsInDex) {
+    if (options.canUseNestBasedAccess()) {
       if (clazz.isNestHost()) {
         annotations.add(
             DexAnnotation.createNestMembersAnnotation(
diff --git a/src/main/java/com/android/tools/r8/dump/CompilerDump.java b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
index 7655a6e..7febf31 100644
--- a/src/main/java/com/android/tools/r8/dump/CompilerDump.java
+++ b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
@@ -45,6 +45,10 @@
     return directory.resolve("proguard.config");
   }
 
+  public boolean hasDesugaredLibrary() {
+    return Files.exists(directory.resolve("desugared-library.json"));
+  }
+
   public Path getDesugaredLibraryFile() {
     return directory.resolve("desugared-library.json");
   }
diff --git a/src/main/java/com/android/tools/r8/errors/DesugarDiagnostic.java b/src/main/java/com/android/tools/r8/errors/DesugarDiagnostic.java
index b541ff1..01dc9d4 100644
--- a/src/main/java/com/android/tools/r8/errors/DesugarDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/errors/DesugarDiagnostic.java
@@ -5,7 +5,8 @@
 
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 
 /** Common interface type for all diagnostics related to desugaring. */
-@KeepForApi
+@KeepForApi(kind = KeepItemKind.ONLY_CLASS)
 public interface DesugarDiagnostic extends Diagnostic {}
diff --git a/src/main/java/com/android/tools/r8/errors/InterfaceDesugarDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InterfaceDesugarDiagnostic.java
index 6156e58..81b9215 100644
--- a/src/main/java/com/android/tools/r8/errors/InterfaceDesugarDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/errors/InterfaceDesugarDiagnostic.java
@@ -4,7 +4,8 @@
 package com.android.tools.r8.errors;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 
 /** Common interface type for all diagnostics related to interface-method desugaring. */
-@KeepForApi
+@KeepForApi(kind = KeepItemKind.ONLY_CLASS)
 public interface InterfaceDesugarDiagnostic extends DesugarDiagnostic {}
diff --git a/src/main/java/com/android/tools/r8/errors/ProguardKeepRuleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ProguardKeepRuleDiagnostic.java
index 95cf81e..d454b19 100644
--- a/src/main/java/com/android/tools/r8/errors/ProguardKeepRuleDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/errors/ProguardKeepRuleDiagnostic.java
@@ -5,7 +5,8 @@
 
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 
 /** Base interface for diagnostics related to proguard keep rules. */
-@KeepForApi
+@KeepForApi(kind = KeepItemKind.ONLY_CLASS)
 public interface ProguardKeepRuleDiagnostic extends Diagnostic {}
diff --git a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
index b252ad4..ead08b8 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
@@ -71,8 +71,7 @@
       OptionalBool callerIsStartupMethod = isStartupMethod(caller, startupProfile);
       if (callerIsStartupMethod.isTrue()) {
         // If the caller is a startup method, then only allow inlining if the callee is also a
-        // startup
-        // method.
+        // startup method.
         if (isStartupMethod(callee, startupProfile).isFalse()) {
           return false;
         }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 07512e5..dc2094f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.origin.GlobalSyntheticOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -293,4 +294,9 @@
         ? FieldResolutionResult.createSingleFieldResolutionResult(clazz, clazz, definition)
         : FieldResolutionResult.unknown();
   }
+
+  public void notifyHorizontalClassMergerFinished(
+      HorizontalClassMerger.Mode horizontalClassMergerMode) {
+    // Intentionally empty.
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index d121559..1b9b0b0 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -8,7 +8,6 @@
 import static com.android.tools.r8.utils.TraversalContinuation.doContinue;
 
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
 import com.android.tools.r8.ir.analysis.type.InterfaceCollection.Builder;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
@@ -95,11 +94,6 @@
         commit, getClassToFeatureSplitMap(), getMainDexInfo(), getMissingClasses());
   }
 
-  public void notifyHorizontalClassMergerFinished(
-      HorizontalClassMerger.Mode horizontalClassMergerMode) {
-    // Intentionally empty.
-  }
-
   public void notifyMinifierFinished() {
     // Intentionally empty.
   }
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 6c86022..3e3fe4c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.graph;
 
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.contexts.CompilationContext;
@@ -13,7 +14,6 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.InitClassLens;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
@@ -64,6 +64,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.threads.ThreadTask;
 import com.android.tools.r8.utils.threads.ThreadTaskUtils;
+import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
@@ -78,9 +79,13 @@
 
 public class AppView<T extends AppInfo> implements DexDefinitionSupplier, LibraryModeledPredicate {
 
-  private enum WholeProgramOptimizations {
+  public enum WholeProgramOptimizations {
     ON,
-    OFF
+    OFF;
+
+    public boolean isOn() {
+      return this == ON;
+    }
   }
 
   private T appInfo;
@@ -144,6 +149,8 @@
 
   private SeedMapper applyMappingSeedMapper;
 
+  R8ResourceShrinkerState resourceShrinkerState = null;
+
   // When input has been (partially) desugared these are the classes which has been library
   // desugared. This information is populated in the IR converter.
   private Set<DexType> alreadyLibraryDesugared = null;
@@ -526,6 +533,10 @@
     return wholeProgramOptimizations == WholeProgramOptimizations.ON;
   }
 
+  public WholeProgramOptimizations getWholeProgramOptimizations() {
+    return wholeProgramOptimizations;
+  }
+
   /**
    * Create a new processor context.
    *
@@ -691,6 +702,7 @@
   }
 
   public void setCfByteCodePassThrough(Set<DexMethod> cfByteCodePassThrough) {
+    assert options().enableCfByteCodePassThrough;
     this.cfByteCodePassThrough = cfByteCodePassThrough;
   }
 
@@ -813,11 +825,9 @@
       HorizontallyMergedClasses horizontallyMergedClasses, HorizontalClassMerger.Mode mode) {
     assert !hasHorizontallyMergedClasses() || mode.isFinal();
     this.horizontallyMergedClasses = horizontallyMergedClasses().extend(horizontallyMergedClasses);
-    if (mode.isFinal()) {
-      testing()
-          .horizontallyMergedClassesConsumer
-          .accept(dexItemFactory(), horizontallyMergedClasses());
-    }
+    testing()
+        .horizontallyMergedClassesConsumer
+        .accept(dexItemFactory(), horizontallyMergedClasses(), mode);
   }
 
   public boolean hasVerticallyMergedClasses() {
@@ -828,7 +838,7 @@
    * Get the result of vertical class merging. Returns null if vertical class merging has not been
    * run.
    */
-  public VerticallyMergedClasses verticallyMergedClasses() {
+  public VerticallyMergedClasses getVerticallyMergedClasses() {
     return verticallyMergedClasses;
   }
 
@@ -861,6 +871,14 @@
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
 
+  public R8ResourceShrinkerState getResourceShrinkerState() {
+    return resourceShrinkerState;
+  }
+
+  public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
+    this.resourceShrinkerState = resourceShrinkerState;
+  }
+
   public boolean validateUnboxedEnumsHaveBeenPruned() {
     for (DexType unboxedEnum : unboxedEnums.computeAllUnboxedEnums()) {
       assert appInfo.definitionForWithoutExistenceAssert(unboxedEnum) == null
@@ -913,9 +931,10 @@
   }
 
   public boolean isCfByteCodePassThrough(DexEncodedMethod method) {
-    if (!options().isGeneratingClassFiles()) {
+    if (!options().enableCfByteCodePassThrough) {
       return false;
     }
+    assert options().isGeneratingClassFiles();
     if (cfByteCodePassThrough.contains(method.getReference())) {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index bc71dd6..fe5b2c4 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -296,13 +296,12 @@
   }
 
   @Override
-  public int estimatedSizeForInlining() {
-    return countNonStackOperations(Integer.MAX_VALUE);
-  }
-
-  @Override
-  public boolean estimatedSizeForInliningAtMost(int threshold) {
-    return countNonStackOperations(threshold) <= threshold;
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    int estimatedSizeForInlining = countNonStackOperations(threshold);
+    if (estimatedSizeForInlining <= threshold) {
+      return estimatedSizeForInlining;
+    }
+    return -1;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/ClassHierarchyTraversal.java b/src/main/java/com/android/tools/r8/graph/ClassHierarchyTraversal.java
index 4b48516..8635555 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassHierarchyTraversal.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassHierarchyTraversal.java
@@ -5,12 +5,12 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.ThrowingConsumer;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
-import java.util.function.Consumer;
 
 abstract class ClassHierarchyTraversal<
     T extends DexClass, CHT extends ClassHierarchyTraversal<T, CHT>> {
@@ -61,7 +61,8 @@
     return self();
   }
 
-  public void visit(Iterable<? extends DexClass> sources, Consumer<T> visitor) {
+  public <E extends Throwable> void visit(
+      Iterable<? extends DexClass> sources, ThrowingConsumer<T, E> visitor) throws E {
     Iterator<? extends DexClass> sourceIterator = sources.iterator();
 
     // Visit the program classes in the order that is implemented by addDependentsToWorklist().
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index 36d8464..dfc2117 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -141,13 +141,17 @@
   }
 
   /** Estimate the number of IR instructions emitted by buildIR(). */
-  public int estimatedSizeForInlining() {
-    return Integer.MAX_VALUE;
+  public final int estimatedSizeForInlining() {
+    return getEstimatedSizeForInliningIfLessThanOrEquals(Integer.MAX_VALUE);
   }
 
   /** Compute estimatedSizeForInlining() <= threshold. */
-  public boolean estimatedSizeForInliningAtMost(int threshold) {
-    return estimatedSizeForInlining() <= threshold;
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    throw new Unreachable(getClass().getTypeName());
+  }
+
+  public final boolean estimatedSizeForInliningAtMost(int threshold) {
+    return getEstimatedSizeForInliningIfLessThanOrEquals(threshold) >= 0;
   }
 
   public abstract int estimatedDexCodeSizeUpperBoundInBytes();
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
index 906a0be..2974981 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -101,7 +101,6 @@
     return this;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private static boolean hasDefaultInstanceInitializerCode(
       ProgramMethod method, AppView<?> appView) {
     if (!method.getDefinition().isInstanceInitializer()) {
@@ -126,7 +125,7 @@
     Iterator<CfInstruction> instructionIterator = cfCode.getInstructions().iterator();
     // Allow skipping CfPosition instructions in instance initializers that only call Object.<init>.
     Predicate<CfInstruction> instructionOfInterest =
-        method.getHolder().getSuperType() == dexItemFactory.objectType
+        method.getHolder().getSuperType().isIdenticalTo(dexItemFactory.objectType)
             ? instruction -> !instruction.isLabel() && !instruction.isPosition()
             : instruction -> !instruction.isLabel();
     CfLoad load = IteratorUtils.nextUntil(instructionIterator, instructionOfInterest).asLoad();
@@ -136,7 +135,7 @@
     CfInvoke invoke = instructionIterator.next().asInvoke();
     if (invoke == null
         || !invoke.isInvokeConstructor(dexItemFactory)
-        || invoke.getMethod() != getParentConstructor(method, dexItemFactory)) {
+        || invoke.getMethod().isNotIdenticalTo(getParentConstructor(method, dexItemFactory))) {
       return false;
     }
     return instructionIterator.next().isReturnVoid();
@@ -235,8 +234,17 @@
   }
 
   @Override
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    int estimatedSizeForInlining = estimatedDexCodeSizeUpperBoundInBytes();
+    if (estimatedSizeForInlining <= threshold) {
+      return estimatedSizeForInlining;
+    }
+    return -1;
+  }
+
+  @Override
   public TryHandler[] getHandlers() {
-    return new TryHandler[0];
+    return TryHandler.EMPTY_ARRAY;
   }
 
   @Override
@@ -278,7 +286,7 @@
 
   @Override
   public Try[] getTries() {
-    return new Try[0];
+    return Try.EMPTY_ARRAY;
   }
 
   @Override
@@ -413,7 +421,7 @@
 
   @Override
   public DexWritableCacheKey getCacheLookupKey(ProgramMethod method, DexItemFactory factory) {
-    return new AmendedDexWritableCodeKey<DexMethod>(
+    return new AmendedDexWritableCodeKey<>(
         this,
         getParentConstructor(method, factory),
         getIncomingRegisterSize(method),
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
new file mode 100644
index 0000000..b8fe0c2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.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.graph;
+
+public class DefaultUseRegistry<T extends Definition> extends UseRegistry<T> {
+
+  public DefaultUseRegistry(AppView<?> appView, T context) {
+    super(appView, context);
+  }
+
+  @Override
+  public void registerInitClass(DexType type) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeStatic(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeVirtual(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {
+    // Intentionally empty.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistryWithResult.java b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistryWithResult.java
new file mode 100644
index 0000000..5b3fec0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistryWithResult.java
@@ -0,0 +1,71 @@
+// 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.graph;
+
+public class DefaultUseRegistryWithResult<R, T extends Definition>
+    extends UseRegistryWithResult<R, T> {
+
+  public DefaultUseRegistryWithResult(AppView<?> appView, T context) {
+    super(appView, context);
+  }
+
+  public DefaultUseRegistryWithResult(AppView<?> appView, T context, R defaultResult) {
+    super(appView, context, defaultResult);
+  }
+
+  @Override
+  public void registerInitClass(DexType type) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeStatic(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerInvokeVirtual(DexMethod method) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {
+    // Intentionally empty.
+  }
+}
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 e8c908a..2a80c0b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -734,6 +734,10 @@
     return superType;
   }
 
+  public void setSuperType(DexType superType) {
+    this.superType = superType;
+  }
+
   public boolean hasClassInitializer() {
     return getClassInitializer() != null;
   }
@@ -1194,6 +1198,8 @@
   /** Returns kotlin class info if the class is synthesized by kotlin compiler. */
   public abstract KotlinClassLevelInfo getKotlinInfo();
 
+  public abstract ClassKind<?> getKind();
+
   public final String getSimpleName() {
     return getType().getSimpleName();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
index 2937cd2..9c11896 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
@@ -126,6 +126,11 @@
   }
 
   @Override
+  public ClassKind<DexClasspathClass> getKind() {
+    return ClassKind.CLASSPATH;
+  }
+
+  @Override
   public DexClasspathClass get() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index e800b21..00febce 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -286,11 +286,6 @@
   }
 
   @Override
-  public int estimatedSizeForInlining() {
-    return codeSizeInBytes();
-  }
-
-  @Override
   public int estimatedDexCodeSizeUpperBoundInBytes() {
     return codeSizeInBytes();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
index 2a85b22..f7cc258 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -106,23 +106,4 @@
   public void setApiLevelForDefinition(ComputedApiLevel apiLevelForDefinition) {
     this.apiLevelForDefinition = apiLevelForDefinition;
   }
-
-  public boolean hasComputedApiReferenceLevel() {
-    return !getApiLevel().isNotSetApiLevel();
-  }
-
-  @Override
-  @SuppressWarnings("EqualsGetClass")
-  public final boolean equals(Object other) {
-    if (other == this) {
-      return true;
-    }
-    return other.getClass() == getClass()
-        && ((DexEncodedMember<?, ?>) other).getReference().equals(getReference());
-  }
-
-  @Override
-  public final int hashCode() {
-    return getReference().hashCode();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index c1f8ac7..730fb6c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -31,15 +31,11 @@
 import com.android.tools.r8.cf.code.CfThrow;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.dex.code.DexConstString;
-import com.android.tools.r8.dex.code.DexInstanceOf;
 import com.android.tools.r8.dex.code.DexInstruction;
 import com.android.tools.r8.dex.code.DexInvokeDirect;
 import com.android.tools.r8.dex.code.DexInvokeStatic;
 import com.android.tools.r8.dex.code.DexNewInstance;
-import com.android.tools.r8.dex.code.DexReturn;
 import com.android.tools.r8.dex.code.DexThrow;
-import com.android.tools.r8.dex.code.DexXorIntLit8;
-import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation.AnnotatedKind;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
@@ -48,7 +44,6 @@
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.NestUtils;
 import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
@@ -63,7 +58,6 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ConsumerUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
@@ -660,75 +654,43 @@
     this.kotlinMemberInfo = kotlinMemberInfo;
   }
 
-  public boolean isKotlinFunction() {
-    return kotlinMemberInfo.isFunction();
-  }
-
-  public boolean isKotlinExtensionFunction() {
-    return kotlinMemberInfo.isFunction() && kotlinMemberInfo.asFunction().isExtensionFunction();
-  }
-
   public boolean isOnlyInlinedIntoNestMembers() {
     return compilationState == PROCESSED_INLINING_CANDIDATE_SAME_NEST;
   }
 
   public boolean isInliningCandidate(
-      ProgramMethod container,
-      Reason inliningReason,
-      AppInfoWithClassHierarchy appInfo,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      ProgramMethod context,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     checkIfObsolete();
-    return isInliningCandidate(
-        container.getHolderType(), inliningReason, appInfo, whyAreYouNotInliningReporter);
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  public boolean isInliningCandidate(
-      DexType containerType,
-      Reason inliningReason,
-      AppInfoWithClassHierarchy appInfo,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    checkIfObsolete();
-
-    if (inliningReason == Reason.FORCE) {
-      // Make sure we would be able to inline this normally.
-      if (!isInliningCandidate(
-          containerType, Reason.SIMPLE, appInfo, whyAreYouNotInliningReporter)) {
-        // If not, raise a flag, because some optimizations that depend on force inlining would
-        // silently produce an invalid code, which is worse than an internal error.
-        throw new InternalCompilerError("FORCE inlining on non-inlinable: " + toSourceString());
-      }
-      return true;
-    }
-
-    // TODO(b/128967328): inlining candidate should satisfy all states if multiple states are there.
+    AppInfoWithClassHierarchy appInfo = appView.appInfo();
     switch (compilationState) {
       case PROCESSED_INLINING_CANDIDATE_ANY:
         return true;
 
       case PROCESSED_INLINING_CANDIDATE_SUBCLASS:
-        if (appInfo.isSubtype(containerType, getReference().holder)) {
+        if (appInfo.isSubtype(context.getHolderType(), getHolderType())) {
           return true;
         }
         whyAreYouNotInliningReporter.reportCallerNotSubtype();
         return false;
 
       case PROCESSED_INLINING_CANDIDATE_SAME_PACKAGE:
-        if (containerType.isSamePackage(getReference().holder)) {
+        if (context.isSamePackage(getHolderType())) {
           return true;
         }
         whyAreYouNotInliningReporter.reportCallerNotSamePackage();
         return false;
 
       case PROCESSED_INLINING_CANDIDATE_SAME_NEST:
-        if (NestUtils.sameNest(containerType, getReference().holder, appInfo)) {
+        if (NestUtils.sameNest(context.getHolderType(), getHolderType(), appInfo)) {
           return true;
         }
         whyAreYouNotInliningReporter.reportCallerNotSameNest();
         return false;
 
       case PROCESSED_INLINING_CANDIDATE_SAME_CLASS:
-        if (containerType == getReference().holder) {
+        if (context.getHolderType().isIdenticalTo(getHolderType())) {
           return true;
         }
         whyAreYouNotInliningReporter.reportCallerNotSameClass();
@@ -968,12 +930,6 @@
         null);
   }
 
-  public Code buildInstanceOfCode(DexType type, boolean negate, InternalOptions options) {
-    return options.isGeneratingClassFiles()
-        ? buildInstanceOfCfCode(type, negate)
-        : buildInstanceOfDexCode(type, negate);
-  }
-
   public CfCode buildInstanceOfCfCode(DexType type, boolean negate) {
     CfInstruction[] instructions = new CfInstruction[3 + BooleanUtils.intValue(negate) * 2];
     int i = 0;
@@ -991,17 +947,6 @@
         Arrays.asList(instructions));
   }
 
-  public DexCode buildInstanceOfDexCode(DexType type, boolean negate) {
-    DexInstruction[] instructions = new DexInstruction[2 + BooleanUtils.intValue(negate)];
-    int i = 0;
-    instructions[i++] = new DexInstanceOf(0, 0, type);
-    if (negate) {
-      instructions[i++] = new DexXorIntLit8(0, 0, 1);
-    }
-    instructions[i] = new DexReturn(0);
-    return generateCodeFromTemplate(1, 0, instructions);
-  }
-
   public DexEncodedMethod toMethodThatLogsError(AppView<?> appView) {
     Builder builder =
         builder(this)
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 75f9968..92ceda8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -126,7 +126,6 @@
       new ConcurrentHashMap<>();
   public final LRUCacheTable<InterfaceCollection, InterfaceCollection, InterfaceCollection>
       leastUpperBoundOfInterfacesTable = LRUCacheTable.create(8, 8);
-
   boolean sorted = false;
 
   // Internal type containing only the null value.
@@ -215,6 +214,7 @@
   public final DexString compareToMethodName = createString("compareTo");
   public final DexString compareToIgnoreCaseMethodName = createString("compareToIgnoreCase");
   public final DexString cloneMethodName = createString("clone");
+  public final DexString formatMethodName = createString("format");
   public final DexString substringName = createString("substring");
   public final DexString trimName = createString("trim");
 
@@ -299,6 +299,7 @@
   public final DexString serviceLoaderDescriptor = createString("Ljava/util/ServiceLoader;");
   public final DexString serviceLoaderConfigurationErrorDescriptor =
       createString("Ljava/util/ServiceConfigurationError;");
+  public final DexString localeDescriptor = createString("Ljava/util/Locale;");
   public final DexString listDescriptor = createString("Ljava/util/List;");
   public final DexString setDescriptor = createString("Ljava/util/Set;");
   public final DexString mapDescriptor = createString("Ljava/util/Map;");
@@ -604,8 +605,10 @@
   public final DexType javaUtilComparatorType = createStaticallyKnownType("Ljava/util/Comparator;");
   public final DexType javaUtilConcurrentTimeUnitType =
       createStaticallyKnownType("Ljava/util/concurrent/TimeUnit;");
+  public final DexType javaUtilFormattableType =
+      createStaticallyKnownType("Ljava/util/Formattable;");
   public final DexType javaUtilListType = createStaticallyKnownType("Ljava/util/List;");
-  public final DexType javaUtilLocaleType = createStaticallyKnownType("Ljava/util/Locale;");
+  public final DexType javaUtilLocaleType = createStaticallyKnownType(localeDescriptor);
   public final DexType javaUtilLoggingLevelType =
       createStaticallyKnownType("Ljava/util/logging/Level;");
   public final DexType javaUtilLoggingLoggerType =
@@ -887,6 +890,38 @@
     return createMethod(boxType, proto, valueOfMethodName);
   }
 
+  public BoxUnboxPrimitiveMethodRoundtrip getBoxUnboxPrimitiveMethodRoundtrip(DexType type) {
+    if (type.isPrimitiveType()) {
+      return new BoxUnboxPrimitiveMethodRoundtrip(
+          getBoxPrimitiveMethod(type), getUnboxPrimitiveMethod(type));
+    } else if (primitiveToBoxed.containsValue(type)) {
+      return new BoxUnboxPrimitiveMethodRoundtrip(
+          getUnboxPrimitiveMethod(type), getBoxPrimitiveMethod(type));
+    } else {
+      return null;
+    }
+  }
+
+  public static class BoxUnboxPrimitiveMethodRoundtrip {
+
+    private final DexMethod boxIfPrimitiveElseUnbox;
+    private final DexMethod unboxIfPrimitiveElseBox;
+
+    public BoxUnboxPrimitiveMethodRoundtrip(
+        DexMethod boxIfPrimitiveElseUnbox, DexMethod unboxIfPrimitiveElseBox) {
+      this.boxIfPrimitiveElseUnbox = boxIfPrimitiveElseUnbox;
+      this.unboxIfPrimitiveElseBox = unboxIfPrimitiveElseBox;
+    }
+
+    public DexMethod getBoxIfPrimitiveElseUnbox() {
+      return boxIfPrimitiveElseUnbox;
+    }
+
+    public DexMethod getUnboxIfPrimitiveElseBox() {
+      return unboxIfPrimitiveElseBox;
+    }
+  }
+
   public DexType getBoxedForPrimitiveType(DexType primitive) {
     assert primitive.isPrimitiveType();
     return primitiveToBoxed.get(primitive);
@@ -989,6 +1024,8 @@
               objectsMethods.requireNonNull,
               objectsMethods.requireNonNullWithMessage,
               objectsMethods.requireNonNullWithMessageSupplier,
+              stringMembers.format,
+              stringMembers.formatWithLocale,
               stringMembers.valueOf)
           .addAll(boxedValueOfMethods())
           .addAll(stringBufferMethods.appendMethods)
@@ -2181,6 +2218,9 @@
     public final DexMethod compareToIgnoreCase;
 
     public final DexMethod hashCode;
+
+    public final DexMethod format;
+    public final DexMethod formatWithLocale;
     public final DexMethod valueOf;
     public final DexMethod toString;
     public final DexMethod intern;
@@ -2227,6 +2267,19 @@
               needsOneString);
 
       hashCode = createMethod(stringType, createProto(intType), hashCodeMethodName);
+      format =
+          createMethod(
+              stringDescriptor,
+              formatMethodName,
+              stringDescriptor,
+              new DexString[] {stringDescriptor, objectArrayDescriptor});
+      formatWithLocale =
+          createMethod(
+              stringDescriptor,
+              formatMethodName,
+              stringDescriptor,
+              new DexString[] {localeDescriptor, stringDescriptor, objectArrayDescriptor});
+
       valueOf = createMethod(
           stringDescriptor, valueOfMethodName, stringDescriptor, needsOneObject);
       toString = createMethod(
diff --git a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
index a145cba..7dc2241 100644
--- a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
@@ -144,6 +144,11 @@
   }
 
   @Override
+  public ClassKind<DexLibraryClass> getKind() {
+    return ClassKind.LIBRARY;
+  }
+
+  @Override
   public DexLibraryClass get() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index c4ee457..a58d4a9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -31,6 +31,10 @@
     return identical(this, other);
   }
 
+  public final boolean isNotIdenticalTo(DexMethod other) {
+    return !isIdenticalTo(other);
+  }
+
   public final DexProto proto;
 
   DexMethod(DexType holder, DexProto proto, DexString name, boolean skipNameValidationForTesting) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 029fd1e..6f5c0fe 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -574,6 +574,11 @@
     return kotlinInfo;
   }
 
+  @Override
+  public ClassKind<DexProgramClass> getKind() {
+    return ClassKind.PROGRAM;
+  }
+
   public void setKotlinInfo(KotlinClassLevelInfo kotlinInfo) {
     assert kotlinInfo != null;
     assert this.kotlinInfo == getNoKotlinInfo();
diff --git a/src/main/java/com/android/tools/r8/graph/DexProto.java b/src/main/java/com/android/tools/r8/graph/DexProto.java
index 9721adc..12b0e3b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProto.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProto.java
@@ -26,6 +26,10 @@
     return identical(this, other);
   }
 
+  public final boolean isNotIdenticalTo(DexProto other) {
+    return !isIdenticalTo(other);
+  }
+
   public static final DexProto SENTINEL = new DexProto(null, null);
 
   public final DexType returnType;
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index ecac370..4809783 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -40,6 +40,10 @@
     return identical(this, other);
   }
 
+  public final boolean isNotIdenticalTo(DexType other) {
+    return !isIdenticalTo(other);
+  }
+
   public static final DexType[] EMPTY_ARRAY = {};
 
   // Bundletool is merging classes that may originate from a build with an old version of R8.
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index c7fe5c9..0257cfd 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -247,13 +247,8 @@
   }
 
   @Override
-  public int estimatedSizeForInlining() {
-    return asCfCode().estimatedSizeForInlining();
-  }
-
-  @Override
-  public boolean estimatedSizeForInliningAtMost(int threshold) {
-    return asCfCode().estimatedSizeForInliningAtMost(threshold);
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    return asCfCode().getEstimatedSizeForInliningIfLessThanOrEquals(threshold);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
index 12da839..8ede6f7 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
@@ -267,6 +267,11 @@
       return this;
     }
 
+    public Builder setAbstract() {
+      flags.setAbstract();
+      return this;
+    }
+
     public Builder setBridge() {
       flags.setBridge();
       return this;
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 99cd8d2..beb2fb3 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
@@ -119,6 +119,10 @@
     virtualInvokes.getOrDefault(method, ProgramMethodSet.empty()).forEach(consumer);
   }
 
+  public boolean isVirtualInvokesDestroyed() {
+    return isThrowingMap(virtualInvokes);
+  }
+
   public MethodAccessInfoCollection rewrittenWithLens(
       DexDefinitionSupplier definitions, GraphLens lens, Timing timing) {
     timing.begin("Rewrite MethodAccessInfoCollection");
diff --git a/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java b/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
index 4905063..dc2bbdc 100644
--- a/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
+++ b/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
@@ -126,6 +126,15 @@
   }
 
   @Override
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    int estimatedSizeForInlining = estimatedDexCodeSizeUpperBoundInBytes();
+    if (estimatedSizeForInlining <= threshold) {
+      return estimatedSizeForInlining;
+    }
+    return -1;
+  }
+
+  @Override
   public TryHandler[] getHandlers() {
     return new TryHandler[0];
   }
diff --git a/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java b/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
index a58f744..ddfbba1 100644
--- a/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
+++ b/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
@@ -142,6 +142,15 @@
   }
 
   @Override
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
+    int estimatedSizeForInlining = estimatedDexCodeSizeUpperBoundInBytes();
+    if (estimatedSizeForInlining <= threshold) {
+      return estimatedSizeForInlining;
+    }
+    return -1;
+  }
+
+  @Override
   public TryHandler[] getHandlers() {
     return TryHandler.EMPTY_ARRAY;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
index 7947f84..3c2e9f6 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
@@ -21,7 +21,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
@@ -69,7 +69,7 @@
   @Override
   public void traceStaticFieldRead(
       DexField field,
-      FieldResolutionResult resolutionResult,
+      SingleFieldResolutionResult<?> resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {
     if (isUsingJavaAssertionsDisabledField(field) || isUsingKotlinAssertionsEnabledField(field)) {
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerFieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerFieldAccessAnalysis.java
index d8dadc5..0642216 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerFieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerFieldAccessAnalysis.java
@@ -6,7 +6,9 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerWorklist;
 
 public interface EnqueuerFieldAccessAnalysis {
@@ -27,7 +29,7 @@
 
   default void traceStaticFieldRead(
       DexField field,
-      FieldResolutionResult resolutionResult,
+      SingleFieldResolutionResult<?> resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
   ;
@@ -38,4 +40,10 @@
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
   ;
+
+  /**
+   * Called when the Enqueuer has reached the final fixpoint. Each analysis may use this callback to
+   * perform some post-processing.
+   */
+  default void done(Enqueuer enqueuer) {}
 }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java b/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
index 48ca34d..ed49777 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerWorklist;
@@ -74,7 +75,7 @@
   @Override
   public void traceStaticFieldRead(
       DexField field,
-      FieldResolutionResult resolutionResult,
+      SingleFieldResolutionResult<?> resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {
     if (isUnsafeToUseFieldOnDalvik(field)) {
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
new file mode 100644
index 0000000..d4ebe2f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -0,0 +1,212 @@
+// 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.graph.analysis;
+
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
+import com.android.tools.r8.AndroidResourceInput;
+import com.android.tools.r8.AndroidResourceInput.Kind;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.EnqueuerWorklist;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
+
+  private final R8ResourceShrinkerState resourceShrinkerState;
+  private final Map<DexProgramClass, RClassFieldToValueStore> fieldToValueMapping =
+      new IdentityHashMap<>();
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+
+  @SuppressWarnings("UnusedVariable")
+  private ResourceAccessAnalysis(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Enqueuer enqueuer,
+      R8ResourceShrinkerState resourceShrinkerState) {
+    this.appView = appView;
+    this.resourceShrinkerState = resourceShrinkerState;
+    appView.setResourceShrinkerState(resourceShrinkerState);
+    try {
+      for (AndroidResourceInput androidResource :
+          appView.options().androidResourceProvider.getAndroidResources()) {
+        if (androidResource.getKind() == Kind.RESOURCE_TABLE) {
+          resourceShrinkerState.setResourceTableInput(androidResource.getByteStream());
+          break;
+        }
+      }
+    } catch (ResourceException e) {
+      throw appView.reporter().fatalError("Failed initializing resource table");
+    }
+  }
+
+  public static void register(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    if (enabled(appView, enqueuer)) {
+      enqueuer.registerFieldAccessAnalysis(
+          new ResourceAccessAnalysis(appView, enqueuer, new R8ResourceShrinkerState()));
+    }
+  }
+
+  @Override
+  public void done(Enqueuer enqueuer) {
+    appView.setResourceShrinkerState(resourceShrinkerState);
+    EnqueuerFieldAccessAnalysis.super.done(enqueuer);
+  }
+
+  private static boolean enabled(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    // For now, we only do the resource tracing in the initial round since we don't track inlining
+    // of values yet.
+    return appView.options().androidResourceProvider != null
+        && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()
+        && enqueuer.getMode().isInitialTreeShaking();
+  }
+
+  @Override
+  public void traceStaticFieldRead(
+      DexField field,
+      SingleFieldResolutionResult<?> resolutionResult,
+      ProgramMethod context,
+      EnqueuerWorklist worklist) {
+    ProgramField resolvedField = resolutionResult.getProgramField();
+    if (resolvedField == null) {
+      return;
+    }
+    if (getMaybeCachedIsRClass(resolvedField.getHolder())) {
+      DexProgramClass holderType = resolvedField.getHolder();
+      if (!fieldToValueMapping.containsKey(holderType)) {
+        populateRClassValues(resolvedField);
+      }
+      assert fieldToValueMapping.containsKey(holderType);
+      RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
+      IntList integers = rClassFieldToValueStore.valueMapping.get(field);
+      for (Integer integer : integers) {
+        resourceShrinkerState.trace(integer);
+      }
+    }
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private void populateRClassValues(ProgramField field) {
+    // TODO(287398085): Pending discussions with the AAPT2 team, we might need to harden this
+    // to not fail if we wrongly classify an unrelated class as R class in our heuristic..
+    RClassFieldToValueStore.Builder rClassValueBuilder = new RClassFieldToValueStore.Builder();
+    ProgramMethod programClassInitializer = field.getHolder().getProgramClassInitializer();
+    if (programClassInitializer == null) {
+      // No initialization of fields, empty R class.
+      return;
+    }
+    IRCode code = programClassInitializer.buildIR(appView, MethodConversionOptions.nonConverting());
+
+    // We handle two cases:
+    //  - Simple integer field assigments.
+    //  - Assigments of integer arrays to fields.
+    for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
+      Value value = staticPut.value();
+      if (value.isPhi()) {
+        continue;
+      }
+      IntList values;
+      Instruction definition = staticPut.value().definition;
+      if (definition.isConstNumber()) {
+        values = new IntArrayList(1);
+        values.add(definition.asConstNumber().getIntValue());
+      } else if (definition.isNewArrayEmpty()) {
+        NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
+        values = new IntArrayList();
+        for (Instruction uniqueUser : newArrayEmpty.outValue().uniqueUsers()) {
+          if (uniqueUser.isArrayPut()) {
+            Value constValue = uniqueUser.asArrayPut().value();
+            if (constValue.isConstNumber()) {
+              values.add(constValue.getDefinition().asConstNumber().getIntValue());
+            }
+          } else {
+            assert uniqueUser == staticPut;
+          }
+        }
+      } else if (definition.isNewArrayFilled()) {
+        values = new IntArrayList();
+        for (Value inValue : definition.asNewArrayFilled().inValues()) {
+          if (value.isPhi()) {
+            continue;
+          }
+          Instruction valueDefinition = inValue.definition;
+          if (valueDefinition.isConstNumber()) {
+            values.add(valueDefinition.asConstNumber().getIntValue());
+          }
+        }
+      } else {
+        continue;
+      }
+      rClassValueBuilder.addMapping(staticPut.getField(), values);
+    }
+
+    fieldToValueMapping.put(field.getHolder(), rClassValueBuilder.build());
+  }
+
+  private final Map<DexProgramClass, Boolean> cachedClassLookups = new IdentityHashMap<>();
+
+  private boolean getMaybeCachedIsRClass(DexProgramClass holder) {
+    Boolean result = cachedClassLookups.get(holder);
+    if (result != null) {
+      return result;
+    }
+    String simpleClassName =
+        DescriptorUtils.getSimpleClassNameFromDescriptor(holder.getType().toDescriptorString());
+    List<String> split = StringUtils.split(simpleClassName, '$');
+
+    if (split.size() < 2) {
+      cachedClassLookups.put(holder, false);
+      return false;
+    }
+    String type = split.get(split.size() - 1);
+    String rClass = split.get(split.size() - 2);
+    // We match on R if:
+    // - The name of the Class is R$type - we allow R to be an inner class.
+    //   - The inner type should be with lower case
+    boolean isRClass = Character.isLowerCase(type.charAt(0)) && rClass.equals("R");
+    cachedClassLookups.put(holder, isRClass);
+    return isRClass;
+  }
+
+  private static class RClassFieldToValueStore {
+    private final Map<DexField, IntList> valueMapping;
+
+    private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
+      this.valueMapping = valueMapping;
+    }
+
+    public static class Builder {
+      private final Map<DexField, IntList> valueMapping = new IdentityHashMap<>();
+
+      public void addMapping(DexField field, IntList values) {
+        assert !valueMapping.containsKey(field);
+        valueMapping.put(field, values);
+      }
+
+      public RClassFieldToValueStore build() {
+        return new RClassFieldToValueStore(valueMapping);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index ef55a1f..b34bf05 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -462,8 +463,10 @@
   }
 
   public <T extends DexReference> boolean assertPinnedNotModified(
-      KeepInfoCollection keepInfo, InternalOptions options) {
+      AppView<AppInfoWithLiveness> appView) {
     List<DexReference> pinnedItems = new ArrayList<>();
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    InternalOptions options = appView.options();
     keepInfo.forEachPinnedType(pinnedItems::add, options);
     keepInfo.forEachPinnedMethod(pinnedItems::add, options);
     keepInfo.forEachPinnedField(pinnedItems::add, options);
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
index cdd27a5..d906b4c 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
@@ -117,7 +117,7 @@
   }
 
   @Override
-  protected DexType getNextClassType(DexType type) {
+  public DexType getNextClassType(DexType type) {
     return typeMap.getRepresentativeValueOrDefault(type, type);
   }
 
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 ce123a1..ab20f7d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -86,8 +86,8 @@
   public void runIfNecessary(
       ExecutorService executorService, Timing timing, RuntimeTypeCheckInfo runtimeTypeCheckInfo)
       throws ExecutionException {
-    if (options.isEnabled(mode)) {
-      timing.begin("HorizontalClassMerger (" + mode.toString() + ")");
+    timing.begin("HorizontalClassMerger (" + mode.toString() + ")");
+    if (shouldRun()) {
       IRCodeProvider codeProvider =
           appView.hasClassHierarchy()
               ? IRCodeProvider.create(appView.withClassHierarchy(), this::getConversionOptions)
@@ -99,11 +99,17 @@
       // Clear type elements cache after IR building.
       appView.dexItemFactory().clearTypeElementsCache();
       appView.notifyOptimizationFinishedForTesting();
-
-      timing.end();
     } else {
       appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty(), mode);
     }
+    appView.appInfo().notifyHorizontalClassMergerFinished(mode);
+    assert ArtProfileCompletenessChecker.verify(appView);
+    timing.end();
+  }
+
+  private boolean shouldRun() {
+    return options.isEnabled(mode, appView.getWholeProgramOptimizations())
+        && !appView.hasCfByteCodePassThroughMethods();
   }
 
   private MutableMethodConversionOptions getConversionOptions() {
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 2e0a307..5416af5 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
@@ -9,8 +9,8 @@
 import static com.android.tools.r8.ir.desugar.LambdaDescriptor.isLambdaMetafactoryMethod;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
-import com.android.tools.r8.dex.code.CfOrDexInstruction;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
@@ -19,7 +19,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.deadlock.SingleCallerInformation;
@@ -39,7 +38,6 @@
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -408,7 +406,7 @@
       return true;
     }
 
-    class TracerUseRegistry extends UseRegistry<ProgramMethod> {
+    class TracerUseRegistry extends DefaultUseRegistry<ProgramMethod> {
 
       TracerUseRegistry(ProgramMethod context) {
         super(appView(), context);
@@ -570,11 +568,6 @@
       }
 
       @Override
-      public void registerTypeReference(DexType type) {
-        // Intentionally empty, new-array etc. does not trigger any class initialization.
-      }
-
-      @Override
       public void registerCallSite(DexCallSite callSite) {
         if (isLambdaMetafactoryMethod(callSite, appView().appInfo())) {
           // Use of lambda metafactory does not trigger any class initialization.
@@ -582,39 +575,6 @@
           fail();
         }
       }
-
-      @Override
-      public void registerCheckCast(DexType type, boolean ignoreCompatRules) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
-
-      @Override
-      public void registerConstClass(
-          DexType type,
-          ListIterator<? extends CfOrDexInstruction> iterator,
-          boolean ignoreCompatRules) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
-
-      @Override
-      public void registerInstanceFieldRead(DexField field) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
-
-      @Override
-      public void registerInstanceFieldWrite(DexField field) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
-
-      @Override
-      public void registerInstanceOf(DexType type) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
-
-      @Override
-      public void registerExceptionGuard(DexType guard) {
-        // Intentionally empty, does not trigger any class initialization.
-      }
     }
   }
 }
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 d47b3fb..7e32fd3 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
@@ -22,7 +22,7 @@
   }
 
   private boolean isSynchronizationClass(DexProgramClass clazz) {
-    return appView.appInfo().isLockCandidate(clazz.type) || clazz.hasStaticSynchronizedMethods();
+    return appView.appInfo().isLockCandidate(clazz) || clazz.hasStaticSynchronizedMethods();
   }
 
   // TODO(b/270398965): Replace LinkedList.
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
index c7534b3..3259fe7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
@@ -22,10 +22,10 @@
 
   @Override
   public boolean canMerge(DexProgramClass program) {
-    if (appView.verticallyMergedClasses() == null) {
+    if (appView.getVerticallyMergedClasses() == null) {
       return true;
     }
-    return !appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(program.type);
+    return !appView.getVerticallyMergedClasses().hasBeenMergedIntoSubtype(program.type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
index 2daa302..cb443c4 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
@@ -19,7 +19,7 @@
 
   @Override
   public boolean canMerge(DexProgramClass clazz) {
-    return !appView.appInfo().isNoHorizontalClassMergingOfType(clazz.getType());
+    return !appView.appInfo().isNoHorizontalClassMergingOfType(clazz);
   }
 
   @Override
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 ded7e1d..12ef6dc 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
@@ -16,8 +16,8 @@
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
-import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
 import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.verticalclassmerging.IllegalAccessDetector;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
index af868d9..9552f5f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
@@ -6,12 +6,12 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
@@ -94,7 +94,7 @@
       method.registerCodeReferences(new InvokeExtractor(appView, method));
     }
 
-    private class InvokeExtractor extends UseRegistry<ProgramMethod> {
+    private class InvokeExtractor extends DefaultUseRegistry<ProgramMethod> {
 
       private final AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierachy;
 
@@ -190,16 +190,6 @@
       }
 
       @Override
-      public void registerInstanceFieldRead(DexField field) {
-        // Intentionally empty.
-      }
-
-      @Override
-      public void registerInstanceFieldWrite(DexField field) {
-        // Intentionally empty.
-      }
-
-      @Override
       public void registerInvokeDirect(DexMethod method) {
         DexMethod rewrittenMethod =
             appViewWithClassHierachy
@@ -267,11 +257,6 @@
         DexField rewrittenField = appViewWithClassHierachy.graphLens().lookupField(field);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
       }
-
-      @Override
-      public void registerTypeReference(DexType type) {
-        // Intentionally empty.
-      }
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index b3b0924..38b433a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -57,9 +57,13 @@
     return true;
   }
 
+  public Map<Value, AbstractValue> analyze(IRCode code) {
+    return new SparseConditionalConstantPropagationOnCode(code).analyze().mapping;
+  }
+
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    return new SparseConditionalConstantPropagationOnCode(code).run();
+    return new SparseConditionalConstantPropagationOnCode(code).analyze().run();
   }
 
   private class SparseConditionalConstantPropagationOnCode {
@@ -86,7 +90,7 @@
       visitedBlocks = new BitSet(maxBlockNumber);
     }
 
-    protected CodeRewriterResult run() {
+    public SparseConditionalConstantPropagationOnCode analyze() {
       BasicBlock firstBlock = code.entryBlock();
       visitInstructions(firstBlock);
 
@@ -113,6 +117,10 @@
           }
         }
       }
+      return this;
+    }
+
+    protected CodeRewriterResult run() {
       boolean hasChanged = rewriteConstants();
       return CodeRewriterResult.hasChanged(hasChanged);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index e033b0b..916b595 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -14,19 +14,18 @@
 import com.android.tools.r8.graph.AbstractAccessContexts;
 import com.android.tools.r8.graph.AbstractAccessContexts.ConcreteAccessContexts;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 import com.android.tools.r8.graph.DexClassAndField;
 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.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeInstructionMetadata;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.ReferenceTypeElement;
@@ -321,7 +320,7 @@
     return true;
   }
 
-  class TrivialFieldAccessUseRegistry extends UseRegistry<ProgramMethod> {
+  class TrivialFieldAccessUseRegistry extends DefaultUseRegistry<ProgramMethod> {
 
     TrivialFieldAccessUseRegistry(ProgramMethod method) {
       super(appView(), method);
@@ -494,32 +493,5 @@
     public void registerStaticFieldWrite(DexField field) {
       registerFieldAccess(field, true, true, BytecodeInstructionMetadata.none());
     }
-
-    @Override
-    public void registerInitClass(DexType clazz) {}
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {}
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {}
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {}
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {}
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {}
-
-    @Override
-    public void registerNewInstance(DexType type) {}
-
-    @Override
-    public void registerTypeReference(DexType type) {}
-
-    @Override
-    public void registerInstanceOf(DexType type) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index 468274c..c1a2b62 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -284,11 +284,8 @@
           if (arrayPut.array() != value) {
             return null;
           }
-          if (!arrayPut.index().isConstNumber()) {
-            return null;
-          }
-          int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
-          if (index < 0 || index >= valuesSize) {
+          int index = arrayPut.indexIfConstAndInBounds(valuesSize);
+          if (index < 0) {
             return null;
           }
           if (!updateEnumValueState(valuesState, valuesTypes, index, arrayPut.value())) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
index a5fa7b7..e8e15bd 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
@@ -14,7 +14,9 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.InliningReasonStrategy;
+import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 
 /**
  * Equivalent to the {@link InliningReasonStrategy} in {@link #parent} except for invocations
@@ -38,7 +40,9 @@
       ProgramMethod target,
       ProgramMethod context,
       DefaultInliningOracle oracle,
-      MethodProcessor methodProcessor) {
+      InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     if (references.isAbstractGeneratedMessageLiteBuilder(context.getHolder())
         && invoke.isInvokeSuper()) {
       // Aggressively inline invoke-super calls inside the GeneratedMessageLite builders. Such
@@ -48,7 +52,14 @@
     }
     return references.isDynamicMethod(target) || references.isDynamicMethodBridge(target)
         ? computeInliningReasonForDynamicMethod(invoke, target, context)
-        : parent.computeInliningReason(invoke, target, context, oracle, methodProcessor);
+        : parent.computeInliningReason(
+            invoke,
+            target,
+            context,
+            oracle,
+            inliningIRProvider,
+            methodProcessor,
+            whyAreYouNotInliningReporter);
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
index ac5f054..049920e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
@@ -380,10 +380,7 @@
       ArrayPut arrayPut = instructionIterator.next().asArrayPut();
 
       // Verify that the index correct.
-      Value indexValue = arrayPut.index().getAliasedValue();
-      if (indexValue.isPhi()
-          || !indexValue.definition.isConstNumber()
-          || indexValue.definition.asConstNumber().getIntValue() != expectedNextIndex) {
+      if (arrayPut.indexOrDefault(-1) != expectedNextIndex) {
         throw new InvalidRawMessageInfoException();
       }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index 27a0112..c71f037 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -101,7 +101,7 @@
     return false;
   }
 
-  public SingleBoxedNumberValue asSingleBoxedPrimitive() {
+  public SingleBoxedPrimitiveValue asSingleBoxedPrimitive() {
     return null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedBooleanValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedBooleanValue.java
index 79b991b..b7520f0 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedBooleanValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedBooleanValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.ValueFactory;
 import com.android.tools.r8.utils.BooleanUtils;
 
-public class SingleBoxedBooleanValue extends SingleBoxedNumberValue {
+public class SingleBoxedBooleanValue extends SingleBoxedPrimitiveValue {
 
   private static final SingleBoxedBooleanValue FALSE_INSTANCE = new SingleBoxedBooleanValue(false);
   private static final SingleBoxedBooleanValue TRUE_INSTANCE = new SingleBoxedBooleanValue(true);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedByteValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedByteValue.java
index f3f1ded..5c9f48c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedByteValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedByteValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedByteValue extends SingleBoxedNumberValue {
+public class SingleBoxedByteValue extends SingleBoxedPrimitiveValue {
 
   private final int value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedCharValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedCharValue.java
index cffa634..ee5cab5 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedCharValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedCharValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedCharValue extends SingleBoxedNumberValue {
+public class SingleBoxedCharValue extends SingleBoxedPrimitiveValue {
 
   private final int value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedDoubleValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedDoubleValue.java
index 2c03c37..f40ad58 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedDoubleValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedDoubleValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedDoubleValue extends SingleBoxedNumberValue {
+public class SingleBoxedDoubleValue extends SingleBoxedPrimitiveValue {
 
   private final long value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedFloatValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedFloatValue.java
index f2fe64a..509b38d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedFloatValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedFloatValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedFloatValue extends SingleBoxedNumberValue {
+public class SingleBoxedFloatValue extends SingleBoxedPrimitiveValue {
 
   private final int value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedIntegerValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedIntegerValue.java
index d7811f7..da49211 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedIntegerValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedIntegerValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedIntegerValue extends SingleBoxedNumberValue {
+public class SingleBoxedIntegerValue extends SingleBoxedPrimitiveValue {
 
   private final int value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedLongValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedLongValue.java
index 51cb06f..86cd49a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedLongValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedLongValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedLongValue extends SingleBoxedNumberValue {
+public class SingleBoxedLongValue extends SingleBoxedPrimitiveValue {
 
   private final long value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedPrimitiveValue.java
similarity index 92%
rename from src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedNumberValue.java
rename to src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedPrimitiveValue.java
index ac91612..3e2dea1 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedPrimitiveValue.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
-public abstract class SingleBoxedNumberValue extends SingleConstValue {
+public abstract class SingleBoxedPrimitiveValue extends SingleConstValue {
 
   @Override
   public InstanceFieldInitializationInfo fixupAfterParametersChanged(
@@ -45,7 +45,7 @@
   }
 
   @Override
-  public SingleBoxedNumberValue asSingleBoxedPrimitive() {
+  public SingleBoxedPrimitiveValue asSingleBoxedPrimitive() {
     return this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedShortValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedShortValue.java
index e47f751..37cf206 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedShortValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleBoxedShortValue.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
 import com.android.tools.r8.ir.code.ValueFactory;
 
-public class SingleBoxedShortValue extends SingleBoxedNumberValue {
+public class SingleBoxedShortValue extends SingleBoxedPrimitiveValue {
 
   private final int value;
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
index 8286eca..9f40455 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
@@ -26,10 +26,27 @@
     return inValues.get(INDEX_INDEX);
   }
 
-  public int getIndexOrDefault(int defaultValue) {
-    return index().isConstant()
-        ? index().getConstInstruction().asConstInstruction().asConstNumber().getIntValue()
-        : defaultValue;
+  public int indexOrDefault(int defaultValue) {
+    int ret = indexIfConstAndInBounds(Integer.MAX_VALUE);
+    return ret == -1 ? defaultValue : ret;
+  }
+
+  public int indexIfConstAndInBounds(int size) {
+    int ret = index().getConstIntValueIfNonNegative();
+    return ret < size ? ret : -1;
+  }
+
+  public int arraySizeIfConst() {
+    Value arrayRoot = array().getAliasedValue();
+    if (arrayRoot.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmptyOrNewArrayFilled)) {
+      Instruction definition = arrayRoot.getDefinition();
+      if (definition.isNewArrayEmpty()) {
+        Value newArraySizeValue = definition.asNewArrayEmpty().size();
+        return newArraySizeValue.getConstIntValueIfNonNegative();
+      }
+      return definition.asNewArrayFilled().size();
+    }
+    return -1;
   }
 
   @Override
@@ -56,31 +73,7 @@
       AbstractValueSupplier abstractValueSupplier,
       SideEffectAssumption assumption) {
     // TODO(b/203731608): Add parameters to the method and use abstract value in R8.
-    int arraySize;
-    Value arrayRoot = array().getAliasedValue();
-    if (arrayRoot.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmptyOrNewArrayFilled)) {
-      Instruction definition = arrayRoot.getDefinition();
-      if (definition.isNewArrayEmpty()) {
-        Value newArraySizeValue = definition.asNewArrayEmpty().size();
-        if (newArraySizeValue.isConstant()) {
-          arraySize = newArraySizeValue.getConstInstruction().asConstNumber().getIntValue();
-        } else {
-          return true;
-        }
-      } else {
-        arraySize = definition.asNewArrayFilled().size();
-      }
-    } else {
-      return true;
-    }
-
-    int index;
-    if (index().isConstant()) {
-      index = index().getConstInstruction().asConstNumber().getIntValue();
-    } else {
-      return true;
-    }
-
-    return arraySize <= 0 || index < 0 || arraySize <= index;
+    int arraySize = arraySizeIfConst();
+    return arraySize < 0 || indexIfConstAndInBounds(arraySize) < 0;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 9aadaff..3570828 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -304,7 +304,7 @@
   }
 
   public List<BasicBlock> getSuccessors() {
-    return Collections.unmodifiableList(successors);
+    return ListUtils.unmodifiableForTesting(successors);
   }
 
   public List<BasicBlock> getMutableSuccessors() {
@@ -368,7 +368,7 @@
   }
 
   public List<BasicBlock> getPredecessors() {
-    return Collections.unmodifiableList(predecessors);
+    return ListUtils.unmodifiableForTesting(predecessors);
   }
 
   public List<BasicBlock> getMutablePredecessors() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 54531af..9fb677b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 import static com.android.tools.r8.ir.code.DominatorTree.Assumption.MAY_HAVE_UNREACHABLE_BLOCKS;
 
 import com.android.tools.r8.graph.AppView;
@@ -21,6 +20,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.NestUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -30,6 +30,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -41,7 +42,7 @@
   protected final BasicBlock block;
   protected final ListIterator<Instruction> listIterator;
   protected Instruction current;
-  protected Position position = null;
+  private Position position = null;
 
   private final IRMetadata metadata;
 
@@ -84,6 +85,16 @@
   }
 
   @Override
+  public Instruction peekNext() {
+    // Reset current since listIterator.remove() changes based on whether next() or previous() was
+    // last called.
+    // E.g.: next() -> current=C
+    // peekNext(): next() -> current=D, previous() -> current=D
+    current = null;
+    return IteratorUtils.peekNext(listIterator);
+  }
+
+  @Override
   public boolean hasPrevious() {
     return listIterator.hasPrevious();
   }
@@ -100,10 +111,28 @@
   }
 
   @Override
+  public Instruction peekPrevious() {
+    // Reset current since listIterator.remove() changes based on whether next() or previous() was
+    // last called.
+    // E.g.: previous() -> current=B
+    // peekPrevious(): previous() -> current=A, next() -> current=A
+    current = null;
+    return IteratorUtils.peekPrevious(listIterator);
+  }
+
+  @Override
   public boolean hasInsertionPosition() {
     return position != null;
   }
 
+  public Position getInsertionPosition() {
+    return position;
+  }
+
+  public Position getInsertionPositionOrDefault(Position defaultValue) {
+    return hasInsertionPosition() ? getInsertionPosition() : defaultValue;
+  }
+
   @Override
   public void setInsertionPosition(Position position) {
     this.position = position;
@@ -111,12 +140,12 @@
 
   @Override
   public void unsetInsertionPosition() {
-    this.position = null;
+    setInsertionPosition(null);
   }
 
   /**
-   * Adds an instruction to the block. The instruction will be added just before the current cursor
-   * position.
+   * Adds an instruction to the block. The instruction will be added just before the instruction
+   * that would be returned by a call to next().
    *
    * <p>The instruction will be assigned to the block it is added to.
    *
@@ -126,65 +155,78 @@
   public void add(Instruction instruction) {
     instruction.setBlock(block);
     assert instruction.getBlock() == block;
-    if (position != null && !instruction.hasPosition()) {
-      instruction.setPosition(position);
+    if (!instruction.hasPosition() && hasInsertionPosition()) {
+      instruction.setPosition(getInsertionPosition());
     }
     listIterator.add(instruction);
     metadata.record(instruction);
   }
 
+  private boolean hasPriorThrowingInstruction() {
+    Instruction next = peekNext();
+    for (Instruction ins : block.getInstructions()) {
+      if (ins == next) {
+        break;
+      }
+      if (ins.instructionTypeCanThrow()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   @Override
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Instruction[] instructions,
+      Collection<Instruction> instructionsToAdd,
       InternalOptions options) {
-    InstructionListIterator iterator = this;
-    if (!block.hasCatchHandlers()) {
-      iterator.addAll(instructions);
-      return iterator;
+    // Assert that we are not inserting after the final jump, and also store peekNext() for later.
+    Instruction origNext = null;
+    assert (origNext = peekNext()) != null;
+    InstructionListIterator ret =
+        addPossiblyThrowingInstructionsToPossiblyThrowingBlockImpl(
+            this, code, blockIterator, instructionsToAdd, options);
+    assert ret.peekNext() == origNext;
+    return ret;
+  }
+
+  // Use a static method to ensure dstIterator is used instead of "this".
+  private static InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlockImpl(
+      BasicBlockInstructionListIterator dstIterator,
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      Collection<Instruction> instructionsToAdd,
+      InternalOptions options) {
+    if (!dstIterator.block.hasCatchHandlers() || instructionsToAdd.isEmpty()) {
+      dstIterator.addAll(instructionsToAdd);
+      return dstIterator;
     }
-    int i = 0;
-    if (!block.canThrow()) {
-      // Add all non-throwing instructions up until the first throwing instruction.
-      for (; i < instructions.length; i++) {
-        Instruction materializingInstruction = instructions[i];
-        if (!materializingInstruction.instructionTypeCanThrow()) {
-          iterator.add(materializingInstruction);
-        } else {
-          break;
-        }
-      }
-      // Add the first throwing instruction without splitting the block.
-      if (i < instructions.length) {
-        assert instructions[i].instructionTypeCanThrow();
-        iterator.add(instructions[i]);
-        i++;
-      }
+
+    Iterator<Instruction> srcIterator = instructionsToAdd.iterator();
+
+    // If the throwing instruction is before the cursor, then we must split the block first.
+    // If there is one afterwards, we can add instructions and when we split, the throwing one
+    // will be moved to the split block.
+    boolean splitBeforeAdding = dstIterator.hasPriorThrowingInstruction();
+    if (splitBeforeAdding) {
+      BasicBlock nextBlock =
+          dstIterator.splitCopyCatchHandlers(
+              code, blockIterator, options, UnaryOperator.identity());
+      dstIterator = nextBlock.listIterator(code);
     }
-    for (; i < instructions.length; i++) {
-      BasicBlock splitBlock = iterator.splitCopyCatchHandlers(code, blockIterator, options);
-      BasicBlock previousBlock = blockIterator.positionAfterPreviousBlock(splitBlock);
-      assert previousBlock == splitBlock;
-      iterator = splitBlock.listIterator(code);
-      // Add all non-throwing instructions up until the next throwing instruction to the split
-      // block.
-      for (; i < instructions.length; i++) {
-        Instruction materializingInstruction = instructions[i];
-        if (!materializingInstruction.instructionTypeCanThrow()) {
-          iterator.add(materializingInstruction);
-        } else {
-          break;
-        }
+    do {
+      boolean addedThrowing = dstIterator.addUntilThrowing(srcIterator);
+      if (!addedThrowing || (!srcIterator.hasNext() && splitBeforeAdding)) {
+        break;
       }
-      // Add the current throwing instruction to the split block.
-      if (i < instructions.length) {
-        assert instructions[i].instructionTypeCanThrow();
-        iterator.add(instructions[i]);
-        i++;
-      }
-    }
-    return iterator;
+      BasicBlock nextBlock =
+          dstIterator.splitCopyCatchHandlers(
+              code, blockIterator, options, UnaryOperator.identity());
+      dstIterator = nextBlock.listIterator(code);
+    } while (srcIterator.hasNext());
+
+    return dstIterator;
   }
 
   @Override
@@ -317,31 +359,29 @@
     current = newInstruction;
   }
 
+  private Position getPreviousPosition() {
+    // Cannot use "current" because it is invalidated by peekNext().
+    Instruction prev = peekPrevious();
+    return prev != null ? prev.getPosition() : block.getPosition();
+  }
+
+  private void addWithPreviousPosition(Instruction instruction, InternalOptions options) {
+    instruction.setPosition(getInsertionPositionOrDefault(getPreviousPosition()), options);
+    add(instruction);
+  }
+
   @Override
   public Value insertConstNumberInstruction(
       IRCode code, InternalOptions options, long value, TypeElement type) {
     ConstNumber constNumberInstruction = code.createNumberConstant(value, type);
-    // Note that we only keep position info for throwing instructions in release mode.
-    if (!hasInsertionPosition()) {
-      Position position;
-      if (options.debug) {
-        position = current != null ? current.getPosition() : block.getPosition();
-      } else {
-        position = Position.none();
-      }
-      constNumberInstruction.setPosition(position);
-    }
-    add(constNumberInstruction);
+    addWithPreviousPosition(constNumberInstruction, options);
     return constNumberInstruction.outValue();
   }
 
   @Override
   public Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value) {
     ConstString constStringInstruction = code.createStringConstant(appView, value);
-    // Note that we only keep position info for throwing instructions in release mode.
-    constStringInstruction.setPosition(
-        appView.options().debug ? current.getPosition() : Position.none());
-    add(constStringInstruction);
+    addWithPreviousPosition(constStringInstruction, appView.options());
     return constStringInstruction.outValue();
   }
 
@@ -437,7 +477,11 @@
 
   @Override
   public void replaceCurrentInstructionWithConstClass(
-      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+      AppView<?> appView,
+      IRCode code,
+      DexType type,
+      DebugLocalInfo localInfo,
+      AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
@@ -445,7 +489,7 @@
     TypeElement typeElement = TypeElement.classClassType(appView, definitelyNotNull());
     Value value = code.createValue(typeElement, localInfo);
     ConstClass constClass = new ConstClass(value, type);
-    replaceCurrentInstruction(constClass);
+    replaceCurrentInstruction(constClass, affectedValues);
   }
 
   @Override
@@ -463,14 +507,14 @@
 
   @Override
   public void replaceCurrentInstructionWithConstString(
-      AppView<?> appView, IRCode code, DexString value) {
+      AppView<?> appView, IRCode code, DexString value, AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
 
     // Replace the instruction by const-string.
     ConstString constString = code.createStringConstant(appView, value, current.getLocalInfo());
-    replaceCurrentInstruction(constString);
+    replaceCurrentInstruction(constString, affectedValues);
   }
 
   @Override
@@ -495,16 +539,10 @@
     }
 
     // Replace the instruction by static-get.
-    TypeElement newType = TypeElement.fromDexType(field.type, maybeNull(), appView);
-    TypeElement oldType = current.getOutType();
+    TypeElement newType = field.getTypeElement(appView);
     Value value = code.createValue(newType, current.getLocalInfo());
     StaticGet staticGet = new StaticGet(value, field);
-    replaceCurrentInstruction(staticGet);
-
-    // Update affected values.
-    if (value.hasAnyUsers() && !newType.equals(oldType)) {
-      affectedValues.addAll(value.affectedValues());
-    }
+    replaceCurrentInstruction(staticGet, affectedValues);
   }
 
   @Override
@@ -558,16 +596,15 @@
     assert !throwBlockInstructionIterator.hasNext();
 
     // Replace the instruction by throw.
-    Throw throwInstruction = new Throw(exceptionValue);
-    if (hasInsertionPosition()) {
-      throwInstruction.setPosition(position);
-    } else if (toBeReplaced.getPosition().isSome()) {
-      throwInstruction.setPosition(toBeReplaced.getPosition());
-    } else {
-      assert !toBeReplaced.instructionTypeCanThrow();
-      throwInstruction.setPosition(Position.syntheticNone());
-    }
-    throwBlockInstructionIterator.replaceCurrentInstruction(throwInstruction);
+    throwBlockInstructionIterator.replaceCurrentInstruction(
+        Throw.builder()
+            .setExceptionValue(exceptionValue)
+            .setPosition(
+                getInsertionPositionOrDefault(
+                    toBeReplaced.getPosition().isSome()
+                        ? toBeReplaced.getPosition()
+                        : Position.syntheticNone()))
+            .build());
   }
 
   @Override
@@ -617,7 +654,7 @@
       throwBlockInstructionIterator = this;
     } else {
       throwBlockInstructionIterator = throwBlock.listIterator(code);
-      throwBlockInstructionIterator.setInsertionPosition(position);
+      throwBlockInstructionIterator.setInsertionPosition(getInsertionPosition());
     }
 
     // Insert constant null before the goto instruction.
@@ -629,18 +666,15 @@
     assert !throwBlockInstructionIterator.hasNext();
 
     // Replace the instruction by throw.
-    Throw throwInstruction = new Throw(nullValue);
-    if (hasInsertionPosition()) {
-      throwInstruction.setPosition(position);
-    } else if (toBeReplaced.getPosition().isSome()) {
-      throwInstruction.setPosition(toBeReplaced.getPosition());
-    } else {
-      // The instruction that is being removed cannot throw, and thus it must be unreachable as we
-      // are replacing it by `throw null`, so we can safely use a none-position.
-      assert !toBeReplaced.instructionTypeCanThrow();
-      throwInstruction.setPosition(Position.syntheticNone());
-    }
-    throwBlockInstructionIterator.replaceCurrentInstruction(throwInstruction);
+    throwBlockInstructionIterator.replaceCurrentInstruction(
+        Throw.builder()
+            .setExceptionValue(nullValue)
+            .setPosition(
+                getInsertionPositionOrDefault(
+                    toBeReplaced.getPosition().isSome()
+                        ? toBeReplaced.getPosition()
+                        : Position.syntheticNone()))
+            .build());
 
     if (block.hasCatchHandlers()) {
       if (block == throwBlock) {
@@ -680,7 +714,7 @@
     assert hasNext();
 
     // Get the position at which the block is being split.
-    Position position = current != null ? current.getPosition() : block.getPosition();
+    Position position = getPreviousPosition();
 
     // Prepare the new block, placing the exception handlers on the block with the throwing
     // instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 41ad960..9beaff7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.lightir.LirBuilder;
+import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
 import java.util.ListIterator;
 
@@ -72,7 +73,11 @@
 
   @Override
   public String toString() {
-    if (getBlock() != null && !getBlock().getSuccessors().isEmpty()) {
+    BasicBlock myBlock = getBlock();
+    // Avoids BasicBlock.exit(), since it will assert when block is invalid.
+    if (myBlock != null
+        && !myBlock.getSuccessors().isEmpty()
+        && ListUtils.last(myBlock.getInstructions()) == this) {
       return super.toString() + "block " + getTarget().getNumberAsString();
     }
     return super.toString() + "block <unknown>";
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 4874229..422824f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
 import java.util.ListIterator;
@@ -23,7 +24,7 @@
 
 public class IRCodeInstructionListIterator implements InstructionListIterator {
 
-  private final ListIterator<BasicBlock> blockIterator;
+  private final BasicBlockIterator blockIterator;
   private InstructionListIterator instructionIterator;
 
   private final IRCode code;
@@ -71,8 +72,13 @@
 
   @Override
   public void replaceCurrentInstructionWithConstClass(
-      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
-    instructionIterator.replaceCurrentInstructionWithConstClass(appView, code, type, localInfo);
+      AppView<?> appView,
+      IRCode code,
+      DexType type,
+      DebugLocalInfo localInfo,
+      AffectedValues affectedValues) {
+    instructionIterator.replaceCurrentInstructionWithConstClass(
+        appView, code, type, localInfo, affectedValues);
   }
 
   @Override
@@ -82,8 +88,9 @@
 
   @Override
   public void replaceCurrentInstructionWithConstString(
-      AppView<?> appView, IRCode code, DexString value) {
-    instructionIterator.replaceCurrentInstructionWithConstString(appView, code, value);
+      AppView<?> appView, IRCode code, DexString value, AffectedValues affectedValues) {
+    instructionIterator.replaceCurrentInstructionWithConstString(
+        appView, code, value, affectedValues);
   }
 
   @Override
@@ -174,6 +181,16 @@
   }
 
   @Override
+  public Instruction peekNext() {
+    // Default impl calls next() / previous(), which affects what remove() does.
+    Instruction next = instructionIterator.peekNext();
+    if (next == null && blockIterator.hasNext()) {
+      next = blockIterator.peekNext().entry();
+    }
+    return next;
+  }
+
+  @Override
   public boolean hasPrevious() {
     return instructionIterator.hasPrevious() || blockIterator.hasPrevious();
   }
@@ -193,6 +210,16 @@
   }
 
   @Override
+  public Instruction peekPrevious() {
+    // Default impl calls next() / previous(), which affects what remove() does.
+    Instruction previous = instructionIterator.peekPrevious();
+    if (previous == null && blockIterator.hasPrevious()) {
+      previous = blockIterator.peekPrevious().exit();
+    }
+    return previous;
+  }
+
+  @Override
   public int nextIndex() {
     throw new UnsupportedOperationException();
   }
@@ -211,10 +238,10 @@
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Instruction[] instructions,
+      Collection<Instruction> instructionsToAdd,
       InternalOptions options) {
     return instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
-        code, blockIterator, instructions, options);
+        code, blockIterator, instructionsToAdd, options);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 034ec81..d0426ec 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 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.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
@@ -88,7 +89,7 @@
   }
 
   public void setPosition(Position position) {
-    assert this.position == null;
+    assert !hasPosition();
     this.position = position;
   }
 
@@ -1328,6 +1329,10 @@
     return false;
   }
 
+  public boolean isInvokeMethod(DexMethod invokedMethod) {
+    return false;
+  }
+
   public InvokeMethod asInvokeMethod() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 247079e..3588346 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -13,11 +13,14 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.ListIterator;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -33,12 +36,39 @@
     }
   }
 
+  default void addAll(Collection<Instruction> instructions) {
+    for (Instruction instruction : instructions) {
+      add(instruction);
+    }
+  }
+
+  default boolean addUntilThrowing(Iterator<Instruction> srcIterator) {
+    while (srcIterator.hasNext()) {
+      // Add all non-throwing instructions up until the first throwing instruction.
+      Instruction instruction = srcIterator.next();
+      add(instruction);
+      if (instruction.instructionTypeCanThrow()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Instruction[] instructions,
+      Collection<Instruction> instructionsToAdd,
       InternalOptions options);
 
+  default InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      Instruction[] instructionsToAdd,
+      InternalOptions options) {
+    return addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+        code, blockIterator, Arrays.asList(instructionsToAdd), options);
+  }
+
   BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
       IRCode code,
       ListIterator<BasicBlock> blockIterator,
@@ -166,7 +196,11 @@
   }
 
   void replaceCurrentInstructionWithConstClass(
-      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo);
+      AppView<?> appView,
+      IRCode code,
+      DexType type,
+      DebugLocalInfo localInfo,
+      AffectedValues affectedValues);
 
   default void replaceCurrentInstructionWithConstFalse(IRCode code) {
     replaceCurrentInstructionWithConstInt(code, 0);
@@ -174,18 +208,13 @@
 
   void replaceCurrentInstructionWithConstInt(IRCode code, int value);
 
-  void replaceCurrentInstructionWithConstString(AppView<?> appView, IRCode code, DexString value);
+  void replaceCurrentInstructionWithConstString(
+      AppView<?> appView, IRCode code, DexString value, AffectedValues affectedValues);
 
   default void replaceCurrentInstructionWithConstTrue(IRCode code) {
     replaceCurrentInstructionWithConstInt(code, 1);
   }
 
-  default void replaceCurrentInstructionWithConstString(
-      AppView<?> appView, IRCode code, String value) {
-    replaceCurrentInstructionWithConstString(
-        appView, code, appView.dexItemFactory().createString(value));
-  }
-
   void replaceCurrentInstructionWithNullCheck(AppView<?> appView, Value object);
 
   void replaceCurrentInstructionWithStaticGet(
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index 434272d..4a9b217 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -33,7 +33,6 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.optimize.library.LibraryOptimizationInfoCollection;
@@ -114,6 +113,11 @@
   }
 
   @Override
+  public boolean isInvokeMethod(DexMethod invokedMethod) {
+    return getInvokedMethod().isIdenticalTo(invokedMethod);
+  }
+
+  @Override
   public InvokeMethod asInvokeMethod() {
     return this;
   }
@@ -230,9 +234,8 @@
     return result;
   }
 
-  public abstract InlineAction computeInlining(
+  public abstract InlineAction.Builder computeInlining(
       ProgramMethod singleTarget,
-      Reason reason,
       DefaultInliningOracle decider,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index edc96f6..58ad2ed 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -21,7 +21,6 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
@@ -55,14 +54,12 @@
   }
 
   @Override
-  public final InlineAction computeInlining(
+  public final InlineAction.Builder computeInlining(
       ProgramMethod singleTarget,
-      Reason reason,
       DefaultInliningOracle decider,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    return decider.computeForInvokeWithReceiver(
-        this, singleTarget, reason, whyAreYouNotInliningReporter);
+    return decider.computeForInvokeWithReceiver(this, singleTarget, whyAreYouNotInliningReporter);
   }
 
   /**
@@ -93,7 +90,7 @@
   }
 
   protected boolean isPrivateNestMethodInvoke(DexBuilder builder) {
-    if (!builder.getOptions().emitNestAnnotationsInDex) {
+    if (!builder.getOptions().canUseNestBasedAccess()) {
       return false;
     }
     DexProgramClass holder = builder.getProgramMethod().getHolder();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index 0061bf3..273a263 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.lightir.LirBuilder;
@@ -147,19 +146,11 @@
   }
 
   @Override
-  public InlineAction computeInlining(
+  public InlineAction.Builder computeInlining(
       ProgramMethod singleTarget,
-      Reason reason,
       DefaultInliningOracle decider,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    // We never determine a single target for invoke-polymorphic.
-    if (singleTarget != null) {
-      throw new Unreachable(
-          "Unexpected invoke-polymorphic with `"
-              + singleTarget.toSourceString()
-              + "` as single target");
-    }
-    throw new Unreachable("Unexpected attempt to inline invoke that does not have a single target");
+    throw new Unreachable();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index f1d4aea..9d5c5bf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.lightir.LirBuilder;
@@ -120,14 +119,13 @@
   }
 
   @Override
-  public InlineAction computeInlining(
+  public InlineAction.Builder computeInlining(
       ProgramMethod singleTarget,
-      Reason reason,
       DefaultInliningOracle decider,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return decider.computeForInvokeStatic(
-        this, singleTarget, reason, classInitializationAnalysis, whyAreYouNotInliningReporter);
+        this, singleTarget, classInitializationAnalysis, whyAreYouNotInliningReporter);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeType.java b/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
index 9d9fad1..4e015d0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
@@ -137,7 +137,7 @@
         context.getHolder().isInterface()
             || (appView.hasVerticallyMergedClasses()
                 && appView
-                    .verticallyMergedClasses()
+                    .getVerticallyMergedClasses()
                     .hasInterfaceBeenMergedIntoSubtype(originalContext.getHolderType()));
     if (originalContextIsInterface) {
       // On interfaces invoke-special should be mapped to invoke-super if the invoke-special
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 24e7793..5eb6dfd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
 import java.util.Collection;
@@ -100,8 +101,13 @@
 
   @Override
   public void replaceCurrentInstructionWithConstClass(
-      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
-    currentBlockIterator.replaceCurrentInstructionWithConstClass(appView, code, type, localInfo);
+      AppView<?> appView,
+      IRCode code,
+      DexType type,
+      DebugLocalInfo localInfo,
+      AffectedValues affectedValues) {
+    currentBlockIterator.replaceCurrentInstructionWithConstClass(
+        appView, code, type, localInfo, affectedValues);
   }
 
   @Override
@@ -111,8 +117,9 @@
 
   @Override
   public void replaceCurrentInstructionWithConstString(
-      AppView<?> appView, IRCode code, DexString value) {
-    currentBlockIterator.replaceCurrentInstructionWithConstString(appView, code, value);
+      AppView<?> appView, IRCode code, DexString value, AffectedValues affectedValues) {
+    currentBlockIterator.replaceCurrentInstructionWithConstString(
+        appView, code, value, affectedValues);
   }
 
   @Override
@@ -192,10 +199,10 @@
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Instruction[] instructions,
+      Collection<Instruction> instructionsToAdd,
       InternalOptions options) {
     return currentBlockIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
-        code, blockIterator, instructions, options);
+        code, blockIterator, instructionsToAdd, options);
   }
 
   @Override
@@ -249,6 +256,27 @@
     return currentBlockIterator.next();
   }
 
+  @Override
+  public Instruction peekNext() {
+    // Default impl calls next() / previous(), which will alter this.seen.
+    Instruction current = currentBlockIterator.peekNext();
+    if (!current.isGoto()) {
+      return current;
+    }
+    BasicBlock target = current.asGoto().getTarget();
+    if (!isLinearEdge(currentBlock, target)) {
+      return current;
+    }
+    while (target.isTrivialGoto()) {
+      BasicBlock candidate = target.exit().asGoto().getTarget();
+      if (!isLinearEdge(target, candidate)) {
+        break;
+      }
+      target = candidate;
+    }
+    return target.entry();
+  }
+
   private BasicBlock getBeginningOfTrivialLinearGotoChain(BasicBlock block) {
     if (block.getPredecessors().size() != 1
         || !isLinearEdge(block.getPredecessors().get(0), block)) {
@@ -290,6 +318,20 @@
   }
 
   @Override
+  public Instruction peekPrevious() {
+    // Default impl calls next() / previous(), which will alter this.seen.
+    Instruction ret = currentBlockIterator.peekPrevious();
+    if (ret != null) {
+      return ret;
+    }
+    BasicBlock target = getBeginningOfTrivialLinearGotoChain(currentBlock);
+    if (target == null || target.size() < 2) {
+      return null;
+    }
+    return target.getInstructions().get(target.size() - 2);
+  }
+
+  @Override
   public int nextIndex() {
     throw new UnsupportedOperationException();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Throw.java b/src/main/java/com/android/tools/r8/ir/code/Throw.java
index e873dd8..ab5db40 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Throw.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Throw.java
@@ -22,6 +22,10 @@
     super(exception);
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.THROW;
@@ -121,4 +125,24 @@
     }
     return false;
   }
+
+  public static class Builder extends BuilderBase<Builder, Throw> {
+
+    private Value exceptionValue;
+
+    public Builder setExceptionValue(Value exceptionValue) {
+      this.exceptionValue = exceptionValue;
+      return this;
+    }
+
+    @Override
+    public Throw build() {
+      return amend(new Throw(exceptionValue));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index f08a4e2..e35a726 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -38,7 +38,6 @@
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.LongInterval;
 import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableSet;
@@ -378,25 +377,19 @@
   }
 
   public Set<Instruction> aliasedUsers(AliasedValueConfiguration configuration) {
-    Set<Instruction> users = SetUtils.newIdentityHashSet(uniqueUsers());
-    Set<Instruction> visited = Sets.newIdentityHashSet();
-    collectAliasedUsersViaAssume(configuration, visited, uniqueUsers(), users);
+    Set<Instruction> users = Sets.newIdentityHashSet();
+    collectAliasedUsersViaAssume(configuration, this, users);
     return users;
   }
 
   private static void collectAliasedUsersViaAssume(
-      AliasedValueConfiguration configuration,
-      Set<Instruction> visited,
-      Set<Instruction> usersToTest,
-      Set<Instruction> collectedUsers) {
-    for (Instruction user : usersToTest) {
-      if (!visited.add(user)) {
+      AliasedValueConfiguration configuration, Value value, Set<Instruction> collectedUsers) {
+    for (Instruction user : value.uniqueUsers()) {
+      if (!collectedUsers.add(user)) {
         continue;
       }
       if (configuration.isIntroducingAnAlias(user)) {
-        collectedUsers.addAll(user.outValue().uniqueUsers());
-        collectAliasedUsersViaAssume(
-            configuration, visited, user.outValue().uniqueUsers(), collectedUsers);
+        collectAliasedUsersViaAssume(configuration, user.outValue(), collectedUsers);
       }
     }
   }
@@ -1020,6 +1013,18 @@
         && getConstInstruction().asConstNumber().isZero();
   }
 
+  public int getConstIntValueIfNonNegative() {
+    if (!isConstant()) {
+      return -1;
+    }
+    ConstNumber constNumber = definition.asConstNumber();
+    if (constNumber == null) {
+      return -1;
+    }
+    int ret = constNumber.getIntValue();
+    return ret >= 0 ? ret : -1;
+  }
+
   /**
    * Overwrites the current type lattice value without any assertions.
    *
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index b47b900..0e13017 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -69,6 +69,7 @@
 import com.android.tools.r8.ir.optimize.membervaluepropagation.D8MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.R8MemberValuePropagation;
+import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxer;
 import com.android.tools.r8.ir.optimize.outliner.Outliner;
 import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.lightir.IR2LirConverter;
@@ -127,6 +128,7 @@
   private final TypeChecker typeChecker;
   protected ServiceLoaderRewriter serviceLoaderRewriter;
   protected final EnumUnboxer enumUnboxer;
+  protected final NumberUnboxer numberUnboxer;
   protected InstanceInitializerOutliner instanceInitializerOutliner;
   protected final RemoveVerificationErrorForUnknownReturnedValues
       removeVerificationErrorForUnknownReturnedValues;
@@ -214,6 +216,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumUnboxer = EnumUnboxer.empty();
+      this.numberUnboxer = NumberUnboxer.empty();
       this.assumeInserter = null;
       this.instanceInitializerOutliner = null;
       this.removeVerificationErrorForUnknownReturnedValues = null;
@@ -255,6 +258,7 @@
               ? new LibraryMethodOverrideAnalysis(appViewWithLiveness)
               : null;
       this.enumUnboxer = EnumUnboxer.create(appViewWithLiveness);
+      this.numberUnboxer = NumberUnboxer.create(appViewWithLiveness);
       this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, enumUnboxer);
       this.inliner = new Inliner(appViewWithLiveness, this, lensCodeRewriter);
       this.outliner = Outliner.create(appViewWithLiveness);
@@ -293,6 +297,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumUnboxer = EnumUnboxer.empty();
+      this.numberUnboxer = NumberUnboxer.empty();
     }
     this.stringSwitchRemover =
         options.isStringSwitchConversionEnabled()
@@ -928,8 +933,13 @@
     appView.withArgumentPropagator(
         argumentPropagator -> argumentPropagator.scan(method, code, methodProcessor, timing));
 
+    if (methodProcessor.isComposeMethodProcessor()) {
+      methodProcessor.asComposeMethodProcessor().scan(method, code, timing);
+    }
+
     if (methodProcessor.isPrimaryMethodProcessor()) {
       enumUnboxer.analyzeEnums(code, methodProcessor);
+      numberUnboxer.analyze(code);
     }
 
     if (inliner != null) {
@@ -1142,6 +1152,7 @@
     assert method.getHolder().lookupMethod(method.getReference()) == null;
     appView.withArgumentPropagator(argumentPropagator -> argumentPropagator.onMethodPruned(method));
     enumUnboxer.onMethodPruned(method);
+    numberUnboxer.onMethodPruned(method);
     outliner.onMethodPruned(method);
     if (inliner != null) {
       inliner.onMethodPruned(method);
@@ -1159,6 +1170,7 @@
     appView.withArgumentPropagator(
         argumentPropagator -> argumentPropagator.onMethodCodePruned(method));
     enumUnboxer.onMethodCodePruned(method);
+    numberUnboxer.onMethodCodePruned(method);
     outliner.onMethodCodePruned(method);
     if (inliner != null) {
       inliner.onMethodCodePruned(method);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
index 87f9a4b..c5e44d0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
@@ -33,7 +33,7 @@
   @Override
   public DexCode finalizeCode(
       IRCode code, BytecodeMetadataProvider bytecodeMetadataProvider, Timing timing) {
-    if (options.emitNestAnnotationsInDex) {
+    if (options.canUseNestBasedAccess()) {
       D8NestBasedAccessDesugaring.checkAndFailOnIncompleteNests(appView);
     }
     DexEncodedMethod method = code.method();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 07ea7c8..9cb88c3 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -50,7 +50,6 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.graph.lens.FieldLookupResult;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
@@ -115,6 +114,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.verticalclassmerging.InterfaceTypeToClassTypeLensCodeRewriterHelper;
+import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -272,7 +272,7 @@
     boolean mayHaveUnreachableBlocks = false;
     while (blocks.hasNext()) {
       BasicBlock block = blocks.next();
-      if (block.hasCatchHandlers() && options.enableVerticalClassMerging) {
+      if (block.hasCatchHandlers() && !appView.getVerticallyMergedClasses().isEmpty()) {
         boolean anyGuardsRenamed = block.renameGuardsInCatchHandlers(graphLens, codeLens);
         if (anyGuardsRenamed) {
           mayHaveUnreachableBlocks |= unlinkDeadCatchHandlers(block, graphLens, codeLens);
@@ -1309,7 +1309,7 @@
   // A and B although this will lead to invalid code, because this code pattern does generally
   // not occur in practice (it leads to a verification error on the JVM, but not on Art).
   private void checkInvokeDirect(DexMethod method, InvokeDirect invoke) {
-    VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
+    VerticallyMergedClasses verticallyMergedClasses = appView.getVerticallyMergedClasses();
     if (verticallyMergedClasses == null) {
       // No need to check the invocation.
       return;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index e5a0728..8f4c441 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -54,7 +54,7 @@
 
   void markAsPropagated(DexEncodedMethod method);
 
-  void setBridgeInfo(DexEncodedMethod method, BridgeInfo bridgeInfo);
+  void setBridgeInfo(ProgramMethod method, BridgeInfo bridgeInfo);
 
   void setClassInlinerMethodConstraint(
       ProgramMethod method, ClassInlinerMethodConstraint classInlinerConstraint);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
index 15dc3d2..a80055a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -5,9 +5,18 @@
 
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
+import com.android.tools.r8.optimize.compose.ComposeMethodProcessor;
 
 public abstract class MethodProcessor {
 
+  public boolean isComposeMethodProcessor() {
+    return false;
+  }
+
+  public ComposeMethodProcessor asComposeMethodProcessor() {
+    return null;
+  }
+
   public boolean isPrimaryMethodProcessor() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
index 8d33d03..d56bdfb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
@@ -98,7 +98,6 @@
     InternalOptions options = appView.options();
     Deque<ProgramMethodSet> waves = new ArrayDeque<>();
     Collection<Node> nodes = callGraph.getNodes();
-    int waveCount = 1;
     while (!nodes.isEmpty()) {
       ProgramMethodSet wave = callGraph.extractLeaves();
       waves.addLast(wave);
@@ -127,11 +126,11 @@
       throws ExecutionException {
     TimingMerger merger = timing.beginMerger("primary-processor", executorService);
     while (!waves.isEmpty()) {
-      processorContext = appView.createProcessorContext();
       wave = waves.removeFirst();
       assert !wave.isEmpty();
       assert waveExtension.isEmpty();
       do {
+        processorContext = appView.createProcessorContext();
         waveStartAction.notifyWaveStart(wave);
         Collection<Timing> timings =
             ThreadUtils.processItemsWithResults(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 1a4a2ec..6b6f2ad 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
+import com.android.tools.r8.optimize.compose.ComposableOptimizationPass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -80,6 +81,7 @@
     appView.withArgumentPropagator(
         argumentPropagator -> argumentPropagator.initializeCodeScanner(executorService, timing));
     enumUnboxer.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
+    numberUnboxer.prepareForPrimaryOptimizationPass(timing, executorService);
     outliner.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
 
     if (fieldAccessAnalysis != null) {
@@ -142,6 +144,7 @@
     // the parameter optimization infos, and rewrite the application.
     // TODO(b/199237357): Automatically rewrite state when lens changes.
     enumUnboxer.rewriteWithLens();
+    numberUnboxer.rewriteWithLens();
     outliner.rewriteWithLens();
     appView.withArgumentPropagator(
         argumentPropagator ->
@@ -157,11 +160,16 @@
           .run(executorService, feedback, timing);
     }
 
+    numberUnboxer.rewriteWithLens();
     outliner.rewriteWithLens();
     enumUnboxer.unboxEnums(
         appView, this, postMethodProcessorBuilder, executorService, feedback, timing);
     appView.unboxedEnums().checkEnumsUnboxed(appView);
 
+    numberUnboxer.rewriteWithLens();
+    outliner.rewriteWithLens();
+    numberUnboxer.unboxNumbers(postMethodProcessorBuilder, timing, executorService);
+
     GraphLens graphLensForSecondaryOptimizationPass = appView.graphLens();
 
     outliner.rewriteWithLens();
@@ -226,6 +234,8 @@
       identifierNameStringMarker.decoupleIdentifierNameStringsInFields(executorService);
     }
 
+    ComposableOptimizationPass.run(appView, this, timing);
+
     // Assure that no more optimization feedback left after post processing.
     assert feedback.noUpdatesLeft();
     return appView.appInfo().app();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
index ca4492f..e92ea14 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
@@ -111,6 +111,9 @@
 
         int numberOfCallSites = node.getNumberOfCallSites();
         if (numberOfCallSites == 1) {
+          if (appView.appInfo().isNeverInlineDueToSingleCallerMethod(method)) {
+            continue;
+          }
           Set<Node> callersWithDeterministicOrder = node.getCallersWithDeterministicOrder();
           DexMethod caller = reference;
           // We can have recursive methods where the recursive call is the only call site. We do
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
index eb09ac7..809f647 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
@@ -7,16 +7,14 @@
 import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 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.DexField;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.LookupResult;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -25,7 +23,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
-public class InvokeExtractor<N extends NodeBase<N>> extends UseRegistry<ProgramMethod> {
+public class InvokeExtractor<N extends NodeBase<N>> extends DefaultUseRegistry<ProgramMethod> {
 
   protected final AppView<AppInfoWithLiveness> appViewWithLiveness;
   protected final N currentMethod;
@@ -200,34 +198,4 @@
   public void registerInvokeVirtual(DexMethod method) {
     processInvoke(InvokeType.VIRTUAL, method);
   }
-
-  @Override
-  public void registerInitClass(DexType type) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
-
-  @Override
-  public void registerInstanceFieldRead(DexField field) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
-
-  @Override
-  public void registerInstanceFieldWrite(DexField field) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
-
-  @Override
-  public void registerStaticFieldRead(DexField field) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
-
-  @Override
-  public void registerStaticFieldWrite(DexField field) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
-
-  @Override
-  public void registerTypeReference(DexType type) {
-    // Intentionally empty. This use registry is only tracing method calls.
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index 3af88e1..2a93788 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -247,19 +247,13 @@
       if (arrayPut == null || arrayPut.array() != arrayValue) {
         return null;
       }
-      if (!arrayPut.index().isConstNumber()) {
+      int index = arrayPut.indexIfConstAndInBounds(values.length);
+      if (index < 0 || values[index] != null) {
         return null;
       }
       if (arrayPut.instructionInstanceCanThrow(appView, code.context())) {
         return null;
       }
-      int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
-      if (index < 0 || index >= values.length) {
-        return null;
-      }
-      if (values[index] != null) {
-        return null;
-      }
       Value value = arrayPut.value();
       if (needsTypeCheck && !value.isAlwaysNull(appView)) {
         DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index 740c180..89b6f2d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -232,10 +232,6 @@
   }
 
   private void shortenLiveRanges(IRCode code, ConstantCanonicalizer canonicalizer) {
-    if (options.debug) {
-      // Shorten live ranges seems to regress code size in debug mode.
-      return;
-    }
     if (options.testing.disableShortenLiveRanges) {
       return;
     }
@@ -244,11 +240,7 @@
     LinkedList<BasicBlock> blocks = code.blocks;
     for (BasicBlock block : blocks) {
       shortenLiveRangesInsideBlock(
-          code,
-          block,
-          dominatorTreeMemoization,
-          addConstantInBlock,
-          canonicalizer::isConstantCanonicalizationCandidate);
+          code, block, dominatorTreeMemoization, addConstantInBlock, canonicalizer::isConstant);
     }
 
     // Heuristic to decide if constant instructions are shared in dominator block
@@ -446,10 +438,13 @@
       DominatorTree dominatorTree = dominatorTreeMemoization.computeIfAbsent();
       BasicBlock dominator = dominatorTree.closestDominator(userBlocks);
 
+      // Do not move the constant if the constant instruction can throw and the dominator or the
+      // original block has catch handlers, or if the code may have monitor instructions, since this
+      // could lead to verification errors.
       if (instruction.instructionTypeCanThrow()) {
-        if (block.hasCatchHandlers() || dominator.hasCatchHandlers()) {
-          // Do not move the constant if the constant instruction can throw
-          // and the dominator or the original block has catch handlers.
+        if (block.hasCatchHandlers()
+            || dominator.hasCatchHandlers()
+            || code.metadata().mayHaveMonitorInstruction()) {
           continue;
         }
       }
@@ -489,7 +484,8 @@
       addConstantInBlock
           .computeIfAbsent(dominator, k -> new LinkedHashMap<>())
           .put(copy.outValue(), copy);
-      assert iterator.peekPrevious() == instruction;
+      // Using peekPrevious() would disable remove().
+      assert iterator.previous() == instruction && iterator.next() == instruction;
       iterator.removeOrReplaceByDebugLocalRead();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
index bcace75..a2dd4ef 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryAPIConverter;
@@ -29,6 +30,10 @@
 
   public static CfInstructionDesugaringCollection create(
       AppView<?> appView, AndroidApiLevelCompute apiLevelCompute) {
+    if (appView.options().desugarState.isOff() && appView.options().forceNestDesugaring) {
+      throw new CompilationError(
+          "Cannot combine -Dcom.android.tools.r8.forceNestDesugaring with desugaring turned off");
+    }
     if (appView.options().desugarState.isOn()) {
       return new NonEmptyCfInstructionDesugaringCollection(appView, apiLevelCompute);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
index cb907d1..929f57a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
@@ -209,7 +209,6 @@
             });
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private static <T extends DexItem> void deduplicateEmulatedInterfaceFlags(
       Map<T, HumanEmulatedInterfaceDescriptor> flags,
       Map<T, HumanEmulatedInterfaceDescriptor> otherFlags,
@@ -217,7 +216,7 @@
       BiConsumer<T, HumanEmulatedInterfaceDescriptor> specific) {
     flags.forEach(
         (k, v) -> {
-          if (otherFlags.get(k) == v) {
+          if (otherFlags.get(k).equals(v)) {
             common.accept(k, v);
           } else {
             specific.accept(k, v);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
index d5dc102..7973e0c 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification;
 
 import static com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecificationParser.CONFIGURATION_FORMAT_VERSION_KEY;
+import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.AMEND_LIBRARY_FIELD_KEY;
 import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.AMEND_LIBRARY_METHOD_KEY;
 import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.API_GENERIC_TYPES_CONVERSION;
 import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.API_LEVEL_BELOW_OR_EQUAL_KEY;
@@ -172,7 +173,7 @@
         toJson.put(AMEND_LIBRARY_METHOD_KEY, amendLibraryToString(flags.getAmendLibraryMethod()));
       }
       if (!flags.getAmendLibraryField().isEmpty()) {
-        toJson.put(AMEND_LIBRARY_METHOD_KEY, amendLibraryToString(flags.getAmendLibraryField()));
+        toJson.put(AMEND_LIBRARY_FIELD_KEY, amendLibraryToString(flags.getAmendLibraryField()));
       }
       list.add(toJson);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
index 14257d8..4fdeb0f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ClasspathMethod;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -13,10 +14,8 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.D8MethodProcessor;
 import com.android.tools.r8.profile.rewriting.ProfileRewritingNestBasedAccessDesugaringEventConsumer;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -138,7 +137,7 @@
                 new NestBasedAccessDesugaringUseRegistry(method, eventConsumer)));
   }
 
-  private class NestBasedAccessDesugaringUseRegistry extends UseRegistry<ClasspathMethod> {
+  private class NestBasedAccessDesugaringUseRegistry extends DefaultUseRegistry<ClasspathMethod> {
 
     private final NestBasedAccessDesugaringEventConsumer eventConsumer;
 
@@ -294,25 +293,5 @@
     public void registerStaticFieldWrite(DexField field) {
       registerFieldAccessFromClasspath(field, false);
     }
-
-    @Override
-    public void registerInitClass(DexType clazz) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerInstanceOf(DexType type) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {
-      // Intentionally empty.
-    }
   }
 }
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 cdd49d1..1b14342 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
@@ -403,6 +403,27 @@
   }
 
   public boolean isConstantCanonicalizationCandidate(Instruction instruction) {
+    if (!isConstant(instruction)) {
+      return false;
+    }
+    // Do not canonicalize throwing instructions if there are monitor operations in the code.
+    // That could lead to unbalanced locking and could lead to situations where OOM exceptions
+    // could leave a synchronized method without unlocking the monitor.
+    if (instruction.instructionTypeCanThrow() && code.metadata().mayHaveMonitorInstruction()) {
+      return false;
+    }
+    // Constants that are used by invoke range are not canonicalized to be compliant with the
+    // optimization splitRangeInvokeConstant that gives the register allocator more freedom in
+    // assigning register to ranged invokes which can greatly reduce the number of register
+    // needed (and thereby code size as well). Thus no need to do a transformation that should
+    // be removed later by another optimization.
+    if (constantUsedByInvokeRange(instruction)) {
+      return false;
+    }
+    return true;
+  }
+
+  public boolean isConstant(Instruction instruction) {
     // Interested only in instructions types that can be canonicalized, i.e., ConstClass,
     // ConstNumber, (DexItemBased)?ConstString, InstanceGet and StaticGet.
     switch (instruction.opcode()) {
@@ -460,20 +481,6 @@
     if (instruction.outValue().hasLocalInfo()) {
       return false;
     }
-    // Do not canonicalize throwing instructions if there are monitor operations in the code.
-    // That could lead to unbalanced locking and could lead to situations where OOM exceptions
-    // could leave a synchronized method without unlocking the monitor.
-    if (instruction.instructionTypeCanThrow() && code.metadata().mayHaveMonitorInstruction()) {
-      return false;
-    }
-    // Constants that are used by invoke range are not canonicalized to be compliant with the
-    // optimization splitRangeInvokeConstant that gives the register allocator more freedom in
-    // assigning register to ranged invokes which can greatly reduce the number of register
-    // needed (and thereby code size as well). Thus no need to do a transformation that should
-    // be removed later by another optimization.
-    if (constantUsedByInvokeRange(instruction)) {
-      return false;
-    }
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 1c9f9fe..3117467 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -7,6 +7,8 @@
 import static com.android.tools.r8.ir.optimize.inliner.InlinerUtils.collectAllMonitorEnterValues;
 import static com.android.tools.r8.utils.AndroidApiLevelUtils.isApiSafeForInlining;
 
+import com.android.tools.r8.dex.code.DexCheckCast;
+import com.android.tools.r8.dex.code.DexInvokeStatic;
 import com.android.tools.r8.dex.code.DexMoveResult;
 import com.android.tools.r8.dex.code.DexMoveResultObject;
 import com.android.tools.r8.dex.code.DexMoveResultWide;
@@ -16,6 +18,8 @@
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.BoxUnboxPrimitiveMethodRoundtrip;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
@@ -23,7 +27,10 @@
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
@@ -36,7 +43,6 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
-import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.Inliner.RetryAction;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
@@ -52,6 +58,7 @@
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 public final class DefaultInliningOracle implements InliningOracle, InliningStrategy {
@@ -126,23 +133,18 @@
 
   @Override
   public boolean passesInliningConstraints(
-      InvokeMethod invoke,
       SingleResolutionResult<?> resolutionResult,
       ProgramMethod singleTarget,
-      Reason reason,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     // Do not inline if the inlinee is greater than the api caller level.
     // TODO(b/188498051): We should not force inline lower api method calls.
-    if (reason != Reason.FORCE
-        && !isApiSafeForInlining(method, singleTarget, options, whyAreYouNotInliningReporter)) {
+    if (!isApiSafeForInlining(method, singleTarget, options, whyAreYouNotInliningReporter)) {
       return false;
     }
 
     // We don't inline into constructors when producing class files since this can mess up
     // the stackmap, see b/136250031
-    if (method.getDefinition().isInstanceInitializer()
-        && options.isGeneratingClassFiles()
-        && reason != Reason.FORCE) {
+    if (method.getDefinition().isInstanceInitializer() && options.isGeneratingClassFiles()) {
       whyAreYouNotInliningReporter.reportNoInliningIntoConstructorsWhenGeneratingClassFiles();
       return false;
     }
@@ -164,7 +166,7 @@
     // or optimized code. Right now this happens for the class class staticizer, as it just
     // processes all relevant methods in parallel with the full optimization pipeline enabled.
     // TODO(sgjesse): Add this assert "assert !isProcessedConcurrently.test(candidate);"
-    if (reason != Reason.FORCE && methodProcessor.isProcessedConcurrently(singleTarget)) {
+    if (methodProcessor.isProcessedConcurrently(singleTarget)) {
       whyAreYouNotInliningReporter.reportProcessedConcurrently();
       return false;
     }
@@ -174,34 +176,21 @@
       return false;
     }
 
-    Set<Reason> validInliningReasons = appView.testing().validInliningReasons;
-    if (validInliningReasons != null && !validInliningReasons.contains(reason)) {
-      whyAreYouNotInliningReporter.reportInvalidInliningReason(reason, validInliningReasons);
-      return false;
-    }
-
     // Abort inlining attempt if method -> target access is not right.
     if (resolutionResult.isAccessibleFrom(method, appView).isPossiblyFalse()) {
       whyAreYouNotInliningReporter.reportInaccessible();
       return false;
     }
 
-    if (reason == Reason.SIMPLE && !satisfiesRequirementsForSimpleInlining(invoke, singleTarget)) {
-      whyAreYouNotInliningReporter.reportInlineeNotSimple();
-      return false;
-    }
-
     // Don't inline code with references beyond root main dex classes into a root main dex class.
     // If we do this it can increase the size of the main dex dependent classes.
-    if (reason != Reason.FORCE
-        && mainDexInfo.disallowInliningIntoContext(
-            appView, method, singleTarget, appView.getSyntheticItems())) {
+    if (mainDexInfo.disallowInliningIntoContext(
+        appView, method, singleTarget, appView.getSyntheticItems())) {
       whyAreYouNotInliningReporter.reportInlineeRefersToClassesNotInMainDex();
       return false;
     }
-    assert reason != Reason.FORCE
-        || !mainDexInfo.disallowInliningIntoContext(
-            appView, method, singleTarget, appView.getSyntheticItems());
+    assert !mainDexInfo.disallowInliningIntoContext(
+        appView, method, singleTarget, appView.getSyntheticItems());
     return true;
   }
 
@@ -217,16 +206,28 @@
         || method.getDefinition().getCode().hasMonitorInstructions();
   }
 
-  public boolean satisfiesRequirementsForSimpleInlining(InvokeMethod invoke, ProgramMethod target) {
+  public boolean satisfiesRequirementsForSimpleInlining(
+      InvokeMethod invoke, ProgramMethod target, Optional<InliningIRProvider> inliningIRProvider) {
     // Code size modified by inlining, so only read for non-concurrent methods.
     boolean deterministic = !methodProcessor.isProcessedConcurrently(target);
     if (deterministic) {
-      // If we are looking for a simple method, only inline if actually simple.
+      // Check if the inlinee is sufficiently small to inline.
       Code code = target.getDefinition().getCode();
-      int instructionLimit =
-          inlinerOptions.getSimpleInliningInstructionLimit()
-              + getInliningInstructionLimitIncrement(invoke, target);
-      if (code.estimatedSizeForInliningAtMost(instructionLimit)) {
+      int instructionLimit = inlinerOptions.getSimpleInliningInstructionLimit();
+      int estimatedMaxIncrement =
+          getEstimatedMaxInliningInstructionLimitIncrement(invoke, target, inliningIRProvider);
+      int estimatedSizeForInlining =
+          code.getEstimatedSizeForInliningIfLessThanOrEquals(
+              instructionLimit + estimatedMaxIncrement);
+      if (estimatedSizeForInlining < 0) {
+        return false;
+      }
+      if (estimatedSizeForInlining <= instructionLimit) {
+        return true;
+      }
+      int actualIncrement =
+          getInliningInstructionLimitIncrement(invoke, target, inliningIRProvider);
+      if (estimatedSizeForInlining <= instructionLimit + actualIncrement) {
         return true;
       }
     }
@@ -237,29 +238,140 @@
     return simpleInliningConstraint.isSatisfied(invoke);
   }
 
-  private int getInliningInstructionLimitIncrement(InvokeMethod invoke, ProgramMethod candidate) {
+  private int getInliningInstructionLimitIncrement(
+      InvokeMethod invoke, ProgramMethod target, Optional<InliningIRProvider> inliningIRProvider) {
+    if (!options.inlinerOptions().enableSimpleInliningInstructionLimitIncrement) {
+      return 0;
+    }
+    return getInliningInstructionLimitIncrementForNonNullParamOrThrow(invoke, target)
+        + getInliningInstructionLimitIncrementForPrelude(invoke, target, inliningIRProvider)
+        + getInliningInstructionLimitIncrementForReturn(invoke);
+  }
+
+  private int getEstimatedMaxInliningInstructionLimitIncrement(
+      InvokeMethod invoke, ProgramMethod target, Optional<InliningIRProvider> inliningIRProvider) {
+    if (!options.inlinerOptions().enableSimpleInliningInstructionLimitIncrement) {
+      return 0;
+    }
+    return getEstimatedMaxInliningInstructionLimitIncrementForNonNullParamOrThrow(invoke, target)
+        + getEstimatedMaxInliningInstructionLimitIncrementForPrelude(
+            invoke, target, inliningIRProvider)
+        + getEstimatedMaxInliningInstructionLimitIncrementForReturn(invoke);
+  }
+
+  private int getInliningInstructionLimitIncrementForNonNullParamOrThrow(
+      InvokeMethod invoke, ProgramMethod target) {
+    BitSet hints = target.getOptimizationInfo().getNonNullParamOrThrow();
+    if (hints == null) {
+      return 0;
+    }
     int instructionLimit = 0;
-    BitSet hints = candidate.getDefinition().getOptimizationInfo().getNonNullParamOrThrow();
-    if (hints != null) {
-      List<Value> arguments = invoke.arguments();
-      for (int index = invoke.getFirstNonReceiverArgumentIndex();
-          index < arguments.size();
-          index++) {
-        Value argument = arguments.get(index);
-        if ((argument.isArgument()
-                || (argument.getType().isReferenceType() && argument.isNeverNull()))
-            && hints.get(index)) {
-          // 5-4 instructions per parameter check are expected to be removed.
-          instructionLimit += 4;
+    for (int index = invoke.getFirstNonReceiverArgumentIndex();
+        index < invoke.arguments().size();
+        index++) {
+      Value argument = invoke.getArgument(index);
+      if (hints.get(index) && argument.getType().isReferenceType() && argument.isNeverNull()) {
+        // 5-4 instructions per parameter check are expected to be removed.
+        instructionLimit += 4;
+      }
+    }
+    return instructionLimit;
+  }
+
+  private int getEstimatedMaxInliningInstructionLimitIncrementForNonNullParamOrThrow(
+      InvokeMethod invoke, ProgramMethod target) {
+    return getInliningInstructionLimitIncrementForNonNullParamOrThrow(invoke, target);
+  }
+
+  private int getInliningInstructionLimitIncrementForPrelude(
+      InvokeMethod invoke, ProgramMethod target, Optional<InliningIRProvider> inliningIRProvider) {
+    if (inliningIRProvider.isEmpty()
+        || invoke.arguments().isEmpty()
+        || !target.getDefinition().getCode().isLirCode()) {
+      return 0;
+    }
+    IRCode code = inliningIRProvider.get().getAndCacheInliningIR(invoke, target);
+    Iterable<Argument> arguments = code::argumentIterator;
+    DexItemFactory factory = appView.dexItemFactory();
+    int increment = 0;
+    for (Argument argument : arguments) {
+      Value argumentValue = argument.outValue();
+      for (Instruction user : argumentValue.uniqueUsers()) {
+        if (user.isCheckCast()) {
+          CheckCast checkCastUser = user.asCheckCast();
+          TypeElement argumentType = invoke.getArgument(argument.getIndex()).getType();
+          TypeElement castType = checkCastUser.getType().toTypeElement(appView);
+          if (argumentType.lessThanOrEqual(castType, appView)) {
+            // We can remove the cast inside the inlinee.
+            increment += DexCheckCast.SIZE;
+          }
+        } else {
+          DexType argumentType = target.getArgumentType(argument.getIndex());
+          BoxUnboxPrimitiveMethodRoundtrip roundtrip =
+              factory.getBoxUnboxPrimitiveMethodRoundtrip(argumentType);
+          if (roundtrip == null) {
+            continue;
+          }
+          Value invokeArgument = invoke.getArgument(argument.getIndex()).getAliasedValue();
+          if (user.isInvokeMethod(roundtrip.getBoxIfPrimitiveElseUnbox())
+              && invokeArgument.isDefinedByInstructionSatisfying(
+                  definition ->
+                      definition.isInvokeMethod(roundtrip.getUnboxIfPrimitiveElseBox()))) {
+            // We can remove the unbox/box operation inside the inlinee.
+            increment += DexInvokeStatic.SIZE + DexMoveResult.SIZE;
+            if (invokeArgument.numberOfAllUsers() == 1 && argumentValue.numberOfAllUsers() == 1) {
+              // We can remove the box/unbox operation inside the caller.
+              increment += DexInvokeStatic.SIZE + DexMoveResult.SIZE;
+            }
+          }
         }
       }
     }
+    return increment;
+  }
+
+  private int getEstimatedMaxInliningInstructionLimitIncrementForPrelude(
+      InvokeMethod invoke, ProgramMethod target, Optional<InliningIRProvider> inliningIRProvider) {
+    if (inliningIRProvider.isEmpty()
+        || invoke.arguments().isEmpty()
+        || !target.getDefinition().getCode().isLirCode()) {
+      return 0;
+    }
+    int increment = 0;
+    for (int argumentIndex = invoke.getFirstNonReceiverArgumentIndex();
+        argumentIndex < invoke.arguments().size();
+        argumentIndex++) {
+      Value argument = invoke.getArgument(argumentIndex).getAliasedValue();
+      if (argument.getType().isReferenceType()) {
+        // We can maybe remove a cast inside the inlinee.
+        increment += DexCheckCast.SIZE;
+      }
+      DexType argumentType = target.getArgumentType(argumentIndex);
+      BoxUnboxPrimitiveMethodRoundtrip roundtrip =
+          appView.dexItemFactory().getBoxUnboxPrimitiveMethodRoundtrip(argumentType);
+      if (roundtrip != null
+          && argument.isDefinedByInstructionSatisfying(
+              definition -> definition.isInvokeMethod(roundtrip.getUnboxIfPrimitiveElseBox()))) {
+        // We can maybe remove a unbox/box operation inside the inlinee.
+        increment += DexInvokeStatic.SIZE + DexMoveResult.SIZE;
+        // We can maybe remove a box/unbox operation inside the caller.
+        increment += DexInvokeStatic.SIZE + DexMoveResult.SIZE;
+      }
+    }
+    return increment;
+  }
+
+  private int getInliningInstructionLimitIncrementForReturn(InvokeMethod invoke) {
     if (options.isGeneratingDex() && invoke.hasOutValue() && invoke.outValue().hasNonDebugUsers()) {
       assert DexMoveResult.SIZE == DexMoveResultObject.SIZE;
       assert DexMoveResult.SIZE == DexMoveResultWide.SIZE;
-      instructionLimit += DexMoveResult.SIZE;
+      return DexMoveResult.SIZE;
     }
-    return instructionLimit;
+    return 0;
+  }
+
+  private int getEstimatedMaxInliningInstructionLimitIncrementForReturn(InvokeMethod invoke) {
+    return getInliningInstructionLimitIncrementForReturn(invoke);
   }
 
   @Override
@@ -294,12 +406,19 @@
     }
 
     Reason reason =
-        reasonStrategy.computeInliningReason(invoke, singleTarget, context, this, methodProcessor);
+        reasonStrategy.computeInliningReason(
+            invoke,
+            singleTarget,
+            context,
+            this,
+            inliningIRProvider,
+            methodProcessor,
+            whyAreYouNotInliningReporter);
     if (reason == Reason.NEVER) {
       return null;
     }
 
-    if (reason == Reason.SIMPLE
+    if (methodProcessor.getCallSiteInformation().hasSingleCallSite(singleTarget, context)
         && !singleTarget.getDefinition().isProcessed()
         && methodProcessor.isPrimaryMethodProcessor()) {
       // The single target has this method as single caller, but the single target is not yet
@@ -310,12 +429,14 @@
 
     if (!singleTarget
         .getDefinition()
-        .isInliningCandidate(method, reason, appView.appInfo(), whyAreYouNotInliningReporter)) {
+        .isInliningCandidate(appView, method, whyAreYouNotInliningReporter)) {
       return null;
     }
 
     if (!passesInliningConstraints(
-        invoke, resolutionResult, singleTarget, reason, whyAreYouNotInliningReporter)) {
+        resolutionResult,
+        singleTarget,
+        whyAreYouNotInliningReporter)) {
       return null;
     }
 
@@ -329,14 +450,14 @@
       }
     }
 
-    InlineAction action =
+    InlineAction.Builder actionBuilder =
         invoke.computeInlining(
-            singleTarget, reason, this, classInitializationAnalysis, whyAreYouNotInliningReporter);
-    if (action == null) {
+            singleTarget, this, classInitializationAnalysis, whyAreYouNotInliningReporter);
+    if (actionBuilder == null) {
       return null;
     }
 
-    if (!setDowncastTypeIfNeeded(appView, action, invoke, singleTarget, context)) {
+    if (!setDowncastTypeIfNeeded(appView, actionBuilder, invoke, singleTarget, context)) {
       return null;
     }
 
@@ -351,7 +472,24 @@
       return null;
     }
 
-    return action;
+    if (reason == Reason.MULTI_CALLER_CANDIDATE) {
+      assert methodProcessor.isPrimaryMethodProcessor();
+      actionBuilder.setReason(
+          satisfiesRequirementsForSimpleInlining(
+                  invoke, singleTarget, Optional.of(inliningIRProvider))
+              ? Reason.SIMPLE
+              : reason);
+    } else {
+      if (reason == Reason.SIMPLE
+          && !satisfiesRequirementsForSimpleInlining(
+              invoke, singleTarget, Optional.of(inliningIRProvider))) {
+        whyAreYouNotInliningReporter.reportInlineeNotSimple();
+        return null;
+      }
+      actionBuilder.setReason(reason);
+    }
+
+    return actionBuilder.build();
   }
 
   private boolean neverInline(
@@ -380,10 +518,9 @@
     return false;
   }
 
-  public InlineAction computeForInvokeWithReceiver(
+  public InlineAction.Builder computeForInvokeWithReceiver(
       InvokeMethodWithReceiver invoke,
       ProgramMethod singleTarget,
-      Reason reason,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     Value receiver = invoke.getReceiver();
     if (receiver.getType().isDefinitelyNull()) {
@@ -391,8 +528,6 @@
       whyAreYouNotInliningReporter.reportReceiverDefinitelyNull();
       return null;
     }
-
-    InlineAction action = new InlineAction(singleTarget, invoke, reason);
     if (receiver.getType().isNullable()) {
       assert !receiver.getType().isDefinitelyNull();
       // When inlining an instance method call, we need to preserve the null check for the
@@ -404,23 +539,22 @@
         return null;
       }
     }
-    return action;
+    return InlineAction.builder().setInvoke(invoke).setTarget(singleTarget);
   }
 
-  public InlineAction computeForInvokeStatic(
+  public InlineAction.Builder computeForInvokeStatic(
       InvokeStatic invoke,
       ProgramMethod singleTarget,
-      Reason reason,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    InlineAction action = new InlineAction(singleTarget, invoke, reason);
+    InlineAction.Builder actionBuilder =
+        InlineAction.builder().setInvoke(invoke).setTarget(singleTarget);
     if (isTargetClassInitialized(invoke, method, singleTarget, classInitializationAnalysis)) {
-      return action;
+      return actionBuilder;
     }
     if (appView.canUseInitClass()
         && inlinerOptions.enableInliningOfInvokesWithClassInitializationSideEffects) {
-      action.setShouldEnsureStaticInitialization();
-      return action;
+      return actionBuilder.setShouldEnsureStaticInitialization();
     }
     whyAreYouNotInliningReporter.reportMustTriggerClassInitialization();
     return null;
@@ -610,7 +744,7 @@
   @Override
   public boolean stillHasBudget(
       InlineAction action, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    if (action.reason.mustBeInlined()) {
+    if (action.mustBeInlined()) {
       return true;
     }
     boolean stillHasBudget = instructionAllowance > 0;
@@ -622,12 +756,13 @@
 
   @Override
   public boolean willExceedBudget(
+      InlineAction action,
       IRCode code,
+      IRCode inlinee,
       InvokeMethod invoke,
-      InlineeWithReason inlinee,
       BasicBlock block,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    if (inlinee.reason.mustBeInlined()) {
+    if (action.mustBeInlined()) {
       return false;
     }
     return willExceedInstructionBudget(inlinee, whyAreYouNotInliningReporter)
@@ -637,9 +772,9 @@
   }
 
   private boolean willExceedInstructionBudget(
-      InlineeWithReason inlinee, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    int numberOfInstructions = Inliner.numberOfInstructions(inlinee.code);
-    if (instructionAllowance < Inliner.numberOfInstructions(inlinee.code)) {
+      IRCode inlinee, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+    int numberOfInstructions = Inliner.numberOfInstructions(inlinee);
+    if (instructionAllowance < Inliner.numberOfInstructions(inlinee)) {
       whyAreYouNotInliningReporter.reportWillExceedInstructionBudget(
           numberOfInstructions, instructionAllowance);
       return true;
@@ -658,13 +793,13 @@
   private boolean willExceedMonitorEnterValuesBudget(
       IRCode code,
       InvokeMethod invoke,
-      InlineeWithReason inlinee,
+      IRCode inlinee,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     if (!code.metadata().mayHaveMonitorInstruction()) {
       return false;
     }
 
-    if (!inlinee.code.metadata().mayHaveMonitorInstruction()) {
+    if (!inlinee.metadata().mayHaveMonitorInstruction()) {
       return false;
     }
 
@@ -675,7 +810,7 @@
       return false;
     }
 
-    for (Monitor monitor : inlinee.code.<Monitor>instructions(Instruction::isMonitorEnter)) {
+    for (Monitor monitor : inlinee.<Monitor>instructions(Instruction::isMonitorEnter)) {
       Value monitorEnterValue = monitor.object().getAliasedValue();
       if (monitorEnterValue.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
         monitorEnterValue =
@@ -722,14 +857,12 @@
    * exception-edge. We therefore abort inlining if the number of exception-edges explode.
    */
   private boolean willExceedControlFlowResolutionBlocksBudget(
-      InlineeWithReason inlinee,
-      BasicBlock block,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+      IRCode inlinee, BasicBlock block, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     if (!block.hasCatchHandlers()) {
       return false;
     }
     int numberOfThrowingInstructionsInInlinee = 0;
-    for (BasicBlock inlineeBlock : inlinee.code.blocks) {
+    for (BasicBlock inlineeBlock : inlinee.blocks) {
       numberOfThrowingInstructionsInInlinee += inlineeBlock.numberOfThrowingInstructions();
     }
     // Estimate the number of "control flow resolution blocks", where we will insert a
@@ -749,9 +882,9 @@
   }
 
   @Override
-  public void markInlined(InlineeWithReason inlinee) {
+  public void markInlined(IRCode inlinee) {
     // TODO(118734615): All inlining use from the budget - should that only be SIMPLE?
-    instructionAllowance -= Inliner.numberOfInstructions(inlinee.code);
+    instructionAllowance -= Inliner.numberOfInstructions(inlinee);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index a73733e..9cb641e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -15,8 +15,6 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
-import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -25,15 +23,12 @@
 final class ForcedInliningOracle implements InliningOracle, InliningStrategy {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final ProgramMethod method;
   private final Map<? extends InvokeMethod, Inliner.InliningInfo> invokesToInline;
 
   ForcedInliningOracle(
       AppView<AppInfoWithLiveness> appView,
-      ProgramMethod method,
       Map<? extends InvokeMethod, Inliner.InliningInfo> invokesToInline) {
     this.appView = appView;
-    this.method = method;
     this.invokesToInline = invokesToInline;
   }
 
@@ -49,10 +44,8 @@
 
   @Override
   public boolean passesInliningConstraints(
-      InvokeMethod invoke,
       SingleResolutionResult<?> resolutionResult,
       ProgramMethod candidate,
-      Reason reason,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return true;
   }
@@ -76,29 +69,16 @@
       ClassInitializationAnalysis classInitializationAnalysis,
       InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    InlineAction action = computeForInvoke(invoke, resolutionResult, whyAreYouNotInliningReporter);
-    if (action == null) {
-      return null;
-    }
-    if (!setDowncastTypeIfNeeded(appView, action, invoke, singleTarget, context)) {
-      return null;
-    }
-    return action;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private InlineAction computeForInvoke(
-      InvokeMethod invoke,
-      SingleResolutionResult<?> resolutionResult,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     Inliner.InliningInfo info = invokesToInline.get(invoke);
     if (info == null) {
       return null;
     }
-    assert method.getDefinition() != info.target.getDefinition();
-    assert passesInliningConstraints(
-        invoke, resolutionResult, info.target, Reason.FORCE, whyAreYouNotInliningReporter);
-    return new InlineAction(info.target, invoke, Reason.FORCE);
+    InlineAction.Builder actionBuilder =
+        InlineAction.builder().setInvoke(invoke).setTarget(info.target);
+    if (!setDowncastTypeIfNeeded(appView, actionBuilder, invoke, singleTarget, context)) {
+      return null;
+    }
+    return actionBuilder.build();
   }
 
   @Override
@@ -119,16 +99,17 @@
 
   @Override
   public boolean willExceedBudget(
+      InlineAction action,
       IRCode code,
+      IRCode inlinee,
       InvokeMethod invoke,
-      InlineeWithReason inlinee,
       BasicBlock block,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return false; // Unlimited allowance.
   }
 
   @Override
-  public void markInlined(InlineeWithReason inlinee) {}
+  public void markInlined(IRCode inlinee) {}
 
   @Override
   public ClassTypeElement getReceiverTypeOrDefault(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 1004186..fd3ec8c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -70,6 +70,7 @@
 import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -122,6 +123,10 @@
             : null;
   }
 
+  public LensCodeRewriter getLensCodeRewriter() {
+    return lensCodeRewriter;
+  }
+
   @SuppressWarnings("ReferenceEquality")
   private ConstraintWithTarget instructionAllowedForInlining(
       Instruction instruction, InliningConstraints inliningConstraints, ProgramMethod context) {
@@ -227,6 +232,10 @@
       return otherConstraint;
     }
 
+    public boolean isNever() {
+      return this == NEVER;
+    }
+
     boolean isSet(int value) {
       return (this.value & value) != 0;
     }
@@ -263,12 +272,17 @@
     }
 
     ConstraintWithTarget(Constraint constraint, DexType targetHolder) {
-      assert constraint != Constraint.NEVER && constraint != Constraint.ALWAYS;
+      assert constraint != Constraint.NEVER;
+      assert constraint != Constraint.ALWAYS;
       assert targetHolder != null;
       this.constraint = constraint;
       this.targetHolder = targetHolder;
     }
 
+    public boolean isNever() {
+      return constraint.isNever();
+    }
+
     @Override
     public int hashCode() {
       if (targetHolder == null) {
@@ -487,7 +501,6 @@
    * that will inline a method irrespective of visibility and instruction checks.
    */
   public enum Reason {
-    FORCE,         // Inlinee is marked for forced inlining (bridge method or renamed constructor).
     ALWAYS,        // Inlinee is marked for inlining due to alwaysinline directive.
     SINGLE_CALLER, // Inlinee has precisely one caller.
     // Inlinee has multiple callers and should not be inlined. Only used during the primary
@@ -495,11 +508,6 @@
     MULTI_CALLER_CANDIDATE,
     SIMPLE,        // Inlinee has simple code suitable for inlining.
     NEVER;         // Inlinee must not be inlined.
-
-    public boolean mustBeInlined() {
-      // TODO(118734615): Include SINGLE_CALLER here as well?
-      return this == FORCE || this == ALWAYS;
-    }
   }
 
   public abstract static class InlineResult {
@@ -529,6 +537,10 @@
       this.reason = reason;
     }
 
+    public static Builder builder() {
+      return new Builder();
+    }
+
     @Override
     InlineAction asInlineAction() {
       return this;
@@ -546,12 +558,15 @@
       shouldEnsureStaticInitialization = true;
     }
 
-    InlineeWithReason buildInliningIR(
+    boolean mustBeInlined() {
+      return reason == Reason.ALWAYS;
+    }
+
+    IRCode buildInliningIR(
         AppView<AppInfoWithLiveness> appView,
         InvokeMethod invoke,
         ProgramMethod context,
-        InliningIRProvider inliningIRProvider,
-        LensCodeRewriter lensCodeRewriter) {
+        InliningIRProvider inliningIRProvider) {
       DexItemFactory dexItemFactory = appView.dexItemFactory();
       InternalOptions options = appView.options();
 
@@ -699,17 +714,12 @@
           }
         }
       }
-
-      if (inliningIRProvider.shouldApplyCodeRewritings(target)) {
-        assert lensCodeRewriter != null;
-        lensCodeRewriter.rewrite(code, target, inliningIRProvider.getMethodProcessor());
-      }
       if (options.testing.inlineeIrModifier != null) {
         options.testing.inlineeIrModifier.accept(code);
       }
       code.removeRedundantBlocks();
       assert code.isConsistentSSA(appView);
-      return new InlineeWithReason(code, reason);
+      return code;
     }
 
     private void handleSimpleEffectAnalysisResult(
@@ -778,6 +788,51 @@
       instruction.forceOverwritePosition(
           position.replacePosition(outermostCaller, removeInnerFrame));
     }
+
+    public static class Builder {
+
+      private DexProgramClass downcastClass;
+      private InvokeMethod invoke;
+      private Reason reason;
+      private boolean shouldEnsureStaticInitialization;
+      private ProgramMethod target;
+
+      Builder setDowncastClass(DexProgramClass downcastClass) {
+        this.downcastClass = downcastClass;
+        return this;
+      }
+
+      Builder setInvoke(InvokeMethod invoke) {
+        this.invoke = invoke;
+        return this;
+      }
+
+      Builder setReason(Reason reason) {
+        this.reason = reason;
+        return this;
+      }
+
+      Builder setShouldEnsureStaticInitialization() {
+        this.shouldEnsureStaticInitialization = true;
+        return this;
+      }
+
+      Builder setTarget(ProgramMethod target) {
+        this.target = target;
+        return this;
+      }
+
+      InlineAction build() {
+        InlineAction action = new InlineAction(target, invoke, reason);
+        if (downcastClass != null) {
+          action.setDowncastClass(downcastClass);
+        }
+        if (shouldEnsureStaticInitialization) {
+          action.setShouldEnsureStaticInitialization();
+        }
+        return action;
+      }
+    }
   }
 
   public static class RetryAction extends InlineResult {
@@ -788,17 +843,6 @@
     }
   }
 
-  static class InlineeWithReason {
-
-    final Reason reason;
-    final IRCode code;
-
-    InlineeWithReason(IRCode code, Reason reason) {
-      this.code = code;
-      this.reason = reason;
-    }
-  }
-
   static int numberOfInstructions(IRCode code) {
     int numberOfInstructions = 0;
     for (BasicBlock block : code.blocks) {
@@ -839,6 +883,10 @@
     public final ProgramMethod target;
     public final DexProgramClass receiverClass; // null, if unknown
 
+    public InliningInfo(ProgramMethod target) {
+      this(target, null);
+    }
+
     public InliningInfo(ProgramMethod target, DexProgramClass receiverClass) {
       this.target = target;
       this.receiverClass = receiverClass;
@@ -852,7 +900,7 @@
       InliningIRProvider inliningIRProvider,
       MethodProcessor methodProcessor,
       Timing timing) {
-    ForcedInliningOracle oracle = new ForcedInliningOracle(appView, method, invokesToInline);
+    ForcedInliningOracle oracle = new ForcedInliningOracle(appView, invokesToInline);
     performInliningImpl(
         oracle,
         oracle,
@@ -894,7 +942,7 @@
             options.inliningInstructionAllowance - numberOfInstructions(code),
             inliningReasonStrategy);
     InliningIRProvider inliningIRProvider =
-        new InliningIRProvider(appView, method, code, methodProcessor);
+        new InliningIRProvider(appView, method, code, lensCodeRewriter, methodProcessor);
     assert inliningIRProvider.verifyIRCacheIsEmpty();
     performInliningImpl(
         oracle, oracle, method, code, feedback, inliningIRProvider, methodProcessor, timing);
@@ -986,13 +1034,14 @@
             continue;
           }
 
+          InliningOracle singleTargetOracle = getSingleTargetOracle(invoke, singleTarget, oracle);
           DexEncodedMethod singleTargetMethod = singleTarget.getDefinition();
           WhyAreYouNotInliningReporter whyAreYouNotInliningReporter =
-              oracle.isForcedInliningOracle()
+              singleTargetOracle.isForcedInliningOracle()
                   ? NopWhyAreYouNotInliningReporter.getInstance()
                   : WhyAreYouNotInliningReporter.createFor(singleTarget, appView, context);
           InlineResult inlineResult =
-              oracle.computeInlining(
+              singleTargetOracle.computeInlining(
                   code,
                   invoke,
                   resolutionResult,
@@ -1022,11 +1071,9 @@
             continue;
           }
 
-          InlineeWithReason inlinee =
-              action.buildInliningIR(
-                  appView, invoke, context, inliningIRProvider, lensCodeRewriter);
+          IRCode inlinee = action.buildInliningIR(appView, invoke, context, inliningIRProvider);
           if (strategy.willExceedBudget(
-              code, invoke, inlinee, block, whyAreYouNotInliningReporter)) {
+              action, code, inlinee, invoke, block, whyAreYouNotInliningReporter)) {
             assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
@@ -1034,21 +1081,16 @@
           // Verify this code went through the full pipeline.
           assert singleTarget.getDefinition().isProcessed();
 
-          boolean inlineeMayHaveInvokeMethod = inlinee.code.metadata().mayHaveInvokeMethod();
+          boolean inlineeMayHaveInvokeMethod = inlinee.metadata().mayHaveInvokeMethod();
 
           // Inline the inlinee code in place of the invoke instruction
           // Back up before the invoke instruction.
           iterator.previous();
           strategy.markInlined(inlinee);
           iterator.inlineInvoke(
-              appView,
-              code,
-              inlinee.code,
-              blockIterator,
-              blocksToRemove,
-              action.getDowncastClass());
+              appView, code, inlinee, blockIterator, blocksToRemove, action.getDowncastClass());
 
-          if (inlinee.reason == Reason.SINGLE_CALLER) {
+          if (methodProcessor.getCallSiteInformation().hasSingleCallSite(singleTarget, context)) {
             assert converter.isInWave();
             feedback.markInlinedIntoSingleCallSite(singleTargetMethod);
             if (singleCallerInlinedMethodsInWave.isEmpty()) {
@@ -1065,12 +1107,12 @@
               code, blockIterator, block, affectedValues, blocksToRemove, timing);
 
           // The synthetic and bridge flags are maintained only if the inlinee has also these flags.
-          if (context.getDefinition().isBridge() && !inlinee.code.method().isBridge()) {
-            context.getDefinition().accessFlags.demoteFromBridge();
+          if (context.getAccessFlags().isBridge() && !singleTarget.getAccessFlags().isBridge()) {
+            context.getAccessFlags().demoteFromBridge();
           }
-          if (context.getDefinition().accessFlags.isSynthetic()
-              && !inlinee.code.method().accessFlags.isSynthetic()) {
-            context.getDefinition().accessFlags.demoteFromSynthetic();
+          if (context.getAccessFlags().isSynthetic()
+              && !singleTarget.getAccessFlags().isSynthetic()) {
+            context.getAccessFlags().demoteFromSynthetic();
           }
 
           context.getDefinition().copyMetadata(appView, singleTargetMethod);
@@ -1098,6 +1140,14 @@
     assert code.isConsistentSSA(appView);
   }
 
+  private InliningOracle getSingleTargetOracle(
+      InvokeMethod invoke, ProgramMethod singleTarget, InliningOracle oracle) {
+    return oracle.isForcedInliningOracle() || !singleTarget.getOptimizationInfo().forceInline()
+        ? oracle
+        : new ForcedInliningOracle(
+            appView, ImmutableMap.of(invoke, new InliningInfo(singleTarget)));
+  }
+
   private boolean tryInlineMethodWithoutSideEffects(
       IRCode code,
       InstructionListIterator iterator,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
index 46dd4a0..8d5d425 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
@@ -24,8 +24,8 @@
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.VerticalClassMerger.SingleTypeMapperGraphLens;
 import com.android.tools.r8.utils.TriFunction;
+import com.android.tools.r8.verticalclassmerging.SingleTypeMapperGraphLens;
 
 // Computes the inlining constraint for a given instruction.
 public class InliningConstraints {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index b484dda..b7f06c8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 
@@ -25,10 +24,8 @@
   ProgramMethod lookupSingleTarget(InvokeMethod invoke, ProgramMethod context);
 
   boolean passesInliningConstraints(
-      InvokeMethod invoke,
       SingleResolutionResult<?> resolutionResult,
       ProgramMethod candidate,
-      Reason reason,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
   InlineResult computeInlining(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
index b76544b..a7bb682 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -41,18 +40,19 @@
    * <p>Return true if the strategy will *not* allow inlining.
    */
   boolean willExceedBudget(
+      InlineAction action,
       IRCode code,
+      IRCode inlinee,
       InvokeMethod invoke,
-      InlineeWithReason inlinee,
       BasicBlock block,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
   /** Inform the strategy that the inlinee has been inlined. */
-  void markInlined(InlineeWithReason inlinee);
+  void markInlined(IRCode inlinee);
 
   default boolean setDowncastTypeIfNeeded(
       AppView<AppInfoWithLiveness> appView,
-      InlineAction action,
+      InlineAction.Builder actionBuilder,
       InvokeMethod invoke,
       ProgramMethod singleTarget,
       ProgramMethod context) {
@@ -61,7 +61,7 @@
       if (AccessControl.isClassAccessible(downcastClass, context, appView).isPossiblyFalse()) {
         return false;
       }
-      action.setDowncastClass(downcastClass);
+      actionBuilder.setDowncastClass(downcastClass);
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
index 4b5f0c9..baa1fc5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
-import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.inliner.FixedInliningReasonStrategy;
@@ -75,7 +74,7 @@
               int inliningInstructionAllowance = Integer.MAX_VALUE;
               return new DefaultInliningOracle(
                   appView,
-                  new FixedInliningReasonStrategy(Reason.MULTI_CALLER_CANDIDATE),
+                  new FixedInliningReasonStrategy(Reason.ALWAYS),
                   method,
                   methodProcessor,
                   inliningInstructionAllowance);
@@ -120,9 +119,6 @@
         stopTrackingCallSitesForMethod(singleTarget);
         continue;
       }
-
-      InlineAction action = inlineResult.asInlineAction();
-      assert action.reason == Reason.MULTI_CALLER_CANDIDATE;
       recordCallEdgeForMultiCallerInlining(method, singleTarget, methodProcessor);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index bd85948..898bd66 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
@@ -48,6 +47,7 @@
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
@@ -507,7 +507,7 @@
     }
 
     private boolean verifyWasInstanceInitializer() {
-      VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
+      VerticallyMergedClasses verticallyMergedClasses = appView.getVerticallyMergedClasses();
       assert verticallyMergedClasses != null;
       assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
           || appView.horizontallyMergedClasses().isMergeTarget(method.getHolderType());
@@ -659,7 +659,7 @@
     }
 
     private void handleArrayPut(ArrayPut arrayPut) {
-      int index = arrayPut.getIndexOrDefault(-1);
+      int index = arrayPut.indexOrDefault(-1);
       MemberType memberType = arrayPut.getMemberType();
 
       // An array-put instruction can potentially write the given array slot on all arrays because
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
index 0aa891a..360d241 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
@@ -27,7 +27,6 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
-import java.util.Set;
 import java.util.function.BiConsumer;
 
 public class ReflectionOptimizer {
@@ -80,7 +79,7 @@
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      Set<Value> affectedValues) {
+      AffectedValues affectedValues) {
     return (type, baseClass) -> {
       InitClass initClass = null;
       if (invoke.getInvokedMethod().match(appView.dexItemFactory().classMethods.forName)) {
@@ -124,9 +123,8 @@
 
       // Otherwise insert a const-class instruction.
       BasicBlock block = invoke.getBlock();
-      affectedValues.addAll(invoke.outValue().affectedValues());
       instructionIterator.replaceCurrentInstructionWithConstClass(
-          appView, code, type, invoke.getLocalInfo());
+          appView, code, type, invoke.getLocalInfo(), affectedValues);
 
       if (initClass != null) {
         if (block.hasCatchHandlers()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 3c4eac4..020390f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -180,7 +180,8 @@
 
         // Is inlining allowed.
         InliningIRProvider inliningIRProvider =
-            new InliningIRProvider(appView, method, code, methodProcessor);
+            new InliningIRProvider(
+                appView, method, code, inliner.getLensCodeRewriter(), methodProcessor);
         ClassInlinerCostAnalysis costAnalysis =
             new ClassInlinerCostAnalysis(appView, inliningIRProvider, processor.getReceivers());
         if (costAnalysis.willExceedInstructionBudget(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 595c8dc..afa1947 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -56,7 +56,6 @@
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.InliningOracle;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner.EligibilityStatus;
 import com.android.tools.r8.ir.optimize.classinliner.analysis.NonEmptyParameterUsage;
@@ -421,34 +420,32 @@
               continue;
             }
 
-            DexMethod invokedMethod = invoke.getInvokedMethod();
-            if (invokedMethod == dexItemFactory.objectMembers.constructor) {
+            SingleResolutionResult<?> resolutionResult =
+                invoke.resolveMethod(appView, code.context()).asSingleResolution();
+            if (resolutionResult == null) {
+              throw new IllegalClassInlinerStateException();
+            }
+
+            DexClassAndMethod resolvedMethod = resolutionResult.getResolutionPair();
+            if (resolvedMethod
+                .getReference()
+                .isIdenticalTo(dexItemFactory.objectMembers.constructor)) {
               continue;
             }
 
-            if (!dexItemFactory.isConstructor(invokedMethod)) {
+            if (!resolvedMethod.getDefinition().isInstanceInitializer()) {
               throw new IllegalClassInlinerStateException();
             }
 
-            DexProgramClass holder =
-                asProgramClassOrNull(appView.definitionForHolder(invokedMethod, method));
-            if (holder == null) {
+            if (!resolvedMethod
+                .getDefinition()
+                .isInliningCandidate(
+                    appView, method, NopWhyAreYouNotInliningReporter.getInstance())) {
               throw new IllegalClassInlinerStateException();
             }
 
-            ProgramMethod singleTarget = holder.lookupProgramMethod(invokedMethod);
-            if (singleTarget == null
-                || !singleTarget
-                    .getDefinition()
-                    .isInliningCandidate(
-                        method,
-                        Reason.ALWAYS,
-                        appView.appInfo(),
-                        NopWhyAreYouNotInliningReporter.getInstance())) {
-              throw new IllegalClassInlinerStateException();
-            }
-
-            directMethodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
+            directMethodCalls.put(
+                invoke, new InliningInfo(resolvedMethod.asProgramMethod(), eligibleClass));
             break;
           }
         }
@@ -482,11 +479,6 @@
 
         if (instruction.isInvokeMethodWithReceiver()) {
           InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (invokedMethod == dexItemFactory.objectMembers.constructor) {
-            continue;
-          }
-
           Value receiver = invoke.getReceiver().getAliasedValue(aliasesThroughAssumeAndCheckCasts);
           if (receiver != eligibleInstance) {
             continue;
@@ -498,6 +490,14 @@
             throw new IllegalClassInlinerStateException();
           }
 
+          DexMethod objectConstructor = dexItemFactory.objectMembers.constructor;
+          if (resolutionResult
+              .getResolvedMethod()
+              .getReference()
+              .isIdenticalTo(objectConstructor)) {
+            continue;
+          }
+
           DispatchTargetLookupResult dispatchTargetLookupResult;
           if (invoke.isInvokeDirect() || invoke.isInvokeSuper()) {
             dispatchTargetLookupResult =
@@ -978,10 +978,7 @@
       }
       DexEncodedMethod encodedParentMethod = encodedParent.getDefinition();
       if (!encodedParentMethod.isInliningCandidate(
-          method,
-          Reason.ALWAYS,
-          appView.appInfo(),
-          NopWhyAreYouNotInliningReporter.getInstance())) {
+          appView, method, NopWhyAreYouNotInliningReporter.getInstance())) {
         return null;
       }
       // Check the api level is allowed to be inlined.
@@ -1163,10 +1160,8 @@
     // Check if the method is inline-able by standard inliner.
     InliningOracle oracle = defaultOracle.computeIfAbsent();
     if (!oracle.passesInliningConstraints(
-        invoke,
         resolutionResult,
         singleTarget,
-        Reason.ALWAYS,
         NopWhyAreYouNotInliningReporter.getInstance())) {
       return false;
     }
@@ -1303,11 +1298,7 @@
     }
     if (!singleTarget
         .getDefinition()
-        .isInliningCandidate(
-            method,
-            Reason.ALWAYS,
-            appView.appInfo(),
-            NopWhyAreYouNotInliningReporter.getInstance())) {
+        .isInliningCandidate(appView, method, NopWhyAreYouNotInliningReporter.getInstance())) {
       // If `singleTarget` is not an inlining candidate, we won't be able to inline it here.
       //
       // Note that there may be some false negatives here since the method may
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 73e2edd..47f2e03 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -140,7 +140,7 @@
       MethodProcessor methodProcessor,
       Timing timing) {
     DexEncodedMethod definition = method.getDefinition();
-    identifyBridgeInfo(definition, code, feedback, timing);
+    identifyBridgeInfo(method, code, feedback, timing);
     analyzeReturns(code, feedback, methodProcessor, timing);
     if (options.enableClassInlining) {
       computeClassInlinerMethodConstraint(method, code, feedback, timing);
@@ -162,9 +162,9 @@
   }
 
   private void identifyBridgeInfo(
-      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
+      ProgramMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
     timing.begin("Identify bridge info");
-    feedback.setBridgeInfo(method, BridgeAnalyzer.analyzeMethod(method, code));
+    feedback.setBridgeInfo(method, BridgeAnalyzer.analyzeMethod(method.getDefinition(), code));
     timing.end();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
index 509c709..0dd4db5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
@@ -168,7 +168,9 @@
           if (!keepInfo.isShrinkingAllowed(appView.options())) {
             // Method is kept and could be overridden outside app (e.g., in tests). Verify we don't
             // have any optimization info recorded for non-abstract methods.
-            assert method.isAbstract() || method.getOptimizationInfo().isDefault();
+            assert method.isAbstract()
+                || method.getOptimizationInfo().isDefault()
+                || method.getOptimizationInfo().returnValueHasBeenPropagated();
             newState.joinMethodOptimizationInfo(
                 appView, method.getSignature(), DefaultMethodOptimizationInfo.getInstance());
           } else if (!method.isAbstract()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoReprocessingEnqueuer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoReprocessingEnqueuer.java
index 2470444..be20d5b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoReprocessingEnqueuer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoReprocessingEnqueuer.java
@@ -5,13 +5,11 @@
 package com.android.tools.r8.ir.optimize.info;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistryWithResult;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -72,7 +70,8 @@
             postMethodProcessorBuilder.addAll(methodsToReprocessInClass, currentGraphLens));
   }
 
-  static class AffectedMethodUseRegistry extends UseRegistryWithResult<Boolean, ProgramMethod> {
+  static class AffectedMethodUseRegistry
+      extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
     private final AppView<AppInfoWithLiveness> appViewWithLiveness;
 
@@ -143,23 +142,5 @@
         markAffected();
       }
     }
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {}
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {}
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {}
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {}
-
-    @Override
-    public void registerInitClass(DexType type) {}
-
-    @Override
-    public void registerTypeReference(DexType type) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index 8a071b8..f4de742 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -228,7 +228,7 @@
   }
 
   @Override
-  public synchronized void setBridgeInfo(DexEncodedMethod method, BridgeInfo bridgeInfo) {
+  public synchronized void setBridgeInfo(ProgramMethod method, BridgeInfo bridgeInfo) {
     getMethodOptimizationInfoForUpdating(method).setBridgeInfo(bridgeInfo);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index c3ff56a..05a1cdc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -94,7 +94,7 @@
   public void markProcessed(DexEncodedMethod method, ConstraintWithTarget state) {}
 
   @Override
-  public void setBridgeInfo(DexEncodedMethod method, BridgeInfo bridgeInfo) {}
+  public void setBridgeInfo(ProgramMethod method, BridgeInfo bridgeInfo) {}
 
   @Override
   public void setClassInlinerMethodConstraint(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index a440900..339d9eb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -151,8 +151,8 @@
   }
 
   @Override
-  public void setBridgeInfo(DexEncodedMethod method, BridgeInfo bridgeInfo) {
-    method.getMutableOptimizationInfo().setBridgeInfo(bridgeInfo);
+  public void setBridgeInfo(ProgramMethod method, BridgeInfo bridgeInfo) {
+    method.getDefinition().getMutableOptimizationInfo().setBridgeInfo(bridgeInfo);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
index 02d13dd..259a701 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import java.util.Set;
 
 public class DefaultInliningReasonStrategy implements InliningReasonStrategy {
 
@@ -35,57 +36,42 @@
       ProgramMethod target,
       ProgramMethod context,
       DefaultInliningOracle oracle,
-      MethodProcessor methodProcessor) {
+      InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     DexEncodedMethod targetMethod = target.getDefinition();
     DexMethod targetReference = target.getReference();
-    if (targetMethod.getOptimizationInfo().forceInline()) {
-      assert appView.getKeepInfo(target).isInliningAllowed(appView.options());
-      return Reason.FORCE;
-    }
+    Reason reason;
     if (appView.appInfo().hasLiveness()
         && appView.withLiveness().appInfo().isAlwaysInlineMethod(targetReference)) {
-      return Reason.ALWAYS;
-    }
-    if (options.disableInliningOfLibraryMethodOverrides
+      reason = Reason.ALWAYS;
+    } else if (options.disableInliningOfLibraryMethodOverrides
         && targetMethod.isLibraryMethodOverride().isTrue()) {
       // This method will always have an implicit call site from the library, so we won't be able to
       // remove it after inlining even if we have single or dual call site information from the
       // program.
-      return Reason.SIMPLE;
+      reason = Reason.SIMPLE;
+    } else if (callSiteInformation.hasSingleCallSite(target, context)) {
+      reason = Reason.SINGLE_CALLER;
+    } else if (isMultiCallerInlineCandidate(target, methodProcessor)) {
+      reason =
+          methodProcessor.isPrimaryMethodProcessor()
+              ? Reason.MULTI_CALLER_CANDIDATE
+              : Reason.ALWAYS;
+    } else {
+      reason = Reason.SIMPLE;
     }
-    if (isSingleCallerInliningTarget(target, context)) {
-      return Reason.SINGLE_CALLER;
+    Set<Reason> validInliningReasons = appView.testing().validInliningReasons;
+    if (validInliningReasons != null && !validInliningReasons.contains(reason)) {
+      reason = Reason.NEVER;
+      whyAreYouNotInliningReporter.reportInvalidInliningReason(reason, validInliningReasons);
     }
-    if (isMultiCallerInlineCandidate(invoke, target, oracle, methodProcessor)) {
-      return methodProcessor.isPrimaryMethodProcessor()
-          ? Reason.MULTI_CALLER_CANDIDATE
-          : Reason.ALWAYS;
-    }
-    return Reason.SIMPLE;
-  }
-
-  private boolean isSingleCallerInliningTarget(ProgramMethod method, ProgramMethod context) {
-    if (!callSiteInformation.hasSingleCallSite(method, context)) {
-      return false;
-    }
-    if (appView.appInfo().isNeverInlineDueToSingleCallerMethod(method)) {
-      return false;
-    }
-    if (appView.testing().validInliningReasons != null
-        && !appView.testing().validInliningReasons.contains(Reason.SINGLE_CALLER)) {
-      return false;
-    }
-    return true;
+    return reason;
   }
 
   private boolean isMultiCallerInlineCandidate(
-      InvokeMethod invoke,
       ProgramMethod singleTarget,
-      DefaultInliningOracle oracle,
       MethodProcessor methodProcessor) {
-    if (oracle.satisfiesRequirementsForSimpleInlining(invoke, singleTarget)) {
-      return false;
-    }
     if (methodProcessor.isPrimaryMethodProcessor()) {
       return callSiteInformation.isMultiCallerInlineCandidate(singleTarget);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
index ec998a7..1c4c78a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
@@ -24,7 +24,9 @@
       ProgramMethod target,
       ProgramMethod context,
       DefaultInliningOracle oracle,
-      MethodProcessor methodProcessor) {
+      InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return reason;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
index b1e76f3..c62a014 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.origin.Origin;
 import java.util.IdentityHashMap;
@@ -20,6 +21,7 @@
 
   private final AppView<?> appView;
   private final ProgramMethod context;
+  private final LensCodeRewriter lensCodeRewriter;
   private final NumberGenerator valueNumberGenerator;
   private final MethodProcessor methodProcessor;
 
@@ -28,14 +30,20 @@
   private InliningIRProvider() {
     this.appView = null;
     this.context = null;
+    this.lensCodeRewriter = null;
     this.valueNumberGenerator = null;
     this.methodProcessor = null;
   }
 
   public InliningIRProvider(
-      AppView<?> appView, ProgramMethod context, IRCode code, MethodProcessor methodProcessor) {
+      AppView<?> appView,
+      ProgramMethod context,
+      IRCode code,
+      LensCodeRewriter lensCodeRewriter,
+      MethodProcessor methodProcessor) {
     this.appView = appView;
     this.context = context;
+    this.lensCodeRewriter = lensCodeRewriter;
     this.valueNumberGenerator = code.valueNumberGenerator;
     this.methodProcessor = methodProcessor;
   }
@@ -66,11 +74,6 @@
       public boolean verifyIRCacheIsEmpty() {
         throw new Unreachable();
       }
-
-      @Override
-      public boolean shouldApplyCodeRewritings(ProgramMethod method) {
-        throw new Unreachable();
-      }
     };
   }
 
@@ -80,13 +83,18 @@
       return cached;
     }
     Origin origin = method.getOrigin();
-    return method.buildInliningIR(
-        context,
-        appView,
-        valueNumberGenerator,
-        Position.getPositionForInlining(invoke, context),
-        origin,
-        methodProcessor);
+    IRCode code =
+        method.buildInliningIR(
+            context,
+            appView,
+            valueNumberGenerator,
+            Position.getPositionForInlining(invoke, context),
+            origin,
+            methodProcessor);
+    if (lensCodeRewriter != null && methodProcessor.shouldApplyCodeRewritings(method)) {
+      lensCodeRewriter.rewrite(code, method, methodProcessor);
+    }
+    return code;
   }
 
   public IRCode getAndCacheInliningIR(InvokeMethod invoke, ProgramMethod method) {
@@ -108,8 +116,4 @@
     assert cache.isEmpty();
     return true;
   }
-
-  public boolean shouldApplyCodeRewritings(ProgramMethod method) {
-    return methodProcessor.shouldApplyCodeRewritings(method);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
index c749be9..17161be 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
@@ -17,5 +17,7 @@
       ProgramMethod target,
       ProgramMethod context,
       DefaultInliningOracle oracle,
-      MethodProcessor methodProcessor);
+      InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
deleted file mode 100644
index 792195a..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.optimize.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.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedBooleanValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.ConstString;
-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.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.utils.StringUtils;
-import java.util.Set;
-
-public class BooleanMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  BooleanMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedBooleanType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    DexMethod singleTargetReference = singleTarget.getReference();
-    if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.booleanValue)) {
-      optimizeBooleanValue(code, instructionIterator, invoke);
-    } else if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.parseBoolean)) {
-      optimizeParseBoolean(code, instructionIterator, invoke);
-    } else if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.valueOf)) {
-      optimizeValueOf(code, instructionIterator, invoke, affectedValues);
-    }
-  }
-
-  private void optimizeBooleanValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod booleanValueInvoke) {
-    // Optimize Boolean.valueOf(b).booleanValue() into b.
-    AbstractValue abstractValue =
-        booleanValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedBoolean()) {
-      if (booleanValueInvoke.hasOutValue()) {
-        SingleBoxedBooleanValue singleBoxedBoolean = abstractValue.asSingleBoxedBoolean();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedBoolean
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, booleanValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-
-  private void optimizeParseBoolean(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke) {
-    Value argument = invoke.getFirstArgument().getAliasedValue();
-    if (argument.isDefinedByInstructionSatisfying(Instruction::isConstString)) {
-      ConstString constString = argument.getDefinition().asConstString();
-      if (!constString.instructionInstanceCanThrow(appView, code.context())) {
-        String value = StringUtils.toLowerCase(constString.getValue().toString());
-        if (value.equals("true")) {
-          instructionIterator.replaceCurrentInstructionWithConstInt(code, 1);
-        } else if (value.equals("false")) {
-          instructionIterator.replaceCurrentInstructionWithConstInt(code, 0);
-        }
-      }
-    }
-  }
-
-  private void optimizeValueOf(
-      IRCode code,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      Set<Value> affectedValues) {
-    // Optimize Boolean.valueOf(b) into Boolean.FALSE or Boolean.TRUE.
-    Value argument = invoke.getFirstOperand();
-    AbstractValue abstractValue = argument.getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleNumberValue()) {
-      instructionIterator.replaceCurrentInstructionWithStaticGet(
-          appView,
-          code,
-          abstractValue.asSingleNumberValue().getBooleanValue()
-              ? dexItemFactory.booleanMembers.TRUE
-              : dexItemFactory.booleanMembers.FALSE,
-          affectedValues);
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ByteMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ByteMethodOptimizer.java
deleted file mode 100644
index 00669f0..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ByteMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.optimize.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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedByteValue;
-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.code.Value;
-import java.util.Set;
-
-public class ByteMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  ByteMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedByteType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.byteMembers.byteValue)) {
-      optimizeByteValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeByteValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod byteValueInvoke) {
-    // Optimize Byte.valueOf(b).byteValue() into b.
-    AbstractValue abstractValue =
-        byteValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedByte()) {
-      if (byteValueInvoke.hasOutValue()) {
-        SingleBoxedByteValue singleBoxedByte = abstractValue.asSingleBoxedByte();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedByte
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, byteValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/CharacterMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/CharacterMethodOptimizer.java
deleted file mode 100644
index 5378e9a..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/CharacterMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedCharValue;
-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.code.Value;
-import java.util.Set;
-
-public class CharacterMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  CharacterMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedCharType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.charMembers.charValue)) {
-      optimizeCharValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeCharValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod charValueInvoke) {
-    // Optimize Char.valueOf(c).charValue() into c.
-    AbstractValue abstractValue =
-        charValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedChar()) {
-      if (charValueInvoke.hasOutValue()) {
-        SingleBoxedCharValue singleBoxedChar = abstractValue.asSingleBoxedChar();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedChar
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, charValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/DoubleMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/DoubleMethodOptimizer.java
deleted file mode 100644
index 9912bc0..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/DoubleMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedDoubleValue;
-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.code.Value;
-import java.util.Set;
-
-public class DoubleMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  DoubleMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedDoubleType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.doubleMembers.doubleValue)) {
-      optimizeDoubleValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeDoubleValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod doubleValueInvoke) {
-    // Optimize Double.valueOf(d).doubleValue() into d.
-    AbstractValue abstractValue =
-        doubleValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedDouble()) {
-      if (doubleValueInvoke.hasOutValue()) {
-        SingleBoxedDoubleValue singleBoxedDouble = abstractValue.asSingleBoxedDouble();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedDouble
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, doubleValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
index a3eba42..f5cc0a5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Set;
 
@@ -37,13 +38,13 @@
 
   @Override
   @SuppressWarnings("ReferenceEquality")
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove) {
     if (appView.hasLiveness()
         && singleTarget.getReference() == appView.dexItemFactory().enumMembers.valueOf
@@ -51,6 +52,7 @@
       insertAssumeDynamicType(
           appView.withLiveness(), code, instructionIterator, invoke, affectedValues);
     }
+    return instructionIterator;
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/FloatMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/FloatMethodOptimizer.java
deleted file mode 100644
index 8cd19ea..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/FloatMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedFloatValue;
-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.code.Value;
-import java.util.Set;
-
-public class FloatMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  FloatMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedFloatType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.floatMembers.floatValue)) {
-      optimizeFloatValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeFloatValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod floatValueInvoke) {
-    // Optimize Float.valueOf(f).floatValue() into f.
-    AbstractValue abstractValue =
-        floatValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedFloat()) {
-      if (floatValueInvoke.hasOutValue()) {
-        SingleBoxedFloatValue singleBoxedFloat = abstractValue.asSingleBoxedFloat();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedFloat
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, floatValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/IntegerMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/IntegerMethodOptimizer.java
deleted file mode 100644
index d34ce58..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/IntegerMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedIntegerValue;
-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.code.Value;
-import java.util.Set;
-
-public class IntegerMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  IntegerMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedIntType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.integerMembers.intValue)) {
-      optimizeIntegerValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeIntegerValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod intValueInvoke) {
-    // Optimize Integer.valueOf(i).intValue() into i.
-    AbstractValue abstractValue =
-        intValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedInteger()) {
-      if (intValueInvoke.hasOutValue()) {
-        SingleBoxedIntegerValue singleBoxedInteger = abstractValue.asSingleBoxedInteger();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedInteger
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, intValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
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 ca3de34..f47e771 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
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.library.primitive.PrimitiveMethodOptimizer;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
@@ -44,16 +45,9 @@
   public LibraryMemberOptimizer(AppView<?> appView, Timing timing) {
     this.appView = appView;
     timing.begin("Register optimizers");
-    register(new BooleanMethodOptimizer(appView));
-    register(new ByteMethodOptimizer(appView));
-    register(new CharacterMethodOptimizer(appView));
-    register(new DoubleMethodOptimizer(appView));
-    register(new FloatMethodOptimizer(appView));
-    register(new IntegerMethodOptimizer(appView));
-    register(new LongMethodOptimizer(appView));
+    PrimitiveMethodOptimizer.forEachPrimitiveOptimizer(appView, this::register);
     register(new ObjectMethodOptimizer(appView));
     register(new ObjectsMethodOptimizer(appView));
-    register(new ShortMethodOptimizer(appView));
     register(new StringBuilderMethodOptimizer(appView));
     register(new StringMethodOptimizer(appView));
     if (appView.enableWholeProgramOptimizations()) {
@@ -160,17 +154,18 @@
         LibraryMethodModelCollection.State optimizationState =
             optimizationStates.computeIfAbsent(
                 optimizer, LibraryMethodModelCollection::createInitialState);
-        optimizer.optimize(
-            code,
-            blockIterator,
-            instructionIterator,
-            invoke,
-            singleTarget,
-            affectedValues,
-            blocksToRemove,
-            optimizationState,
-            methodProcessor,
-            methodProcessingContext);
+        instructionIterator =
+            optimizer.optimize(
+                code,
+                blockIterator,
+                instructionIterator,
+                invoke,
+                singleTarget,
+                affectedValues,
+                blocksToRemove,
+                optimizationState,
+                methodProcessor,
+                methodProcessingContext);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
index fef1563..ab2ea9c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
@@ -12,9 +12,10 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodModelCollection.State;
+import com.android.tools.r8.ir.optimize.library.primitive.BooleanMethodOptimizer;
 import java.util.Set;
 
 /** Used to model the behavior of library methods for optimization purposes. */
@@ -34,31 +35,31 @@
    * Invoked for instructions in {@param code} that invoke a method on the class returned by {@link
    * #getType()}. The given {@param singleTarget} is guaranteed to be non-null.
    */
-  void optimize(
+  InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove,
       T state,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext);
 
   @SuppressWarnings("unchecked")
-  default void optimize(
+  default InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove,
       Object state,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    optimize(
+    return optimize(
         code,
         blockIterator,
         instructionIterator,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
index 3e3b850..f4de53a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import java.util.Set;
@@ -107,13 +108,13 @@
   }
 
   @Override
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove) {
     // Replace Android logging statements like Log.w(...) and Log.IsLoggable(..., WARNING) at or
     // below a certain logging level by false.
@@ -122,6 +123,7 @@
     if (VERBOSE <= logLevel && logLevel <= maxRemovedAndroidLogLevel) {
       instructionIterator.replaceCurrentInstructionWithConstFalse(code);
     }
+    return instructionIterator;
   }
 
   private int getMaxRemovedAndroidLogLevel(ProgramMethod context) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LongMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LongMethodOptimizer.java
deleted file mode 100644
index 51dfd00..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LongMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedLongValue;
-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.code.Value;
-import java.util.Set;
-
-public class LongMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  LongMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedLongType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.longMembers.longValue)) {
-      optimizeLongValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeLongValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod longValueInvoke) {
-    // Optimize Long.valueOf(l).longValue() into l.
-    AbstractValue abstractValue =
-        longValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedLong()) {
-      if (longValueInvoke.hasOutValue()) {
-        SingleBoxedLongValue singleBoxedLong = abstractValue.asSingleBoxedLong();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedLong
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, longValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
index e545164..64d1715 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import java.util.Set;
 
 public class NopLibraryMethodModelCollection extends StatelessLibraryMethodModelCollection {
@@ -32,12 +32,14 @@
   }
 
   @Override
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {}
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    return instructionIterator;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
index 72c4a3e..90ad902 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import java.util.Set;
 
 public class ObjectMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -31,17 +31,18 @@
 
   @Override
   @SuppressWarnings("ReferenceEquality")
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove) {
     if (singleTarget.getReference() == dexItemFactory.objectMembers.getClass) {
       optimizeGetClass(instructionIterator, invoke);
     }
+    return instructionIterator;
   }
 
   private void optimizeGetClass(InstructionListIterator instructionIterator, InvokeMethod invoke) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
index 9890bca..8fe29c9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexItemFactory.ObjectsMethods;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -19,6 +20,7 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.util.Set;
@@ -45,13 +47,13 @@
 
   @Override
   @SuppressWarnings("ReferenceEquality")
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove) {
     DexMethod singleTargetReference = singleTarget.getReference();
     switch (singleTargetReference.getName().byteAt(0)) {
@@ -97,6 +99,7 @@
         // Intentionally empty.
         break;
     }
+    return instructionIterator;
   }
 
   private void optimizeEquals(
@@ -219,7 +222,7 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       DexClassAndMethod singleTarget) {
     Value object = invoke.getFirstArgument();
     TypeElement type = object.getType();
@@ -227,10 +230,9 @@
     // Optimize Objects.toString(null) into "null".
     if (type.isDefinitelyNull()) {
       if (singleTarget.getReference() == objectsMethods.toStringWithObject) {
-        if (invoke.hasOutValue()) {
-          affectedValues.addAll(invoke.outValue().affectedValues());
-        }
-        instructionIterator.replaceCurrentInstructionWithConstString(appView, code, "null");
+        DexString nullString = dexItemFactory.createString("null");
+        instructionIterator.replaceCurrentInstructionWithConstString(
+            appView, code, nullString, affectedValues);
       } else {
         assert singleTarget.getReference() == objectsMethods.toStringWithObjectAndNullDefault;
         if (invoke.hasOutValue()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ShortMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ShortMethodOptimizer.java
deleted file mode 100644
index 399ada9..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ShortMethodOptimizer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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.DexType;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedShortValue;
-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.code.Value;
-import java.util.Set;
-
-public class ShortMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
-
-  ShortMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  @Override
-  public DexType getType() {
-    return dexItemFactory.boxedShortType;
-  }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.shortMembers.shortValue)) {
-      optimizeShortValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeShortValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod shortValueInvoke) {
-    // Optimize Short.valueOf(s).shortValue() into s.
-    AbstractValue abstractValue =
-        shortValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedShort()) {
-      if (shortValueInvoke.hasOutValue()) {
-        SingleBoxedShortValue singleBoxedShort = abstractValue.asSingleBoxedShort();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedShort
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, shortValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
index 9cfdcbd..4a73b82 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
@@ -11,8 +11,8 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection.State;
 import java.util.Set;
 
@@ -24,29 +24,29 @@
     return null;
   }
 
-  public abstract void optimize(
+  public abstract InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove);
 
   @Override
-  public final void optimize(
+  public final InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove,
       State state,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     assert state == null;
-    optimize(
+    return optimize(
         code,
         blockIterator,
         instructionIterator,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
index f5e8538..5f1d875 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.library.StringBuilderMethodOptimizer.State;
@@ -69,13 +70,13 @@
 
   @Override
   @SuppressWarnings("ReferenceEquality")
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove,
       State state,
       MethodProcessor methodProcessor,
@@ -95,6 +96,7 @@
         optimizeToString(instructionIterator, invokeWithReceiver);
       }
     }
+    return instructionIterator;
   }
 
   private void optimizeAppend(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index f49beb1..a43cbe6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -5,33 +5,79 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.JavaUtilLocaleMembers;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.DexItemBasedConstString;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.ValueUtils;
+import com.android.tools.r8.utils.ValueUtils.ArrayValues;
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 public class StringMethodOptimizer extends StatelessLibraryMethodModelCollection {
+  private static boolean DEBUG =
+      System.getProperty("com.android.tools.r8.debug.StringMethodOptimizer") != null;
 
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
+  private final boolean enableStringFormatOptimizations;
+  private final ImmutableMap<DexMethod, DexMethod> valueOfToStringAppend;
 
   StringMethodOptimizer(AppView<?> appView) {
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
+    this.enableStringFormatOptimizations = appView.options().enableStringFormatOptimization;
+    this.valueOfToStringAppend =
+        ImmutableMap.<DexMethod, DexMethod>builder()
+            .put(
+                dexItemFactory.integerMembers.valueOf,
+                dexItemFactory.stringBuilderMethods.appendInt)
+            .put(dexItemFactory.byteMembers.valueOf, dexItemFactory.stringBuilderMethods.appendInt)
+            .put(dexItemFactory.shortMembers.valueOf, dexItemFactory.stringBuilderMethods.appendInt)
+            .put(dexItemFactory.longMembers.valueOf, dexItemFactory.stringBuilderMethods.appendLong)
+            .put(dexItemFactory.charMembers.valueOf, dexItemFactory.stringBuilderMethods.appendChar)
+            .put(
+                dexItemFactory.booleanMembers.valueOf,
+                dexItemFactory.stringBuilderMethods.appendBoolean)
+            .put(
+                dexItemFactory.floatMembers.valueOf,
+                dexItemFactory.stringBuilderMethods.appendFloat)
+            .put(
+                dexItemFactory.doubleMembers.valueOf,
+                dexItemFactory.stringBuilderMethods.appendDouble)
+            .build();
+  }
+
+  private static void debugLog(IRCode code, String message) {
+    System.err.println(message + " method=" + code.context().getReference());
   }
 
   @Override
@@ -41,20 +87,28 @@
 
   @Override
   @SuppressWarnings("ReferenceEquality")
-  public void optimize(
+  public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove) {
     DexMethod singleTargetReference = singleTarget.getReference();
-    if (singleTargetReference == dexItemFactory.stringMembers.equals) {
+    var stringMembers = dexItemFactory.stringMembers;
+    if (singleTargetReference == stringMembers.equals) {
       optimizeEquals(code, instructionIterator, invoke.asInvokeMethodWithReceiver());
-    } else if (singleTargetReference == dexItemFactory.stringMembers.valueOf) {
+    } else if (singleTargetReference == stringMembers.valueOf) {
       optimizeValueOf(code, instructionIterator, invoke.asInvokeStatic(), affectedValues);
+    } else if (enableStringFormatOptimizations
+        && (singleTargetReference == stringMembers.format
+            || singleTargetReference == stringMembers.formatWithLocale)) {
+      instructionIterator =
+          optimizeFormat(
+              code, instructionIterator, blockIterator, invoke.asInvokeStatic(), affectedValues);
     }
+    return instructionIterator;
   }
 
   private void optimizeEquals(
@@ -70,20 +124,338 @@
     }
   }
 
+  private static class SimpleStringFormatSpec {
+    private static class Part {
+      final String value;
+      final int placeholderIdx;
+      private final char formatChar;
+
+      Part(String value) {
+        this.value = value;
+        this.placeholderIdx = -1;
+        this.formatChar = '\0';
+      }
+
+      Part(int placeholderIdx, char formatChar) {
+        this.value = null;
+        this.placeholderIdx = placeholderIdx;
+        this.formatChar = formatChar;
+      }
+
+      boolean isPlaceholder() {
+        return value == null;
+      }
+
+      public boolean isLiteral() {
+        return value != null;
+      }
+    }
+
+    final List<Part> parts;
+    final int placeholderCount;
+
+    SimpleStringFormatSpec(List<Part> parts) {
+      this.parts = parts;
+      placeholderCount = (int) parts.stream().filter(Part::isPlaceholder).count();
+      assert placeholderCount >= 1 || parts.size() <= 1;
+    }
+
+    static SimpleStringFormatSpec parse(boolean allowNumbers, String spec) {
+      ArrayList<Part> parts = new ArrayList<>();
+      int startIdx = 0;
+      int curPlaceholderIdx = 0;
+      int specLen = spec.length();
+      String curPartValue = "";
+      while (true) {
+        int percentIdx = spec.indexOf('%', startIdx);
+        if (percentIdx == -1) {
+          if (startIdx < specLen) {
+            curPartValue = curPartValue.concat(spec.substring(startIdx));
+          }
+          if (!curPartValue.isEmpty() || parts.isEmpty()) {
+            parts.add(new Part(curPartValue));
+          }
+          return new SimpleStringFormatSpec(parts);
+        }
+        // Trailing % is invalid.
+        if (percentIdx + 1 == specLen) {
+          return null;
+        }
+        curPartValue = curPartValue.concat(spec.substring(startIdx, percentIdx));
+        char formatChar = spec.charAt(percentIdx + 1);
+        switch (formatChar) {
+          case 'd':
+            if (!allowNumbers) {
+              return null;
+            }
+            // Intentional fall-through.
+          case 'b':
+          case 's':
+            if (!curPartValue.isEmpty()) {
+              parts.add(new Part(curPartValue));
+              curPartValue = "";
+            }
+            parts.add(new Part(curPlaceholderIdx, formatChar));
+            curPlaceholderIdx += 1;
+            break;
+          case '%':
+            curPartValue = curPartValue.concat("%");
+            break;
+          default:
+            // Do not handle modifiers or other types, because only simple %s result
+            // in smaller code to change to StringBuilder (and are sufficiently common).
+            return null;
+        }
+        startIdx = percentIdx + 2;
+      }
+    }
+  }
+
+  private boolean isDefinitelyNotFormattable(TypeElement type) {
+    ClassTypeElement classType = type.asClassType();
+    if (classType == null) {
+      return false;
+    }
+    DexClass clazz = appView.definitionFor(classType.getClassType());
+    if (clazz == null || !clazz.isFinal()) {
+      // TODO(b/244238384): Extend to non-final classes.
+      return false;
+    }
+    TypeElement formattableType = dexItemFactory.javaUtilFormattableType.toTypeElement(appView);
+    return !type.lessThanOrEqualUpToNullability(formattableType, appView);
+  }
+
+  private boolean isSupportedFormatType(char formatChar, TypeElement type) {
+    switch (formatChar) {
+      case 'b':
+        // String.format() converts null to "false" and non-Boolean objects to "true", which we
+        // cannot replicate without inserting extra logic.
+        return type.isDefinitelyNotNull() && type.isClassType(dexItemFactory.boxedBooleanType);
+      case 'd':
+        // %d requires Byte, Short, Integer, or Long, and prints null as "null".
+        // TODO(b/244238384): Extend to BigInteger.
+        return type.isClassType(dexItemFactory.boxedIntType)
+            || type.isClassType(dexItemFactory.boxedLongType)
+            || type.isClassType(dexItemFactory.boxedByteType)
+            || type.isClassType(dexItemFactory.boxedShortType);
+      default:
+        assert formatChar == 's';
+        // Check for string as an optimization since it's the common case.
+        return type.isStringType(dexItemFactory) || isDefinitelyNotFormattable(type);
+    }
+  }
+
+  private boolean localeIsNullOrRootOrEnglish(Value value) {
+    if (value.isAlwaysNull(appView)) {
+      return true;
+    }
+    if (!value.isDefinedByInstructionSatisfying(Instruction::isStaticGet)) {
+      return false;
+    }
+    StaticGet staticGet = value.definition.asStaticGet();
+    DexField field = staticGet.getField();
+    JavaUtilLocaleMembers localeMembers = dexItemFactory.javaUtilLocaleMembers;
+    return field.isIdenticalTo(localeMembers.ENGLISH)
+        || field.isIdenticalTo(localeMembers.ROOT)
+        || field.isIdenticalTo(localeMembers.US);
+  }
+
+  private InstructionListIterator optimizeFormat(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      BasicBlockIterator blockIterator,
+      InvokeStatic formatInvoke,
+      AffectedValues affectedValues) {
+    boolean hasLocale =
+        formatInvoke
+            .getInvokedMethod()
+            .isIdenticalTo(dexItemFactory.stringMembers.formatWithLocale);
+    int specParamIdx = hasLocale ? 1 : 0;
+    Value specValue = formatInvoke.getArgument(specParamIdx).getAliasedValue();
+    if (!specValue.isConstString()) {
+      if (DEBUG) {
+        debugLog(code, "optimizeFormat: Non-Const Spec");
+      }
+      return instructionIterator;
+    }
+    Instruction specInstruction = specValue.getDefinition();
+    String specString = specInstruction.asConstString().getValue().toString();
+    boolean allowNumbers =
+        hasLocale && localeIsNullOrRootOrEnglish(formatInvoke.getFirstArgument().getAliasedValue());
+    SimpleStringFormatSpec parsedSpec = SimpleStringFormatSpec.parse(allowNumbers, specString);
+    if (parsedSpec == null) {
+      if (DEBUG) {
+        debugLog(code, "optimizeFormat: Unsupported format with allowNumbers=" + allowNumbers);
+      }
+      return instructionIterator;
+    }
+
+    Value paramsValue = formatInvoke.getArgument(specParamIdx + 1);
+    List<Value> elementValues;
+    if (paramsValue.isAlwaysNull(appView)) {
+      elementValues = Collections.emptyList();
+    } else {
+      ArrayValues arrayValues =
+          ValueUtils.computeSingleUseArrayValues(paramsValue, formatInvoke, code);
+      if (arrayValues == null) {
+        return instructionIterator;
+      }
+      elementValues = arrayValues.getElementValues();
+    }
+
+    // Extra args are ignored, while too few throw.
+    if (elementValues.size() < parsedSpec.placeholderCount) {
+      // TODO(b/244238384): Raise IllegalFormatException.
+      return instructionIterator;
+    }
+
+    // Optimize no placeholders.
+    if (parsedSpec.placeholderCount == 0) {
+      instructionIterator.replaceCurrentInstructionWithConstString(
+          appView,
+          code,
+          dexItemFactory.createString(parsedSpec.parts.get(0).value),
+          affectedValues);
+      if (DEBUG) {
+        debugLog(code, "String.format(): Optimized no placeholders");
+      }
+      return instructionIterator;
+    }
+
+    for (SimpleStringFormatSpec.Part part : parsedSpec.parts) {
+      if (part.isPlaceholder()) {
+        Value paramValue = elementValues.get(part.placeholderIdx);
+        if (paramValue == null || paramValue.isAlwaysNull(appView)) {
+          // Save having to call isAlwaysNull() again.
+          elementValues.set(part.placeholderIdx, null);
+          continue;
+        }
+        if (!isSupportedFormatType(part.formatChar, paramValue.getType())) {
+          if (DEBUG) {
+            debugLog(
+                code,
+                String.format(
+                    "String.format(): Unsupported param %s type %%%s: %s",
+                    part.placeholderIdx, part.formatChar, paramValue.getType()));
+          }
+          return instructionIterator;
+        }
+      }
+    }
+
+    ArrayList<Instruction> newInstructions = new ArrayList<>();
+
+    // Rely on StringBuilder optimizations to convert this to using the string constructor (plus
+    // other StringBuilder / valueOf optimizations that may apply).
+    NewInstance newInstance =
+        NewInstance.builder()
+            .setType(dexItemFactory.stringBuilderType)
+            .setPosition(formatInvoke)
+            .setFreshOutValue(
+                code,
+                dexItemFactory.stringBuilderType.toTypeElement(
+                    appView, Nullability.definitelyNotNull()))
+            .build();
+    Value stringBuilderValue = newInstance.outValue();
+    newInstructions.add(newInstance);
+
+    newInstructions.add(
+        InvokeDirect.builder()
+            .setMethod(dexItemFactory.stringBuilderMethods.defaultConstructor)
+            .setSingleArgument(stringBuilderValue)
+            .setPosition(formatInvoke)
+            .build());
+
+    for (SimpleStringFormatSpec.Part part : parsedSpec.parts) {
+      Value paramValue;
+      DexMethod appendMethod = null;
+      if (part.isLiteral()) {
+        // Create strings for non-placeholder parts of the spec string.
+        ConstString constString =
+            ConstString.builder()
+                .setValue(dexItemFactory.createString(part.value))
+                .setPosition(specInstruction)
+                .setFreshOutValue(
+                    code, TypeElement.stringClassType(appView, Nullability.definitelyNotNull()))
+                .build();
+        newInstructions.add(constString);
+        paramValue = constString.outValue();
+        appendMethod = dexItemFactory.stringBuilderMethods.appendString;
+      } else {
+        paramValue = elementValues.get(part.placeholderIdx);
+        if (paramValue == null) {
+          ConstString constString =
+              ConstString.builder()
+                  .setValue(dexItemFactory.createString(part.formatChar == 'b' ? "false" : "null"))
+                  .setPosition(specInstruction)
+                  .setFreshOutValue(
+                      code, TypeElement.stringClassType(appView, Nullability.definitelyNotNull()))
+                  .build();
+          newInstructions.add(constString);
+          paramValue = constString.outValue();
+          appendMethod = dexItemFactory.stringBuilderMethods.appendString;
+        } else {
+          Value paramValueRoot = paramValue.getAliasedValue();
+          InvokeStatic paramInvoke =
+              paramValueRoot.isPhi() ? null : paramValueRoot.definition.asInvokeStatic();
+          // See if the parameter is a call to Integer.valueOf, Boolean.valueOf, etc.
+          if (paramInvoke != null) {
+            DexMethod invokedMethod = paramInvoke.getInvokedMethod();
+            appendMethod = valueOfToStringAppend.get(invokedMethod);
+            if (appendMethod != null) {
+              paramValue = paramInvoke.getFirstArgument();
+            }
+          }
+          if (appendMethod == null) {
+            appendMethod =
+                paramValue.getType().isStringType(dexItemFactory)
+                    ? dexItemFactory.stringBuilderMethods.appendString
+                    : dexItemFactory.stringBuilderMethods.appendObject;
+          }
+        }
+      }
+      InvokeVirtual appendInvoke =
+          InvokeVirtual.builder()
+              .setMethod(appendMethod)
+              .setPosition(formatInvoke)
+              .setArguments(stringBuilderValue, paramValue)
+              .build();
+      newInstructions.add(appendInvoke);
+    }
+    InvokeVirtual toStringInvoke =
+        InvokeVirtual.builder()
+            .setMethod(dexItemFactory.stringBuilderMethods.toString)
+            .setPosition(formatInvoke)
+            .setSingleArgument(stringBuilderValue)
+            .setFreshOutValue(code, dexItemFactory.stringType.toTypeElement(appView))
+            .build();
+
+    // Replace the String.format(), but for simplicity, leave all other array and valueOf() invokes
+    // to be removed by dead code elimination.
+    instructionIterator.replaceCurrentInstruction(toStringInvoke, affectedValues);
+    instructionIterator.previous();
+    instructionIterator =
+        instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+            code, blockIterator, newInstructions, appView.options());
+    if (DEBUG) {
+      debugLog(code, "String.format(): Optimized.");
+    }
+    return instructionIterator;
+  }
+
   private void optimizeValueOf(
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeStatic invoke,
-      Set<Value> affectedValues) {
+      AffectedValues affectedValues) {
     Value object = invoke.getFirstArgument();
     TypeElement type = object.getType();
 
     // Optimize String.valueOf(null) into "null".
     if (type.isDefinitelyNull()) {
-      instructionIterator.replaceCurrentInstructionWithConstString(appView, code, "null");
-      if (invoke.hasOutValue()) {
-        affectedValues.addAll(invoke.outValue().affectedValues());
-      }
+      DexString nullString = dexItemFactory.createString("null");
+      instructionIterator.replaceCurrentInstructionWithConstString(
+          appView, code, nullString, affectedValues);
       return;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java
new file mode 100644
index 0000000..395386d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java
@@ -0,0 +1,104 @@
+// 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.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.ConstString;
+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.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Set;
+
+public class BooleanMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  BooleanMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.booleanMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.booleanMembers.booleanValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedBoolean();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedBooleanType;
+  }
+
+  @Override
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.booleanMembers.parseBoolean)) {
+      optimizeParseBoolean(code, instructionIterator, invoke);
+    } else {
+      optimizeBoxingMethods(code, instructionIterator, invoke, singleTarget, affectedValues);
+    }
+    return instructionIterator;
+  }
+
+  private void optimizeParseBoolean(
+      IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke) {
+    Value argument = invoke.getFirstArgument().getAliasedValue();
+    if (argument.isDefinedByInstructionSatisfying(Instruction::isConstString)) {
+      ConstString constString = argument.getDefinition().asConstString();
+      if (!constString.instructionInstanceCanThrow(appView, code.context())) {
+        String value = StringUtils.toLowerCase(constString.getValue().toString());
+        if (value.equals("true")) {
+          instructionIterator.replaceCurrentInstructionWithConstInt(code, 1);
+        } else if (value.equals("false")) {
+          instructionIterator.replaceCurrentInstructionWithConstInt(code, 0);
+        }
+      }
+    }
+  }
+
+  @Override
+  void optimizeBoxMethod(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod boxInvoke,
+      AffectedValues affectedValues) {
+    // Optimize Boolean.valueOf(b) into Boolean.FALSE or Boolean.TRUE.
+    Value argument = boxInvoke.getFirstOperand();
+    AbstractValue abstractValue = argument.getAbstractValue(appView, code.context());
+    if (abstractValue.isSingleNumberValue()) {
+      instructionIterator.replaceCurrentInstructionWithStaticGet(
+          appView,
+          code,
+          abstractValue.asSingleNumberValue().getBooleanValue()
+              ? dexItemFactory.booleanMembers.TRUE
+              : dexItemFactory.booleanMembers.FALSE,
+          affectedValues);
+      return;
+    }
+    super.optimizeBoxMethod(code, instructionIterator, boxInvoke, affectedValues);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java
new file mode 100644
index 0000000..8f97ed0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class ByteMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  ByteMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.byteMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.byteMembers.byteValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedByte();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedByteType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java
new file mode 100644
index 0000000..689155d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.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.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class CharacterMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  CharacterMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.charMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.charMembers.charValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedChar();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedCharType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java
new file mode 100644
index 0000000..780cc81
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.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.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class DoubleMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  DoubleMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.doubleMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.doubleMembers.doubleValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedDouble();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedDoubleType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java
new file mode 100644
index 0000000..c3206b4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.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.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class FloatMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  FloatMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.floatMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.floatMembers.floatValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedFloat();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedFloatType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java
new file mode 100644
index 0000000..dba75d4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.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.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class IntegerMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  IntegerMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.integerMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.integerMembers.intValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedInteger();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedIntType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java
new file mode 100644
index 0000000..960baaa
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.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.ir.optimize.library.primitive;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class LongMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  LongMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.longMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.longMembers.longValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedLong();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedLongType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java
new file mode 100644
index 0000000..96d23ce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java
@@ -0,0 +1,125 @@
+// 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.primitive;
+
+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.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.SingleBoxedPrimitiveValue;
+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.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public abstract class PrimitiveMethodOptimizer extends StatelessLibraryMethodModelCollection {
+
+  final AppView<?> appView;
+  final DexItemFactory dexItemFactory;
+
+  PrimitiveMethodOptimizer(AppView<?> appView) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  public static void forEachPrimitiveOptimizer(
+      AppView<?> appView, Consumer<PrimitiveMethodOptimizer> register) {
+    register.accept(new BooleanMethodOptimizer(appView));
+    register.accept(new ByteMethodOptimizer(appView));
+    register.accept(new CharacterMethodOptimizer(appView));
+    register.accept(new DoubleMethodOptimizer(appView));
+    register.accept(new FloatMethodOptimizer(appView));
+    register.accept(new IntegerMethodOptimizer(appView));
+    register.accept(new LongMethodOptimizer(appView));
+    register.accept(new ShortMethodOptimizer(appView));
+  }
+
+  abstract DexMethod getBoxMethod();
+
+  abstract DexMethod getUnboxMethod();
+
+  abstract boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue);
+
+  @Override
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    optimizeBoxingMethods(code, instructionIterator, invoke, singleTarget, affectedValues);
+    return instructionIterator;
+  }
+
+  void optimizeBoxingMethods(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues) {
+    if (singleTarget.getReference().isIdenticalTo(getUnboxMethod())) {
+      optimizeUnboxMethod(code, instructionIterator, invoke);
+    } else if (singleTarget.getReference().isIdenticalTo(getBoxMethod())) {
+      optimizeBoxMethod(code, instructionIterator, invoke, affectedValues);
+    }
+  }
+
+  void optimizeBoxMethod(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod boxInvoke,
+      AffectedValues affectedValues) {
+    Value firstArg = boxInvoke.getFirstArgument();
+    if (firstArg
+        .getAliasedValue()
+        .isDefinedByInstructionSatisfying(i -> i.isInvokeMethod(getUnboxMethod()))) {
+      // Optimize Primitive.box(boxed.unbox()) into boxed.
+      InvokeMethod unboxInvoke = firstArg.getAliasedValue().getDefinition().asInvokeMethod();
+      assert unboxInvoke.isInvokeVirtual();
+      Value src = boxInvoke.outValue();
+      Value replacement = unboxInvoke.getFirstArgument();
+      // We need to update affected values if the nullability is different.
+      src.replaceUsers(replacement, affectedValues);
+      instructionIterator.removeOrReplaceByDebugLocalRead();
+    }
+  }
+
+  void optimizeUnboxMethod(
+      IRCode code, InstructionListIterator instructionIterator, InvokeMethod unboxInvoke) {
+    Value firstArg = unboxInvoke.getFirstArgument();
+    AbstractValue abstractValue = firstArg.getAbstractValue(appView, code.context());
+    if (isMatchingSingleBoxedPrimitive(abstractValue)) {
+      // Optimize Primitive.box(cst).unbox() into cst, possibly inter-procedurally.
+      if (unboxInvoke.hasOutValue()) {
+        SingleBoxedPrimitiveValue singleBoxedNumber = abstractValue.asSingleBoxedPrimitive();
+        instructionIterator.replaceCurrentInstruction(
+            singleBoxedNumber
+                .toPrimitive(appView.abstractValueFactory())
+                .createMaterializingInstruction(appView, code, unboxInvoke));
+      } else {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+      }
+      return;
+    }
+    if (firstArg
+        .getAliasedValue()
+        .isDefinedByInstructionSatisfying(i -> i.isInvokeMethod(getBoxMethod()))) {
+      // Optimize Primitive.box(unboxed).unbox() into unboxed.
+      InvokeMethod boxInvoke = firstArg.getAliasedValue().getDefinition().asInvokeMethod();
+      assert boxInvoke.isInvokeStatic();
+      unboxInvoke.outValue().replaceUsers(boxInvoke.getFirstArgument());
+      instructionIterator.replaceCurrentInstructionByNullCheckIfPossible(appView, code.context());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java
new file mode 100644
index 0000000..8bae65e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.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.ir.optimize.library.primitive;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+
+public class ShortMethodOptimizer extends PrimitiveMethodOptimizer {
+
+  ShortMethodOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.shortMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.shortMembers.shortValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedShort();
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.boxedShortType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/MethodBoxingStatus.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/MethodBoxingStatus.java
new file mode 100644
index 0000000..75d2340
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/MethodBoxingStatus.java
@@ -0,0 +1,80 @@
+// 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.numberunboxer;
+
+import com.android.tools.r8.utils.ArrayUtils;
+
+public class MethodBoxingStatus {
+
+  public static final MethodBoxingStatus NONE_UNBOXABLE = new MethodBoxingStatus(null, null);
+
+  private final ValueBoxingStatus returnStatus;
+  private final ValueBoxingStatus[] argStatuses;
+
+  public static MethodBoxingStatus create(
+      ValueBoxingStatus returnStatus, ValueBoxingStatus[] argStatuses) {
+    if (returnStatus.isNotUnboxable()
+        && ArrayUtils.all(argStatuses, ValueBoxingStatus.NOT_UNBOXABLE)) {
+      return NONE_UNBOXABLE;
+    }
+    return new MethodBoxingStatus(returnStatus, argStatuses);
+  }
+
+  private MethodBoxingStatus(ValueBoxingStatus returnStatus, ValueBoxingStatus[] argStatuses) {
+    this.returnStatus = returnStatus;
+    this.argStatuses = argStatuses;
+  }
+
+  public MethodBoxingStatus merge(MethodBoxingStatus other) {
+    if (isNoneUnboxable() || other.isNoneUnboxable()) {
+      return NONE_UNBOXABLE;
+    }
+    assert argStatuses.length == other.argStatuses.length;
+    ValueBoxingStatus[] newArgStatuses = new ValueBoxingStatus[argStatuses.length];
+    for (int i = 0; i < other.argStatuses.length; i++) {
+      newArgStatuses[i] = other.argStatuses[i].merge(argStatuses[i]);
+    }
+    return create(returnStatus.merge(other.returnStatus), newArgStatuses);
+  }
+
+  public boolean isNoneUnboxable() {
+    return this == NONE_UNBOXABLE;
+  }
+
+  public ValueBoxingStatus getReturnStatus() {
+    assert !isNoneUnboxable();
+    return returnStatus;
+  }
+
+  public ValueBoxingStatus getArgStatus(int i) {
+    assert !isNoneUnboxable();
+    return argStatuses[i];
+  }
+
+  public ValueBoxingStatus[] getArgStatuses() {
+    assert !isNoneUnboxable();
+    return argStatuses;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("MethodBoxingStatus[");
+    if (this == NONE_UNBOXABLE) {
+      sb.append("NONE_UNBOXABLE");
+    } else {
+      for (int i = 0; i < argStatuses.length; i++) {
+        if (argStatuses[i].mayBeUnboxable()) {
+          sb.append(i).append(":").append(argStatuses[i]).append(";");
+        }
+      }
+      if (returnStatus.mayBeUnboxable()) {
+        sb.append("ret").append(":").append(returnStatus).append(";");
+      }
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java
new file mode 100644
index 0000000..dbfb37a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java
@@ -0,0 +1,81 @@
+// 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.numberunboxer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public abstract class NumberUnboxer {
+
+  public static NumberUnboxer create(AppView<AppInfoWithLiveness> appView) {
+    if (appView.testing().enableNumberUnboxer) {
+      return new NumberUnboxerImpl(appView);
+    }
+    return empty();
+  }
+
+  public static NumberUnboxer empty() {
+    return new Empty();
+  }
+
+  public abstract void prepareForPrimaryOptimizationPass(
+      Timing timing, ExecutorService executorService) throws ExecutionException;
+
+  public abstract void analyze(IRCode code);
+
+  public abstract void unboxNumbers(
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
+      Timing timing,
+      ExecutorService executorService);
+
+  public abstract void onMethodPruned(ProgramMethod method);
+
+  public abstract void onMethodCodePruned(ProgramMethod method);
+
+  public abstract void rewriteWithLens();
+
+  static class Empty extends NumberUnboxer {
+
+    @Override
+    public void prepareForPrimaryOptimizationPass(Timing timing, ExecutorService executorService)
+        throws ExecutionException {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void analyze(IRCode code) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void unboxNumbers(
+        PostMethodProcessor.Builder postMethodProcessorBuilder,
+        Timing timing,
+        ExecutorService executorService) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void onMethodPruned(ProgramMethod method) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void onMethodCodePruned(ProgramMethod method) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void rewriteWithLens() {
+      // Intentionally empty.
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java
new file mode 100644
index 0000000..2f0a128
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java
@@ -0,0 +1,196 @@
+// 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.numberunboxer;
+
+import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.NO_UNBOX;
+import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.TO_PROCESS;
+import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.UNBOX;
+import static com.android.tools.r8.utils.ListUtils.*;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult;
+import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodArg;
+import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodRet;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.WorkList;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NumberUnboxerBoxingStatusResolution {
+
+  // TODO(b/307872552): Add threshold to NumberUnboxing options.
+  private static final int UNBOX_DELTA_THRESHOLD = 0;
+  private final Map<DexMethod, MethodBoxingStatusResult> boxingStatusResultMap =
+      new IdentityHashMap<>();
+
+  static class MethodBoxingStatusResult {
+
+    public static MethodBoxingStatusResult createNonUnboxable(DexMethod method) {
+      // Replace by singleton.
+      return new MethodBoxingStatusResult(method, NO_UNBOX);
+    }
+
+    public static MethodBoxingStatusResult create(DexMethod method) {
+      return new MethodBoxingStatusResult(method, TO_PROCESS);
+    }
+
+    MethodBoxingStatusResult(DexMethod method, BoxingStatusResult init) {
+      this.ret = init;
+      this.args = new BoxingStatusResult[method.getArity()];
+      Arrays.fill(args, init);
+    }
+
+    enum BoxingStatusResult {
+      TO_PROCESS,
+      UNBOX,
+      NO_UNBOX
+    }
+
+    BoxingStatusResult ret;
+    BoxingStatusResult[] args;
+
+    public void setRet(BoxingStatusResult ret) {
+      this.ret = ret;
+    }
+
+    public BoxingStatusResult getRet() {
+      return ret;
+    }
+
+    public void setArg(BoxingStatusResult arg, int i) {
+      this.args[i] = arg;
+    }
+
+    public BoxingStatusResult getArg(int i) {
+      return args[i];
+    }
+
+    public BoxingStatusResult[] getArgs() {
+      return args;
+    }
+  }
+
+  void markNoneUnboxable(DexMethod method) {
+    boxingStatusResultMap.put(method, MethodBoxingStatusResult.createNonUnboxable(method));
+  }
+
+  private MethodBoxingStatusResult getMethodBoxingStatusResult(DexMethod method) {
+    return boxingStatusResultMap.computeIfAbsent(method, MethodBoxingStatusResult::create);
+  }
+
+  BoxingStatusResult get(TransitiveDependency transitiveDependency) {
+    assert transitiveDependency.isMethodDependency();
+    MethodBoxingStatusResult methodBoxingStatusResult =
+        getMethodBoxingStatusResult(transitiveDependency.asMethodDependency().getMethod());
+    if (transitiveDependency.isMethodRet()) {
+      return methodBoxingStatusResult.getRet();
+    }
+    assert transitiveDependency.isMethodArg();
+    return methodBoxingStatusResult.getArg(transitiveDependency.asMethodArg().getParameterIndex());
+  }
+
+  void register(TransitiveDependency transitiveDependency, BoxingStatusResult boxingStatusResult) {
+    assert transitiveDependency.isMethodDependency();
+    MethodBoxingStatusResult methodBoxingStatusResult =
+        getMethodBoxingStatusResult(transitiveDependency.asMethodDependency().getMethod());
+    if (transitiveDependency.isMethodRet()) {
+      methodBoxingStatusResult.setRet(boxingStatusResult);
+      return;
+    }
+    assert transitiveDependency.isMethodArg();
+    methodBoxingStatusResult.setArg(
+        boxingStatusResult, transitiveDependency.asMethodArg().getParameterIndex());
+  }
+
+  public Map<DexMethod, MethodBoxingStatusResult> resolve(
+      Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+    List<DexMethod> methods = ListUtils.sort(methodBoxingStatus.keySet(), DexMethod::compareTo);
+    for (DexMethod method : methods) {
+      MethodBoxingStatus status = methodBoxingStatus.get(method);
+      if (status.isNoneUnboxable()) {
+        markNoneUnboxable(method);
+        continue;
+      }
+      MethodBoxingStatusResult methodBoxingStatusResult = getMethodBoxingStatusResult(method);
+      if (status.getReturnStatus().isNotUnboxable()) {
+        methodBoxingStatusResult.setRet(NO_UNBOX);
+      } else {
+        if (methodBoxingStatusResult.getRet() == TO_PROCESS) {
+          resolve(methodBoxingStatus, new MethodRet(method));
+        }
+      }
+      for (int i = 0; i < status.getArgStatuses().length; i++) {
+        ValueBoxingStatus argStatus = status.getArgStatus(i);
+        if (argStatus.isNotUnboxable()) {
+          methodBoxingStatusResult.setArg(NO_UNBOX, i);
+        } else {
+          if (methodBoxingStatusResult.getArg(i) == TO_PROCESS) {
+            resolve(methodBoxingStatus, new MethodArg(i, method));
+          }
+        }
+      }
+    }
+    assert allProcessed();
+    return boxingStatusResultMap;
+  }
+
+  private boolean allProcessed() {
+    boxingStatusResultMap.forEach(
+        (k, v) -> {
+          assert v.getRet() != TO_PROCESS;
+          for (BoxingStatusResult arg : v.getArgs()) {
+            assert arg != TO_PROCESS;
+          }
+        });
+    return true;
+  }
+
+  private ValueBoxingStatus getValueBoxingStatus(
+      TransitiveDependency dep, Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+    // Later we will implement field dependencies.
+    assert dep.isMethodDependency();
+    MethodBoxingStatus status = methodBoxingStatus.get(dep.asMethodDependency().getMethod());
+    if (dep.isMethodRet()) {
+      return status.getReturnStatus();
+    }
+    assert dep.isMethodArg();
+    return status.getArgStatus(dep.asMethodArg().getParameterIndex());
+  }
+
+  private void resolve(
+      Map<DexMethod, MethodBoxingStatus> methodBoxingStatus, TransitiveDependency dep) {
+    WorkList<TransitiveDependency> workList = WorkList.newIdentityWorkList(dep);
+    int delta = 0;
+    while (workList.hasNext()) {
+      TransitiveDependency next = workList.next();
+      BoxingStatusResult boxingStatusResult = get(next);
+      if (boxingStatusResult == UNBOX) {
+        delta++;
+        continue;
+      }
+      ValueBoxingStatus valueBoxingStatus = getValueBoxingStatus(next, methodBoxingStatus);
+      if (boxingStatusResult == NO_UNBOX || valueBoxingStatus.isNotUnboxable()) {
+        // TODO(b/307872552): Unbox when a non unboxable non null dependency is present.
+        // If a dependency is not unboxable, we need to prove it's non-null, else we cannot unbox.
+        // In this first version we bail out by setting a negative delta.
+        delta = -1;
+        break;
+      }
+      assert boxingStatusResult == TO_PROCESS;
+      workList.addIfNotSeen(valueBoxingStatus.getTransitiveDependencies());
+      // Each dependency has been pessimistically marked as requiring extra boxing operation.
+      // TODO(b/307872552): Test and re-evaluate the delta computation.
+      delta += valueBoxingStatus.getTransitiveDependencies().size();
+      delta += valueBoxingStatus.getBoxingDelta();
+    }
+    BoxingStatusResult boxingStatusResult = delta > UNBOX_DELTA_THRESHOLD ? UNBOX : NO_UNBOX;
+    for (TransitiveDependency transitiveDependency : workList.getSeenSet()) {
+      assert transitiveDependency.isMethodDependency();
+      register(transitiveDependency.asMethodDependency(), boxingStatusResult);
+    }
+  }
+}
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
new file mode 100644
index 0000000..cf8f2ec
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
@@ -0,0 +1,351 @@
+// 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.numberunboxer;
+
+import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.UNBOX;
+import static com.android.tools.r8.ir.optimize.numberunboxer.ValueBoxingStatus.NOT_UNBOXABLE;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult;
+import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodArg;
+import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodRet;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class NumberUnboxerImpl extends NumberUnboxer {
+
+  private AppView<AppInfoWithLiveness> appView;
+  private DexItemFactory factory;
+  private Set<DexType> boxedTypes;
+
+  // Temporarily keep the information here, and not in the MethodOptimizationInfo as the
+  // optimization is developed and unfinished.
+  private Map<DexMethod, MethodBoxingStatus> methodBoxingStatus = new ConcurrentHashMap<>();
+  private Map<DexMethod, DexMethod> virtualMethodsRepresentative;
+
+  public NumberUnboxerImpl(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.factory = appView.dexItemFactory();
+    this.boxedTypes = factory.primitiveToBoxed.values();
+  }
+
+  /**
+   * The preparation agglomerate targets or virtual calls into a deterministic method amongst them.
+   * This allows R8 to compute the boxing status once for all targets of the same call.
+   */
+  @Override
+  public void prepareForPrimaryOptimizationPass(Timing timing, ExecutorService executorService)
+      throws ExecutionException {
+    timing.begin("Prepare number unboxer tree fixer");
+    ImmediateProgramSubtypingInfo immediateSubtypingInfo =
+        ImmediateProgramSubtypingInfo.create(appView);
+    List<Set<DexProgramClass>> connectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
+    Set<Map<DexMethod, DexMethod>> virtualMethodsRepresentativeToMerge =
+        ConcurrentHashMap.newKeySet();
+    ThreadUtils.processItems(
+        connectedComponents,
+        component ->
+            virtualMethodsRepresentativeToMerge.add(computeVirtualMethodRepresentative(component)),
+        appView.options().getThreadingModule(),
+        executorService);
+    virtualMethodsRepresentative =
+        MapUtils.newImmutableMap(
+            builder -> virtualMethodsRepresentativeToMerge.forEach(builder::putAll));
+    timing.end();
+  }
+
+  // TODO(b/307872552): Do not store irrelevant representative.
+  private Map<DexMethod, DexMethod> computeVirtualMethodRepresentative(
+      Set<DexProgramClass> component) {
+    DexMethodSignatureMap<List<DexMethod>> componentVirtualMethods = DexMethodSignatureMap.create();
+    for (DexProgramClass clazz : component) {
+      for (ProgramMethod virtualProgramMethod : clazz.virtualProgramMethods()) {
+        DexMethod reference = virtualProgramMethod.getReference();
+        List<DexMethod> set =
+            componentVirtualMethods.computeIfAbsent(virtualProgramMethod, k -> new ArrayList<>());
+        set.add(reference);
+      }
+    }
+    Map<DexMethod, DexMethod> vMethodRepresentative = new IdentityHashMap<>();
+    for (List<DexMethod> vMethods : componentVirtualMethods.values()) {
+      if (vMethods.size() > 1) {
+        vMethods.sort(Comparator.naturalOrder());
+        DexMethod representative = vMethods.get(0);
+        for (int i = 1; i < vMethods.size(); i++) {
+          vMethodRepresentative.put(vMethods.get(i), representative);
+        }
+      }
+    }
+    return vMethodRepresentative;
+  }
+
+  private void registerMethodUnboxingStatusIfNeeded(
+      ProgramMethod method, ValueBoxingStatus returnStatus, ValueBoxingStatus[] args) {
+    if (args == null && returnStatus == null) {
+      // We don't register anything if nothing unboxable was found.
+      return;
+    }
+    ValueBoxingStatus nonNullReturnStatus = returnStatus == null ? NOT_UNBOXABLE : returnStatus;
+    ValueBoxingStatus[] nonNullArgs =
+        args == null ? ValueBoxingStatus.notUnboxableArray(method.getReference().getArity()) : args;
+    MethodBoxingStatus unboxingStatus = MethodBoxingStatus.create(nonNullReturnStatus, nonNullArgs);
+    assert !unboxingStatus.isNoneUnboxable();
+    DexMethod representative =
+        virtualMethodsRepresentative.getOrDefault(method.getReference(), method.getReference());
+    methodBoxingStatus.compute(
+        representative,
+        (m, old) -> {
+          if (old == null) {
+            return unboxingStatus;
+          }
+          return old.merge(unboxingStatus);
+        });
+  }
+
+  /**
+   * Analysis phase: Figures out in each method if parameters, invoke, field accesses and return
+   * values are used in boxing operations.
+   */
+  @Override
+  public void analyze(IRCode code) {
+    DexMethod contextReference = code.context().getReference();
+    ValueBoxingStatus[] args = null;
+    ValueBoxingStatus returnStatus = null;
+    for (Instruction next : code.instructions()) {
+      if (next.isArgument()) {
+        ValueBoxingStatus unboxingStatus = analyzeOutput(next.outValue());
+        if (unboxingStatus.mayBeUnboxable()) {
+          if (args == null) {
+            args = new ValueBoxingStatus[contextReference.getArity()];
+          }
+          args[next.asArgument().getIndex()] = unboxingStatus;
+        }
+      } else if (next.isReturn()) {
+        Return ret = next.asReturn();
+        if (ret.hasReturnValue() && (returnStatus == null || returnStatus.mayBeUnboxable())) {
+          ValueBoxingStatus unboxingStatus = analyzeInput(ret.returnValue(), code.context());
+          if (unboxingStatus.mayBeUnboxable()) {
+            returnStatus =
+                returnStatus == null ? unboxingStatus : returnStatus.merge(unboxingStatus);
+          } else {
+            returnStatus = NOT_UNBOXABLE;
+          }
+        }
+      } else if (next.isInvokeMethod()) {
+        analyzeInvoke(next.asInvokeMethod(), code.context());
+      } else if (next.isInvokeCustom()) {
+        throw new Unimplemented();
+      }
+    }
+    // TODO(b/307872552): Analyse field access to unbox fields.
+    registerMethodUnboxingStatusIfNeeded(code.context(), returnStatus, args);
+  }
+
+  private void analyzeInvoke(InvokeMethod invoke, ProgramMethod context) {
+    ProgramMethod resolvedMethod =
+        appView
+            .appInfo()
+            .resolveMethodLegacy(invoke.getInvokedMethod(), invoke.getInterfaceBit())
+            .getResolvedProgramMethod();
+    if (resolvedMethod == null) {
+      return;
+    }
+    ValueBoxingStatus[] args = null;
+    int shift = invoke.getFirstNonReceiverArgumentIndex();
+    for (int i = shift; i < invoke.inValues().size(); i++) {
+      ValueBoxingStatus unboxingStatus = analyzeInput(invoke.getArgument(i), context);
+      if (unboxingStatus.mayBeUnboxable()) {
+        if (args == null) {
+          args = new ValueBoxingStatus[invoke.getInvokedMethod().getArity()];
+          Arrays.fill(args, NOT_UNBOXABLE);
+        }
+        args[i - shift] = unboxingStatus;
+      }
+    }
+    ValueBoxingStatus returnVal = null;
+    if (invoke.hasOutValue()) {
+      ValueBoxingStatus unboxingStatus = analyzeOutput(invoke.outValue());
+      if (unboxingStatus.mayBeUnboxable()) {
+        returnVal = unboxingStatus;
+      }
+    }
+    registerMethodUnboxingStatusIfNeeded(resolvedMethod, returnVal, args);
+  }
+
+  private boolean shouldConsiderForUnboxing(Value value) {
+    // TODO(b/307872552): So far we consider only boxed type value to unbox them into their
+    // corresponding primitive type, for example, Integer -> int. It would be nice to support
+    // the pattern checkCast(BoxType) followed by a boxing operation, so that for example when
+    // we have MyClass<T> and T is proven to be an Integer, we can unbox into int.
+    return value.getType().isClassType()
+        && boxedTypes.contains(value.getType().asClassType().getClassType());
+  }
+
+  // Inputs are values flowing into a method return, an invoke argument or a field write.
+  private ValueBoxingStatus analyzeInput(Value inValue, ProgramMethod context) {
+    if (!shouldConsiderForUnboxing(inValue)) {
+      return NOT_UNBOXABLE;
+    }
+    DexType boxedType = inValue.getType().asClassType().getClassType();
+    DexType primitiveType = factory.primitiveToBoxed.inverse().get(boxedType);
+    DexMethod boxPrimitiveMethod = factory.getBoxPrimitiveMethod(primitiveType);
+    if (!inValue.isPhi()) {
+      Instruction definition = inValue.getAliasedValue().getDefinition();
+      if (definition.isArgument()) {
+        return ValueBoxingStatus.with(
+            new MethodArg(definition.asArgument().getIndex(), context.getReference()));
+      }
+      if (definition.isInvokeMethod()) {
+        if (boxPrimitiveMethod.isIdenticalTo(definition.asInvokeMethod().getInvokedMethod())) {
+          // The result of a boxing operation is non nullable.
+          if (!inValue.hasPhiUsers() && inValue.hasSingleUniqueUser()) {
+            // Unboxing would remove a boxing operation.
+            return ValueBoxingStatus.with(1);
+          }
+          // Unboxing would add and remove a boxing operation.
+          return ValueBoxingStatus.with(0);
+        }
+        InvokeMethod invoke = definition.asInvokeMethod();
+        ProgramMethod resolvedMethod =
+            appView
+                .appInfo()
+                .resolveMethodLegacy(invoke.getInvokedMethod(), invoke.getInterfaceBit())
+                .getResolvedProgramMethod();
+        if (resolvedMethod != null) {
+          return ValueBoxingStatus.with(new MethodRet(invoke.getInvokedMethod()));
+        }
+      }
+    }
+    // TODO(b/307872552) We should support field reads as transitive dependencies.
+    if (inValue.getType().isNullable()) {
+      return NOT_UNBOXABLE;
+    }
+    // TODO(b/307872552): We could analyze simple phis, for example,
+    //  Integer i = bool ? integer.valueOf(1) : Integer.valueOf(2);
+    //  removes 2 operations and does not add 1.
+    // Since we cannot interpret the definition, unboxing adds a boxing operation.
+    return ValueBoxingStatus.with(-1);
+  }
+
+  // Outputs are method arguments, invoke return values and field reads.
+  private ValueBoxingStatus analyzeOutput(Value outValue) {
+    if (!shouldConsiderForUnboxing(outValue)) {
+      return NOT_UNBOXABLE;
+    }
+    DexType boxedType = outValue.getType().asClassType().getClassType();
+    DexMethod unboxPrimitiveMethod = factory.getUnboxPrimitiveMethod(boxedType);
+    boolean metUnboxingOperation = false;
+    boolean metOtherOperation = outValue.hasPhiUsers();
+    for (Instruction uniqueUser : outValue.aliasedUsers()) {
+      if (uniqueUser.isAssumeWithNonNullAssumption()) {
+        // Nothing to do, the assume will be removed by unboxing.
+      } else if (uniqueUser.isInvokeMethod()
+          && unboxPrimitiveMethod.isIdenticalTo(uniqueUser.asInvokeMethod().getInvokedMethod())) {
+        metUnboxingOperation = true;
+      } else {
+        metOtherOperation = true;
+      }
+    }
+    return ValueBoxingStatus.with(computeBoxingDelta(metUnboxingOperation, metOtherOperation));
+  }
+
+  private int computeBoxingDelta(boolean metUnboxingOperation, boolean metOtherOperation) {
+    if (metUnboxingOperation) {
+      if (metOtherOperation) {
+        // Unboxing would add and remove a boxing operation.
+        return 0;
+      }
+      // Unboxing would remove a boxing operation.
+      return 1;
+    }
+    if (metOtherOperation) {
+      // Unboxing would add a boxing operation.
+      return -1;
+    }
+    // Unused, unboxing won't change the number of boxing operations.
+    return 0;
+  }
+
+  @Override
+  public void unboxNumbers(
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
+      Timing timing,
+      ExecutorService executorService) {
+
+    Map<DexMethod, MethodBoxingStatusResult> result =
+        new NumberUnboxerBoxingStatusResolution().resolve(methodBoxingStatus);
+
+    // TODO(b/307872552): The result encodes for each method which return value and parameter of
+    //  each method should be unboxed. We need here to implement the treefixer using it, and set up
+    //  correctly the reprocessing with a code rewriter similar to the enum unboxing code rewriter.
+    //  We should implement the optimization, so far, we just print out the result.
+    StringBuilder stringBuilder = new StringBuilder();
+    result.forEach(
+        (k, v) -> {
+          if (v.getRet() == UNBOX) {
+            stringBuilder
+                .append("Unboxing of return value of ")
+                .append(k)
+                .append(System.lineSeparator());
+          }
+          for (int i = 0; i < v.getArgs().length; i++) {
+            if (v.getArg(i) == UNBOX) {
+              stringBuilder
+                  .append("Unboxing of arg ")
+                  .append(i)
+                  .append(" of ")
+                  .append(k)
+                  .append(System.lineSeparator());
+            }
+          }
+        });
+    appView.reporter().warning(stringBuilder.toString());
+  }
+
+  @Override
+  public void onMethodPruned(ProgramMethod method) {
+    // TODO(b/307872552): Should we do something about this? We might need to change the
+    //  representative.
+  }
+
+  @Override
+  public void onMethodCodePruned(ProgramMethod method) {
+    // TODO(b/307872552): I don't think we should do anything here.
+  }
+
+  @Override
+  public void rewriteWithLens() {
+    // TODO(b/307872552): This needs to rewrite the methodBoxingStatus.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/TransitiveDependency.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/TransitiveDependency.java
new file mode 100644
index 0000000..f368e52
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/TransitiveDependency.java
@@ -0,0 +1,159 @@
+// 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.numberunboxer;
+
+import com.android.tools.r8.graph.DexMethod;
+
+// Transitive dependencies are computed only one way, the other way is always pessimistic.
+// This means that invoke arguments, method return value and field write have information such as
+// "A value flowing into it is a method argument, a field read or an invoke return value".
+// However, method argument, field reads and invoke return value are analyzed pessimistically,
+// i.e., if they are used as an invoke argument, a method return value or a field write, then
+// the delta is pessimistically increased.
+public interface TransitiveDependency {
+
+  default boolean isMethodDependency() {
+    return false;
+  }
+
+  default MethodDependency asMethodDependency() {
+    return null;
+  }
+
+  default boolean isMethodArg() {
+    return false;
+  }
+
+  default MethodArg asMethodArg() {
+    return null;
+  }
+
+  default boolean isMethodRet() {
+    return false;
+  }
+
+  default MethodRet asMethodRet() {
+    return null;
+  }
+
+  @Override
+  int hashCode();
+
+  @Override
+  boolean equals(Object o);
+
+  abstract class MethodDependency implements TransitiveDependency {
+
+    private final DexMethod method;
+
+    public MethodDependency(DexMethod method) {
+      this.method = method;
+    }
+
+    @Override
+    public boolean isMethodDependency() {
+      return true;
+    }
+
+    @Override
+    public MethodDependency asMethodDependency() {
+      return this;
+    }
+
+    public DexMethod getMethod() {
+      return method;
+    }
+  }
+
+  class MethodRet extends MethodDependency {
+
+    public MethodRet(DexMethod method) {
+      super(method);
+    }
+
+    @Override
+    public boolean isMethodRet() {
+      return true;
+    }
+
+    @Override
+    public MethodRet asMethodRet() {
+      return this;
+    }
+
+    @Override
+    public int hashCode() {
+      return getMethod().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof MethodRet)) {
+        return false;
+      }
+      return getMethod().isIdenticalTo(((MethodRet) obj).getMethod());
+    }
+
+    @Override
+    public String toString() {
+      return "MethodRet("
+          + getMethod().getHolderType().getSimpleName()
+          + "#"
+          + getMethod().name
+          + ')';
+    }
+  }
+
+  class MethodArg extends MethodDependency {
+
+    private final int parameterIndex;
+
+    public MethodArg(int parameterIndex, DexMethod method) {
+      super(method);
+      assert parameterIndex >= 0;
+      this.parameterIndex = parameterIndex;
+    }
+
+    public int getParameterIndex() {
+      return parameterIndex;
+    }
+
+    @Override
+    public boolean isMethodArg() {
+      return true;
+    }
+
+    @Override
+    public MethodArg asMethodArg() {
+      return this;
+    }
+
+    @Override
+    public int hashCode() {
+      return Integer.hashCode(parameterIndex) + 7 * getMethod().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof MethodArg)) {
+        return false;
+      }
+      MethodArg other = (MethodArg) obj;
+      return getMethod().isIdenticalTo(other.getMethod())
+          && getParameterIndex() == other.getParameterIndex();
+    }
+
+    @Override
+    public String toString() {
+      return "MethodArg("
+          + getMethod().getHolderType().getSimpleName()
+          + "#"
+          + getMethod().name
+          + "#"
+          + parameterIndex
+          + ')';
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/ValueBoxingStatus.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/ValueBoxingStatus.java
new file mode 100644
index 0000000..53a7867
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/ValueBoxingStatus.java
@@ -0,0 +1,113 @@
+// 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.numberunboxer;
+
+import com.google.common.collect.ImmutableMultiset;
+import java.util.Arrays;
+
+/**
+ * The value boxing status represents the result of the number unboxer analysis on a given value,
+ * such as a method argument, return value or a field. It contains a boxingDelta which encodes the
+ * number of boxing operation that would be introduced or removed if the value is unboxed and
+ * optionally a list of transitive dependency. To use the value boxing status, the number unboxer
+ * needs to decide for each of the transitive dependency if they're going to be unboxed or not,
+ * compute the concrete boxing delta and unbox if relevant.
+ */
+public class ValueBoxingStatus {
+
+  // TODO(b/307872552): Add threshold to NumberUnboxing options.
+  private static final int MAX_TRANSITIVE_DEPENDENCIES = 7;
+  public static final ValueBoxingStatus NOT_UNBOXABLE =
+      new ValueBoxingStatus(0, ImmutableMultiset.of());
+  private final int boxingDelta;
+  private final ImmutableMultiset<TransitiveDependency> transitiveDependencies;
+
+  public static ValueBoxingStatus[] notUnboxableArray(int size) {
+    ValueBoxingStatus[] valueBoxingStatuses = new ValueBoxingStatus[size];
+    Arrays.fill(valueBoxingStatuses, ValueBoxingStatus.NOT_UNBOXABLE);
+    return valueBoxingStatuses;
+  }
+
+  public static ValueBoxingStatus with(int boxingDelta) {
+    return with(boxingDelta, ImmutableMultiset.of());
+  }
+
+  public static ValueBoxingStatus with(TransitiveDependency transitiveDependency) {
+    return with(0, ImmutableMultiset.of(transitiveDependency));
+  }
+
+  public static ValueBoxingStatus with(
+      int boxingDelta, ImmutableMultiset<TransitiveDependency> transitiveDependencies) {
+    if (transitiveDependencies.size() > MAX_TRANSITIVE_DEPENDENCIES) {
+      return NOT_UNBOXABLE;
+    }
+    return new ValueBoxingStatus(boxingDelta, transitiveDependencies);
+  }
+
+  private ValueBoxingStatus(
+      int boxingDelta, ImmutableMultiset<TransitiveDependency> transitiveDependencies) {
+    this.boxingDelta = boxingDelta;
+    this.transitiveDependencies = transitiveDependencies;
+  }
+
+  public boolean mayBeUnboxable() {
+    return !isNotUnboxable();
+  }
+
+  public boolean isNotUnboxable() {
+    return this == NOT_UNBOXABLE;
+  }
+
+  public int getBoxingDelta() {
+    assert mayBeUnboxable();
+    return boxingDelta;
+  }
+
+  public ImmutableMultiset<TransitiveDependency> getTransitiveDependencies() {
+    return transitiveDependencies;
+  }
+
+  public ValueBoxingStatus merge(ValueBoxingStatus unboxingStatus) {
+    if (isNotUnboxable() || unboxingStatus.isNotUnboxable()) {
+      return NOT_UNBOXABLE;
+    }
+    int newDelta = boxingDelta + unboxingStatus.getBoxingDelta();
+    if (unboxingStatus.getTransitiveDependencies().isEmpty()) {
+      if (newDelta == boxingDelta) {
+        return this;
+      }
+      return with(newDelta, transitiveDependencies);
+    }
+    if (transitiveDependencies.isEmpty()) {
+      if (newDelta == unboxingStatus.getBoxingDelta()) {
+        return unboxingStatus;
+      }
+      return with(newDelta, unboxingStatus.getTransitiveDependencies());
+    }
+    ImmutableMultiset<TransitiveDependency> newDeps =
+        ImmutableMultiset.<TransitiveDependency>builder()
+            .addAll(transitiveDependencies)
+            .addAll(unboxingStatus.getTransitiveDependencies())
+            .build();
+    return with(newDelta, newDeps);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("ValueBoxingStatus[");
+    if (isNotUnboxable()) {
+      sb.append("NOT_UNBOXABLE");
+    } else {
+      sb.append(boxingDelta);
+      for (TransitiveDependency transitiveDependency : transitiveDependencies) {
+        sb.append(";");
+        sb.append(transitiveDependency.toString());
+      }
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index b4ed86d..9297330 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -1842,12 +1842,6 @@
     }
 
     @Override
-    public int estimatedSizeForInlining() {
-      // We just onlined this, so do not inline it again.
-      return Integer.MAX_VALUE;
-    }
-
-    @Override
     public int estimatedDexCodeSizeUpperBoundInBytes() {
       return Integer.MAX_VALUE;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
index 1b36879..8696c95 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -87,10 +88,8 @@
         AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       assert oracle.isToString(instruction, instruction.getFirstOperand());
-      if (instruction.hasOutValue()) {
-        instruction.outValue().addAffectedValuesTo(affectedValues);
-      }
-      iterator.replaceCurrentInstructionWithConstString(appView, code, replacement);
+      DexString string = appView.dexItemFactory().createString(replacement);
+      iterator.replaceCurrentInstructionWithConstString(appView, code, string, affectedValues);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index dbbfe16..f60a3fe 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -439,9 +439,8 @@
         Value out = invoke.outValue();
         TypeElement inType = in.getType();
         if (out != null && in.isAlwaysNull(appView)) {
-          affectedValues.addAll(out.affectedValues());
           it.replaceCurrentInstructionWithConstString(
-              appView, code, dexItemFactory.createString("null"));
+              appView, code, dexItemFactory.createString("null"), affectedValues);
         } else if (inType.nullability().isDefinitelyNotNull()
             && inType.isClassType()
             && inType.asClassType().getClassType().equals(dexItemFactory.stringType)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
index d89c044..a6b6c18 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.PrimaryMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -61,7 +62,7 @@
     if (isCandidateForInstanceOfOptimization(method, abstractReturnValue)) {
       synchronized (this) {
         if (candidatesForInstanceOfOptimization.isEmpty()) {
-          converter.addWaveDoneAction(() -> execute(methodProcessor));
+          converter.addWaveDoneAction(() -> execute(methodProcessor.asPrimaryMethodProcessor()));
         }
         candidatesForInstanceOfOptimization.add(method);
       }
@@ -74,7 +75,7 @@
         && abstractReturnValue.isSingleBoolean();
   }
 
-  public void execute(MethodProcessor methodProcessor) {
+  public void execute(PrimaryMethodProcessor methodProcessor) {
     assert !candidatesForInstanceOfOptimization.isEmpty();
     ProgramMethodSet processed = ProgramMethodSet.create();
     for (ProgramMethod method : candidatesForInstanceOfOptimization) {
@@ -86,7 +87,7 @@
 
   @SuppressWarnings("ReferenceEquality")
   private void processCandidateForInstanceOfOptimization(
-      ProgramMethod method, MethodProcessor methodProcessor) {
+      ProgramMethod method, PrimaryMethodProcessor methodProcessor) {
     DexEncodedMethod definition = method.getDefinition();
     if (!definition.isNonPrivateVirtualMethod()) {
       return;
@@ -135,8 +136,8 @@
       // The parent method is already guaranteed to return the same value.
     } else if (isClassAccessible(method.getHolder(), parentMethod, appView).isTrue()) {
       parentMethod.setCode(
-          parentMethodDefinition.buildInstanceOfCode(
-              method.getHolderType(), abstractParentReturnValue.isTrue(), appView.options()),
+          parentMethodDefinition.buildInstanceOfCfCode(
+              method.getHolderType(), abstractParentReturnValue.isTrue()),
           appView);
       // Rebuild inlining constraints.
       IRCode code =
@@ -146,6 +147,7 @@
       feedback.fixupUnusedArguments(parentMethod, unusedArguments -> unusedArguments.clear(0));
       feedback.unsetAbstractReturnValue(parentMethod);
       feedback.unsetClassInlinerMethodConstraint(parentMethod);
+      methodProcessor.scheduleDesugaredMethodForProcessing(parentMethod);
     } else {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
index 615cd59..e583aea 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
@@ -113,7 +113,9 @@
               m -> keepByteCodeFunctions.add(m.getReference()));
         }
       }
-      appView.setCfByteCodePassThrough(keepByteCodeFunctions);
+      if (appView.options().enableCfByteCodePassThrough) {
+        appView.setCfByteCodePassThrough(keepByteCodeFunctions);
+      }
     } else {
       assert enqueuer.getMode().isFinalTreeShaking();
       enqueuer.forAllLiveClasses(
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index 8270d8a..54a9c8c 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -613,11 +613,14 @@
   }
 
   @Override
-  public int estimatedSizeForInlining() {
+  public int getEstimatedSizeForInliningIfLessThanOrEquals(int threshold) {
     if (useDexEstimationStrategy) {
       LirSizeEstimation<EV> estimation = new LirSizeEstimation<>(this);
       for (LirInstructionView view : this) {
         estimation.onInstructionView(view);
+        if (estimation.getSizeEstimate() > threshold) {
+          return -1;
+        }
       }
       return estimation.getSizeEstimate();
     } else {
@@ -625,23 +628,11 @@
       //  (even switches!) and ignores stack instructions, thus loads to arguments are not included.
       //  The result is a much smaller estimate than for DEX. Once LIR is in place we should use the
       //  same estimate for both.
-      return instructionCount;
-    }
-  }
-
-  @Override
-  public boolean estimatedSizeForInliningAtMost(int threshold) {
-    if (useDexEstimationStrategy) {
-      LirSizeEstimation<EV> estimation = new LirSizeEstimation<>(this);
-      for (LirInstructionView view : this) {
-        estimation.onInstructionView(view);
-        if (estimation.getSizeEstimate() > threshold) {
-          return false;
-        }
+      int estimatedSizedForInlining = instructionCount;
+      if (estimatedSizedForInlining <= threshold) {
+        return estimatedSizedForInlining;
       }
-      return true;
-    } else {
-      return estimatedSizeForInlining() <= threshold;
+      return -1;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index 9700c94..9074bdb 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -476,11 +476,8 @@
           continue;
         }
         ArrayPut arrayPut = instruction.asArrayPut();
-        if (!arrayPut.index().isConstNumber()) {
-          return null;
-        }
-        int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
-        if (index < 0 || index >= values.length) {
+        int index = arrayPut.indexIfConstAndInBounds(values.length);
+        if (index < 0) {
           return null;
         }
         DexType type = getTypeFromConstClassOrBoxedPrimitive(arrayPut.value(), factory);
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 74c76d1..96275ab 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -41,7 +41,7 @@
     this.appView = appView;
   }
 
-  public NamingLens run(ExecutorService executorService, Timing timing) throws ExecutionException {
+  public void run(ExecutorService executorService, Timing timing) throws ExecutionException {
     assert appView.options().isMinifying();
     SubtypingInfo subtypingInfo = MinifierUtils.createSubtypingInfo(appView);
     timing.begin("ComputeInterfaces");
@@ -90,8 +90,9 @@
     new RecordInvokeDynamicInvokeCustomRewriter(appView, lens).run(executorService);
     timing.end();
 
+    appView.testing().namingLensConsumer.accept(appView.dexItemFactory(), lens);
     appView.notifyOptimizationFinishedForTesting();
-    return lens;
+    appView.setNamingLens(lens);
   }
 
   abstract static class BaseMinificationNamingStrategy {
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 8868840..71ff9e8 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -820,7 +820,15 @@
     skipWhitespace();
     int to = parseNumber();
     if (from > to) {
-      throw new ParseException("From is larger than to in range: " + from + ":" + to);
+      // Past versions of R8 would incorrectly put 0 as the "to" range in some instances
+      // and fail to order the range. For 0-values assume the range is a singleton position.
+      if (to == 0) {
+        to = from;
+      } else {
+        int tmp = to;
+        to = from;
+        from = tmp;
+      }
     }
     return nonCardinalRangeCache.get(from, to);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeHoistingToSharedSyntheticSuperClass.java b/src/main/java/com/android/tools/r8/optimize/BridgeHoistingToSharedSyntheticSuperClass.java
new file mode 100644
index 0000000..f06f226
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeHoistingToSharedSyntheticSuperClass.java
@@ -0,0 +1,338 @@
+// 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.optimize;
+
+import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.contexts.CompilationContext.MainThreadContext;
+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.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.optimize.info.bridge.BridgeAnalyzer;
+import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
+import com.android.tools.r8.ir.optimize.info.bridge.VirtualBridgeInfo;
+import com.android.tools.r8.optimize.bridgehoisting.BridgeHoisting;
+import com.android.tools.r8.profile.rewriting.ConcreteProfileCollectionAdditions;
+import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.TestingOptions;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+
+public class BridgeHoistingToSharedSyntheticSuperClass {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  BridgeHoistingToSharedSyntheticSuperClass(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    InternalOptions options = appView.options();
+    if (!options.isOptimizing() || !options.isShrinking()) {
+      return;
+    }
+    if (!appView.options().canHaveNonReboundConstructorInvoke()) {
+      // TODO(b/309575527): Extend to all runtimes.
+      return;
+    }
+    TestingOptions testingOptions = options.getTestingOptions();
+    if (!testingOptions.enableBridgeHoistingToSharedSyntheticSuperclass) {
+      return;
+    }
+    timing.time(
+        "BridgeHoistingToSharedSyntheticSuperClass",
+        () ->
+            new BridgeHoistingToSharedSyntheticSuperClass(appView)
+                .internalRun(executorService, timing));
+  }
+
+  private void internalRun(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    Collection<Group> groups = createInitialGroups(appView);
+    groups = refineGroups(groups);
+    if (!groups.isEmpty()) {
+      rewriteApplication(groups);
+      commitPendingSyntheticClasses();
+      updateArtProfiles(groups);
+      new BridgeHoisting(appView).run(executorService, timing);
+    }
+  }
+
+  /** Returns the set of (non-singleton) groups that have the same superclass. */
+  private Collection<Group> createInitialGroups(AppView<AppInfoWithLiveness> appView) {
+    Map<DexClass, Group> groups = new LinkedHashMap<>();
+    for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
+      if (!clazz.hasSuperType()) {
+        continue;
+      }
+      DexClass superclass = appView.definitionFor(clazz.getSuperType());
+      if (superclass != null) {
+        groups.computeIfAbsent(superclass, ignoreKey(Group::new)).addClass(clazz);
+      }
+    }
+    groups.values().removeIf(Group::isSingleton);
+    return groups.values();
+  }
+
+  private Collection<Group> refineGroups(Collection<Group> groups) {
+    Collection<Group> newGroups = new ArrayList<>();
+    for (Group group : groups) {
+      Iterables.addAll(newGroups, refineGroup(group));
+    }
+    return newGroups;
+  }
+
+  /**
+   * Splits the group into a collection of smaller groups that should receive a shared superclass.
+   *
+   * <p>For each class, this creates a specification of the bridges (a mapping from bridge method
+   * signatures to their bridge implementation). Two classes are selected for getting a shared
+   * synthetic super class if the bridge specification of one is a subset of the other (i.e., a
+   * subset of the bridges can be shared and there are no bridges with the same signature that have
+   * different behavior).
+   */
+  private Iterable<Group> refineGroup(Group group) {
+    List<Group> newGroups = new ArrayList<>();
+    for (DexProgramClass clazz : group) {
+      BridgeSpecification bridgeSpecification = getBridgeSpecification(clazz);
+      if (bridgeSpecification.isEmpty()) {
+        continue;
+      }
+      Group targetGroup = getGroupForClass(newGroups, clazz, bridgeSpecification);
+      if (targetGroup == null) {
+        newGroups.add(new Group(clazz, bridgeSpecification));
+      }
+    }
+    // Only introduce a shared super class for non-singleton groups that do not already have a
+    // shared superclass in the first place.
+    return Iterables.filter(
+        newGroups, newGroup -> !newGroup.isSingleton() && newGroup.size() < group.size());
+  }
+
+  // TODO(b/309575527): Avoid building IR for all methods.
+  private BridgeSpecification getBridgeSpecification(DexProgramClass clazz) {
+    BridgeSpecification bridgeSpecification = new BridgeSpecification();
+    clazz.forEachProgramVirtualMethodMatching(
+        DexEncodedMethod::hasCode,
+        method -> {
+          IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
+          BridgeInfo bridgeInfo = BridgeAnalyzer.analyzeMethod(method.getDefinition(), code);
+          if (bridgeInfo != null) {
+            getSimpleFeedback().setBridgeInfo(method, bridgeInfo);
+            if (bridgeInfo.isVirtualBridgeInfo()) {
+              bridgeSpecification.addBridge(
+                  method.getMethodSignature(), bridgeInfo.asVirtualBridgeInfo());
+            }
+          }
+        });
+    return bridgeSpecification;
+  }
+
+  private Group getGroupForClass(
+      Collection<Group> groups, DexProgramClass clazz, BridgeSpecification bridgeSpecification) {
+    for (Group group : groups) {
+      if (bridgeSpecification.lessThanOrEquals(group.getBridgeSpecification())) {
+        group.addClass(clazz);
+        return group;
+      } else if (group.getBridgeSpecification().lessThanOrEquals(bridgeSpecification)) {
+        group.addClass(clazz);
+        group.setBridgeSpecification(bridgeSpecification);
+        return group;
+      }
+    }
+    return null;
+  }
+
+  private void rewriteApplication(Collection<Group> groups) {
+    MainThreadContext mainThreadContext =
+        appView.createProcessorContext().createMainThreadContext();
+    for (Group group : groups) {
+      DexProgramClass representative = ListUtils.first(group.getClasses());
+      Set<DexType> interfaces = SetUtils.newIdentityHashSet(representative.getInterfaces());
+      for (DexProgramClass clazz : Iterables.skip(group.getClasses(), 1)) {
+        interfaces.removeIf(type -> !clazz.getInterfaces().contains(type));
+      }
+      DexProgramClass syntheticSuperclass =
+          appView
+              .getSyntheticItems()
+              .createClass(
+                  kinds -> kinds.SHARED_SUPER_CLASS,
+                  mainThreadContext.createUniqueContext(representative),
+                  appView,
+                  classBuilder -> {
+                    classBuilder
+                        .setAbstract()
+                        .setSuperType(representative.getSuperType())
+                        .setInterfaces(ListUtils.sort(interfaces, Comparator.naturalOrder()));
+                    group
+                        .getBridgeSpecification()
+                        .forEach(
+                            (bridge, target) ->
+                                classBuilder.addMethod(
+                                    methodBuilder ->
+                                        methodBuilder
+                                            .setAccessFlags(
+                                                MethodAccessFlags.builder()
+                                                    .setAbstract()
+                                                    .setPublic()
+                                                    .build())
+                                            // TODO(b/309575527): Set correct api level.
+                                            .setApiLevelForDefinition(appView.computedMinApiLevel())
+                                            // TODO(b/309575527): Set correct library override info.
+                                            .setIsLibraryMethodOverride(OptionalBool.FALSE)
+                                            .setName(target.getName())
+                                            .setProto(target.getProto())));
+                  });
+      for (DexProgramClass clazz : group) {
+        clazz.setSuperType(syntheticSuperclass.getType());
+      }
+    }
+  }
+
+  private void commitPendingSyntheticClasses() {
+    assert appView.getSyntheticItems().hasPendingSyntheticClasses();
+    appView.setAppInfo(
+        appView.appInfo().rebuildWithLiveness(appView.getSyntheticItems().commit(appView.app())));
+  }
+
+  private void updateArtProfiles(Collection<Group> groups) {
+    ConcreteProfileCollectionAdditions profileCollectionAdditions =
+        ProfileCollectionAdditions.create(appView).asConcrete();
+    if (profileCollectionAdditions == null) {
+      return;
+    }
+    for (Group group : groups) {
+      for (DexProgramClass clazz : group) {
+        profileCollectionAdditions.applyIfContextIsInProfile(
+            clazz, additionsBuilder -> additionsBuilder.addClassRule(clazz.getSuperType()));
+        group
+            .getBridgeSpecification()
+            .forEach(
+                (bridge, target) -> {
+                  DexEncodedMethod targetMethod = clazz.getMethodCollection().getMethod(target);
+                  if (targetMethod != null) {
+                    profileCollectionAdditions.applyIfContextIsInProfile(
+                        targetMethod.getReference(),
+                        additionsBuilder ->
+                            additionsBuilder.addMethodRule(
+                                target.withHolder(clazz.getSuperType(), appView.dexItemFactory())));
+                  }
+                });
+      }
+    }
+    profileCollectionAdditions.commit(appView);
+  }
+
+  private static class Group implements Iterable<DexProgramClass> {
+
+    private final List<DexProgramClass> classes;
+    private BridgeSpecification bridgeSpecification;
+
+    public Group() {
+      this.classes = new ArrayList<>();
+      this.bridgeSpecification = null;
+    }
+
+    public Group(DexProgramClass clazz, BridgeSpecification bridgeSpecification) {
+      this.classes = ListUtils.newArrayList(clazz);
+      this.bridgeSpecification = bridgeSpecification;
+    }
+
+    void addClass(DexProgramClass clazz) {
+      classes.add(clazz);
+    }
+
+    BridgeSpecification getBridgeSpecification() {
+      return bridgeSpecification;
+    }
+
+    List<DexProgramClass> getClasses() {
+      return classes;
+    }
+
+    void setBridgeSpecification(BridgeSpecification bridgeSpecification) {
+      this.bridgeSpecification = bridgeSpecification;
+    }
+
+    boolean isSingleton() {
+      return size() == 1;
+    }
+
+    @Override
+    public Iterator<DexProgramClass> iterator() {
+      return classes.iterator();
+    }
+
+    public int size() {
+      return classes.size();
+    }
+  }
+
+  private static class BridgeSpecification {
+
+    private final DexMethodSignatureMap<DexMethodSignature> bridges =
+        DexMethodSignatureMap.create();
+
+    void addBridge(DexMethodSignature method, VirtualBridgeInfo bridgeInfo) {
+      bridges.put(method, bridgeInfo.getInvokedMethod().getSignature());
+    }
+
+    boolean containsBridgeWithTarget(DexMethodSignature method, DexMethodSignature target) {
+      return target.equals(bridges.get(method));
+    }
+
+    void forEach(BiConsumer<? super DexMethodSignature, ? super DexMethodSignature> consumer) {
+      bridges.forEach(consumer);
+    }
+
+    boolean isEmpty() {
+      return bridges.isEmpty();
+    }
+
+    boolean lessThanOrEquals(BridgeSpecification bridgeSpecification) {
+      if (size() > bridgeSpecification.size()) {
+        return false;
+      }
+      for (Entry<DexMethodSignature, DexMethodSignature> entry : bridges.entrySet()) {
+        DexMethodSignature method = entry.getKey();
+        DexMethodSignature target = entry.getValue();
+        if (!bridgeSpecification.containsBridgeWithTarget(method, target)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    int size() {
+      return bridges.size();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
deleted file mode 100644
index c51b13c..0000000
--- a/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.optimize;
-
-import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
-import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
-import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.FieldAccessFlags;
-import com.android.tools.r8.graph.InnerClassAttribute;
-import com.android.tools.r8.graph.MethodAccessFlags;
-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.SubtypingInfo;
-import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
-import com.android.tools.r8.ir.optimize.MethodPoolCollection;
-import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
-import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
-import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.OptionalBool;
-import com.android.tools.r8.utils.Timing;
-import com.google.common.base.Equivalence.Wrapper;
-import java.util.LinkedHashSet;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-public final class LegacyAccessModifier {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final SubtypingInfo subtypingInfo;
-  private final MethodPoolCollection methodPoolCollection;
-
-  private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
-
-  private LegacyAccessModifier(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
-    this.subtypingInfo = appView.appInfo().computeSubtypingInfo();
-    this.methodPoolCollection =
-        // We will add private instance methods when we promote them.
-        new MethodPoolCollection(
-            appView, subtypingInfo, MethodPoolCollection::excludesPrivateInstanceMethod);
-  }
-
-  /**
-   * Marks all package private and protected methods and fields as public. Makes all private static
-   * methods public. Makes private instance methods public final instance methods, if possible.
-   *
-   * <p>This will destructively update the DexApplication passed in as argument.
-   */
-  public static void run(
-      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
-      throws ExecutionException {
-    AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
-    if (accessModifierOptions.isAccessModificationEnabled()
-        && accessModifierOptions.isLegacyAccessModifierEnabled()) {
-      timing.begin("Access modification");
-      new LegacyAccessModifier(appView).internalRun(executorService, timing);
-      timing.end();
-      if (appView.graphLens().isPublicizerLens()) {
-        // We can now remove redundant bridges. Note that we do not need to update the
-        // invoke-targets here, as the existing invokes will simply dispatch to the now
-        // visible super-method. MemberRebinding, if run, will then dispatch it correctly.
-        new RedundantBridgeRemover(appView.withLiveness()).run(executorService, timing);
-      }
-    }
-  }
-
-  private void internalRun(ExecutorService executorService, Timing timing)
-      throws ExecutionException {
-    // Phase 1: Collect methods to check if private instance methods don't have conflicts.
-    methodPoolCollection.buildAll(executorService, timing);
-
-    // Phase 2: Visit classes and promote class/member to public if possible.
-    timing.begin("Phase 2: promoteToPublic");
-    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
-    processType(appView.dexItemFactory().objectType);
-    timing.end();
-
-    PublicizerLens publicizerLens = lensBuilder.build(appView);
-    if (publicizerLens != null) {
-      appView.setGraphLens(publicizerLens);
-    }
-
-    appView.notifyOptimizationFinishedForTesting();
-  }
-
-  private void doPublicize(ProgramDefinition definition) {
-    definition.getAccessFlags().promoteToPublic();
-  }
-
-  private void processType(DexType type) {
-    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
-    if (clazz != null) {
-      processClass(clazz);
-    }
-    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
-  }
-
-  private void processClass(DexProgramClass clazz) {
-    if (appView.appInfo().isAccessModificationAllowed(clazz)) {
-      doPublicize(clazz);
-    }
-
-    // Publicize fields.
-    clazz.forEachProgramField(this::processField);
-
-    // Publicize methods.
-    Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
-    clazz.forEachProgramMethod(
-        method -> {
-          if (publicizeMethod(method)) {
-            privateInstanceMethods.add(method.getDefinition());
-          }
-        });
-    if (!privateInstanceMethods.isEmpty()) {
-      clazz.virtualizeMethods(privateInstanceMethods);
-    }
-
-    // Publicize inner class attribute.
-    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
-    if (attr != null) {
-      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
-      clazz.replaceInnerClassAttributeForThisClass(
-          new InnerClassAttribute(
-              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
-    }
-  }
-
-  private void processField(ProgramField field) {
-    if (appView.appInfo().isAccessModificationAllowed(field)) {
-      publicizeField(field);
-    }
-  }
-
-  private void publicizeField(ProgramField field) {
-    FieldAccessFlags flags = field.getAccessFlags();
-    if (!flags.isPublic()) {
-      flags.promoteToPublic();
-    }
-  }
-
-  private boolean publicizeMethod(ProgramMethod method) {
-    MethodAccessFlags accessFlags = method.getAccessFlags();
-    if (accessFlags.isPublic()) {
-      return false;
-    }
-    // If this method is mentioned in keep rules, do not transform (rule applications changed).
-    DexEncodedMethod definition = method.getDefinition();
-    if (!appView.appInfo().isAccessModificationAllowed(method)) {
-      // TODO(b/131130038): Also do not publicize package-private and protected methods that are
-      //  kept.
-      if (definition.isPrivate()) {
-        return false;
-      }
-    }
-
-    if (method.getDefinition().isInstanceInitializer() || accessFlags.isProtected()) {
-      doPublicize(method);
-      return false;
-    }
-
-    if (accessFlags.isPackagePrivate()) {
-      // If we publicize a package private method we have to ensure there is no overrides of it. We
-      // could potentially publicize a method if it only has package-private overrides.
-      // TODO(b/182136236): See if we can break the hierarchy for clusters.
-      MemberPool<DexMethod> memberPool = methodPoolCollection.get(method.getHolder());
-      Wrapper<DexMethod> methodKey = MethodSignatureEquivalence.get().wrap(method.getReference());
-      if (memberPool.below(
-          methodKey,
-          false,
-          true,
-          (clazz, ignored) ->
-              !method.getContextType().getPackageName().equals(clazz.getType().getPackageName()))) {
-        return false;
-      }
-      doPublicize(method);
-      return false;
-    }
-
-    assert accessFlags.isPrivate();
-
-    if (accessFlags.isStatic()) {
-      // For private static methods we can just relax the access to public, since
-      // even though JLS prevents from declaring static method in derived class if
-      // an instance method with same signature exists in superclass, JVM actually
-      // does not take into account access of the static methods.
-      doPublicize(method);
-      return false;
-    }
-
-    // We can't publicize private instance methods in interfaces or methods that are copied from
-    // interfaces to lambda-desugared classes because this will be added as a new default method.
-    // TODO(b/111118390): It might be possible to transform it into static methods, though.
-    if (method.getHolder().isInterface() || accessFlags.isSynthetic()) {
-      return false;
-    }
-
-    boolean wasSeen = methodPoolCollection.markIfNotSeen(method.getHolder(), method.getReference());
-    if (wasSeen) {
-      // We can't do anything further because even renaming is not allowed due to the keep rule.
-      if (!appView.appInfo().isMinificationAllowed(method)) {
-        return false;
-      }
-      // TODO(b/111118390): Renaming will enable more private instance methods to be publicized.
-      return false;
-    }
-    lensBuilder.add(method.getReference());
-    accessFlags.promoteToFinal();
-    doPublicize(method);
-    // The method just became public and is therefore not a library override.
-    definition.setLibraryMethodOverride(OptionalBool.FALSE);
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index d8514a0..388638c 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -390,8 +390,12 @@
                                       appView.computedMinApiLevel()));
                         }
                         builder.setIsLibraryMethodOverrideIf(
-                            target.isLibraryMethod(), OptionalBool.TRUE);
+                            // Treat classpath override as library override.
+                            target.isLibraryMethod() || target.isClasspathMethod(),
+                            OptionalBool.TRUE);
                       });
+              assert !bridgeMethodDefinition.belongsToVirtualPool()
+                  || !bridgeMethodDefinition.isLibraryMethodOverride().isUnknown();
               bridgeHolder.addMethod(bridgeMethodDefinition);
               eventConsumer.acceptMemberRebindingBridgeMethod(
                   bridgeMethodDefinition.asProgramMethod(bridgeHolder), target);
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java
index dfd16e6..331d29e 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java
@@ -7,17 +7,16 @@
 import com.android.tools.r8.graph.AbstractAccessContexts.ConcreteAccessContexts;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldAccessInfoImpl;
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -101,7 +100,8 @@
         executorService);
   }
 
-  private static class NonReboundMemberReferencesRegistry extends UseRegistry<ProgramMethod> {
+  private static class NonReboundMemberReferencesRegistry
+      extends DefaultUseRegistry<ProgramMethod> {
 
     private final AppInfoWithClassHierarchy appInfo;
     private final FieldAccessInfoCollectionImpl fieldAccessInfoCollection;
@@ -232,25 +232,5 @@
       // simply use the empty set.
       invokes.put(method, ProgramMethodSet.empty());
     }
-
-    @Override
-    public void registerInitClass(DexType type) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public void registerInstanceOf(DexType type) {
-      // Intentionally empty.
-    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
index 64570f5..086b81f 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
@@ -62,8 +62,7 @@
       throws ExecutionException {
     timing.begin("Access modification");
     AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
-    if (accessModifierOptions.isAccessModificationEnabled()
-        && !accessModifierOptions.isLegacyAccessModifierEnabled()) {
+    if (accessModifierOptions.isAccessModificationEnabled()) {
       new AccessModifier(appView)
           .processStronglyConnectedComponents(executorService)
           .installLens(executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
index 81c1f03..6891352 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
@@ -5,18 +5,13 @@
 package com.android.tools.r8.optimize.accessmodification;
 
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.SystemPropertyUtils;
 
 public class AccessModifierOptions {
 
-  private boolean enableLegacyAccessModifier =
-      SystemPropertyUtils.parseSystemPropertyOrDefault(
-          "com.android.tools.r8.accessmodification.legacy", false);
-
   // TODO(b/131130038): Do not allow accessmodification when kept.
   private boolean forceModifyPackagePrivateAndProtectedMethods = true;
 
-  private InternalOptions options;
+  private final InternalOptions options;
 
   public AccessModifierOptions(InternalOptions options) {
     this.options = options;
@@ -30,9 +25,6 @@
     if (isAccessModificationRulePresent()) {
       return true;
     }
-    if (isLegacyAccessModifierEnabled()) {
-      return false;
-    }
     // TODO(b/288062771): Enable access modification by default for L8.
     return options.synthesizedClassPrefix.isEmpty()
         && !options.forceProguardCompatibility
@@ -53,8 +45,4 @@
     this.forceModifyPackagePrivateAndProtectedMethods =
         forceModifyPackagePrivateAndProtectedMethods;
   }
-
-  public boolean isLegacyAccessModifierEnabled() {
-    return enableLegacyAccessModifier;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 2f90ff5..97ead9d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
@@ -112,7 +113,9 @@
       ProgramMethod method, IRCode code, MethodProcessor methodProcessor, Timing timing) {
     if (codeScanner != null) {
       assert methodProcessor.isPrimaryMethodProcessor();
-      codeScanner.scan(method, code, timing);
+      AbstractValueSupplier abstractValueSupplier =
+          value -> value.getAbstractValue(appView, method);
+      codeScanner.scan(method, code, abstractValueSupplier, timing);
 
       assert effectivelyUnusedArgumentsAnalysis != null;
       effectivelyUnusedArgumentsAnalysis.scan(method, code);
@@ -226,14 +229,16 @@
     postMethodProcessorBuilder.rewrittenWithLens(appView);
 
     timing.begin("Compute optimization info");
-    new ArgumentPropagatorOptimizationInfoPopulator(
+    new ArgumentPropagatorOptimizationInfoPropagator(
             appView,
             converter,
             immediateSubtypingInfo,
             codeScannerResult,
-            postMethodProcessorBuilder,
             stronglyConnectedProgramComponents,
             interfaceDispatchOutsideProgram)
+        .propagateOptimizationInfo(executorService, timing);
+    new ArgumentPropagatorOptimizationInfoPopulator(
+            appView, converter, codeScannerResult, postMethodProcessorBuilder)
         .populateOptimizationInfo(executorService, timing);
     timing.end();
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 49d6400..0dc55d3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.AliasedValueConfiguration;
 import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
 import com.android.tools.r8.ir.code.IRCode;
@@ -89,6 +90,10 @@
   private final MethodStateCollectionByReference methodStates =
       MethodStateCollectionByReference.createConcurrent();
 
+  public ArgumentPropagatorCodeScanner(AppView<AppInfoWithLiveness> appView) {
+    this(appView, new ArgumentPropagatorReprocessingCriteriaCollection(appView));
+  }
+
   ArgumentPropagatorCodeScanner(
       AppView<AppInfoWithLiveness> appView,
       ArgumentPropagatorReprocessingCriteriaCollection reprocessingCriteriaCollection) {
@@ -105,7 +110,7 @@
     virtualRootMethods.putAll(extension);
   }
 
-  MethodStateCollectionByReference getMethodStates() {
+  public MethodStateCollectionByReference getMethodStates() {
     return methodStates;
   }
 
@@ -113,7 +118,8 @@
     return virtualRootMethods.get(method.getReference());
   }
 
-  boolean isMethodParameterAlreadyUnknown(MethodParameter methodParameter, ProgramMethod method) {
+  protected boolean isMethodParameterAlreadyUnknown(
+      MethodParameter methodParameter, ProgramMethod method) {
     MethodState methodState =
         methodStates.get(
             method.getDefinition().belongsToDirectPool() || isMonomorphicVirtualMethod(method)
@@ -141,19 +147,27 @@
     return monomorphicVirtualMethods.contains(method);
   }
 
-  void scan(ProgramMethod method, IRCode code, Timing timing) {
+  public void scan(
+      ProgramMethod method,
+      IRCode code,
+      AbstractValueSupplier abstractValueSupplier,
+      Timing timing) {
     timing.begin("Argument propagation scanner");
     for (Invoke invoke : code.<Invoke>instructions(Instruction::isInvoke)) {
       if (invoke.isInvokeMethod()) {
-        scan(invoke.asInvokeMethod(), method, timing);
+        scan(invoke.asInvokeMethod(), abstractValueSupplier, method, timing);
       } else if (invoke.isInvokeCustom()) {
-        scan(invoke.asInvokeCustom(), method);
+        scan(invoke.asInvokeCustom());
       }
     }
     timing.end();
   }
 
-  private void scan(InvokeMethod invoke, ProgramMethod context, Timing timing) {
+  private void scan(
+      InvokeMethod invoke,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
     if (invokedMethod.getHolderType().isArrayType()) {
       // Nothing to propagate; the targeted method is not a program method.
@@ -231,13 +245,27 @@
     // possible dispatch targets and propagate the information to these methods (this is expensive).
     // Instead we record the information in one place and then later propagate the information to
     // all dispatch targets.
-    ProgramMethod finalResolvedMethod = resolvedMethod;
+    addTemporaryMethodState(invoke, resolvedMethod, abstractValueSupplier, context, timing);
+  }
+
+  protected void addTemporaryMethodState(
+      InvokeMethod invoke,
+      ProgramMethod resolvedMethod,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
     timing.begin("Add method state");
     methodStates.addTemporaryMethodState(
         appView,
         getRepresentative(invoke, resolvedMethod),
         existingMethodState ->
-            computeMethodState(invoke, finalResolvedMethod, context, existingMethodState, timing),
+            computeMethodState(
+                invoke,
+                resolvedMethod,
+                abstractValueSupplier,
+                context,
+                existingMethodState,
+                timing),
         timing);
     timing.end();
   }
@@ -245,6 +273,7 @@
   private MethodState computeMethodState(
       InvokeMethod invoke,
       ProgramMethod resolvedMethod,
+      AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
       MethodState existingMethodState,
       Timing timing) {
@@ -262,6 +291,7 @@
           computePolymorphicMethodState(
               invoke.asInvokeMethodWithReceiver(),
               resolvedMethod,
+              abstractValueSupplier,
               context,
               existingMethodState.asPolymorphicOrBottom());
     } else {
@@ -271,6 +301,7 @@
               invoke,
               resolvedMethod,
               invoke.lookupSingleProgramTarget(appView, context),
+              abstractValueSupplier,
               context,
               existingMethodState.asMonomorphicOrBottom());
     }
@@ -285,6 +316,7 @@
   private MethodState computePolymorphicMethodState(
       InvokeMethodWithReceiver invoke,
       ProgramMethod resolvedMethod,
+      AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
       ConcretePolymorphicMethodStateOrBottom existingMethodState) {
     DynamicTypeWithUpperBound dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
@@ -320,6 +352,7 @@
             invoke,
             resolvedMethod,
             singleTarget,
+            abstractValueSupplier,
             context,
             existingMethodStateForBounds.asMonomorphicOrBottom(),
             dynamicReceiverType);
@@ -363,12 +396,14 @@
       InvokeMethod invoke,
       ProgramMethod resolvedMethod,
       ProgramMethod singleTarget,
+      AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
       ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
     return computeMonomorphicMethodState(
         invoke,
         resolvedMethod,
         singleTarget,
+        abstractValueSupplier,
         context,
         existingMethodState,
         invoke.isInvokeMethodWithReceiver()
@@ -381,6 +416,7 @@
       InvokeMethod invoke,
       ProgramMethod resolvedMethod,
       ProgramMethod singleTarget,
+      AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
       ConcreteMonomorphicMethodStateOrBottom existingMethodState,
       DynamicType dynamicReceiverType) {
@@ -410,6 +446,7 @@
               singleTarget,
               argumentIndex,
               invoke.getArgument(argumentIndex),
+              abstractValueSupplier,
               context,
               existingMethodState));
     }
@@ -454,11 +491,12 @@
       ProgramMethod singleTarget,
       int argumentIndex,
       Value argument,
+      AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
       ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
     ParameterState modeledState =
         modeling.modelParameterStateForArgumentToFunction(
-            invoke, singleTarget, argumentIndex, argument);
+            invoke, singleTarget, argumentIndex, argument, context);
     if (modeledState != null) {
       return modeledState;
     }
@@ -504,7 +542,7 @@
           : new ConcreteArrayTypeParameterState(nullability);
     }
 
-    AbstractValue abstractValue = argument.getAbstractValue(appView, context);
+    AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(argument);
 
     // For class types, we track both the abstract value and the dynamic type. If both are unknown,
     // then use UnknownParameterState.
@@ -555,8 +593,7 @@
         && !isMonomorphicVirtualMethod(getRepresentative(invoke, resolvedMethod));
   }
 
-  @SuppressWarnings("UnusedVariable")
-  private void scan(InvokeCustom invoke, ProgramMethod context) {
+  private void scan(InvokeCustom invoke) {
     // If the bootstrap method is program declared it will be called. The call is with runtime
     // provided arguments so ensure that the argument information is unknown.
     DexMethodHandle bootstrapMethod = invoke.getCallSite().bootstrapMethod;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
index 7ab506e..7931180 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
@@ -23,10 +23,14 @@
   }
 
   ParameterState modelParameterStateForArgumentToFunction(
-      InvokeMethod invoke, ProgramMethod singleTarget, int argumentIndex, Value argument) {
+      InvokeMethod invoke,
+      ProgramMethod singleTarget,
+      int argumentIndex,
+      Value argument,
+      ProgramMethod context) {
     if (composeModeling != null) {
       return composeModeling.modelParameterStateForChangedOrDefaultArgumentToComposableFunction(
-          invoke, singleTarget, argumentIndex, argument);
+          invoke, singleTarget, argumentIndex, argument, context);
     }
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
index 9b0f212..b857d7d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
@@ -5,16 +5,15 @@
 package com.android.tools.r8.optimize.argumentpropagation;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistryWithResult;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
@@ -145,7 +144,8 @@
             postMethodProcessorBuilder.addAll(methodsToReprocessInClass, currentGraphLens));
   }
 
-  static class AffectedMethodUseRegistry extends UseRegistryWithResult<Boolean, ProgramMethod> {
+  static class AffectedMethodUseRegistry
+      extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
     private final AppView<AppInfoWithLiveness> appViewWithLiveness;
     private final ArgumentPropagatorGraphLens graphLens;
@@ -242,11 +242,5 @@
         markAffected();
       }
     }
-
-    @Override
-    public void registerInitClass(DexType type) {}
-
-    @Override
-    public void registerTypeReference(DexType type) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index c626347..ea9d69a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -7,10 +7,8 @@
 import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexMethodSignature;
 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.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -28,9 +26,6 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
-import com.android.tools.r8.optimize.argumentpropagation.propagation.InParameterFlowPropagator;
-import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
-import com.android.tools.r8.optimize.argumentpropagation.propagation.VirtualDispatchMethodArgumentPropagator;
 import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -40,10 +35,8 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.function.BiConsumer;
 
 /**
  * Propagates the argument flow information collected by the {@link ArgumentPropagatorCodeScanner}.
@@ -58,28 +51,16 @@
   private final InternalOptions options;
   private final PostMethodProcessor.Builder postMethodProcessorBuilder;
 
-  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
-  private final List<Set<DexProgramClass>> stronglyConnectedProgramComponents;
-
-  private final BiConsumer<Set<DexProgramClass>, DexMethodSignature>
-      interfaceDispatchOutsideProgram;
-
-  ArgumentPropagatorOptimizationInfoPopulator(
+  public ArgumentPropagatorOptimizationInfoPopulator(
       AppView<AppInfoWithLiveness> appView,
       PrimaryR8IRConverter converter,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       MethodStateCollectionByReference methodStates,
-      PostMethodProcessor.Builder postMethodProcessorBuilder,
-      List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
-      BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram) {
+      PostMethodProcessor.Builder postMethodProcessorBuilder) {
     this.appView = appView;
     this.converter = converter;
-    this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.methodStates = methodStates;
     this.options = appView.options();
     this.postMethodProcessorBuilder = postMethodProcessorBuilder;
-    this.stronglyConnectedProgramComponents = stronglyConnectedProgramComponents;
-    this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
   }
 
   /**
@@ -88,24 +69,6 @@
    */
   void populateOptimizationInfo(ExecutorService executorService, Timing timing)
       throws ExecutionException {
-    // TODO(b/190154391): Propagate argument information to handle virtual dispatch.
-    // TODO(b/190154391): To deal with arguments that are themselves passed as arguments to invoke
-    //  instructions, build a flow graph where nodes are parameters and there is an edge from a
-    //  parameter p1 to p2 if the value of p2 is at least the value of p1. Then propagate the
-    //  collected argument information throughout the flow graph.
-    timing.begin("Propagate argument information for virtual methods");
-    ThreadUtils.processItems(
-        stronglyConnectedProgramComponents,
-        this::processStronglyConnectedComponent,
-        appView.options().getThreadingModule(),
-        executorService);
-    timing.end();
-
-    // Solve the parameter flow constraints.
-    timing.begin("Solve flow constraints");
-    new InParameterFlowPropagator(appView, converter, methodStates).run(executorService);
-    timing.end();
-
     // The information stored on each method is now sound, and can be used as optimization info.
     timing.begin("Set optimization info");
     setOptimizationInfo(executorService);
@@ -114,41 +77,6 @@
     assert methodStates.isEmpty();
   }
 
-  private void processStronglyConnectedComponent(Set<DexProgramClass> stronglyConnectedComponent) {
-    // Invoke instructions that target interface methods may dispatch to methods that are not
-    // defined on a subclass of the interface method holder.
-    //
-    // Example: Calling I.m() will dispatch to A.m(), but A is not a subtype of I.
-    //
-    //   class A { public void m() {} }
-    //   interface I { void m(); }
-    //   class B extends A implements I {}
-    //
-    // To handle this we first propagate any argument information stored for I.m() to A.m() by doing
-    // a top-down traversal over the interfaces in the strongly connected component.
-    new InterfaceMethodArgumentPropagator(
-            appView,
-            immediateSubtypingInfo,
-            methodStates,
-            signature ->
-                interfaceDispatchOutsideProgram.accept(stronglyConnectedComponent, signature))
-        .run(stronglyConnectedComponent);
-
-    // Now all the argument information for a given method is guaranteed to be stored on a supertype
-    // of the method's holder. All that remains is to propagate the information downwards in the
-    // class hierarchy to propagate the argument information for a non-private virtual method to its
-    // overrides.
-    // TODO(b/190154391): Before running the top-down traversal, consider lowering the argument
-    //  information for non-private virtual methods. If we have some argument information with upper
-    //  bound=B, which is stored on a method on class A, we could move this argument information
-    //  from class A to B. This way we could potentially get rid of the "inactive argument
-    //  information" during the depth-first class hierarchy traversal, since the argument
-    //  information would be active by construction when it is first seen during the top-down class
-    //  hierarchy traversal.
-    new VirtualDispatchMethodArgumentPropagator(appView, immediateSubtypingInfo, methodStates)
-        .run(stronglyConnectedComponent);
-  }
-
   private void setOptimizationInfo(ExecutorService executorService) throws ExecutionException {
     ProgramMethodSet prunedMethods = ProgramMethodSet.createConcurrent();
     ThreadUtils.processItems(
@@ -170,8 +98,12 @@
     return prunedMethods;
   }
 
-  private void setOptimizationInfo(ProgramMethod method, ProgramMethodSet prunedMethods) {
-    MethodState methodState = methodStates.remove(method);
+  public void setOptimizationInfo(ProgramMethod method, ProgramMethodSet prunedMethods) {
+    setOptimizationInfo(method, prunedMethods, methodStates.remove(method));
+  }
+
+  public void setOptimizationInfo(
+      ProgramMethod method, ProgramMethodSet prunedMethods, MethodState methodState) {
     if (methodState.isBottom()) {
       if (method.getDefinition().isClassInitializer()) {
         return;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
new file mode 100644
index 0000000..2c8f5e4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.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.optimize.argumentpropagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InParameterFlowPropagator;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.VirtualDispatchMethodArgumentPropagator;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+
+/**
+ * Propagates the argument flow information collected by the {@link ArgumentPropagatorCodeScanner}.
+ * This is needed to propagate argument information from call sites to all possible dispatch
+ * targets.
+ */
+public class ArgumentPropagatorOptimizationInfoPropagator {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final PrimaryR8IRConverter converter;
+  private final MethodStateCollectionByReference methodStates;
+
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+  private final List<Set<DexProgramClass>> stronglyConnectedProgramComponents;
+
+  private final BiConsumer<Set<DexProgramClass>, DexMethodSignature>
+      interfaceDispatchOutsideProgram;
+
+  ArgumentPropagatorOptimizationInfoPropagator(
+      AppView<AppInfoWithLiveness> appView,
+      PrimaryR8IRConverter converter,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      MethodStateCollectionByReference methodStates,
+      List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
+      BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram) {
+    this.appView = appView;
+    this.converter = converter;
+    this.immediateSubtypingInfo = immediateSubtypingInfo;
+    this.methodStates = methodStates;
+    this.stronglyConnectedProgramComponents = stronglyConnectedProgramComponents;
+    this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
+  }
+
+  /** Computes an over-approximation of each parameter's value and type. */
+  void propagateOptimizationInfo(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    timing.begin("Propagate argument information for virtual methods");
+    ThreadUtils.processItems(
+        stronglyConnectedProgramComponents,
+        this::processStronglyConnectedComponent,
+        appView.options().getThreadingModule(),
+        executorService);
+    timing.end();
+
+    // Solve the parameter flow constraints.
+    timing.begin("Solve flow constraints");
+    new InParameterFlowPropagator(appView, converter, methodStates).run(executorService);
+    timing.end();
+  }
+
+  private void processStronglyConnectedComponent(Set<DexProgramClass> stronglyConnectedComponent) {
+    // Invoke instructions that target interface methods may dispatch to methods that are not
+    // defined on a subclass of the interface method holder.
+    //
+    // Example: Calling I.m() will dispatch to A.m(), but A is not a subtype of I.
+    //
+    //   class A { public void m() {} }
+    //   interface I { void m(); }
+    //   class B extends A implements I {}
+    //
+    // To handle this we first propagate any argument information stored for I.m() to A.m() by doing
+    // a top-down traversal over the interfaces in the strongly connected component.
+    new InterfaceMethodArgumentPropagator(
+            appView,
+            immediateSubtypingInfo,
+            methodStates,
+            signature ->
+                interfaceDispatchOutsideProgram.accept(stronglyConnectedComponent, signature))
+        .run(stronglyConnectedComponent);
+
+    // Now all the argument information for a given method is guaranteed to be stored on a supertype
+    // of the method's holder. All that remains is to propagate the information downwards in the
+    // class hierarchy to propagate the argument information for a non-private virtual method to its
+    // overrides.
+    // TODO(b/190154391): Before running the top-down traversal, consider lowering the argument
+    //  information for non-private virtual methods. If we have some argument information with upper
+    //  bound=B, which is stored on a method on class A, we could move this argument information
+    //  from class A to B. This way we could potentially get rid of the "inactive argument
+    //  information" during the depth-first class hierarchy traversal, since the argument
+    //  information would be active by construction when it is first seen during the top-down class
+    //  hierarchy traversal.
+    new VirtualDispatchMethodArgumentPropagator(appView, immediateSubtypingInfo, methodStates)
+        .run(stronglyConnectedComponent);
+  }
+}
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 387d67d..5a8e0de 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
@@ -99,17 +99,22 @@
 
       // Record the invokes from the newly synthesized bridge methods in the method access info
       // collection.
-      MethodAccessInfoCollection.Modifier methodAccessInfoCollectionModifier =
-          appView.appInfo().getMethodAccessInfoCollection().modifier();
-      result.forEachHoistedBridge(
-          (bridge, bridgeInfo) -> {
-            if (bridgeInfo.isVirtualBridgeInfo()) {
-              DexMethod reference = bridgeInfo.asVirtualBridgeInfo().getInvokedMethod();
-              methodAccessInfoCollectionModifier.registerInvokeVirtualInContext(reference, bridge);
-            } else {
-              assert false;
-            }
-          });
+      MethodAccessInfoCollection methodAccessInfoCollection =
+          appView.appInfo().getMethodAccessInfoCollection();
+      if (!methodAccessInfoCollection.isVirtualInvokesDestroyed()) {
+        MethodAccessInfoCollection.Modifier methodAccessInfoCollectionModifier =
+            methodAccessInfoCollection.modifier();
+        result.forEachHoistedBridge(
+            (bridge, bridgeInfo) -> {
+              if (bridgeInfo.isVirtualBridgeInfo()) {
+                DexMethod reference = bridgeInfo.asVirtualBridgeInfo().getInvokedMethod();
+                methodAccessInfoCollectionModifier.registerInvokeVirtualInContext(
+                    reference, bridge);
+              } else {
+                assert false;
+              }
+            });
+      }
     }
 
     appView.notifyOptimizationFinishedForTesting();
@@ -257,7 +262,7 @@
     // Now update the code of the bridge method chosen as representative.
     representative
         .setCode(createCodeForVirtualBridge(representative, methodToInvoke), appView);
-    feedback.setBridgeInfo(representative.getDefinition(), new VirtualBridgeInfo(methodToInvoke));
+    feedback.setBridgeInfo(representative, new VirtualBridgeInfo(methodToInvoke));
 
     // Move the bridge method to the super class, and record this in the graph lens.
     DexMethod newMethodReference =
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
new file mode 100644
index 0000000..5d0b693
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.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.optimize.compose;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.AbstractValueSupplier;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
+
+public class ArgumentPropagatorCodeScannerForComposableFunctions
+    extends ArgumentPropagatorCodeScanner {
+
+  private final ComposableCallGraph callGraph;
+
+  public ArgumentPropagatorCodeScannerForComposableFunctions(
+      AppView<AppInfoWithLiveness> appView, ComposableCallGraph callGraph) {
+    super(appView);
+    this.callGraph = callGraph;
+  }
+
+  @Override
+  protected void addTemporaryMethodState(
+      InvokeMethod invoke,
+      ProgramMethod resolvedMethod,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
+    ComposableCallGraphNode node = callGraph.getNodes().get(resolvedMethod);
+    if (node != null && node.isComposable()) {
+      super.addTemporaryMethodState(invoke, resolvedMethod, abstractValueSupplier, context, timing);
+    }
+  }
+
+  @Override
+  protected boolean isMethodParameterAlreadyUnknown(
+      MethodParameter methodParameter, ProgramMethod method) {
+    // We haven't defined the virtual root mapping, so we can't tell.
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
index 49cd7d5..6ffeb6d 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
@@ -4,8 +4,10 @@
 package com.android.tools.r8.optimize.compose;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.InstanceGet;
@@ -19,16 +21,31 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BitUtils;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.google.common.collect.Iterables;
 
 public class ArgumentPropagatorComposeModeling {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final ComposeReferences composeReferences;
+  private final ComposeReferences rewrittenComposeReferences;
+
+  private final DexType rewrittenFunction2Type;
+  private final DexString invokeName;
 
   public ArgumentPropagatorComposeModeling(AppView<AppInfoWithLiveness> appView) {
     assert appView.testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
     this.appView = appView;
-    this.composeReferences = appView.getComposeReferences();
+    this.rewrittenComposeReferences =
+        appView
+            .getComposeReferences()
+            .rewrittenWithLens(appView.graphLens(), GraphLens.getIdentityLens());
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.rewrittenFunction2Type =
+        appView
+            .graphLens()
+            .lookupType(
+                dexItemFactory.createType("Lkotlin/jvm/functions/Function2;"),
+                GraphLens.getIdentityLens());
+    this.invokeName = dexItemFactory.createString("invoke");
   }
 
   /**
@@ -68,13 +85,28 @@
    * </pre>
    */
   public ParameterState modelParameterStateForChangedOrDefaultArgumentToComposableFunction(
-      InvokeMethod invoke, ProgramMethod singleTarget, int argumentIndex, Value argument) {
+      InvokeMethod invoke,
+      ProgramMethod singleTarget,
+      int argumentIndex,
+      Value argument,
+      ProgramMethod context) {
+    // TODO(b/302483644): Add some robust way of detecting restart lambda contexts.
+    if (!context.getHolder().getInterfaces().contains(rewrittenFunction2Type)
+        || !invoke.getPosition().getOutermostCaller().getMethod().getName().isEqualTo(invokeName)
+        || Iterables.isEmpty(
+            context
+                .getHolder()
+                .instanceFields(
+                    f -> f.getName().isIdenticalTo(rewrittenComposeReferences.changedFieldName)))) {
+      return null;
+    }
+
     // First check if this is an invoke to a @Composable function.
     if (singleTarget == null
         || !singleTarget
             .getDefinition()
             .annotations()
-            .hasAnnotation(composeReferences.composableType)) {
+            .hasAnnotation(rewrittenComposeReferences.composableType)) {
       return null;
     }
 
@@ -103,7 +135,7 @@
         invokedMethod.getArity() - 2 - BooleanUtils.intValue(hasDefaultParameter);
     if (!invokedMethod
         .getParameter(composerParameterIndex)
-        .isIdenticalTo(composeReferences.composerType)) {
+        .isIdenticalTo(rewrittenComposeReferences.composerType)) {
       return null;
     }
 
@@ -122,13 +154,9 @@
       // We generally expect this argument to be defined by a call to updateChangedFlags().
       if (argument.isDefinedByInstructionSatisfying(Instruction::isInvokeStatic)) {
         InvokeStatic invokeStatic = argument.getDefinition().asInvokeStatic();
-        DexMethod maybeUpdateChangedFlagsMethod =
-            appView
-                .graphLens()
-                .getOriginalMethodSignature(
-                    invokeStatic.getInvokedMethod(), GraphLens.getIdentityLens());
+        DexMethod maybeUpdateChangedFlagsMethod = invokeStatic.getInvokedMethod();
         if (!maybeUpdateChangedFlagsMethod.isIdenticalTo(
-            composeReferences.updatedChangedFlagsMethod)) {
+            rewrittenComposeReferences.updatedChangedFlagsMethod)) {
           return null;
         }
         // Assume the call does not impact the $$changed capture and strip the call.
@@ -154,10 +182,10 @@
                     .createDefiniteBitsNumberValue(
                         BitUtils.ALL_BITS_SET_MASK, BitUtils.ALL_BITS_SET_MASK << 1));
       }
-      expectedFieldName = composeReferences.changedFieldName;
+      expectedFieldName = rewrittenComposeReferences.changedFieldName;
     } else {
       // We are looking at an argument to the $$default parameter of the @Composable function.
-      expectedFieldName = composeReferences.defaultFieldName;
+      expectedFieldName = rewrittenComposeReferences.defaultFieldName;
     }
 
     // At this point we expect that the restart lambda is reading either this.$$changed or
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraph.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraph.java
new file mode 100644
index 0000000..0cbf138
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraph.java
@@ -0,0 +1,170 @@
+// 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.optimize.compose;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import java.util.function.Consumer;
+
+/**
+ * A partial call graph that stores call edges to @Composable functions. By processing all the call
+ * sites of a given @Composable function we can reapply arguent propagation for the @Composable
+ * function.
+ */
+public class ComposableCallGraph {
+
+  private final ProgramMethodMap<ComposableCallGraphNode> nodes;
+
+  public ComposableCallGraph(ProgramMethodMap<ComposableCallGraphNode> nodes) {
+    this.nodes = nodes;
+  }
+
+  public static Builder builder(AppView<AppInfoWithLiveness> appView) {
+    return new Builder(appView);
+  }
+
+  public static ComposableCallGraph empty() {
+    return new ComposableCallGraph(ProgramMethodMap.empty());
+  }
+
+  public void forEachNode(Consumer<ComposableCallGraphNode> consumer) {
+    nodes.forEachValue(consumer);
+  }
+
+  public ProgramMethodMap<ComposableCallGraphNode> getNodes() {
+    return nodes;
+  }
+
+  public boolean isEmpty() {
+    return nodes.isEmpty();
+  }
+
+  public static class Builder {
+
+    private final AppView<AppInfoWithLiveness> appView;
+    private final ProgramMethodMap<ComposableCallGraphNode> nodes = ProgramMethodMap.create();
+
+    Builder(AppView<AppInfoWithLiveness> appView) {
+      this.appView = appView;
+    }
+
+    public ComposableCallGraph build() {
+      createCallGraphNodesForComposableFunctions();
+      if (!nodes.isEmpty()) {
+        addCallEdgesToComposableFunctions();
+      }
+      return new ComposableCallGraph(nodes);
+    }
+
+    private void createCallGraphNodesForComposableFunctions() {
+      ComposeReferences rewrittenComposeReferences =
+          appView
+              .getComposeReferences()
+              .rewrittenWithLens(appView.graphLens(), GraphLens.getIdentityLens());
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        clazz.forEachProgramDirectMethodMatching(
+            method -> method.annotations().hasAnnotation(rewrittenComposeReferences.composableType),
+            method -> {
+              // TODO(b/302483644): Don't include kept @Composable functions, since we can't
+              //  optimize them anyway.
+              assert method.getAccessFlags().isStatic();
+              nodes.put(method, new ComposableCallGraphNode(method, true));
+            });
+      }
+    }
+
+    // TODO(b/302483644): Parallelize identification of @Composable call sites.
+    private void addCallEdgesToComposableFunctions() {
+      // Code is fully rewritten so no need to lens rewrite in registry.
+      assert appView.codeLens() == appView.graphLens();
+
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        clazz.forEachProgramMethodMatching(
+            DexEncodedMethod::hasCode,
+            method -> {
+              Code code = method.getDefinition().getCode();
+
+              // TODO(b/302483644): Leverage LIR code constant pool for efficient checking.
+              // TODO(b/302483644): Maybe remove the possibility of CF/DEX at this point.
+              assert code.isLirCode()
+                  || code.isCfCode()
+                  || code.isDexCode()
+                  || code.isDefaultInstanceInitializerCode()
+                  || code.isThrowNullCode();
+
+              code.registerCodeReferences(
+                  method,
+                  new UseRegistry<>(appView, method) {
+
+                    private final AppView<AppInfoWithLiveness> appViewWithLiveness =
+                        appView.withLiveness();
+
+                    @Override
+                    public void registerInvokeStatic(DexMethod method) {
+                      ProgramMethod resolvedMethod =
+                          appViewWithLiveness
+                              .appInfo()
+                              .unsafeResolveMethodDueToDexFormat(method)
+                              .getResolvedProgramMethod();
+                      if (resolvedMethod == null) {
+                        return;
+                      }
+
+                      ComposableCallGraphNode callee = nodes.get(resolvedMethod);
+                      if (callee == null || !callee.isComposable()) {
+                        // Only record calls to Composable functions.
+                        return;
+                      }
+
+                      ComposableCallGraphNode caller =
+                          nodes.computeIfAbsent(
+                              getContext(), context -> new ComposableCallGraphNode(context, false));
+                      callee.addCaller(caller);
+                    }
+
+                    @Override
+                    public void registerInitClass(DexType type) {}
+
+                    @Override
+                    public void registerInvokeDirect(DexMethod method) {}
+
+                    @Override
+                    public void registerInvokeInterface(DexMethod method) {}
+
+                    @Override
+                    public void registerInvokeSuper(DexMethod method) {}
+
+                    @Override
+                    public void registerInvokeVirtual(DexMethod method) {}
+
+                    @Override
+                    public void registerInstanceFieldRead(DexField field) {}
+
+                    @Override
+                    public void registerInstanceFieldWrite(DexField field) {}
+
+                    @Override
+                    public void registerStaticFieldRead(DexField field) {}
+
+                    @Override
+                    public void registerStaticFieldWrite(DexField field) {}
+
+                    @Override
+                    public void registerTypeReference(DexType type) {}
+                  });
+            });
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraphNode.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraphNode.java
new file mode 100644
index 0000000..13ec14db
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraphNode.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.optimize.compose;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ComposableCallGraphNode {
+
+  private final ProgramMethod method;
+  private final boolean isComposable;
+
+  private final Set<ComposableCallGraphNode> callers = SetUtils.newIdentityHashSet();
+  private final Set<ComposableCallGraphNode> callees = SetUtils.newIdentityHashSet();
+
+  ComposableCallGraphNode(ProgramMethod method, boolean isComposable) {
+    this.method = method;
+    this.isComposable = isComposable;
+  }
+
+  public void addCaller(ComposableCallGraphNode caller) {
+    callers.add(caller);
+    caller.callees.add(this);
+  }
+
+  public void forEachComposableCallee(Consumer<ComposableCallGraphNode> consumer) {
+    for (ComposableCallGraphNode callee : callees) {
+      if (callee.isComposable()) {
+        consumer.accept(callee);
+      }
+    }
+  }
+
+  public Set<ComposableCallGraphNode> getCallers() {
+    return callers;
+  }
+
+  public ProgramMethod getMethod() {
+    return method;
+  }
+
+  public boolean isComposable() {
+    return isComposable;
+  }
+
+  @Override
+  public String toString() {
+    return method.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java
new file mode 100644
index 0000000..244dbb4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java
@@ -0,0 +1,116 @@
+// 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.optimize.compose;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.TestingOptions;
+import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class ComposableOptimizationPass {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final PrimaryR8IRConverter converter;
+
+  private ComposableOptimizationPass(
+      AppView<AppInfoWithLiveness> appView, PrimaryR8IRConverter converter) {
+    this.appView = appView;
+    this.converter = converter;
+  }
+
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, PrimaryR8IRConverter converter, Timing timing) {
+    InternalOptions options = appView.options();
+    if (!options.isOptimizing() || !options.isShrinking()) {
+      return;
+    }
+    TestingOptions testingOptions = options.getTestingOptions();
+    if (!testingOptions.enableComposableOptimizationPass
+        || !testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions) {
+      return;
+    }
+    timing.time(
+        "ComposableOptimizationPass",
+        () -> new ComposableOptimizationPass(appView, converter).processWaves());
+  }
+
+  void processWaves() {
+    ComposableCallGraph callGraph = ComposableCallGraph.builder(appView).build();
+    ComposeMethodProcessor methodProcessor =
+        new ComposeMethodProcessor(appView, callGraph, converter);
+    Set<ComposableCallGraphNode> wave = createInitialWave(callGraph);
+    while (!wave.isEmpty()) {
+      Set<ComposableCallGraphNode> optimizedComposableFunctions = methodProcessor.processWave(wave);
+      wave = createNextWave(methodProcessor, optimizedComposableFunctions);
+    }
+  }
+
+  // TODO(b/302483644): Should we skip root @Composable functions that don't have any nested
+  //  @Composable functions (?).
+  private Set<ComposableCallGraphNode> computeComposableRoots(ComposableCallGraph callGraph) {
+    Set<ComposableCallGraphNode> composableRoots = Sets.newIdentityHashSet();
+    callGraph.forEachNode(
+        node -> {
+          if (!node.isComposable()
+              || Iterables.any(node.getCallers(), ComposableCallGraphNode::isComposable)) {
+            // This is not a @Composable root.
+            return;
+          }
+          if (node.getCallers().isEmpty()) {
+            // Don't include root @Composable functions that are never called. These are either kept
+            // or will be removed in tree shaking.
+            return;
+          }
+          composableRoots.add(node);
+        });
+    return composableRoots;
+  }
+
+  private Set<ComposableCallGraphNode> createInitialWave(ComposableCallGraph callGraph) {
+    Set<ComposableCallGraphNode> wave = Sets.newIdentityHashSet();
+    Set<ComposableCallGraphNode> composableRoots = computeComposableRoots(callGraph);
+    composableRoots.forEach(composableRoot -> wave.addAll(composableRoot.getCallers()));
+    return wave;
+  }
+
+  // TODO(b/302483644): Consider repeatedly extracting the roots from the graph similar to the way
+  //  we extract leaves in the primary optimization pass.
+  private static Set<ComposableCallGraphNode> createNextWave(
+      ComposeMethodProcessor methodProcessor,
+      Set<ComposableCallGraphNode> optimizedComposableFunctions) {
+    Set<ComposableCallGraphNode> nextWave =
+        SetUtils.newIdentityHashSet(optimizedComposableFunctions);
+
+    // If the new wave contains two @Composable functions where one calls the other, then defer the
+    // processing of the callee to a later wave, to ensure that we have seen all of its callers
+    // before processing the callee.
+    List<ComposableCallGraphNode> deferredComposableFunctions = new ArrayList<>();
+    nextWave.forEach(
+        node -> {
+          if (SetUtils.containsAnyOf(nextWave, node.getCallers())) {
+            deferredComposableFunctions.add(node);
+          }
+        });
+    deferredComposableFunctions.forEach(nextWave::remove);
+
+    // To optimize the @Composable functions that are called from the @Composable functions of the
+    // next wave in the wave after that, we need to include their callers in the next wave as well.
+    Set<ComposableCallGraphNode> callersOfCalledComposableFunctions = Sets.newIdentityHashSet();
+    nextWave.forEach(
+        node ->
+            node.forEachComposableCallee(
+                callee -> callersOfCalledComposableFunctions.addAll(callee.getCallers())));
+    nextWave.addAll(callersOfCalledComposableFunctions);
+    nextWave.removeIf(methodProcessor::isProcessed);
+    return nextWave;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
new file mode 100644
index 0000000..8ee6237
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
@@ -0,0 +1,171 @@
+// 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.optimize.compose;
+
+import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.AbstractValueSupplier;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
+import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorOptimizationInfoPopulator;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteParameterState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.LazyBox;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Map;
+import java.util.Set;
+
+public class ComposeMethodProcessor extends MethodProcessor {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ArgumentPropagatorCodeScanner codeScanner;
+  private final PrimaryR8IRConverter converter;
+
+  private final Set<ComposableCallGraphNode> processed = Sets.newIdentityHashSet();
+
+  public ComposeMethodProcessor(
+      AppView<AppInfoWithLiveness> appView,
+      ComposableCallGraph callGraph,
+      PrimaryR8IRConverter converter) {
+    this.appView = appView;
+    this.codeScanner = new ArgumentPropagatorCodeScannerForComposableFunctions(appView, callGraph);
+    this.converter = converter;
+  }
+
+  // TODO(b/302483644): Process wave concurrently.
+  public Set<ComposableCallGraphNode> processWave(Set<ComposableCallGraphNode> wave) {
+    ProcessorContext processorContext = appView.createProcessorContext();
+    wave.forEach(
+        node -> {
+          assert !processed.contains(node);
+          converter.processDesugaredMethod(
+              node.getMethod(),
+              OptimizationFeedback.getIgnoreFeedback(),
+              this,
+              processorContext.createMethodProcessingContext(node.getMethod()),
+              MethodConversionOptions.forLirPhase(appView));
+        });
+    processed.addAll(wave);
+    return optimizeComposableFunctionsCalledFromWave(wave);
+  }
+
+  private Set<ComposableCallGraphNode> optimizeComposableFunctionsCalledFromWave(
+      Set<ComposableCallGraphNode> wave) {
+    ArgumentPropagatorOptimizationInfoPopulator optimizationInfoPopulator =
+        new ArgumentPropagatorOptimizationInfoPopulator(appView, null, null, null);
+    Set<ComposableCallGraphNode> optimizedComposableFunctions = Sets.newIdentityHashSet();
+    wave.forEach(
+        node ->
+            node.forEachComposableCallee(
+                callee -> {
+                  if (Iterables.all(callee.getCallers(), this::isProcessed)) {
+                    optimizationInfoPopulator.setOptimizationInfo(
+                        callee.getMethod(), ProgramMethodSet.empty(), getMethodState(callee));
+                    // TODO(b/302483644): Only enqueue this callee if its optimization info changed.
+                    optimizedComposableFunctions.add(callee);
+                  }
+                }));
+    return optimizedComposableFunctions;
+  }
+
+  private MethodState getMethodState(ComposableCallGraphNode node) {
+    assert processed.containsAll(node.getCallers());
+    MethodState methodState = codeScanner.getMethodStates().get(node.getMethod());
+    return widenMethodState(methodState);
+  }
+
+  /**
+   * If a parameter state of the current method state encodes that it is greater than (lattice wise)
+   * than another parameter in the program, then widen the parameter state to unknown. This is
+   * needed since we are not guaranteed to have seen all possible call sites of the callers of this
+   * method.
+   */
+  private MethodState widenMethodState(MethodState methodState) {
+    assert !methodState.isBottom();
+    assert !methodState.isPolymorphic();
+    if (methodState.isMonomorphic()) {
+      ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+      for (int i = 0; i < monomorphicMethodState.size(); i++) {
+        if (monomorphicMethodState.getParameterState(i).isConcrete()) {
+          ConcreteParameterState concreteParameterState =
+              monomorphicMethodState.getParameterState(i).asConcrete();
+          if (concreteParameterState.hasInParameters()) {
+            monomorphicMethodState.setParameterState(i, ParameterState.unknown());
+          }
+        }
+      }
+    } else {
+      assert methodState.isUnknown();
+    }
+    return methodState;
+  }
+
+  public void scan(ProgramMethod method, IRCode code, Timing timing) {
+    LazyBox<Map<Value, AbstractValue>> abstractValues =
+        new LazyBox<>(() -> new SparseConditionalConstantPropagation(appView).analyze(code));
+    AbstractValueSupplier abstractValueSupplier =
+        value -> {
+          AbstractValue abstractValue = abstractValues.computeIfAbsent().get(value);
+          assert abstractValue != null;
+          return abstractValue;
+        };
+    codeScanner.scan(method, code, abstractValueSupplier, timing);
+  }
+
+  public boolean isProcessed(ComposableCallGraphNode node) {
+    return processed.contains(node);
+  }
+
+  @Override
+  public CallSiteInformation getCallSiteInformation() {
+    return CallSiteInformation.empty();
+  }
+
+  @Override
+  public MethodProcessorEventConsumer getEventConsumer() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isComposeMethodProcessor() {
+    return true;
+  }
+
+  @Override
+  public ComposeMethodProcessor asComposeMethodProcessor() {
+    return this;
+  }
+
+  @Override
+  public boolean isProcessedConcurrently(ProgramMethod method) {
+    return false;
+  }
+
+  @Override
+  public void scheduleDesugaredMethodForProcessing(ProgramMethod method) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean shouldApplyCodeRewritings(ProgramMethod method) {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
index 7a489a7..e441188 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.lens.GraphLens;
 
 public class ComposeReferences {
 
@@ -31,4 +32,26 @@
             factory.createProto(factory.intType, factory.intType),
             "updateChangedFlags");
   }
+
+  public ComposeReferences(
+      DexString changedFieldName,
+      DexString defaultFieldName,
+      DexType composableType,
+      DexType composerType,
+      DexMethod updatedChangedFlagsMethod) {
+    this.changedFieldName = changedFieldName;
+    this.defaultFieldName = defaultFieldName;
+    this.composableType = composableType;
+    this.composerType = composerType;
+    this.updatedChangedFlagsMethod = updatedChangedFlagsMethod;
+  }
+
+  public ComposeReferences rewrittenWithLens(GraphLens graphLens, GraphLens codeLens) {
+    return new ComposeReferences(
+        changedFieldName,
+        defaultFieldName,
+        graphLens.lookupClassType(composableType, codeLens),
+        graphLens.lookupClassType(composerType, codeLens),
+        graphLens.getRenamedMethodSignature(updatedChangedFlagsMethod, codeLens));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
index d7fde7d..78235d8 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
@@ -59,7 +59,7 @@
     for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
       if (appView.horizontallyMergedClasses().hasBeenMergedIntoDifferentType(clazz.getType())
           || (appView.hasVerticallyMergedClasses()
-              && appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(clazz.getType()))
+              && appView.getVerticallyMergedClasses().hasBeenMergedIntoSubtype(clazz.getType()))
           || appView.unboxedEnums().isUnboxedEnum(clazz)) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java b/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
index 9d133de..7fa618c 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
@@ -90,7 +90,7 @@
     }
   }
 
-  void applyIfContextIsInProfile(
+  public void applyIfContextIsInProfile(
       DexProgramClass context, Consumer<ProfileAdditionsBuilder> builderConsumer) {
     accept(additions -> additions.applyIfContextIsInProfile(context.getType(), builderConsumer));
   }
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
index 55501d8..f7836d3 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -77,6 +77,7 @@
     RepackagingLens lens = repackageClasses(appBuilder, executorService);
     if (lens != null) {
       appView.rewriteWithLensAndApplication(lens, appBuilder.build(), executorService, timing);
+      appView.testing().repackagingLensConsumer.accept(appView.dexItemFactory(), lens);
     }
     appView.notifyOptimizationFinishedForTesting();
     timing.end();
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 43ce750..794ef20 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -473,21 +473,17 @@
   @Override
   public void notifyHorizontalClassMergerFinished(
       HorizontalClassMerger.Mode horizontalClassMergerMode) {
-    if (horizontalClassMergerMode.isInitial()
-        && !options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
+    if (horizontalClassMergerMode.isInitial()) {
       getMethodAccessInfoCollection().destroy();
     }
   }
 
   public void notifyMemberRebindingFinished(AppView<AppInfoWithLiveness> appView) {
     getFieldAccessInfoCollection().restrictToProgram(appView);
-    if (!options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
-      getMethodAccessInfoCollection().destroyNonDirectNonSuperInvokes();
-    }
   }
 
   public void notifyRedundantBridgeRemoverFinished(boolean initial) {
-    if (initial && !options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
+    if (initial) {
       getMethodAccessInfoCollection().destroySuperInvokes();
     }
   }
@@ -495,9 +491,7 @@
   @Override
   public void notifyMinifierFinished() {
     liveMethods = ThrowingSet.get();
-    if (!options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
-      getMethodAccessInfoCollection().destroy();
-    }
+    getMethodAccessInfoCollection().destroy();
   }
 
   public void notifyTreePrunerFinished(Enqueuer.Mode mode) {
@@ -743,10 +737,6 @@
     return alwaysInline.contains(method);
   }
 
-  public boolean hasNoAlwaysInlineMethods() {
-    return alwaysInline.isEmpty();
-  }
-
   public boolean isNeverInlineDueToSingleCallerMethod(ProgramMethod method) {
     return neverInlineDueToSingleCaller.contains(method.getReference());
   }
@@ -854,8 +844,8 @@
    * can potentially cause incorrect behavior when merging classes. A conservative choice is to not
    * merge any const-class classes. More info at b/142438687.
    */
-  public boolean isLockCandidate(DexType type) {
-    return lockCandidates.contains(type);
+  public boolean isLockCandidate(DexProgramClass clazz) {
+    return lockCandidates.contains(clazz.getType());
   }
 
   public Set<DexType> getDeadProtoTypes() {
@@ -1332,8 +1322,8 @@
         .isDefinitelyInstanceOfStaticType(appView, () -> dynamicReceiverType, staticReceiverType)) {
       return null;
     }
-    DexClass initialResolutionHolder = definitionFor(method.holder);
-    if (initialResolutionHolder == null || initialResolutionHolder.isInterface() != isInterface) {
+    DexClass initialResolutionHolder = resolutionResult.getInitialResolutionHolder();
+    if (initialResolutionHolder.isInterface() != isInterface) {
       return null;
     }
     DexType refinedReceiverType =
@@ -1343,27 +1333,24 @@
       // The refined receiver is not defined in the program and we cannot determine the target.
       return null;
     }
-    if (!dynamicReceiverType.hasDynamicLowerBoundType()) {
-      if (singleTargetLookupCache.hasPositiveCacheHit(refinedReceiverType, method)) {
-        return singleTargetLookupCache.getPositiveCacheHit(refinedReceiverType, method);
-      }
-      if (singleTargetLookupCache.hasNegativeCacheHit(refinedReceiverType, method)) {
-        return null;
-      }
+    if (singleTargetLookupCache.hasPositiveCacheHit(refinedReceiverType, method)) {
+      return singleTargetLookupCache.getPositiveCacheHit(refinedReceiverType, method);
+    }
+    if (!dynamicReceiverType.hasDynamicLowerBoundType()
+        && singleTargetLookupCache.hasNegativeCacheHit(refinedReceiverType, method)) {
+      return null;
     }
     if (resolutionResult
         .isAccessibleForVirtualDispatchFrom(context.getHolder(), appView)
         .isFalse()) {
       return null;
     }
-    // If the method is modeled, return the resolution.
+    // If the resolved method is final, return the resolution.
     DexClassAndMethod resolvedMethod = resolutionResult.getResolutionPair();
-    if (modeledPredicate.isModeled(resolutionResult.getResolvedHolder().getType())) {
-      if (resolutionResult.getResolvedHolder().isFinal()
-          || (resolvedMethod.getAccessFlags().isFinal()
-              && resolvedMethod.getAccessFlags().isPublic())) {
-        singleTargetLookupCache.addToCache(refinedReceiverType, method, resolvedMethod);
-        return resolvedMethod;
+    if (resolvedMethod.getHolder().isFinal() || resolvedMethod.getAccessFlags().isFinal()) {
+      if (!resolvedMethod.isLibraryMethod()
+          || modeledPredicate.isModeled(resolvedMethod.getHolderType())) {
+        return singleTargetLookupCache.addToCache(refinedReceiverType, method, resolvedMethod);
       }
     }
     DispatchTargetLookupResult exactTarget =
@@ -1527,13 +1514,15 @@
   }
 
   /** Predicate on types that *must* never be merged horizontally. */
-  public boolean isNoHorizontalClassMergingOfType(DexType type) {
-    return noClassMerging.contains(type) || noHorizontalClassMerging.contains(type);
+  public boolean isNoHorizontalClassMergingOfType(DexProgramClass clazz) {
+    return noClassMerging.contains(clazz.getType())
+        || noHorizontalClassMerging.contains(clazz.getType());
   }
 
   /** Predicate on types that *must* never be merged vertically. */
-  public boolean isNoVerticalClassMergingOfType(DexType type) {
-    return noClassMerging.contains(type) || noVerticalClassMerging.contains(type);
+  public boolean isNoVerticalClassMergingOfType(DexProgramClass clazz) {
+    return noClassMerging.contains(clazz.getType())
+        || noVerticalClassMerging.contains(clazz.getType());
   }
 
   public boolean verifyNoIteratingOverPrunedClasses() {
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 6bcbfc7..c8d9806 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -95,6 +95,7 @@
 import com.android.tools.r8.graph.analysis.EnqueuerInvokeAnalysis;
 import com.android.tools.r8.graph.analysis.GetArrayOfMissingTypeVerifyErrorWorkaround;
 import com.android.tools.r8.graph.analysis.InvokeVirtualToInterfaceVerifyErrorWorkaround;
+import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis;
 import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension;
 import com.android.tools.r8.ir.code.ArrayPut;
@@ -506,6 +507,7 @@
       }
       appView.withGeneratedMessageLiteBuilderShrinker(
           shrinker -> registerAnalysis(shrinker.createEnqueuerAnalysis()));
+      ResourceAccessAnalysis.register(appView, this);
     }
 
     targetedMethods = new LiveMethodsSet(graphReporter::registerMethod);
@@ -3677,6 +3679,7 @@
     timing.end();
     timing.begin("Finish analysis");
     analyses.forEach(analyses -> analyses.done(this));
+    fieldAccessAnalyses.forEach(fieldAccessAnalyses -> fieldAccessAnalyses.done(this));
     timing.end();
     assert verifyKeptGraph();
     timing.begin("Finish compat building");
@@ -5144,7 +5147,7 @@
       initializer = clazz.getProgramDefaultInitializer();
     } else {
       DexType[] parameterTypes = new DexType[parametersSize];
-      int missingIndices = parametersSize;
+      int missingIndices;
 
       if (newArrayEmpty != null) {
         missingIndices = parametersSize;
@@ -5168,12 +5171,8 @@
             return;
           }
 
-          Value indexValue = arrayPutInstruction.index();
-          if (indexValue.isPhi() || !indexValue.definition.isConstNumber()) {
-            return;
-          }
-          int index = indexValue.definition.asConstNumber().getIntValue();
-          if (index >= parametersSize) {
+          int index = arrayPutInstruction.indexIfConstAndInBounds(parametersSize);
+          if (index < 0) {
             return;
           }
 
@@ -5407,7 +5406,7 @@
     }
 
     Set<T> getItems() {
-      return Collections.unmodifiableSet(items);
+      return SetUtils.unmodifiableForTesting(items);
     }
   }
 
@@ -5463,7 +5462,7 @@
     }
 
     Set<DexEncodedMethod> getItems() {
-      return Collections.unmodifiableSet(items);
+      return SetUtils.unmodifiableForTesting(items);
     }
   }
 
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 16b3d76..1410247 100644
--- a/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
+++ b/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
@@ -107,9 +107,9 @@
             }
 
             // Check if one of the types that have been merged into `clazz` satisfies the if-rule.
-            if (appView.verticallyMergedClasses() != null) {
+            if (appView.getVerticallyMergedClasses() != null) {
               Iterable<DexType> sources =
-                  appView.verticallyMergedClasses().getSourcesFor(clazz.type);
+                  appView.getVerticallyMergedClasses().getSourcesFor(clazz.type);
               for (DexType sourceType : sources) {
                 // Note that, although `sourceType` has been merged into `type`, the dex class for
                 // `sourceType` is still available until the second round of tree shaking. This
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 a0af134..6099834 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -159,9 +159,9 @@
     }
     if (hasInheritanceClassName() && getInheritanceClassName().hasSpecificType()) {
       DexType type = getInheritanceClassName().getSpecificType();
-      if (appView.verticallyMergedClasses() != null
-          && appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(type)) {
-        DexType target = appView.verticallyMergedClasses().getTargetFor(type);
+      if (appView.getVerticallyMergedClasses() != null
+          && appView.getVerticallyMergedClasses().hasBeenMergedIntoSubtype(type)) {
+        DexType target = appView.getVerticallyMergedClasses().getTargetFor(type);
         DexClass clazz = appView.definitionFor(target);
         assert clazz != null && clazz.isProgramClass();
         return Iterables.concat(
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 71057cd..a8233bf 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
@@ -49,8 +49,9 @@
     if (matches(type)) {
       return true;
     }
-    if (appView.verticallyMergedClasses() != null) {
-      return appView.verticallyMergedClasses().getSourcesFor(type).stream().anyMatch(this::matches);
+    if (appView.getVerticallyMergedClasses() != null) {
+      return appView.getVerticallyMergedClasses().getSourcesFor(type).stream()
+          .anyMatch(this::matches);
     }
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 1e8f56d..200fbc6 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -931,8 +931,8 @@
         DexClass clazz, ProguardConfigurationRule rule, boolean isInterface) {
       // TODO(b/110141157): Figure out what to do with annotations. Should the annotations of
       // the DexClass corresponding to `sourceType` satisfy the `annotation`-matcher?
-      return appView.verticallyMergedClasses() != null
-          && appView.verticallyMergedClasses().getSourcesFor(clazz.type).stream()
+      return appView.getVerticallyMergedClasses() != null
+          && appView.getVerticallyMergedClasses().getSourcesFor(clazz.type).stream()
               .filter(
                   sourceType ->
                       appView.definitionFor(sourceType).accessFlags.isInterface() == isInterface)
diff --git a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
index cb0f347..5b6511d 100644
--- a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.Sets;
 import java.util.Set;
+import java.util.function.Function;
 
 public class RuntimeTypeCheckInfo {
 
@@ -31,6 +32,31 @@
     this.exceptionGuardTypes = exceptionGuardTypes;
   }
 
+  public boolean isCheckCastType(DexProgramClass clazz) {
+    return checkCastTypes.contains(clazz.type);
+  }
+
+  public boolean isInstanceOfType(DexProgramClass clazz) {
+    return instanceOfTypes.contains(clazz.type);
+  }
+
+  public boolean isExceptionGuardType(DexProgramClass clazz) {
+    return exceptionGuardTypes.contains(clazz.type);
+  }
+
+  public boolean isRuntimeCheckType(DexProgramClass clazz) {
+    return isInstanceOfType(clazz) || isCheckCastType(clazz) || isExceptionGuardType(clazz);
+  }
+
+  public RuntimeTypeCheckInfo rewriteWithLens(
+      NonIdentityGraphLens graphLens, GraphLens appliedGraphLens) {
+    Function<DexType, DexType> typeRewriter = type -> graphLens.lookupType(type, appliedGraphLens);
+    return new RuntimeTypeCheckInfo(
+        SetUtils.mapIdentityHashSet(instanceOfTypes, typeRewriter),
+        SetUtils.mapIdentityHashSet(checkCastTypes, typeRewriter),
+        SetUtils.mapIdentityHashSet(exceptionGuardTypes, typeRewriter));
+  }
+
   public static class Builder
       implements EnqueuerInstanceOfAnalysis,
           EnqueuerCheckCastAnalysis,
@@ -52,7 +78,7 @@
       RuntimeTypeCheckInfo runtimeTypeCheckInfo =
           new RuntimeTypeCheckInfo(instanceOfTypes, checkCastTypes, exceptionGuardTypes);
       return graphLens.isNonIdentityLens() && graphLens != appliedGraphLens
-          ? runtimeTypeCheckInfo.rewriteWithLens(graphLens.asNonIdentityLens())
+          ? runtimeTypeCheckInfo.rewriteWithLens(graphLens.asNonIdentityLens(), appliedGraphLens)
           : runtimeTypeCheckInfo;
     }
 
@@ -90,27 +116,4 @@
           .registerExceptionGuardAnalysis(this);
     }
   }
-
-  public boolean isCheckCastType(DexProgramClass clazz) {
-    return checkCastTypes.contains(clazz.type);
-  }
-
-  public boolean isInstanceOfType(DexProgramClass clazz) {
-    return instanceOfTypes.contains(clazz.type);
-  }
-
-  public boolean isExceptionGuardType(DexProgramClass clazz) {
-    return exceptionGuardTypes.contains(clazz.type);
-  }
-
-  public boolean isRuntimeCheckType(DexProgramClass clazz) {
-    return isInstanceOfType(clazz) || isCheckCastType(clazz) || isExceptionGuardType(clazz);
-  }
-
-  public RuntimeTypeCheckInfo rewriteWithLens(NonIdentityGraphLens graphLens) {
-    return new RuntimeTypeCheckInfo(
-        SetUtils.mapIdentityHashSet(instanceOfTypes, graphLens::lookupType),
-        SetUtils.mapIdentityHashSet(checkCastTypes, graphLens::lookupType),
-        SetUtils.mapIdentityHashSet(exceptionGuardTypes, graphLens::lookupType));
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java b/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
index 0c576e0..fbe53c8 100644
--- a/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
+++ b/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
@@ -31,10 +31,11 @@
         .add(method);
   }
 
-  public void addToCache(DexType refinedReceiverType, DexMethod method, DexClassAndMethod target) {
+  public DexClassAndMethod addToCache(
+      DexType refinedReceiverType, DexMethod method, DexClassAndMethod target) {
     if (target == null) {
       addNoSingleTargetToCache(refinedReceiverType, method);
-      return;
+      return null;
     }
     assert !ObjectUtils.identical(target.getDefinition(), DexEncodedMethod.SENTINEL);
     assert !hasNegativeCacheHit(refinedReceiverType, method);
@@ -43,6 +44,7 @@
     positiveCache
         .computeIfAbsent(refinedReceiverType, ignoreKey(ConcurrentHashMap::new))
         .put(method, target);
+    return target;
   }
 
   public void removeInstantiatedType(DexType instantiatedType, AppInfoWithLiveness appInfo) {
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
deleted file mode 100644
index f96b242..0000000
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ /dev/null
@@ -1,2317 +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 com.android.tools.r8.shaking;
-
-import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
-import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.android.tools.r8.ir.code.InvokeType.DIRECT;
-import static com.android.tools.r8.ir.code.InvokeType.STATIC;
-import static com.android.tools.r8.ir.code.InvokeType.VIRTUAL;
-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.cf.CfVersion;
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
-import com.android.tools.r8.graph.AccessControl;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
-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.DefaultInstanceInitializerCode;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassAndField;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMember;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMember;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexReference;
-import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.graph.GenericSignature.ClassSignature;
-import com.android.tools.r8.graph.GenericSignature.ClassSignature.ClassSignatureBuilder;
-import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
-import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
-import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
-import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
-import com.android.tools.r8.graph.GenericSignatureContextBuilder;
-import com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext;
-import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper;
-import com.android.tools.r8.graph.GenericSignaturePartialTypeArgumentApplier;
-import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
-import com.android.tools.r8.graph.MethodAccessFlags;
-import com.android.tools.r8.graph.MethodResolutionResult;
-import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.graph.SubtypingInfo;
-import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
-import com.android.tools.r8.graph.UseRegistry;
-import com.android.tools.r8.graph.UseRegistryWithResult;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
-import com.android.tools.r8.graph.fixup.TreeFixerBase;
-import com.android.tools.r8.graph.lens.FieldLookupResult;
-import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.lens.MethodLookupResult;
-import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
-import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
-import com.android.tools.r8.ir.code.InvokeType;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
-import com.android.tools.r8.ir.synthetic.AbstractSynthesizedCode;
-import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
-import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.CollectionUtils;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.OptionalBool;
-import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
-import it.unimi.dsi.fastutil.ints.Int2IntMap;
-import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
-
-/**
- * Merges Supertypes with a single implementation into their single subtype.
- *
- * <p>A common use-case for this is to merge an interface into its single implementation.
- *
- * <p>The class merger only fixes the structure of the graph but leaves the actual instructions
- * untouched. Fixup of instructions is deferred via a {@link GraphLens} to the IR building phase.
- */
-public class VerticalClassMerger {
-
-  private enum AbortReason {
-    ALREADY_MERGED,
-    ALWAYS_INLINE,
-    CONFLICT,
-    ILLEGAL_ACCESS,
-    MAIN_DEX_ROOT_OUTSIDE_REFERENCE,
-    MERGE_ACROSS_NESTS,
-    NATIVE_METHOD,
-    NO_SIDE_EFFECTS,
-    PINNED_SOURCE,
-    RESOLUTION_FOR_FIELDS_MAY_CHANGE,
-    RESOLUTION_FOR_METHODS_MAY_CHANGE,
-    SERVICE_LOADER,
-    SOURCE_AND_TARGET_LOCK_CANDIDATES,
-    STATIC_INITIALIZERS,
-    UNHANDLED_INVOKE_DIRECT,
-    UNHANDLED_INVOKE_SUPER,
-    UNSAFE_INLINING,
-    UNSUPPORTED_ATTRIBUTES,
-    API_REFERENCE_LEVEL
-  }
-
-  private enum Rename {
-    ALWAYS,
-    IF_NEEDED,
-    NEVER
-  }
-
-  private final DexApplication application;
-  private final AppInfoWithLiveness appInfo;
-  private final AppView<AppInfoWithLiveness> appView;
-  private final InternalOptions options;
-  private final SubtypingInfo subtypingInfo;
-  private final ExecutorService executorService;
-  private final Timing timing;
-  private Collection<DexMethod> invokes;
-  private final AndroidApiLevelCompute apiLevelCompute;
-
-  private final OptimizationFeedback feedback = OptimizationFeedbackSimple.getInstance();
-
-  // Set of merge candidates. Note that this must have a deterministic iteration order.
-  private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
-
-  // Map from source class to target class.
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
-      BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
-
-  private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedInterfaces =
-      BidirectionalManyToOneHashMap.newIdentityHashMap();
-
-  // Set of types that must not be merged into their subtype.
-  private final Set<DexType> pinnedTypes = Sets.newIdentityHashSet();
-
-  // The resulting graph lens that should be used after class merging.
-  private final VerticalClassMergerGraphLens.Builder lensBuilder;
-
-  // All the bridge methods that have been synthesized during vertical class merging.
-  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
-
-  private final MainDexInfo mainDexInfo;
-
-  public VerticalClassMerger(
-      DexApplication application,
-      AppView<AppInfoWithLiveness> appView,
-      ExecutorService executorService,
-      Timing timing) {
-    this.application = application;
-    this.appInfo = appView.appInfo();
-    this.appView = appView;
-    this.options = appView.options();
-    this.mainDexInfo = appInfo.getMainDexInfo();
-    this.subtypingInfo = appInfo.computeSubtypingInfo();
-    this.executorService = executorService;
-    this.lensBuilder = new VerticalClassMergerGraphLens.Builder(appView.dexItemFactory());
-    this.apiLevelCompute = appView.apiLevelCompute();
-    this.timing = timing;
-
-    Iterable<DexProgramClass> classes = application.classesWithDeterministicOrder();
-    initializePinnedTypes(classes); // Must be initialized prior to mergeCandidates.
-    initializeMergeCandidates(classes);
-  }
-
-  private void initializeMergeCandidates(Iterable<DexProgramClass> classes) {
-    for (DexProgramClass sourceClass : classes) {
-      DexType singleSubtype = subtypingInfo.getSingleDirectSubtype(sourceClass.type);
-      if (singleSubtype == null) {
-        continue;
-      }
-      DexProgramClass targetClass = asProgramClassOrNull(appView.definitionFor(singleSubtype));
-      if (targetClass == null) {
-        continue;
-      }
-      if (!isMergeCandidate(sourceClass, targetClass, pinnedTypes)) {
-        continue;
-      }
-      if (!isStillMergeCandidate(sourceClass, targetClass)) {
-        continue;
-      }
-      if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)) {
-        continue;
-      }
-      mergeCandidates.add(sourceClass);
-    }
-  }
-
-  // Returns a set of types that must not be merged into other types.
-  private void initializePinnedTypes(Iterable<DexProgramClass> classes) {
-    // For all pinned fields, also pin the type of the field (because changing the type of the field
-    // implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
-    // the return type and the parameter types of the method.
-    // TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
-    List<DexReference> pinnedItems = new ArrayList<>();
-    appInfo.getKeepInfo().forEachPinnedType(pinnedItems::add, options);
-    appInfo.getKeepInfo().forEachPinnedMethod(pinnedItems::add, options);
-    appInfo.getKeepInfo().forEachPinnedField(pinnedItems::add, options);
-    extractPinnedItems(pinnedItems, AbortReason.PINNED_SOURCE);
-
-    for (DexProgramClass clazz : classes) {
-      for (DexEncodedMethod method : clazz.methods()) {
-        if (method.accessFlags.isNative()) {
-          markTypeAsPinned(clazz.type, AbortReason.NATIVE_METHOD);
-        }
-      }
-    }
-
-    // It is valid to have an invoke-direct instruction in a default interface method that targets
-    // another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
-    // SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
-    // Therefore, we disallow merging such interfaces into their subtypes.
-    for (DexMethod signature : appInfo.getVirtualMethodsTargetedByInvokeDirect()) {
-      markTypeAsPinned(signature.holder, AbortReason.UNHANDLED_INVOKE_DIRECT);
-    }
-
-    // The set of targets that must remain for proper resolution error cases should not be merged.
-    // TODO(b/192821424): Can be removed if handled.
-    extractPinnedItems(
-        appInfo.getFailedMethodResolutionTargets(), AbortReason.RESOLUTION_FOR_METHODS_MAY_CHANGE);
-  }
-
-  private <T extends DexReference> void extractPinnedItems(Iterable<T> items, AbortReason reason) {
-    for (DexReference item : items) {
-      if (item.isDexType()) {
-        markTypeAsPinned(item.asDexType(), reason);
-      } else if (item.isDexField()) {
-        // Pin the holder and the type of the field.
-        DexField field = item.asDexField();
-        markTypeAsPinned(field.holder, reason);
-        markTypeAsPinned(field.type, reason);
-      } else {
-        assert item.isDexMethod();
-        // Pin the holder, the return type and the parameter types of the method. If we were to
-        // merge any of these types into their sub classes, then we would implicitly change the
-        // signature of this method.
-        DexMethod method = item.asDexMethod();
-        markTypeAsPinned(method.holder, reason);
-        markTypeAsPinned(method.proto.returnType, reason);
-        for (DexType parameterType : method.proto.parameters.values) {
-          markTypeAsPinned(parameterType, reason);
-        }
-      }
-    }
-  }
-
-  @SuppressWarnings("UnusedVariable")
-  private void markTypeAsPinned(DexType type, AbortReason reason) {
-    DexType baseType = type.toBaseType(appView.dexItemFactory());
-    if (!baseType.isClassType() || appInfo.isPinnedWithDefinitionLookup(baseType)) {
-      // We check for the case where the type is pinned according to appInfo.isPinned,
-      // so we only need to add it here if it is not the case.
-      return;
-    }
-
-    DexClass clazz = appInfo.definitionFor(baseType);
-    if (clazz != null && clazz.isProgramClass()) {
-      pinnedTypes.add(baseType);
-    }
-  }
-
-  // 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.
-  @SuppressWarnings("ReferenceEquality")
-  private boolean isMergeCandidate(
-      DexProgramClass sourceClass, DexProgramClass targetClass, Set<DexType> pinnedTypes) {
-    assert targetClass != null;
-    ObjectAllocationInfoCollection allocationInfo = appInfo.getObjectAllocationInfoCollection();
-    if (allocationInfo.isInstantiatedDirectly(sourceClass)
-        || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
-        || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
-        || appInfo.isPinned(sourceClass)
-        || pinnedTypes.contains(sourceClass.type)
-        || appInfo.isNoVerticalClassMergingOfType(sourceClass.type)) {
-      return false;
-    }
-
-    assert Streams.stream(Iterables.concat(sourceClass.fields(), sourceClass.methods()))
-        .noneMatch(appInfo::isPinned);
-
-    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (appView.appServices().allServiceTypes().contains(sourceClass.type)
-        && appInfo.isPinned(targetClass)) {
-      return false;
-    }
-    if (sourceClass.isAnnotation()) {
-      return false;
-    }
-    if (!sourceClass.isInterface()
-        && targetClass.isSerializable(appView)
-        && !appInfo.isSerializable(sourceClass.type)) {
-      // 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 -> {
-                AbortReason reason = disallowInlining(method, targetClass);
-                if (reason != null) {
-                  // Cannot guarantee that markForceInline() will work.
-                  return TraversalContinuation.doBreak();
-                }
-                return TraversalContinuation.doContinue();
-              });
-      if (result.shouldBreak()) {
-        return false;
-      }
-    }
-    if (sourceClass.getEnclosingMethodAttribute() != null
-        || !sourceClass.getInnerClasses().isEmpty()) {
-      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
-      return false;
-    }
-    // We abort class merging when merging across nests or from a nest to non-nest.
-    // Without nest this checks null == null.
-    if (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;
-      }
-    }
-    return true;
-  }
-
-  // Returns true if [clazz] 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 [clazz] into its subtype.
-  @SuppressWarnings("ReferenceEquality")
-  private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    assert isMergeCandidate(sourceClass, targetClass, pinnedTypes);
-    assert !mergedClasses.containsValue(sourceClass.getType());
-    // 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 == sourceClass.type)
-        || (sourceClass.isInterface()
-            && sourceClass.classInitializationMayHaveSideEffects(appView))) {
-      // TODO(herhut): Handle class initializers.
-      return false;
-    }
-    boolean sourceCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(sourceClass.type)
-            || sourceClass.hasStaticSynchronizedMethods();
-    boolean targetCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(targetClass.type)
-            || targetClass.hasStaticSynchronizedMethods();
-    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
-      return false;
-    }
-    if (targetClass.getEnclosingMethodAttribute() != null
-        || !targetClass.getInnerClasses().isEmpty()) {
-      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
-      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()) {
-      ComputedApiLevel sourceApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
-      ComputedApiLevel targetApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
-      if (!sourceApiLevel.equals(targetApiLevel)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  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 (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();
-            });
-    return result.shouldBreak();
-  }
-
-  private Collection<DexMethod> getInvokes() {
-    if (invokes == null) {
-      invokes = new OverloadedMethodSignaturesRetriever().get();
-    }
-    return invokes;
-  }
-
-  // Collects all potentially overloaded method signatures that reference at least one type that
-  // may be the source or target of a merge operation.
-  private class OverloadedMethodSignaturesRetriever {
-    private final Reference2BooleanOpenHashMap<DexProto> cache =
-        new Reference2BooleanOpenHashMap<>();
-    private final Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
-    private final Set<DexType> mergeeCandidates = new HashSet<>();
-
-    public OverloadedMethodSignaturesRetriever() {
-      for (DexProgramClass mergeCandidate : mergeCandidates) {
-        DexType singleSubtype = subtypingInfo.getSingleDirectSubtype(mergeCandidate.type);
-        mergeeCandidates.add(singleSubtype);
-      }
-    }
-
-    @SuppressWarnings("ReferenceEquality")
-    public Collection<DexMethod> get() {
-      Map<DexString, DexProto> overloadingInfo = new HashMap<>();
-
-      // Find all signatures that may reference a type that could be the source or target of a
-      // merge operation.
-      Set<Wrapper<DexMethod>> filteredSignatures = new HashSet<>();
-      for (DexProgramClass clazz : appInfo.classes()) {
-        for (DexEncodedMethod encodedMethod : clazz.methods()) {
-          DexMethod method = encodedMethod.getReference();
-          DexClass definition = appInfo.definitionFor(method.holder);
-          if (definition != null
-              && definition.isProgramClass()
-              && protoMayReferenceMergedSourceOrTarget(method.proto)) {
-            filteredSignatures.add(equivalence.wrap(method));
-
-            // Record that we have seen a method named [signature.name] with the proto
-            // [signature.proto]. If at some point, we find a method with the same name, but a
-            // different proto, it could be the case that a method with the given name is
-            // overloaded.
-            DexProto existing = overloadingInfo.computeIfAbsent(method.name, key -> method.proto);
-            if (existing != DexProto.SENTINEL && !existing.equals(method.proto)) {
-              // Mark that this signature is overloaded by mapping it to SENTINEL.
-              overloadingInfo.put(method.name, DexProto.SENTINEL);
-            }
-          }
-        }
-      }
-
-      List<DexMethod> result = new ArrayList<>();
-      for (Wrapper<DexMethod> wrappedSignature : filteredSignatures) {
-        DexMethod signature = wrappedSignature.get();
-
-        // Ignore those method names that are definitely not overloaded since they cannot lead to
-        // any collisions.
-        if (overloadingInfo.get(signature.name) == DexProto.SENTINEL) {
-          result.add(signature);
-        }
-      }
-      return result;
-    }
-
-    private boolean protoMayReferenceMergedSourceOrTarget(DexProto proto) {
-      boolean result;
-      if (cache.containsKey(proto)) {
-        result = cache.getBoolean(proto);
-      } else {
-        result = false;
-        if (typeMayReferenceMergedSourceOrTarget(proto.returnType)) {
-          result = true;
-        } else {
-          for (DexType type : proto.parameters.values) {
-            if (typeMayReferenceMergedSourceOrTarget(type)) {
-              result = true;
-              break;
-            }
-          }
-        }
-        cache.put(proto, result);
-      }
-      return result;
-    }
-
-    private boolean typeMayReferenceMergedSourceOrTarget(DexType type) {
-      type = type.toBaseType(appView.dexItemFactory());
-      if (type.isClassType()) {
-        if (mergeeCandidates.contains(type)) {
-          return true;
-        }
-        DexClass clazz = appInfo.definitionFor(type);
-        if (clazz != null && clazz.isProgramClass()) {
-          return mergeCandidates.contains(clazz.asProgramClass());
-        }
-      }
-      return false;
-    }
-  }
-
-  public VerticalClassMergerGraphLens run() throws ExecutionException {
-    timing.begin("merge");
-    // Visit the program classes in a top-down order according to the class hierarchy.
-    TopDownClassHierarchyTraversal.forProgramClasses(appView)
-        .visit(mergeCandidates, this::mergeClassIfPossible);
-    timing.end();
-
-    VerticallyMergedClasses verticallyMergedClasses =
-        new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
-    appView.setVerticallyMergedClasses(verticallyMergedClasses);
-    if (verticallyMergedClasses.isEmpty()) {
-      return null;
-    }
-
-    timing.begin("fixup");
-    VerticalClassMergerGraphLens lens =
-        new VerticalClassMergerTreeFixer(
-                appView, lensBuilder, verticallyMergedClasses, synthesizedBridges)
-            .fixupTypeReferences();
-    KeepInfoCollection keepInfo = appView.getKeepInfo();
-    keepInfo.mutate(
-        mutator ->
-            mutator.removeKeepInfoForMergedClasses(
-                PrunedItems.builder().setRemovedClasses(mergedClasses.keySet()).build()));
-    timing.end();
-
-    assert lens != null;
-    assert verifyGraphLens(lens);
-
-    // Include bridges in art profiles.
-    ProfileCollectionAdditions profileCollectionAdditions =
-        ProfileCollectionAdditions.create(appView);
-    if (!profileCollectionAdditions.isNop()) {
-      for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
-        profileCollectionAdditions.applyIfContextIsInProfile(
-            lens.getPreviousMethodSignature(synthesizedBridge.method),
-            additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.method));
-      }
-    }
-    profileCollectionAdditions.commit(appView);
-
-    // Rewrite collections using the lens.
-    appView.rewriteWithLens(lens, executorService, timing);
-
-    // Copy keep info to newly synthesized methods.
-    keepInfo.mutate(
-        mutator -> {
-          for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
-            ProgramMethod bridge =
-                asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.method));
-            ProgramMethod target =
-                asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.invocationTarget));
-            if (bridge != null && target != null) {
-              mutator.joinMethod(bridge, info -> info.merge(appView.getKeepInfo(target).joiner()));
-              continue;
-            }
-            assert false;
-          }
-        });
-
-    appView.notifyOptimizationFinishedForTesting();
-    return lens;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private boolean verifyGraphLens(VerticalClassMergerGraphLens graphLens) {
-    // Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
-    // getRenamedMethodSignature() instead of lookupField() and lookupMethod(). This is important
-    // for this check to succeed, since it is not guaranteed that calling lookupMethod() with a
-    // pinned method will return the method itself.
-    //
-    // Consider the following example.
-    //
-    //   class A {
-    //     public void method() {}
-    //   }
-    //   class B extends A {
-    //     @Override
-    //     public void method() {}
-    //   }
-    //   class C extends B {
-    //     @Override
-    //     public void method() {}
-    //   }
-    //
-    // If A.method() is pinned, then A cannot be merged into B, but B can still be merged into C.
-    // Now, if there is an invoke-super instruction in C that hits B.method(), then this needs to
-    // be rewritten into an invoke-direct instruction. In particular, there could be an instruction
-    // `invoke-super A.method` in C. This would hit B.method(). Therefore, the graph lens records
-    // that `invoke-super A.method` instructions, which are in one of the methods from C, needs to
-    // be rewritten to `invoke-direct C.method$B`. This is valid even though A.method() is actually
-    // pinned, because this rewriting does not affect A.method() in any way.
-    assert graphLens.assertPinnedNotModified(appInfo.getKeepInfo(), options);
-
-    for (DexProgramClass clazz : appInfo.classes()) {
-      for (DexEncodedMethod encodedMethod : clazz.methods()) {
-        DexMethod method = encodedMethod.getReference();
-        DexMethod originalMethod = graphLens.getOriginalMethodSignature(method);
-        DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod);
-
-        // Must be able to map back and forth.
-        if (encodedMethod.hasCode() && encodedMethod.getCode() instanceof SynthesizedBridgeCode) {
-          // For virtual methods, the vertical class merger creates two methods in the sub class
-          // in order to deal with invoke-super instructions (one that is private and one that is
-          // virtual). Therefore, it is not possible to go back and forth. Instead, we check that
-          // the two methods map back to the same original method, and that the original method
-          // can be mapped to the implementation method.
-          DexMethod implementationMethod =
-              ((SynthesizedBridgeCode) encodedMethod.getCode()).invocationTarget;
-          DexMethod originalImplementationMethod =
-              graphLens.getOriginalMethodSignature(implementationMethod);
-          assert originalMethod == originalImplementationMethod;
-          assert implementationMethod == renamedMethod;
-        } else {
-          assert method == renamedMethod;
-        }
-
-        // Verify that all types are up-to-date. After vertical class merging, there should be no
-        // more references to types that have been merged into another type.
-        assert !mergedClasses.containsKey(method.proto.returnType);
-        assert Arrays.stream(method.proto.parameters.values).noneMatch(mergedClasses::containsKey);
-      }
-    }
-    return true;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  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 =
-            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 (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 void mergeClassIfPossible(DexProgramClass clazz) {
-    if (!mergeCandidates.contains(clazz)) {
-      return;
-    }
-
-    DexType singleSubtype = subtypingInfo.getSingleDirectSubtype(clazz.type);
-    DexProgramClass targetClass = appView.definitionFor(singleSubtype).asProgramClass();
-    assert !mergedClasses.containsKey(targetClass.type);
-    if (mergedClasses.containsValue(clazz.type)) {
-      return;
-    }
-    assert isMergeCandidate(clazz, targetClass, pinnedTypes);
-    if (mergedClasses.containsValue(targetClass.type)) {
-      if (!isStillMergeCandidate(clazz, targetClass)) {
-        return;
-      }
-    } else {
-      assert isStillMergeCandidate(clazz, targetClass);
-    }
-
-    // Guard against the case where we have two methods that may get the same signature
-    // if we replace types. This is rare, so we approximate and err on the safe side here.
-    if (new CollisionDetector(clazz.type, targetClass.type).mayCollide()) {
-      return;
-    }
-
-    // Check with main dex classes to see if we are allowed to merge.
-    if (!mainDexInfo.canMerge(clazz, targetClass, appView.getSyntheticItems())) {
-      return;
-    }
-
-    ClassMerger merger = new ClassMerger(clazz, targetClass);
-    boolean merged;
-    try {
-      merged = merger.merge();
-    } catch (ExecutionException e) {
-      throw new RuntimeException(e);
-    }
-    if (merged) {
-      // Commit the changes to the graph lens.
-      lensBuilder.merge(merger.getRenamings());
-      synthesizedBridges.addAll(merger.getSynthesizedBridges());
-    }
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
-    if (source.type == target.superType) {
-      // 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.interfaces.values) {
-        DexClass clazz = appInfo.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 class ClassMerger {
-
-    private final DexProgramClass source;
-    private final DexProgramClass target;
-    private final VerticalClassMergerGraphLens.Builder deferredRenamings =
-        new VerticalClassMergerGraphLens.Builder(appView.dexItemFactory());
-    private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
-
-    private boolean abortMerge = false;
-
-    private ClassMerger(DexProgramClass source, DexProgramClass target) {
-      this.source = source;
-      this.target = target;
-    }
-
-    public boolean merge() throws ExecutionException {
-      // Merge the class [clazz] into [targetClass] by adding all methods to
-      // targetClass that are not currently contained.
-      // Step 1: Merge methods
-      Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
-      addAll(existingMethods, target.methods(), MethodSignatureEquivalence.get());
-
-      Map<Wrapper<DexMethod>, DexEncodedMethod> directMethods = new HashMap<>();
-      Map<Wrapper<DexMethod>, DexEncodedMethod> virtualMethods = new HashMap<>();
-
-      Predicate<DexMethod> availableMethodSignatures =
-          (method) -> {
-            Wrapper<DexMethod> wrapped = MethodSignatureEquivalence.get().wrap(method);
-            return !existingMethods.contains(wrapped)
-                && !directMethods.containsKey(wrapped)
-                && !virtualMethods.containsKey(wrapped);
-          };
-
-      source.forEachProgramDirectMethod(
-          directMethod -> {
-            DexEncodedMethod definition = directMethod.getDefinition();
-            if (definition.isInstanceInitializer()) {
-              DexEncodedMethod resultingConstructor =
-                  renameConstructor(
-                      definition,
-                      candidate ->
-                          availableMethodSignatures.test(candidate)
-                              && source.lookupVirtualMethod(candidate) == null);
-              add(directMethods, resultingConstructor, MethodSignatureEquivalence.get());
-              blockRedirectionOfSuperCalls(resultingConstructor.getReference());
-            } else {
-              DexEncodedMethod resultingDirectMethod =
-                  renameMethod(
-                      definition,
-                      availableMethodSignatures,
-                      definition.isClassInitializer() ? Rename.NEVER : Rename.IF_NEEDED);
-              add(directMethods, resultingDirectMethod, MethodSignatureEquivalence.get());
-              deferredRenamings.map(
-                  directMethod.getReference(), resultingDirectMethod.getReference());
-              deferredRenamings.recordMove(
-                  directMethod.getReference(), resultingDirectMethod.getReference());
-              blockRedirectionOfSuperCalls(resultingDirectMethod.getReference());
-
-              // Private methods in the parent class may be targeted with invoke-super if the two
-              // classes are in the same nest. Ensure such calls are mapped to invoke-direct.
-              if (definition.isInstance()
-                  && definition.isPrivate()
-                  && AccessControl.isMemberAccessible(directMethod, source, target, appView)
-                      .isTrue()) {
-                deferredRenamings.mapVirtualMethodToDirectInType(
-                    directMethod.getReference(),
-                    prototypeChanges ->
-                        new MethodLookupResult(
-                            resultingDirectMethod.getReference(), null, DIRECT, prototypeChanges),
-                    target.getType());
-              }
-            }
-          });
-
-      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
-        DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
-        if (shadowedBy != null) {
-          if (virtualMethod.isAbstract()) {
-            // 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
-                .map(virtualMethod.getReference(), shadowedBy.getReference())
-                .map(shadowedBy.getReference(), shadowedBy.getReference())
-                .recordMerge(virtualMethod.getReference(), shadowedBy.getReference());
-
-            // The override now corresponds to the method in the parent, so unset its synthetic flag
-            // if the method in the parent is not synthetic.
-            if (!virtualMethod.isSyntheticMethod() && shadowedBy.isSyntheticMethod()) {
-              shadowedBy.accessFlags.demoteFromSynthetic();
-            }
-            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;
-            }
-            // Update the holder of [virtualMethod] using renameMethod().
-            DexEncodedMethod resultingVirtualMethod =
-                renameMethod(virtualMethod, availableMethodSignatures, Rename.NEVER);
-            resultingVirtualMethod.setLibraryMethodOverride(
-                virtualMethod.isLibraryMethodOverride());
-            deferredRenamings.map(
-                virtualMethod.getReference(), resultingVirtualMethod.getReference());
-            deferredRenamings.recordMove(
-                virtualMethod.getReference(), resultingVirtualMethod.getReference());
-            add(virtualMethods, resultingVirtualMethod, MethodSignatureEquivalence.get());
-            continue;
-          }
-        }
-
-        DexEncodedMethod resultingMethod;
-        if (source.accessFlags.isInterface()) {
-          // Moving a default interface method into its subtype. This method could be hit directly
-          // via an invoke-super instruction from any of the transitive subtypes of this interface,
-          // due to the way invoke-super works on default interface methods. In order to be able
-          // to hit this method directly after the merge, we need to make it public, and find a
-          // method name that does not collide with one in the hierarchy of this class.
-          DexItemFactory dexItemFactory = appView.dexItemFactory();
-          String resultingMethodBaseName =
-              virtualMethod.getName().toString() + '$' + source.getTypeName().replace('.', '$');
-          DexMethod resultingMethodReference =
-              dexItemFactory.createMethod(
-                  target.getType(),
-                  virtualMethod.getProto().prependParameter(source.getType(), dexItemFactory),
-                  dexItemFactory.createGloballyFreshMemberString(resultingMethodBaseName));
-          assert availableMethodSignatures.test(resultingMethodReference);
-          resultingMethod =
-              virtualMethod.toTypeSubstitutedMethodAsInlining(
-                  resultingMethodReference, dexItemFactory);
-          makeStatic(resultingMethod);
-        } else {
-          // This virtual method could be called directly from a sub class via an invoke-super in-
-          // struction. Therefore, we translate this virtual method into an instance method with a
-          // unique name, such that relevant invoke-super instructions can be rewritten to target
-          // this method directly.
-          resultingMethod = renameMethod(virtualMethod, availableMethodSignatures, Rename.ALWAYS);
-          if (appView.options().getProguardConfiguration().isAccessModificationAllowed()) {
-            makePublic(resultingMethod);
-          } else {
-            makePrivate(resultingMethod);
-          }
-        }
-
-        add(
-            resultingMethod.belongsToDirectPool() ? directMethods : virtualMethods,
-            resultingMethod,
-            MethodSignatureEquivalence.get());
-
-        // Record that invoke-super instructions in the target class should be redirected to the
-        // newly created direct method.
-        redirectSuperCallsInTarget(virtualMethod, resultingMethod);
-        blockRedirectionOfSuperCalls(resultingMethod.getReference());
-
-        if (shadowedBy == null) {
-          // In addition to the newly added direct method, create a virtual method such that we do
-          // not accidentally remove the method from the interface of this class.
-          // Note that this method is added independently of whether it will actually be used. If
-          // it turns out that the method is never used, it will be removed by the final round
-          // of tree shaking.
-          shadowedBy = buildBridgeMethod(virtualMethod, resultingMethod);
-          deferredRenamings.recordCreationOfBridgeMethod(
-              virtualMethod.getReference(), shadowedBy.getReference());
-          add(virtualMethods, shadowedBy, MethodSignatureEquivalence.get());
-        }
-
-        // Copy over any keep info from the original virtual method.
-        ProgramMethod programMethod = new ProgramMethod(target, shadowedBy);
-        appView
-            .getKeepInfo()
-            .mutate(
-                mutableKeepInfoCollection ->
-                    mutableKeepInfoCollection.joinMethod(
-                        programMethod,
-                        info ->
-                            info.merge(
-                                mutableKeepInfoCollection
-                                    .getMethodInfo(virtualMethod, source)
-                                    .joiner())));
-
-        deferredRenamings.map(virtualMethod.getReference(), shadowedBy.getReference());
-        deferredRenamings.recordMove(
-            virtualMethod.getReference(),
-            resultingMethod.getReference(),
-            resultingMethod.isStatic());
-      }
-
-      if (abortMerge) {
-        assert restoreDebuggingState(
-            Streams.concat(directMethods.values().stream(), virtualMethods.values().stream()));
-        return false;
-      }
-
-      // Rewrite generic signatures before we merge a base with a generic signature.
-      rewriteGenericSignatures(target, source, directMethods.values(), virtualMethods.values());
-
-      // Convert out of DefaultInstanceInitializerCode, since this piece of code will require lens
-      // code rewriting.
-      target.forEachProgramInstanceInitializerMatching(
-          method -> method.getCode().isDefaultInstanceInitializerCode(),
-          method -> DefaultInstanceInitializerCode.uncanonicalizeCode(appView, method));
-
-      // Step 2: Merge fields
-      Set<DexString> existingFieldNames = new HashSet<>();
-      for (DexEncodedField field : target.fields()) {
-        existingFieldNames.add(field.getReference().name);
-      }
-
-      // In principle, we could allow multiple fields with the same name, and then only rename the
-      // field in the end when we are done merging all the classes, if it it turns out that the two
-      // fields ended up having the same type. This would not be too expensive, since we visit the
-      // entire program using VerticalClassMerger.TreeFixer anyway.
-      //
-      // For now, we conservatively report that a signature is already taken if there is a field
-      // with the same name. If minification is used with -overloadaggressively, this is solved
-      // later anyway.
-      Predicate<DexField> availableFieldSignatures =
-          field -> !existingFieldNames.contains(field.name);
-
-      DexEncodedField[] mergedInstanceFields =
-          mergeFields(
-              source.instanceFields(),
-              target.instanceFields(),
-              availableFieldSignatures,
-              existingFieldNames);
-
-      DexEncodedField[] mergedStaticFields =
-          mergeFields(
-              source.staticFields(),
-              target.staticFields(),
-              availableFieldSignatures,
-              existingFieldNames);
-
-      // Step 3: Merge interfaces
-      Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
-      // Now destructively update the class.
-      // Step 1: Update supertype or fix interfaces.
-      if (source.isInterface()) {
-        interfaces.remove(source.type);
-      } else {
-        assert !target.isInterface();
-        target.superType = source.superType;
-      }
-      target.interfaces =
-          interfaces.isEmpty()
-              ? DexTypeList.empty()
-              : new DexTypeList(interfaces.toArray(DexType.EMPTY_ARRAY));
-      // Step 2: ensure -if rules cannot target the members that were merged into the target class.
-      directMethods.values().forEach(feedback::markMethodCannotBeKept);
-      virtualMethods.values().forEach(feedback::markMethodCannotBeKept);
-      for (int i = 0; i < source.instanceFields().size(); i++) {
-        feedback.markFieldCannotBeKept(mergedInstanceFields[i]);
-      }
-      for (int i = 0; i < source.staticFields().size(); i++) {
-        feedback.markFieldCannotBeKept(mergedStaticFields[i]);
-      }
-      // Step 3: replace fields and methods.
-      target.addDirectMethods(directMethods.values());
-      target.addVirtualMethods(virtualMethods.values());
-      target.setInstanceFields(mergedInstanceFields);
-      target.setStaticFields(mergedStaticFields);
-      // Step 4: Clear the members of the source class since they have now been moved to the target.
-      source.getMethodCollection().clearDirectMethods();
-      source.getMethodCollection().clearVirtualMethods();
-      source.clearInstanceFields();
-      source.clearStaticFields();
-      // Step 5: Record merging.
-      mergedClasses.put(source.type, target.type);
-      if (source.isInterface()) {
-        mergedInterfaces.put(source.type, target.type);
-      }
-      assert !abortMerge;
-      assert GenericSignatureCorrectnessHelper.createForVerification(
-              appView, GenericSignatureContextBuilder.createForSingleClass(appView, target))
-          .evaluateSignaturesForClass(target)
-          .isValid();
-      return true;
-    }
-
-    /**
-     * The rewriting of generic signatures is pretty simple, but require some bookkeeping. We take
-     * the arguments to the base type:
-     *
-     * <pre>
-     *   class Sub<X> extends Base<X, String>
-     * </pre>
-     *
-     * for
-     *
-     * <pre>
-     *   class Base<T,R> extends OtherBase<T> implements I<R> {
-     *     T t() { ... };
-     *   }
-     * </pre>
-     *
-     * and substitute T -> X and R -> String
-     */
-    private void rewriteGenericSignatures(
-        DexProgramClass target,
-        DexProgramClass source,
-        Collection<DexEncodedMethod> directMethods,
-        Collection<DexEncodedMethod> virtualMethods) {
-      ClassSignature targetSignature = target.getClassSignature();
-      if (targetSignature.hasNoSignature()) {
-        // Null out all source signatures that is moved, but do not clear out the class since this
-        // could be referred to by other generic signatures.
-        // TODO(b/147504070): If merging classes with enclosing/innerclasses, this needs to be
-        //  reconsidered.
-        directMethods.forEach(DexEncodedMethod::clearGenericSignature);
-        virtualMethods.forEach(DexEncodedMethod::clearGenericSignature);
-        source.fields().forEach(DexEncodedMember::clearGenericSignature);
-        return;
-      }
-      GenericSignaturePartialTypeArgumentApplier classApplier =
-          getGenericSignatureArgumentApplier(target, source);
-      if (classApplier == null) {
-        target.clearClassSignature();
-        target.members().forEach(DexEncodedMember::clearGenericSignature);
-        return;
-      }
-      // We could generate a substitution map.
-      ClassSignature rewrittenSource = classApplier.visitClassSignature(source.getClassSignature());
-      // The variables in the class signature is now rewritten to use the targets argument.
-      ClassSignatureBuilder builder = ClassSignature.builder();
-      builder.addFormalTypeParameters(targetSignature.getFormalTypeParameters());
-      if (!source.isInterface()) {
-        if (rewrittenSource.hasSignature()) {
-          builder.setSuperClassSignature(rewrittenSource.getSuperClassSignatureOrNull());
-        } else {
-          builder.setSuperClassSignature(new ClassTypeSignature(source.superType));
-        }
-      } else {
-        builder.setSuperClassSignature(targetSignature.getSuperClassSignatureOrNull());
-      }
-      // Compute the seen set for interfaces to add. This is similar to the merging of interfaces
-      // but allow us to maintain the type arguments.
-      Set<DexType> seenInterfaces = new HashSet<>();
-      if (source.isInterface()) {
-        seenInterfaces.add(source.type);
-      }
-      for (ClassTypeSignature iFace : targetSignature.getSuperInterfaceSignatures()) {
-        if (seenInterfaces.add(iFace.type())) {
-          builder.addSuperInterfaceSignature(iFace);
-        }
-      }
-      if (rewrittenSource.hasSignature()) {
-        for (ClassTypeSignature iFace : rewrittenSource.getSuperInterfaceSignatures()) {
-          if (!seenInterfaces.contains(iFace.type())) {
-            builder.addSuperInterfaceSignature(iFace);
-          }
-        }
-      } else {
-        // Synthesize raw uses of interfaces to align with the actual class
-        for (DexType iFace : source.interfaces) {
-          if (!seenInterfaces.contains(iFace)) {
-            builder.addSuperInterfaceSignature(new ClassTypeSignature(iFace));
-          }
-        }
-      }
-      target.setClassSignature(builder.build(appView.dexItemFactory()));
-
-      // Go through all type-variable references for members and update them.
-      CollectionUtils.forEach(
-          method -> {
-            MethodTypeSignature methodSignature = method.getGenericSignature();
-            if (methodSignature.hasNoSignature()) {
-              return;
-            }
-            method.setGenericSignature(
-                classApplier
-                    .buildForMethod(methodSignature.getFormalTypeParameters())
-                    .visitMethodSignature(methodSignature));
-          },
-          directMethods,
-          virtualMethods);
-
-      source.forEachField(
-          field -> {
-            if (field.getGenericSignature().hasNoSignature()) {
-              return;
-            }
-            field.setGenericSignature(
-                classApplier.visitFieldTypeSignature(field.getGenericSignature()));
-          });
-    }
-
-    private GenericSignaturePartialTypeArgumentApplier getGenericSignatureArgumentApplier(
-        DexProgramClass target, DexProgramClass source) {
-      assert target.getClassSignature().hasSignature();
-      // We can assert proper structure below because the generic signature validator has run
-      // before and pruned invalid signatures.
-      List<FieldTypeSignature> genericArgumentsToSuperType =
-          target
-              .getClassSignature()
-              .getGenericArgumentsToSuperType(source.type, appView.dexItemFactory());
-      if (genericArgumentsToSuperType == null) {
-        assert false : "Type should be present in generic signature";
-        return null;
-      }
-      Map<String, FieldTypeSignature> substitutionMap = new HashMap<>();
-      List<FormalTypeParameter> formals = source.getClassSignature().getFormalTypeParameters();
-      if (genericArgumentsToSuperType.size() != formals.size()) {
-        if (!genericArgumentsToSuperType.isEmpty()) {
-          assert false : "Invalid argument count to formals";
-          return null;
-        }
-      } else {
-        for (int i = 0; i < formals.size(); i++) {
-          // It is OK to override a generic type variable so we just use put.
-          substitutionMap.put(formals.get(i).getName(), genericArgumentsToSuperType.get(i));
-        }
-      }
-      return GenericSignaturePartialTypeArgumentApplier.build(
-          appView,
-          TypeParameterContext.empty().addPrunedSubstitutions(substitutionMap),
-          (type1, type2) -> true,
-          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, DexEncodedMethod newTarget) {
-      DexMethod oldTargetReference = oldTarget.getReference();
-      DexMethod newTargetReference = newTarget.getReference();
-      InvokeType newTargetType = newTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT;
-      if (source.accessFlags.isInterface()) {
-        // If we merge a default interface method from interface I to its subtype C, then we need
-        // to rewrite invocations on the form "invoke-super I.m()" to "invoke-direct C.m$I()".
-        //
-        // Unlike when we merge a class into its subclass (the else-branch below), we should *not*
-        // 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,
-            prototypeChanges ->
-                new MethodLookupResult(newTargetReference, null, STATIC, prototypeChanges),
-            target.type);
-      } 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
-        // method C.m$B denotes the direct/virtual method that has been created in C for B.m). In
-        // particular, there might be an instruction "invoke-super A.m()" in C that resolves to B.m
-        // at runtime (A is a superclass of B), which also needs to be rewritten to
-        // "invoke-{direct,virtual} C.m$B()".
-        //
-        // We handle this by adding a mapping for [target] and all of its supertypes.
-        DexProgramClass holder = target;
-        while (holder != null && holder.isProgramClass()) {
-          DexMethod signatureInHolder =
-              oldTargetReference.withHolder(holder, appView.dexItemFactory());
-          // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
-          boolean resolutionSucceeds =
-              holder.lookupVirtualMethod(signatureInHolder) != null
-                  || appInfo.lookupSuperTarget(signatureInHolder, holder, appView) != null;
-          if (resolutionSucceeds) {
-            deferredRenamings.mapVirtualMethodToDirectInType(
-                signatureInHolder,
-                prototypeChanges ->
-                    new MethodLookupResult(
-                        newTargetReference, null, newTargetType, prototypeChanges),
-                target.type);
-          } else {
-            break;
-          }
-
-          // Consider that A gets merged into B and B's subclass C gets merged into D. Instructions
-          // on the form "invoke-super {B,C,D}.m()" in D are changed into "invoke-direct D.m$C()" by
-          // the code above. However, instructions on the form "invoke-super A.m()" should also be
-          // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
-          // that have been merged into [holder].
-          Set<DexType> mergedTypes = mergedClasses.getKeys(holder.type);
-          for (DexType type : mergedTypes) {
-            DexMethod signatureInType =
-                oldTargetReference.withHolder(type, appView.dexItemFactory());
-            // Resolution would have succeeded if the method used to be in [type], or if one of
-            // its super classes declared the method.
-            boolean resolutionSucceededBeforeMerge =
-                lensBuilder.hasMappingForSignatureInContext(holder, signatureInType)
-                    || appInfo.lookupSuperTarget(signatureInHolder, holder, appView) != null;
-            if (resolutionSucceededBeforeMerge) {
-              deferredRenamings.mapVirtualMethodToDirectInType(
-                  signatureInType,
-                  prototypeChanges ->
-                      new MethodLookupResult(
-                          newTargetReference, null, newTargetType, prototypeChanges),
-                  target.type);
-            }
-          }
-          holder =
-              holder.superType != null
-                  ? asProgramClassOrNull(appInfo.definitionFor(holder.superType))
-                  : null;
-        }
-      }
-    }
-
-    private void blockRedirectionOfSuperCalls(DexMethod method) {
-      // We are merging a class B into C. The methods from B are being moved into C, and then we
-      // subsequently rewrite the invoke-super instructions in C that hit a method in B, such that
-      // they use an invoke-direct instruction instead. In this process, we need to avoid rewriting
-      // the invoke-super instructions that originally was in the superclass B.
-      //
-      // Example:
-      //   class A {
-      //     public void m() {}
-      //   }
-      //   class B extends A {
-      //     public void m() { super.m(); } <- invoke must not be rewritten to invoke-direct
-      //                                       (this would lead to an infinite loop)
-      //   }
-      //   class C extends B {
-      //     public void m() { super.m(); } <- invoke needs to be rewritten to invoke-direct
-      //   }
-      deferredRenamings.markMethodAsMerged(method);
-    }
-
-    private DexEncodedMethod buildBridgeMethod(
-        DexEncodedMethod method, DexEncodedMethod invocationTarget) {
-      DexType holder = target.type;
-      DexProto proto = method.getReference().proto;
-      DexString name = method.getReference().name;
-      DexMethod newMethod = application.dexItemFactory.createMethod(holder, proto, name);
-      MethodAccessFlags accessFlags = method.accessFlags.copy();
-      accessFlags.setBridge();
-      accessFlags.setSynthetic();
-      accessFlags.unsetAbstract();
-
-      assert invocationTarget.isStatic()
-          || invocationTarget.isNonPrivateVirtualMethod()
-          || invocationTarget.isNonStaticPrivateMethod();
-      SynthesizedBridgeCode code =
-          new SynthesizedBridgeCode(
-              newMethod,
-              invocationTarget.getReference(),
-              invocationTarget.isStatic()
-                  ? STATIC
-                  : (invocationTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT),
-              target.isInterface());
-
-      // Add the bridge to the list of synthesized bridges such that the method signatures will
-      // be updated by the end of vertical class merging.
-      synthesizedBridges.add(code);
-
-      CfVersion classFileVersion =
-          method.hasClassFileVersion() ? method.getClassFileVersion() : null;
-      DexEncodedMethod bridge =
-          DexEncodedMethod.syntheticBuilder()
-              .setMethod(newMethod)
-              .setAccessFlags(accessFlags)
-              .setCode(code)
-              .setClassFileVersion(classFileVersion)
-              .setApiLevelForDefinition(method.getApiLevelForDefinition())
-              .setApiLevelForCode(method.getApiLevelForDefinition())
-              .setIsLibraryMethodOverride(method.isLibraryMethodOverride())
-              .setGenericSignature(method.getGenericSignature())
-              .build();
-      if (method.accessFlags.isPromotedToPublic()) {
-        // The bridge is now the public method serving the role of the original method, and should
-        // reflect that this method was publicized.
-        assert bridge.accessFlags.isPromotedToPublic();
-      }
-      return bridge;
-    }
-
-    @SuppressWarnings("ReferenceEquality")
-    // Returns the method that shadows the given method, or null if method is not shadowed.
-    private DexEncodedMethod findMethodInTarget(DexEncodedMethod method) {
-      MethodResolutionResult resolutionResult =
-          appInfo.resolveMethodOnLegacy(target, method.getReference());
-      if (!resolutionResult.isSingleResolution()) {
-        // May happen in case of missing classes, or if multiple implementations were found.
-        abortMerge = true;
-        return null;
-      }
-      DexEncodedMethod actual = resolutionResult.getSingleTarget();
-      if (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.
-      return null;
-    }
-
-    private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> void add(
-        Map<Wrapper<R>, D> map, D item, Equivalence<R> equivalence) {
-      map.put(equivalence.wrap(item.getReference()), item);
-    }
-
-    private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> void addAll(
-        Collection<Wrapper<R>> collection, Iterable<D> items, Equivalence<R> equivalence) {
-      for (D item : items) {
-        collection.add(equivalence.wrap(item.getReference()));
-      }
-    }
-
-    private <T> Set<T> mergeArrays(T[] one, T[] other) {
-      Set<T> merged = new LinkedHashSet<>();
-      Collections.addAll(merged, one);
-      Collections.addAll(merged, other);
-      return merged;
-    }
-
-    private DexEncodedField[] mergeFields(
-        Collection<DexEncodedField> sourceFields,
-        Collection<DexEncodedField> targetFields,
-        Predicate<DexField> availableFieldSignatures,
-        Set<DexString> existingFieldNames) {
-      DexEncodedField[] result = new DexEncodedField[sourceFields.size() + targetFields.size()];
-      // Add fields from source
-      int i = 0;
-      for (DexEncodedField field : sourceFields) {
-        DexEncodedField resultingField = renameFieldIfNeeded(field, availableFieldSignatures);
-        existingFieldNames.add(resultingField.getReference().name);
-        deferredRenamings.map(field.getReference(), resultingField.getReference());
-        result[i] = resultingField;
-        i++;
-      }
-      // Add fields from target.
-      for (DexEncodedField field : targetFields) {
-        result[i] = field;
-        i++;
-      }
-      return result;
-    }
-
-    // Note that names returned by this function are not necessarily unique. Clients should
-    // repeatedly try to generate a fresh name until it is unique.
-    private DexString getFreshName(String nameString, int index, DexType holder) {
-      String freshName = nameString + "$" + holder.toSourceString().replace('.', '$');
-      if (index > 1) {
-        freshName += index;
-      }
-      return application.dexItemFactory.createString(freshName);
-    }
-
-    private DexEncodedMethod renameConstructor(
-        DexEncodedMethod method, Predicate<DexMethod> availableMethodSignatures) {
-      assert method.isInstanceInitializer();
-      DexType oldHolder = method.getHolderType();
-
-      DexMethod newSignature;
-      int count = 1;
-      do {
-        DexString newName = getFreshName(TEMPORARY_INSTANCE_INITIALIZER_PREFIX, count, oldHolder);
-        newSignature =
-            application.dexItemFactory.createMethod(
-                target.type, method.getReference().proto, newName);
-        count++;
-      } while (!availableMethodSignatures.test(newSignature));
-
-      DexEncodedMethod result =
-          method.toTypeSubstitutedMethodAsInlining(newSignature, appView.dexItemFactory());
-      result.getMutableOptimizationInfo().markForceInline();
-      deferredRenamings.map(method.getReference(), result.getReference());
-      deferredRenamings.recordMove(method.getReference(), result.getReference());
-      // 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.accessFlags.unsetConstructor();
-      makePrivate(result);
-      return result;
-    }
-
-    private DexEncodedMethod renameMethod(
-        DexEncodedMethod method, Predicate<DexMethod> availableMethodSignatures, Rename strategy) {
-      return renameMethod(method, availableMethodSignatures, strategy, method.getReference().proto);
-    }
-
-    private DexEncodedMethod renameMethod(
-        DexEncodedMethod method,
-        Predicate<DexMethod> availableMethodSignatures,
-        Rename strategy,
-        DexProto newProto) {
-      // We cannot handle renaming static initializers yet and constructors should have been
-      // renamed already.
-      assert !method.accessFlags.isConstructor() || strategy == Rename.NEVER;
-      DexString oldName = method.getReference().name;
-      DexType oldHolder = method.getHolderType();
-
-      DexMethod newSignature;
-      switch (strategy) {
-        case IF_NEEDED:
-          newSignature = application.dexItemFactory.createMethod(target.type, newProto, oldName);
-          if (availableMethodSignatures.test(newSignature)) {
-            break;
-          }
-          // Fall-through to ALWAYS so that we assign a new name.
-
-        case ALWAYS:
-          int count = 1;
-          do {
-            DexString newName = getFreshName(oldName.toSourceString(), count, oldHolder);
-            newSignature = application.dexItemFactory.createMethod(target.type, newProto, newName);
-            count++;
-          } while (!availableMethodSignatures.test(newSignature));
-          break;
-
-        case NEVER:
-          newSignature = application.dexItemFactory.createMethod(target.type, newProto, oldName);
-          assert availableMethodSignatures.test(newSignature);
-          break;
-
-        default:
-          throw new Unreachable();
-      }
-
-      return method.toTypeSubstitutedMethodAsInlining(newSignature, appView.dexItemFactory());
-    }
-
-    private DexEncodedField renameFieldIfNeeded(
-        DexEncodedField field, Predicate<DexField> availableFieldSignatures) {
-      DexString oldName = field.getReference().name;
-      DexType oldHolder = field.getHolderType();
-
-      DexField newSignature =
-          application.dexItemFactory.createField(target.type, field.getReference().type, oldName);
-      if (!availableFieldSignatures.test(newSignature)) {
-        int count = 1;
-        do {
-          DexString newName = getFreshName(oldName.toSourceString(), count, oldHolder);
-          newSignature =
-              application.dexItemFactory.createField(
-                  target.type, field.getReference().type, newName);
-          count++;
-        } while (!availableFieldSignatures.test(newSignature));
-      }
-
-      return field.toTypeSubstitutedField(appView, newSignature);
-    }
-
-    private void makeStatic(DexEncodedMethod method) {
-      method.accessFlags.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;
-      }
-    }
-  }
-
-  private static void makePrivate(DexEncodedMethod method) {
-    assert !method.accessFlags.isAbstract();
-    method.accessFlags.unsetPublic();
-    method.accessFlags.unsetProtected();
-    method.accessFlags.setPrivate();
-  }
-
-  private static void makePublic(DexEncodedMethod method) {
-    MethodAccessFlags accessFlags = method.getAccessFlags();
-    assert !accessFlags.isAbstract();
-    accessFlags.unsetPrivate();
-    accessFlags.unsetProtected();
-    accessFlags.setPublic();
-  }
-
-  private static class VerticalClassMergerTreeFixer extends TreeFixerBase {
-
-    private final AppView<AppInfoWithLiveness> appView;
-    private final VerticalClassMergerGraphLens.Builder lensBuilder;
-    private final VerticallyMergedClasses mergedClasses;
-    private final List<SynthesizedBridgeCode> synthesizedBridges;
-
-    VerticalClassMergerTreeFixer(
-        AppView<AppInfoWithLiveness> appView,
-        VerticalClassMergerGraphLens.Builder lensBuilder,
-        VerticallyMergedClasses mergedClasses,
-        List<SynthesizedBridgeCode> synthesizedBridges) {
-      super(appView);
-      this.appView = appView;
-      this.lensBuilder =
-          VerticalClassMergerGraphLens.Builder.createBuilderForFixup(lensBuilder, mergedClasses);
-      this.mergedClasses = mergedClasses;
-      this.synthesizedBridges = synthesizedBridges;
-    }
-
-    private VerticalClassMergerGraphLens fixupTypeReferences() {
-      // Globally substitute merged class types in protos and holders.
-      for (DexProgramClass clazz : appView.appInfo().classes()) {
-        clazz.getMethodCollection().replaceMethods(this::fixupMethod);
-        clazz.setStaticFields(fixupFields(clazz.staticFields()));
-        clazz.setInstanceFields(fixupFields(clazz.instanceFields()));
-        clazz.setPermittedSubclassAttributes(
-            fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
-      }
-      for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
-        synthesizedBridge.updateMethodSignatures(this::fixupMethodReference);
-      }
-      VerticalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
-      if (lens != null) {
-        new AnnotationFixer(lens, appView.graphLens()).run(appView.appInfo().classes());
-      }
-      return lens;
-    }
-
-    @Override
-    public DexType mapClassType(DexType type) {
-      while (mergedClasses.hasBeenMergedIntoSubtype(type)) {
-        type = mergedClasses.getTargetFor(type);
-      }
-      return type;
-    }
-
-    @Override
-    public void recordClassChange(DexType from, DexType to) {
-      // Fixup of classes is not used so no class type should change.
-      throw new Unreachable();
-    }
-
-    @Override
-    public void recordFieldChange(DexField from, DexField to) {
-      if (!lensBuilder.hasOriginalSignatureMappingFor(to)) {
-        lensBuilder.map(from, to);
-      }
-    }
-
-    @Override
-    public void recordMethodChange(DexMethod from, DexMethod to) {
-      if (!lensBuilder.hasOriginalSignatureMappingFor(to)) {
-        lensBuilder.map(from, to).recordMove(from, to);
-      }
-    }
-
-    @Override
-    public DexEncodedMethod recordMethodChange(
-        DexEncodedMethod method, DexEncodedMethod newMethod) {
-      recordMethodChange(method.getReference(), newMethod.getReference());
-      if (newMethod.isNonPrivateVirtualMethod()) {
-        // Since we changed the return type or one of the parameters, this method cannot be a
-        // classpath or library method override, since we only class merge program classes.
-        assert !method.isLibraryMethodOverride().isTrue();
-        newMethod.setLibraryMethodOverride(OptionalBool.FALSE);
-      }
-      return newMethod;
-    }
-  }
-
-  private class CollisionDetector {
-
-    private static final int NOT_FOUND = Integer.MIN_VALUE;
-
-    // TODO(herhut): Maybe cache seenPositions for target classes.
-    private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
-    private final Reference2IntMap<DexProto> targetProtoCache;
-    private final Reference2IntMap<DexProto> sourceProtoCache;
-    private final DexType source, target;
-    private final Collection<DexMethod> invokes = getInvokes();
-
-    private CollisionDetector(DexType source, DexType target) {
-      this.source = source;
-      this.target = target;
-      this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
-      this.targetProtoCache.defaultReturnValue(NOT_FOUND);
-      this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
-      this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
-    }
-
-    boolean mayCollide() {
-      timing.begin("collision detection");
-      fillSeenPositions();
-      boolean result = false;
-      // If the type is not used in methods at all, there cannot be any conflict.
-      if (!seenPositions.isEmpty()) {
-        for (DexMethod method : invokes) {
-          Int2IntMap positionsMap = seenPositions.get(method.name);
-          if (positionsMap != null) {
-            int arity = method.getArity();
-            int previous = positionsMap.get(arity);
-            if (previous != NOT_FOUND) {
-              assert previous != 0;
-              int positions = computePositionsFor(method.proto, source, sourceProtoCache);
-              if ((positions & previous) != 0) {
-                result = true;
-                break;
-              }
-            }
-          }
-        }
-      }
-      timing.end();
-      return result;
-    }
-
-    private void fillSeenPositions() {
-      for (DexMethod method : invokes) {
-        DexType[] parameters = method.proto.parameters.values;
-        int arity = parameters.length;
-        int positions = computePositionsFor(method.proto, target, targetProtoCache);
-        if (positions != 0) {
-          Int2IntMap positionsMap =
-              seenPositions.computeIfAbsent(method.name, k -> {
-                Int2IntMap result = new Int2IntOpenHashMap();
-                result.defaultReturnValue(NOT_FOUND);
-                return result;
-              });
-          int value = 0;
-          int previous = positionsMap.get(arity);
-          if (previous != NOT_FOUND) {
-            value = previous;
-          }
-          value |= positions;
-          positionsMap.put(arity, value);
-        }
-      }
-
-    }
-
-    @SuppressWarnings("ReferenceEquality")
-    // Given a method signature and a type, this method computes a bit vector that denotes the
-    // positions at which the given type is used in the method signature.
-    private int computePositionsFor(
-        DexProto proto, DexType type, Reference2IntMap<DexProto> cache) {
-      int result = cache.getInt(proto);
-      if (result != NOT_FOUND) {
-        return result;
-      }
-      result = 0;
-      int bitsUsed = 0;
-      int accumulator = 0;
-      for (DexType parameterType : proto.parameters.values) {
-        DexType parameterBaseType = parameterType.toBaseType(appView.dexItemFactory());
-        // Substitute the type with the already merged class to estimate what it will look like.
-        DexType mappedType = mergedClasses.getOrDefault(parameterBaseType, parameterBaseType);
-        accumulator <<= 1;
-        bitsUsed++;
-        if (mappedType == type) {
-          accumulator |= 1;
-        }
-        // Handle overflow on 31 bit boundary.
-        if (bitsUsed == Integer.SIZE - 1) {
-          result |= accumulator;
-          accumulator = 0;
-          bitsUsed = 0;
-        }
-      }
-      // We also take the return type into account for potential conflicts.
-      DexType returnBaseType = proto.returnType.toBaseType(appView.dexItemFactory());
-      DexType mappedReturnType = mergedClasses.getOrDefault(returnBaseType, returnBaseType);
-      accumulator <<= 1;
-      if (mappedReturnType == type) {
-        accumulator |= 1;
-      }
-      result |= accumulator;
-      cache.put(proto, result);
-      return result;
-    }
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private AbortReason disallowInlining(ProgramMethod method, DexProgramClass context) {
-    if (appView.options().inlinerOptions().enableInlining) {
-      Code code = method.getDefinition().getCode();
-      if (code.isCfCode()) {
-        CfCode cfCode = code.asCfCode();
-        ConstraintWithTarget constraint =
-            cfCode.computeInliningConstraint(
-                method,
-                appView,
-                new SingleTypeMapperGraphLens(method.getHolderType(), context),
-                context.programInstanceInitializers().iterator().next());
-        if (constraint == ConstraintWithTarget.NEVER) {
-          return AbortReason.UNSAFE_INLINING;
-        }
-        // 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, appView.getSyntheticItems())) {
-          return AbortReason.MAIN_DEX_ROOT_OUTSIDE_REFERENCE;
-        }
-        return null;
-      } else if (code.isDefaultInstanceInitializerCode()) {
-        return null;
-      }
-      // For non-jar/cf code we currently cannot guarantee that markForceInline() will succeed.
-    }
-    return AbortReason.UNSAFE_INLINING;
-  }
-
-  public class SingleTypeMapperGraphLens extends NonIdentityGraphLens {
-
-    private final DexType source;
-    private final DexProgramClass target;
-
-    public SingleTypeMapperGraphLens(DexType source, DexProgramClass target) {
-      super(appView.dexItemFactory(), GraphLens.getIdentityLens());
-      this.source = source;
-      this.target = target;
-    }
-
-    @Override
-    public Iterable<DexType> getOriginalTypes(DexType type) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexType getPreviousClassType(DexType type) {
-      throw new Unreachable();
-    }
-
-    @Override
-    @SuppressWarnings("ReferenceEquality")
-    public final DexType getNextClassType(DexType type) {
-      return type == source ? target.type : mergedClasses.getOrDefault(type, type);
-    }
-
-    @Override
-    public DexField getPreviousFieldSignature(DexField field) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexField getNextFieldSignature(DexField field) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexMethod getPreviousMethodSignature(DexMethod method) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexMethod getNextMethodSignature(DexMethod method) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public MethodLookupResult lookupMethod(
-        DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
-      // First look up the method using the existing graph lens (for example, the type will have
-      // changed if the method was publicized by ClassAndMemberPublicizer).
-      MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type, codeLens);
-      // Then check if there is a renaming due to the vertical class merger.
-      DexMethod newMethod = lensBuilder.methodMap.get(lookup.getReference());
-      if (newMethod == null) {
-        return lookup;
-      }
-      MethodLookupResult.Builder methodLookupResultBuilder =
-          MethodLookupResult.builder(this)
-              .setReference(newMethod)
-              .setPrototypeChanges(lookup.getPrototypeChanges())
-              .setType(lookup.getType());
-      if (lookup.getType() == InvokeType.INTERFACE) {
-        // If an interface has been merged into a class, invoke-interface needs to be translated
-        // to invoke-virtual.
-        DexClass clazz = appInfo.definitionFor(newMethod.holder);
-        if (clazz != null && !clazz.accessFlags.isInterface()) {
-          assert appInfo.definitionFor(method.holder).accessFlags.isInterface();
-          methodLookupResultBuilder.setType(VIRTUAL);
-        }
-      }
-      return methodLookupResultBuilder.build();
-    }
-
-    @Override
-    protected MethodLookupResult internalDescribeLookupMethod(
-        MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-      // This is unreachable since we override the implementation of lookupMethod() above.
-      throw new Unreachable();
-    }
-
-    @Override
-    public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
-        DexMethod method, GraphLens codeLens) {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexField lookupField(DexField field, GraphLens codeLens) {
-      return lensBuilder.fieldMap.getOrDefault(field, field);
-    }
-
-    @Override
-    protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-      // This is unreachable since we override the implementation of lookupField() above.
-      throw new Unreachable();
-    }
-
-    @Override
-    @SuppressWarnings("HidingField")
-    public boolean isContextFreeForMethods(GraphLens codeLens) {
-      return true;
-    }
-  }
-
-  // Searches for a reference to a non-private, non-public class, field or method declared in the
-  // same package as [source].
-  public static class IllegalAccessDetector extends UseRegistryWithResult<Boolean, ProgramMethod> {
-
-    private final AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy;
-
-    public IllegalAccessDetector(
-        AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy,
-        ProgramMethod context) {
-      super(appViewWithClassHierarchy, context, false);
-      this.appViewWithClassHierarchy = appViewWithClassHierarchy;
-    }
-
-    protected boolean checkFoundPackagePrivateAccess() {
-      assert getResult();
-      return true;
-    }
-
-    protected boolean setFoundPackagePrivateAccess() {
-      setResult(true);
-      return true;
-    }
-
-    protected static boolean continueSearchForPackagePrivateAccess() {
-      return false;
-    }
-
-    private boolean checkFieldReference(DexField field) {
-      return checkRewrittenFieldReference(appViewWithClassHierarchy.graphLens().lookupField(field));
-    }
-
-    private boolean checkRewrittenFieldReference(DexField field) {
-      assert field.getHolderType().isClassType();
-      DexType fieldHolder = field.getHolderType();
-      if (fieldHolder.isSamePackage(getContext().getHolderType())) {
-        if (checkRewrittenTypeReference(fieldHolder)) {
-          return checkFoundPackagePrivateAccess();
-        }
-        DexClassAndField resolvedField =
-            appViewWithClassHierarchy.appInfo().resolveField(field).getResolutionPair();
-        if (resolvedField == null) {
-          return setFoundPackagePrivateAccess();
-        }
-        if (resolvedField.getHolder() != getContext().getHolder()
-            && !resolvedField.getAccessFlags().isPublic()) {
-          return setFoundPackagePrivateAccess();
-        }
-        if (checkRewrittenFieldType(resolvedField)) {
-          return checkFoundPackagePrivateAccess();
-        }
-      }
-      return continueSearchForPackagePrivateAccess();
-    }
-
-    protected boolean checkRewrittenFieldType(DexClassAndField field) {
-      return continueSearchForPackagePrivateAccess();
-    }
-
-    private boolean checkRewrittenMethodReference(
-        DexMethod rewrittenMethod, OptionalBool isInterface) {
-      DexType baseType =
-          rewrittenMethod.getHolderType().toBaseType(appViewWithClassHierarchy.dexItemFactory());
-      if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
-        if (checkTypeReference(rewrittenMethod.getHolderType())) {
-          return checkFoundPackagePrivateAccess();
-        }
-        MethodResolutionResult resolutionResult =
-            isInterface.isUnknown()
-                ? appViewWithClassHierarchy
-                    .appInfo()
-                    .unsafeResolveMethodDueToDexFormat(rewrittenMethod)
-                : appViewWithClassHierarchy
-                    .appInfo()
-                    .resolveMethod(rewrittenMethod, isInterface.isTrue());
-        if (!resolutionResult.isSingleResolution()) {
-          return setFoundPackagePrivateAccess();
-        }
-        DexClassAndMethod resolvedMethod =
-            resolutionResult.asSingleResolution().getResolutionPair();
-        if (resolvedMethod.getHolder() != getContext().getHolder()
-            && !resolvedMethod.getAccessFlags().isPublic()) {
-          return setFoundPackagePrivateAccess();
-        }
-      }
-      return continueSearchForPackagePrivateAccess();
-    }
-
-    private boolean checkTypeReference(DexType type) {
-      return internalCheckTypeReference(type, appViewWithClassHierarchy.graphLens());
-    }
-
-    private boolean checkRewrittenTypeReference(DexType type) {
-      return internalCheckTypeReference(type, GraphLens.getIdentityLens());
-    }
-
-    private boolean internalCheckTypeReference(DexType type, GraphLens graphLens) {
-      DexType baseType =
-          graphLens.lookupType(type.toBaseType(appViewWithClassHierarchy.dexItemFactory()));
-      if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
-        DexClass clazz = appViewWithClassHierarchy.definitionFor(baseType);
-        if (clazz == null || !clazz.isPublic()) {
-          return setFoundPackagePrivateAccess();
-        }
-      }
-      return continueSearchForPackagePrivateAccess();
-    }
-
-    @Override
-    public void registerInitClass(DexType clazz) {
-      if (appViewWithClassHierarchy.initClassLens().isFinal()) {
-        // The InitClass lens is always rewritten up until the most recent graph lens, so first map
-        // the class type to the most recent graph lens.
-        DexType rewrittenType = appViewWithClassHierarchy.graphLens().lookupType(clazz);
-        DexField initClassField =
-            appViewWithClassHierarchy.initClassLens().getInitClassField(rewrittenType);
-        checkRewrittenFieldReference(initClassField);
-      } else {
-        checkTypeReference(clazz);
-      }
-    }
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {
-      MethodLookupResult lookup =
-          appViewWithClassHierarchy.graphLens().lookupInvokeVirtual(method, getContext());
-      checkRewrittenMethodReference(lookup.getReference(), OptionalBool.FALSE);
-    }
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {
-      MethodLookupResult lookup =
-          appViewWithClassHierarchy.graphLens().lookupInvokeDirect(method, getContext());
-      checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
-    }
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {
-      MethodLookupResult lookup =
-          appViewWithClassHierarchy.graphLens().lookupInvokeStatic(method, getContext());
-      checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
-    }
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {
-      MethodLookupResult lookup =
-          appViewWithClassHierarchy.graphLens().lookupInvokeInterface(method, getContext());
-      checkRewrittenMethodReference(lookup.getReference(), OptionalBool.TRUE);
-    }
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {
-      MethodLookupResult lookup =
-          appViewWithClassHierarchy.graphLens().lookupInvokeSuper(method, getContext());
-      checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
-    }
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {
-      checkFieldReference(field);
-    }
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {
-      checkFieldReference(field);
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      checkTypeReference(type);
-    }
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {
-      checkFieldReference(field);
-    }
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {
-      checkFieldReference(field);
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {
-      checkTypeReference(type);
-    }
-
-    @Override
-    public void registerInstanceOf(DexType type) {
-      checkTypeReference(type);
-    }
-  }
-
-  public static class InvokeSpecialToDefaultLibraryMethodUseRegistry
-      extends UseRegistryWithResult<Boolean, ProgramMethod> {
-
-    InvokeSpecialToDefaultLibraryMethodUseRegistry(
-        AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-      super(appView, context, false);
-      assert context.getHolder().isInterface();
-    }
-
-    @Override
-    @SuppressWarnings("ReferenceEquality")
-    public void registerInvokeSpecial(DexMethod method) {
-      ProgramMethod context = getContext();
-      if (method.getHolderType() != context.getHolderType()) {
-        return;
-      }
-
-      DexEncodedMethod definition = context.getHolder().lookupMethod(method);
-      if (definition != null && definition.belongsToVirtualPool()) {
-        setResult(true);
-      }
-    }
-
-    @Override
-    public void registerInitClass(DexType type) {}
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {}
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {}
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {}
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {}
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {}
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {}
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {}
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {}
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {}
-
-    @Override
-    public void registerTypeReference(DexType type) {}
-  }
-
-  public static class SynthesizedBridgeCode extends AbstractSynthesizedCode {
-
-    private DexMethod method;
-    private DexMethod invocationTarget;
-    private InvokeType type;
-    private final boolean isInterface;
-
-    public SynthesizedBridgeCode(
-        DexMethod method,
-        DexMethod invocationTarget,
-        InvokeType type,
-        boolean isInterface) {
-      this.method = method;
-      this.invocationTarget = invocationTarget;
-      this.type = type;
-      this.isInterface = isInterface;
-    }
-
-    // By the time the synthesized code object is created, vertical class merging still has not
-    // finished. Therefore it is possible that the method signatures `method` and `invocationTarget`
-    // will change as a result of additional class merging operations. To deal with this, the
-    // vertical class merger explicitly invokes this method to update `method` and `invocation-
-    // Target` when vertical class merging has finished.
-    //
-    // Note that, without this step, these method signatures might refer to intermediate signatures
-    // that are only present in the middle of vertical class merging, which means that the graph
-    // lens will not work properly (since the graph lens generated by vertical class merging only
-    // expects to be applied to method signatures from *before* vertical class merging or *after*
-    // vertical class merging).
-    public void updateMethodSignatures(Function<DexMethod, DexMethod> transformer) {
-      method = transformer.apply(method);
-      invocationTarget = transformer.apply(invocationTarget);
-    }
-
-    @Override
-    public SourceCodeProvider getSourceCodeProvider() {
-      ForwardMethodSourceCode.Builder forwardSourceCodeBuilder =
-          ForwardMethodSourceCode.builder(method);
-      forwardSourceCodeBuilder
-          .setReceiver(method.holder)
-          .setTargetReceiver(type.isStatic() ? null : method.holder)
-          .setTarget(invocationTarget)
-          .setInvokeType(type)
-          .setIsInterface(isInterface);
-      return forwardSourceCodeBuilder::build;
-    }
-
-    @Override
-    public Consumer<UseRegistry> getRegistryCallback(DexClassAndMethod method) {
-      return registry -> {
-        assert registry.getTraversalContinuation().shouldContinue();
-        switch (type) {
-          case DIRECT:
-            registry.registerInvokeDirect(invocationTarget);
-            break;
-          case STATIC:
-            registry.registerInvokeStatic(invocationTarget);
-            break;
-          case VIRTUAL:
-            registry.registerInvokeVirtual(invocationTarget);
-            break;
-          default:
-            throw new Unreachable("Unexpected invocation type: " + type);
-        }
-      };
-    }
-  }
-
-  public Collection<DexType> getRemovedClasses() {
-    return Collections.unmodifiableCollection(mergedClasses.keySet());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
index da83f1e..068a1a7 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -178,7 +178,7 @@
     EnclosingMethodAttribute enclosingMembers = null;
     List<InnerClassAttribute> innerClasses = Collections.emptyList();
     for (SyntheticMethodBuilder builder : methods) {
-      DexEncodedMethod method = builder.build();
+      DexEncodedMethod method = builder.build(getClassKind());
       if (method.isNonPrivateVirtualMethod()) {
         virtualMethods.add(method);
       } else {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 781be07..41c19f5 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -1002,7 +1002,7 @@
       builder.setName(methodReference.getName());
       builder.setProto(methodReference.getProto());
       buildMethodCallback.accept(builder);
-      methodDefinition = builder.build();
+      methodDefinition = builder.build(clazz.getKind());
       methodCollection.addMethod(methodDefinition);
       newMethodCallback.accept((T) DexClassAndMethod.create(clazz, methodDefinition));
       return methodDefinition;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
index 64746c9..b758859 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -19,6 +20,7 @@
 import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.OptionalBool;
 
 public class SyntheticMethodBuilder {
 
@@ -40,6 +42,7 @@
   private ComputedApiLevel apiLevelForDefinition = ComputedApiLevel.notSet();
   private ComputedApiLevel apiLevelForCode = ComputedApiLevel.notSet();
   private MethodOptimizationInfo optimizationInfo = DefaultMethodOptimizationInfo.getInstance();
+  private OptionalBool isLibraryMethodOverride = OptionalBool.UNKNOWN;
 
   private boolean checkAndroidApiLevels = true;
 
@@ -55,8 +58,9 @@
     this.syntheticKind = syntheticKind;
   }
 
-  public boolean hasName() {
-    return name != null;
+  public SyntheticMethodBuilder setIsLibraryMethodOverride(OptionalBool isLibraryMethodOverride) {
+    this.isLibraryMethodOverride = isLibraryMethodOverride;
+    return this;
   }
 
   public SyntheticMethodBuilder setName(String name) {
@@ -127,7 +131,7 @@
     return this;
   }
 
-  DexEncodedMethod build() {
+  DexEncodedMethod build(ClassKind<?> classKind) {
     assert name != null;
     DexMethod methodSignature = getMethodSignature();
     MethodAccessFlags accessFlags = getAccessFlags();
@@ -145,6 +149,11 @@
             .setApiLevelForCode(apiLevelForCode)
             .setOptimizationInfo(optimizationInfo)
             .applyIf(!checkAndroidApiLevels, DexEncodedMethod.Builder::disableAndroidApiLevelCheck)
+            .applyIf(
+                classKind == ClassKind.PROGRAM
+                    && accessFlags.belongsToVirtualPool()
+                    && !isLibraryMethodOverride.isUnknown(),
+                builder -> builder.setIsLibraryMethodOverride(isLibraryMethodOverride))
             .build();
     assert !syntheticKind.isSingleSyntheticMethod()
         || isValidSingleSyntheticMethod(method, syntheticKind);
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 4707606..a1795a7 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -61,6 +61,10 @@
   public final SyntheticKind LAMBDA = generator.forInstanceClass("Lambda");
   public final SyntheticKind THREAD_LOCAL = generator.forInstanceClass("ThreadLocal");
 
+  // Merging not permitted since this could defeat the purpose of the synthetic class.
+  public final SyntheticKind SHARED_SUPER_CLASS =
+      generator.forNonSharableInstanceClass("SharedSuper");
+
   // TODO(b/214901256): Sharing of synthetic classes may lead to duplicate method errors.
   public final SyntheticKind NON_FIXED_INIT_TYPE_ARGUMENT =
       generator.forNonSharableInstanceClass("$IA");
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 7762a9c..27d7516 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -151,6 +151,15 @@
     return array;
   }
 
+  public static <T> boolean all(T[] elements, T elementToLookFor) {
+    for (Object element : elements) {
+      if (!Objects.equals(element, elementToLookFor)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public static <T> boolean contains(T[] elements, T elementToLookFor) {
     for (Object element : elements) {
       if (Objects.equals(element, elementToLookFor)) {
diff --git a/src/main/java/com/android/tools/r8/utils/ConsumerUtils.java b/src/main/java/com/android/tools/r8/utils/ConsumerUtils.java
index 4b20c06..47df9b4 100644
--- a/src/main/java/com/android/tools/r8/utils/ConsumerUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ConsumerUtils.java
@@ -47,6 +47,10 @@
     return (s, t) -> {};
   }
 
+  public static <S, T, U> TriConsumer<S, T, U> emptyTriConsumer() {
+    return (s, t, u) -> {};
+  }
+
   public static <T> ThrowingConsumer<T, RuntimeException> emptyThrowingConsumer() {
     return ignore -> {};
   }
diff --git a/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java b/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
index 368974b..da6b74e 100644
--- a/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
+++ b/src/main/java/com/android/tools/r8/utils/DumpInputFlags.java
@@ -46,6 +46,11 @@
       public boolean shouldFailCompilation() {
         throw new Unreachable();
       }
+
+      @Override
+      public boolean shouldLogDumpInfoMessage() {
+        throw new Unreachable();
+      }
     };
   }
 
@@ -85,6 +90,8 @@
 
   public abstract boolean shouldFailCompilation();
 
+  public abstract boolean shouldLogDumpInfoMessage();
+
   abstract static class DumpInputToFileOrDirectoryFlags extends DumpInputFlags {
 
     @Override
@@ -99,5 +106,10 @@
       }
       return true;
     }
+
+    @Override
+    public boolean shouldLogDumpInfoMessage() {
+      return true;
+    }
   }
 }
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 174afcc..de093c4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.GlobalSyntheticsConsumer;
 import com.android.tools.r8.MapIdProvider;
 import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.ResourceShrinkerConfiguration;
 import com.android.tools.r8.SourceFileProvider;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.SyntheticInfoConsumer;
@@ -46,6 +47,7 @@
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+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.DexClasspathClass;
@@ -59,8 +61,8 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.Policy;
@@ -75,7 +77,6 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.nest.Nest;
 import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.lightir.IR2LirConverter;
 import com.android.tools.r8.lightir.LirCode;
@@ -83,6 +84,7 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
+import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemovalOptions;
@@ -97,6 +99,7 @@
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.repackaging.Repackaging.DefaultRepackagingConfiguration;
 import com.android.tools.r8.repackaging.Repackaging.RepackagingConfiguration;
+import com.android.tools.r8.repackaging.RepackagingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.GlobalKeepInfoConfiguration;
@@ -107,6 +110,8 @@
 import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.structural.Ordered;
+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;
@@ -195,6 +200,9 @@
   public CancelCompilationChecker cancelCompilationChecker = null;
   public AndroidResourceProvider androidResourceProvider = null;
   public AndroidResourceConsumer androidResourceConsumer = null;
+  public ResourceShrinkerConfiguration resourceShrinkerConfiguration =
+      ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
+  public ResourceAccessAnalysis resourceAccessAnalysis = null;
 
   public boolean checkIfCancelled() {
     if (cancelCompilationChecker == null) {
@@ -272,6 +280,10 @@
     itemFactory = proguardConfiguration.getDexItemFactory();
     enableTreeShaking = proguardConfiguration.isShrinking();
     enableMinification = proguardConfiguration.isObfuscating();
+    // TODO(b/244238384): Enable.
+    enableStringFormatOptimization =
+        System.getProperty("com.android.tools.r8.optimizeStringFormat") != null;
+
     if (!proguardConfiguration.isOptimizing()) {
       // TODO(b/171457102): Avoid the need for this.
       // -dontoptimize disables optimizations by flipping related flags.
@@ -316,13 +328,13 @@
     disableGlobalOptimizations();
     enableNameReflectionOptimization = false;
     enableStringConcatenationOptimization = false;
+    enableStringFormatOptimization = false;
   }
 
   public void disableGlobalOptimizations() {
     inlinerOptions.enableInlining = false;
     enableClassInlining = false;
     enableDevirtualization = false;
-    enableVerticalClassMerging = false;
     enableEnumUnboxing = false;
     outline.enabled = false;
     enableEnumValueOptimization = false;
@@ -331,6 +343,7 @@
     enableInitializedClassesAnalysis = false;
     callSiteOptimizationOptions.disableOptimization();
     horizontalClassMergerOptions.setRestrictToSynthetics();
+    verticalClassMergerOptions.disable();
   }
 
   // Configure options according to platform build assumptions.
@@ -386,7 +399,6 @@
   // Optimization-related flags. These should conform to -dontoptimize and disableAllOptimizations.
   public boolean enableFieldBitAccessAnalysis =
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
-  public boolean enableVerticalClassMerging = true;
   public boolean enableUnusedInterfaceRemoval = true;
   public boolean enableDevirtualization = true;
   public boolean enableEnumUnboxing = true;
@@ -400,6 +412,8 @@
   public boolean enableServiceLoaderRewriting = true;
   public boolean enableNameReflectionOptimization = true;
   public boolean enableStringConcatenationOptimization = true;
+  // Enabled only for R8 (not D8).
+  public boolean enableStringFormatOptimization;
   public boolean enableTreeShakingOfLibraryMethodOverrides = false;
   public boolean encodeChecksums = false;
   public BiPredicate<String, Long> dexClassChecksumFilter = (name, checksum) -> true;
@@ -474,7 +488,7 @@
   public boolean createSingletonsForStatelessLambdas =
       System.getProperty("com.android.tools.r8.createSingletonsForStatelessLambdas") != null;
 
-  // TODO(b/293591931): Remove this flag.
+  // TODO(b/293591931): Remove this flag when records are stable in Platform
   //  Flag to allow record annotations in DEX. See b/231930852 for context.
   private final boolean emitRecordAnnotationsInDex =
       System.getProperty("com.android.tools.r8.emitRecordAnnotationsInDex") != null;
@@ -482,6 +496,9 @@
   // Flag to allow nest annotations in DEX. See b/231930852 for context.
   public boolean emitNestAnnotationsInDex =
       System.getProperty("com.android.tools.r8.emitNestAnnotationsInDex") != null;
+  // Flag to allow force nest desugaring, even if natively supported on the chosen API level.
+  public boolean forceNestDesugaring =
+      System.getProperty("com.android.tools.r8.forceNestDesugaring") != null;
 
   // TODO(b/293591931): Remove this flag.
   // Flag to allow permitted subclasses annotations in DEX. See b/231930852 for context.
@@ -644,10 +661,12 @@
     if (featureSplitConfiguration != null) {
       for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
         ProgramConsumer programConsumer = featureSplit.getProgramConsumer();
-        programConsumer.finished(reporter);
-        DataResourceConsumer dataResourceConsumer = programConsumer.getDataResourceConsumer();
-        if (dataResourceConsumer != null) {
-          dataResourceConsumer.finished(reporter);
+        if (programConsumer != null) {
+          programConsumer.finished(reporter);
+          DataResourceConsumer dataResourceConsumer = programConsumer.getDataResourceConsumer();
+          if (dataResourceConsumer != null) {
+            dataResourceConsumer.finished(reporter);
+          }
         }
       }
     }
@@ -852,13 +871,15 @@
    * and check cast instructions needs to be collected.
    */
   public boolean isClassMergingExtensionRequired(Enqueuer.Mode mode) {
+    WholeProgramOptimizations wholeProgramOptimizations = WholeProgramOptimizations.ON;
     if (mode.isInitialTreeShaking()) {
-      return (horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.INITIAL)
-              && !horizontalClassMergerOptions.isRestrictedToSynthetics())
-          || enableVerticalClassMerging;
+      return horizontalClassMergerOptions.isEnabled(
+              HorizontalClassMerger.Mode.INITIAL, wholeProgramOptimizations)
+          && !horizontalClassMergerOptions.isRestrictedToSynthetics();
     }
     if (mode.isFinalTreeShaking()) {
-      return horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.FINAL)
+      return horizontalClassMergerOptions.isEnabled(
+              HorizontalClassMerger.Mode.FINAL, wholeProgramOptimizations)
           && !horizontalClassMergerOptions.isRestrictedToSynthetics();
     }
     assert false;
@@ -910,6 +931,8 @@
   private final InlinerOptions inlinerOptions = new InlinerOptions(this);
   private final HorizontalClassMergerOptions horizontalClassMergerOptions =
       new HorizontalClassMergerOptions();
+  private final VerticalClassMergerOptions verticalClassMergerOptions =
+      new VerticalClassMergerOptions(this);
   private final OpenClosedInterfacesOptions openClosedInterfacesOptions =
       new OpenClosedInterfacesOptions();
   private final ProtoShrinkingOptions protoShrinking = new ProtoShrinkingOptions();
@@ -959,6 +982,10 @@
     return horizontalClassMergerOptions;
   }
 
+  public VerticalClassMergerOptions getVerticalClassMergerOptions() {
+    return verticalClassMergerOptions;
+  }
+
   public ProtoShrinkingOptions protoShrinking() {
     return protoShrinking;
   }
@@ -1714,6 +1741,8 @@
         parseSystemPropertyForDevelopmentOrDefault(
             "com.android.tools.r8.inliningInstructionLimit", -1);
 
+    public boolean enableSimpleInliningInstructionLimitIncrement = true;
+
     public int[] multiCallerInliningInstructionLimits =
         new int[] {Integer.MAX_VALUE, 28, 16, 12, 10};
 
@@ -1746,7 +1775,7 @@
     }
 
     public static void setOnlyForceInlining(InternalOptions options) {
-      options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+      options.testing.validInliningReasons = ImmutableSet.of();
     }
 
     public int getSimpleInliningInstructionLimit() {
@@ -1839,12 +1868,18 @@
       return enableClassInitializerDeadlockDetection;
     }
 
-    public boolean isEnabled(HorizontalClassMerger.Mode mode) {
+    public boolean isEnabled(
+        HorizontalClassMerger.Mode mode, WholeProgramOptimizations wholeProgramOptimizations) {
       if (!enable || debug || intermediate) {
         return false;
       }
+      if (wholeProgramOptimizations.isOn()) {
+        if (!isOptimizing() || !isShrinking()) {
+          return false;
+        }
+      }
       if (mode.isInitial()) {
-        return enableInitial && inlinerOptions.enableInlining && isShrinking();
+        return enableInitial && inlinerOptions.enableInlining;
       }
       assert mode.isFinal();
       return true;
@@ -2142,6 +2177,7 @@
 
   public static class TestingOptions {
 
+    public boolean enableNumberUnboxer = false;
     public boolean roundtripThroughLir = false;
 
     public boolean canUseLir(AppView<?> appView) {
@@ -2306,13 +2342,19 @@
     public Function<AppView<AppInfoWithLiveness>, RepackagingConfiguration>
         repackagingConfigurationFactory = DefaultRepackagingConfiguration::new;
 
-    public BiConsumer<DexItemFactory, HorizontallyMergedClasses> horizontallyMergedClassesConsumer =
-        ConsumerUtils.emptyBiConsumer();
+    public TriConsumer<DexItemFactory, HorizontallyMergedClasses, HorizontalClassMerger.Mode>
+        horizontallyMergedClassesConsumer = ConsumerUtils.emptyTriConsumer();
     public Function<List<Policy>, List<Policy>> horizontalClassMergingPolicyRewriter =
         Function.identity();
     public TriFunction<AppView<?>, Iterable<DexProgramClass>, DexProgramClass, DexProgramClass>
         horizontalClassMergingTarget = (appView, candidates, target) -> target;
 
+    public BiConsumer<DexItemFactory, NamingLens> namingLensConsumer =
+        ConsumerUtils.emptyBiConsumer();
+
+    public BiConsumer<DexItemFactory, RepackagingLens> repackagingLensConsumer =
+        ConsumerUtils.emptyBiConsumer();
+
     public BiConsumer<DexItemFactory, EnumDataMap> unboxedEnumsConsumer =
         ConsumerUtils.emptyBiConsumer();
 
@@ -2351,6 +2393,7 @@
     public boolean allowUnusedDontWarnRules = true;
     public boolean alwaysUseExistingAccessInfoCollectionsInMemberRebinding = true;
     public boolean alwaysUsePessimisticRegisterAllocation = false;
+    public boolean enableBridgeHoistingToSharedSyntheticSuperclass = false;
     public boolean enableCheckCastAndInstanceOfRemoval = true;
     public boolean enableDeadSwitchCaseElimination = true;
     public boolean enableInvokeSuperToInvokeVirtualRewriting = true;
@@ -2360,6 +2403,7 @@
     public boolean enableEnumUnboxingDebugLogs =
         System.getProperty("com.android.tools.r8.enableEnumUnboxingDebugLogs") != null;
     public boolean enableEnumWithSubtypesUnboxing = true;
+    public boolean enableVerticalClassMergerLensAssertion = false;
     public boolean forceRedundantConstNumberRemoval = false;
     public boolean enableExperimentalDesugaredLibraryKeepRuleGenerator = false;
     public boolean invertConditionals = false;
@@ -2404,6 +2448,9 @@
         System.getProperty("com.android.tools.r8.disableMarkingClassesFinal") != null;
     public boolean testEnableTestAssertions = false;
     public boolean keepMetadataInR8IfNotRewritten = true;
+    public boolean enableComposableOptimizationPass =
+        SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault(
+            "com.android.tools.r8.enableComposableOptimizationPass", false);
     public boolean modelUnknownChangedAndDefaultArgumentsToComposableFunctions =
         SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault(
             "com.android.tools.r8.modelUnknownChangedAndDefaultArgumentsToComposableFunctions",
@@ -2640,11 +2687,11 @@
   }
 
   public boolean canUseNestBasedAccess() {
-    return hasFeaturePresentFrom(null) || emitNestAnnotationsInDex;
+    return (hasFeaturePresentFrom(null) || emitNestAnnotationsInDex) && !forceNestDesugaring;
   }
 
   public boolean canUseRecords() {
-    return hasFeaturePresentFrom(AndroidApiLevel.U) || emitRecordAnnotationsInDex;
+    return hasFeaturePresentFrom(null) || emitRecordAnnotationsInDex;
   }
 
   public boolean canUseSealedClasses() {
diff --git a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
index 506d1e3b..6730a23 100644
--- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -88,13 +88,24 @@
     return null;
   }
 
+  /**
+   * Returns the previous element or null if !hasPrevious(). A subsequent call to iterator.remove()
+   * will remove the peeked element.
+   */
   public static <T> T peekPrevious(ListIterator<T> iterator) {
-    T previous = iterator.previous();
-    T next = iterator.next();
-    assert previous == next;
-    return previous;
+    if (iterator.hasPrevious()) {
+      T previous = iterator.previous();
+      T next = iterator.next();
+      assert previous == next;
+      return previous;
+    }
+    return null;
   }
 
+  /**
+   * Returns the next element or null if !hasNext(). A subsequent call to iterator.remove() will
+   * remove the peeked element.
+   */
   public static <T> T peekNext(ListIterator<T> iterator) {
     if (iterator.hasNext()) {
       T next = iterator.next();
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 d09475d..4be2a4e 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -8,6 +8,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.List;
@@ -354,4 +355,8 @@
     ts.addAll(other);
     return ts;
   }
+
+  public static <T> List<T> unmodifiableForTesting(List<T> list) {
+    return InternalOptions.assertionsEnabled() ? Collections.unmodifiableList(list) : list;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ObjectUtils.java b/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
index 9dfd1e0..1ceaf95 100644
--- a/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
@@ -21,6 +21,10 @@
     return a == b;
   }
 
+  public static boolean notIdentical(Object a, Object b) {
+    return !identical(a, b);
+  }
+
   public static <S, T> T mapNotNull(S object, Function<? super S, ? extends T> fn) {
     if (object != null) {
       return fn.apply(object);
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 60b2e4e..dcb72ad 100644
--- a/src/main/java/com/android/tools/r8/utils/ValueUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
@@ -6,12 +6,29 @@
 
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.ArrayPut;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
+import com.android.tools.r8.ir.code.NewArrayFilled;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Value;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
 
 public class ValueUtils {
+  // We allocate an array of this size, so guard against it getting too big.
+  private static int MAX_ARRAY_SIZE = 100000;
+  private static boolean DEBUG =
+      System.getProperty("com.android.tools.r8.debug.computeSingleUseArrayValues") != null;
 
   @SuppressWarnings("ReferenceEquality")
   public static boolean isStringBuilder(Value value, DexItemFactory dexItemFactory) {
@@ -45,4 +62,306 @@
       return false;
     }
   }
+
+  public static final class ArrayValues {
+    private List<Value> elementValues;
+    private ArrayPut[] arrayPutsByIndex;
+
+    private ArrayValues(List<Value> elementValues) {
+      this.elementValues = elementValues;
+    }
+
+    private ArrayValues(ArrayPut[] arrayPutsByIndex) {
+      this.arrayPutsByIndex = arrayPutsByIndex;
+    }
+
+    /** May contain null entries when array has null entries. */
+    public List<Value> getElementValues() {
+      if (elementValues == null) {
+        ArrayPut[] puts = arrayPutsByIndex;
+        Value[] elementValuesArr = new Value[puts.length];
+        for (int i = 0; i < puts.length; ++i) {
+          ArrayPut arrayPut = puts[i];
+          elementValuesArr[i] = arrayPut == null ? null : arrayPut.value();
+        }
+        elementValues = Arrays.asList(elementValuesArr);
+      }
+      return elementValues;
+    }
+
+    public int size() {
+      return elementValues != null ? elementValues.size() : arrayPutsByIndex.length;
+    }
+  }
+
+  private static BasicBlock findNextUnseen(
+      int startIdx, List<BasicBlock> predecessors, Set<BasicBlock> seen) {
+    int size = predecessors.size();
+    for (int i = startIdx; i < size; ++i) {
+      BasicBlock next = predecessors.get(i);
+      if (!seen.contains(next)) {
+        return next;
+      }
+    }
+    return null;
+  }
+
+  private static void debugLog(IRCode code, String message) {
+    System.err.println(message + " method=" + code.context().getReference());
+  }
+
+  /**
+   * Returns all dominator blocks of destBlock between (and including) it and sourceBlock. Returns
+   * null if sourceBlock is not a dominator of destBlock. Returns null if the algorithm is taking
+   * too long.
+   */
+  private static Set<BasicBlock> computeSimpleCaseDominatorBlocks(
+      BasicBlock sourceBlock, BasicBlock destBlock, IRCode code) {
+    // Fast-path: blocks are the same.
+    // As of Nov 2023 in Chrome for String.format() optimization, this is hit 77% of the time .
+    if (destBlock == sourceBlock) {
+      if (DEBUG) {
+        debugLog(code, "computeSimpleCaseDominatorBlocks: SAME BLOCK");
+      }
+      return Collections.singleton(sourceBlock);
+    }
+
+    // Fast-path: Linear path from source -> dest.
+    // As of Nov 2023 in Chrome for String.format() optimization, this is hit 14% of the time .
+    BasicBlock curBlock = destBlock;
+    List<BasicBlock> curPredecessors;
+    Set<BasicBlock> ret = Sets.newIdentityHashSet();
+    while (true) {
+      curPredecessors = curBlock.getPredecessors();
+      ret.add(curBlock);
+      if (curBlock == sourceBlock) {
+        if (DEBUG) {
+          debugLog(code, "computeSimpleCaseDominatorBlocks: LINEAR PATH");
+        }
+        return ret;
+      }
+
+      BasicBlock nextBlock = findNextUnseen(0, curPredecessors, ret);
+      if (nextBlock == null) {
+        debugLog(code, "computeSimpleCaseDominatorBlocks: Not a dominator.");
+        return null;
+      }
+      if (findNextUnseen(curPredecessors.indexOf(nextBlock) + 1, curPredecessors, ret) != null) {
+        // Multiple predecessors.
+        break;
+      }
+
+      curBlock = nextBlock;
+    }
+
+    // Algorithm: Traverse all predecessor paths between curBlock and sourceBlock, tracking the
+    // number of times each block is visited.
+    // Returns blocks with a "visit count" equal to the number of paths.
+    // This algorithm has worst case exponential complexity (for a fully connected graph).
+    // Rather than falling back to a DominatorTree for complex cases, this currently just returns
+    // an empty set when the number of paths is not small. Thus, it should not be used when false
+    // negatives are not acceptable.
+    //
+    // As of Nov 2023 in Chrome for String.format() optimization, the totalPathCounts were:
+    // 2 * 12, 3 * 6, 4 * 2, 5 * 1, 12 * 1, 22 * 1, 24 * 1, 35 * 4,
+    // MAX_PATH_COUNT * 2 (even with MAX_PATH_COUNT=3200)
+    final int MAX_PATH_COUNT = 36;
+
+    // TODO(agrieve): Lower MAX_PATH_COUNT and use DominatorTree for complicated cases. Or...
+    //     Algorithm vNext:
+    //     Track the fraction of paths that reach each node. Doing so would be ~linear for graphs
+    //     without cycles. E.g.:
+    //       DestNodeValue=1
+    //       NodeValue=SUM(block.NodeValue / block.numSuccessors for block in successors)
+    //       Dominators=(b for b in blocks if b.NodeValue == 1)
+    //     To choose which block to visit next, choose any where all predecessors have already been
+    //     visited. If no block has all predecessors visited, then an unvisited block is from a
+    //     cycle (they cannot be from outside of the sourceBlock->destBlock subgraph so long as
+    //     sourceBlock dominates destBlock).
+    //     To deal with cycles (clearly not linear time now):
+    //     * Find all blocks that participate in a cycle by doing a full traversal starting from
+    //       each candidate until one is found. Store the set of edges that do not reach sourceBlock
+    //       (except through the cycle).
+    //     * When computing the value of a block whose successors participate in a cycle:
+    //       NodeValue=SUM(block.NodeValue / effectiveNumSuccessors for block in successors)
+    //       where effectiveNumSuccessors=SUM(1 for s in successors if (b->s) not in cycleOnlyEdges)
+
+    Multiset<BasicBlock> blockCounts = HashMultiset.create();
+    int totalPathCount = 0;
+
+    // Should never need to re-visit initial single-track nodes.
+    Set<BasicBlock> seen = Sets.newIdentityHashSet();
+    seen.addAll(ret);
+    ArrayDeque<BasicBlock> pathStack = new ArrayDeque<>();
+
+    pathStack.add(curBlock);
+    pathStack.add(curPredecessors.get(0));
+    while (true) {
+      curBlock = pathStack.getLast();
+      curPredecessors = curBlock.getPredecessors();
+      if (curBlock == sourceBlock) {
+        // Add every block for every connected path.
+        blockCounts.addAll(pathStack);
+        totalPathCount += 1;
+        if (totalPathCount > MAX_PATH_COUNT) {
+          if (DEBUG) {
+            debugLog(code, "computeSimpleCaseDominatorBlocks: Reached MAX_PATH_COUNT");
+          }
+          return null;
+        }
+      } else if (!seen.contains(curBlock)) {
+        if (curPredecessors.isEmpty()) {
+          // Finding the entry block means the sourceBlock is not a dominator.
+          if (DEBUG) {
+            debugLog(code, "computeSimpleCaseDominatorBlocks: sourceBlock not a dominator");
+          }
+          return null;
+        }
+        // Going deeper.
+        BasicBlock nextBlock = findNextUnseen(0, curPredecessors, seen);
+        if (nextBlock != null) {
+          seen.add(curBlock);
+          pathStack.add(nextBlock);
+          continue;
+        }
+      } else {
+        seen.remove(curBlock);
+      }
+      // Popping.
+      pathStack.removeLast();
+      List<BasicBlock> prevPredecessors = pathStack.getLast().getPredecessors();
+      int nextBlockIdx = prevPredecessors.indexOf(curBlock) + 1;
+      BasicBlock nextBlock = findNextUnseen(nextBlockIdx, prevPredecessors, seen);
+      if (nextBlock != null) {
+        pathStack.add(nextBlock);
+      } else if (pathStack.size() == 1) {
+        break;
+      }
+    }
+
+    for (var entry : blockCounts.entrySet()) {
+      if (entry.getCount() == totalPathCount) {
+        ret.add(entry.getElement());
+      }
+    }
+    if (DEBUG) {
+      debugLog(code, "computeSimpleCaseDominatorBlocks: PATH COUNT " + totalPathCount);
+    }
+
+    return ret;
+  }
+
+  /**
+   * Attempts to determine all values for the given array. This will work only when:
+   *
+   * <pre>
+   * 1) The Array has a single users (other than array-puts)
+   *   * This constraint is to ensure other users do not modify the array.
+   *   * When users are in different blocks, their order is hard to know.
+   * 2) The array size is a constant.
+   * 3) All array-put instructions have constant and unique indices.
+   *   * Indices must be unique because order is hard to know when multiple blocks are concerned.
+   * 4) The array-put instructions are guaranteed to be executed before singleUser.
+   * </pre>
+   *
+   * @param arrayValue The Value for the array.
+   * @param singleUser The only non-array-put user, or null to auto-detect.
+   * @return The computed array values, or null if they could not be determined.
+   */
+  public static ArrayValues computeSingleUseArrayValues(
+      Value arrayValue, Instruction singleUser, IRCode code) {
+    assert singleUser == null || arrayValue.uniqueUsers().contains(singleUser);
+    TypeElement arrayType = arrayValue.getType();
+    if (!arrayType.isArrayType() || arrayValue.hasDebugUsers() || arrayValue.isPhi()) {
+      return null;
+    }
+
+    Instruction definition = arrayValue.definition;
+    NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
+    NewArrayFilled newArrayFilled = definition.asNewArrayFilled();
+    if (newArrayFilled != null) {
+      // It would be possible to have new-array-filled followed by aput-array, but that sequence of
+      // instructions does not commonly occur, so we don't support it here.
+      if (!arrayValue.hasSingleUniqueUser() || arrayValue.hasPhiUsers()) {
+        return null;
+      }
+      return new ArrayValues(newArrayFilled.inValues());
+    } else if (newArrayEmpty == null) {
+      return null;
+    }
+
+    int arraySize = newArrayEmpty.sizeIfConst();
+    if (arraySize < 0 || arraySize > MAX_ARRAY_SIZE) {
+      // Array is non-const size.
+      return null;
+    }
+
+    if (singleUser == null) {
+      for (Instruction user : arrayValue.uniqueUsers()) {
+        ArrayPut arrayPut = user.asArrayPut();
+        if (arrayPut == null || arrayPut.array() != arrayValue || arrayPut.value() == arrayValue) {
+          if (singleUser == null) {
+            singleUser = user;
+          } else {
+            return null;
+          }
+        }
+      }
+    }
+
+    // Ensure that all paths from new-array-empty to |usage| contain all array-put instructions.
+    Set<BasicBlock> dominatorBlocks =
+        computeSimpleCaseDominatorBlocks(definition.getBlock(), singleUser.getBlock(), code);
+    if (dominatorBlocks == null) {
+      return null;
+    }
+    BasicBlock usageBlock = singleUser.getBlock();
+
+    ArrayPut[] arrayPutsByIndex = new ArrayPut[arraySize];
+    for (Instruction user : arrayValue.uniqueUsers()) {
+      ArrayPut arrayPut = user.asArrayPut();
+      if (arrayPut == null || arrayPut.array() != arrayValue || arrayPut.value() == arrayValue) {
+        if (user == singleUser) {
+          continue;
+        }
+        // Found a second non-array-put user.
+        return null;
+      }
+      int index = arrayPut.indexIfConstAndInBounds(arraySize);
+      if (index < 0) {
+        return null;
+      }
+      if (arrayPut.getBlock() == usageBlock) {
+        // Process these later.
+        continue;
+      } else if (!dominatorBlocks.contains(arrayPut.getBlock())) {
+        return null;
+      }
+      // We do not know what order blocks are in, so do not allow re-assignment.
+      if (arrayPutsByIndex[index] != null) {
+        return null;
+      }
+      arrayPutsByIndex[index] = arrayPut;
+    }
+    boolean seenSingleUser = false;
+    for (Instruction inst : usageBlock.getInstructions()) {
+      if (inst == singleUser) {
+        seenSingleUser = true;
+        continue;
+      }
+      ArrayPut arrayPut = inst.asArrayPut();
+      if (arrayPut == null || arrayPut.array() != arrayValue) {
+        continue;
+      }
+      if (seenSingleUser) {
+        // Found an array-put after the array was used. This is too uncommon of a thing to support.
+        return null;
+      }
+      int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
+      // We can allow reassignment at this point since we are visiting in order.
+      arrayPutsByIndex[index] = arrayPut;
+    }
+
+    return new ArrayValues(arrayPutsByIndex);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index 5aa700a..cdfb34c 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -6,7 +6,6 @@
 
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
-import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.Set;
@@ -166,7 +165,7 @@
   }
 
   public Set<T> getSeenSet() {
-    return Collections.unmodifiableSet(seen);
+    return SetUtils.unmodifiableForTesting(seen);
   }
 
   public Set<T> getMutableSeenSet() {
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 bbc27a2..eb747e4 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
@@ -12,6 +12,7 @@
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -47,6 +48,10 @@
     backing.forEach((wrapper, value) -> consumer.accept(wrapper.get(), value));
   }
 
+  public void forEachValue(Consumer<V> consumer) {
+    backing.values().forEach(consumer);
+  }
+
   public V get(K member) {
     return backing.get(wrap(member));
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
new file mode 100644
index 0000000..44b2d2c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -0,0 +1,861 @@
+// 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 static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.ir.code.InvokeType.DIRECT;
+import static com.android.tools.r8.ir.code.InvokeType.STATIC;
+import static com.android.tools.r8.ir.code.InvokeType.VIRTUAL;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AccessControl;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultInstanceInitializerCode;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature.ClassSignatureBuilder;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext;
+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.graph.lens.MethodLookupResult;
+import com.android.tools.r8.ir.code.InvokeType;
+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.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+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;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+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 {
+
+  private enum Rename {
+    ALWAYS,
+    IF_NEEDED,
+    NEVER
+  }
+
+  private static final OptimizationFeedbackSimple feedback =
+      OptimizationFeedback.getSimpleFeedback();
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final VerticalClassMergerGraphLens.Builder deferredRenamings;
+  private final DexItemFactory dexItemFactory;
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
+
+  private final DexProgramClass source;
+  private final DexProgramClass target;
+
+  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
+
+  private boolean abortMerge = false;
+
+  ClassMerger(
+      AppView<AppInfoWithLiveness> appView,
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
+      DexProgramClass source,
+      DexProgramClass target) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.appView = appView;
+    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
+    this.dexItemFactory = dexItemFactory;
+    this.lensBuilder = lensBuilder;
+    this.mergedClasses = mergedClasses;
+    this.source = source;
+    this.target = target;
+  }
+
+  public boolean merge() throws ExecutionException {
+    // Merge the class [clazz] into [targetClass] by adding all methods to
+    // targetClass that are not currently contained.
+    // Step 1: Merge methods
+    Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
+    addAll(existingMethods, target.methods(), MethodSignatureEquivalence.get());
+
+    Map<Wrapper<DexMethod>, DexEncodedMethod> directMethods = new HashMap<>();
+    Map<Wrapper<DexMethod>, DexEncodedMethod> virtualMethods = new HashMap<>();
+
+    Predicate<DexMethod> availableMethodSignatures =
+        (method) -> {
+          Wrapper<DexMethod> wrapped = MethodSignatureEquivalence.get().wrap(method);
+          return !existingMethods.contains(wrapped)
+              && !directMethods.containsKey(wrapped)
+              && !virtualMethods.containsKey(wrapped);
+        };
+
+    source.forEachProgramDirectMethod(
+        directMethod -> {
+          DexEncodedMethod definition = directMethod.getDefinition();
+          if (definition.isInstanceInitializer()) {
+            DexEncodedMethod resultingConstructor =
+                renameConstructor(
+                    definition,
+                    candidate ->
+                        availableMethodSignatures.test(candidate)
+                            && source.lookupVirtualMethod(candidate) == null);
+            add(directMethods, resultingConstructor, MethodSignatureEquivalence.get());
+            blockRedirectionOfSuperCalls(resultingConstructor.getReference());
+          } else {
+            DexEncodedMethod resultingDirectMethod =
+                renameMethod(
+                    definition,
+                    availableMethodSignatures,
+                    definition.isClassInitializer() ? Rename.NEVER : Rename.IF_NEEDED);
+            add(directMethods, resultingDirectMethod, MethodSignatureEquivalence.get());
+            deferredRenamings.map(
+                directMethod.getReference(), resultingDirectMethod.getReference());
+            deferredRenamings.recordMove(
+                directMethod.getReference(), resultingDirectMethod.getReference());
+            blockRedirectionOfSuperCalls(resultingDirectMethod.getReference());
+
+            // Private methods in the parent class may be targeted with invoke-super if the two
+            // classes are in the same nest. Ensure such calls are mapped to invoke-direct.
+            if (definition.isInstance()
+                && definition.isPrivate()
+                && AccessControl.isMemberAccessible(directMethod, source, target, appView)
+                    .isTrue()) {
+              deferredRenamings.mapVirtualMethodToDirectInType(
+                  directMethod.getReference(),
+                  prototypeChanges ->
+                      new MethodLookupResult(
+                          resultingDirectMethod.getReference(), null, DIRECT, prototypeChanges),
+                  target.getType());
+            }
+          }
+        });
+
+    for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+      DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
+      if (shadowedBy != null) {
+        if (virtualMethod.isAbstract()) {
+          // 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
+              .map(virtualMethod.getReference(), shadowedBy.getReference())
+              .map(shadowedBy.getReference(), shadowedBy.getReference())
+              .recordMerge(virtualMethod.getReference(), shadowedBy.getReference());
+
+          // The override now corresponds to the method in the parent, so unset its synthetic flag
+          // if the method in the parent is not synthetic.
+          if (!virtualMethod.isSyntheticMethod() && shadowedBy.isSyntheticMethod()) {
+            shadowedBy.accessFlags.demoteFromSynthetic();
+          }
+          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;
+          }
+          // Update the holder of [virtualMethod] using renameMethod().
+          DexEncodedMethod resultingVirtualMethod =
+              renameMethod(virtualMethod, availableMethodSignatures, Rename.NEVER);
+          resultingVirtualMethod.setLibraryMethodOverride(virtualMethod.isLibraryMethodOverride());
+          deferredRenamings.map(
+              virtualMethod.getReference(), resultingVirtualMethod.getReference());
+          deferredRenamings.recordMove(
+              virtualMethod.getReference(), resultingVirtualMethod.getReference());
+          add(virtualMethods, resultingVirtualMethod, MethodSignatureEquivalence.get());
+          continue;
+        }
+      }
+
+      DexEncodedMethod resultingMethod;
+      if (source.accessFlags.isInterface()) {
+        // Moving a default interface method into its subtype. This method could be hit directly
+        // via an invoke-super instruction from any of the transitive subtypes of this interface,
+        // due to the way invoke-super works on default interface methods. In order to be able
+        // to hit this method directly after the merge, we need to make it public, and find a
+        // method name that does not collide with one in the hierarchy of this class.
+        String resultingMethodBaseName =
+            virtualMethod.getName().toString() + '$' + source.getTypeName().replace('.', '$');
+        DexMethod resultingMethodReference =
+            dexItemFactory.createMethod(
+                target.getType(),
+                virtualMethod.getProto().prependParameter(source.getType(), dexItemFactory),
+                dexItemFactory.createGloballyFreshMemberString(resultingMethodBaseName));
+        assert availableMethodSignatures.test(resultingMethodReference);
+        resultingMethod =
+            virtualMethod.toTypeSubstitutedMethodAsInlining(
+                resultingMethodReference, dexItemFactory);
+        makeStatic(resultingMethod);
+      } else {
+        // This virtual method could be called directly from a sub class via an invoke-super in-
+        // struction. Therefore, we translate this virtual method into an instance method with a
+        // unique name, such that relevant invoke-super instructions can be rewritten to target
+        // this method directly.
+        resultingMethod = renameMethod(virtualMethod, availableMethodSignatures, Rename.ALWAYS);
+        if (appView.options().getProguardConfiguration().isAccessModificationAllowed()) {
+          makePublic(resultingMethod);
+        } else {
+          makePrivate(resultingMethod);
+        }
+      }
+
+      add(
+          resultingMethod.belongsToDirectPool() ? directMethods : virtualMethods,
+          resultingMethod,
+          MethodSignatureEquivalence.get());
+
+      // Record that invoke-super instructions in the target class should be redirected to the
+      // newly created direct method.
+      redirectSuperCallsInTarget(virtualMethod, resultingMethod);
+      blockRedirectionOfSuperCalls(resultingMethod.getReference());
+
+      if (shadowedBy == null) {
+        // In addition to the newly added direct method, create a virtual method such that we do
+        // not accidentally remove the method from the interface of this class.
+        // Note that this method is added independently of whether it will actually be used. If
+        // it turns out that the method is never used, it will be removed by the final round
+        // of tree shaking.
+        shadowedBy = buildBridgeMethod(virtualMethod, resultingMethod);
+        deferredRenamings.recordCreationOfBridgeMethod(
+            virtualMethod.getReference(), shadowedBy.getReference());
+        add(virtualMethods, shadowedBy, MethodSignatureEquivalence.get());
+      }
+
+      // Copy over any keep info from the original virtual method.
+      ProgramMethod programMethod = new ProgramMethod(target, shadowedBy);
+      appView
+          .getKeepInfo()
+          .mutate(
+              mutableKeepInfoCollection ->
+                  mutableKeepInfoCollection.joinMethod(
+                      programMethod,
+                      info ->
+                          info.merge(
+                              mutableKeepInfoCollection
+                                  .getMethodInfo(virtualMethod, source)
+                                  .joiner())));
+
+      deferredRenamings.map(virtualMethod.getReference(), shadowedBy.getReference());
+      deferredRenamings.recordMove(
+          virtualMethod.getReference(), resultingMethod.getReference(), resultingMethod.isStatic());
+    }
+
+    if (abortMerge) {
+      assert restoreDebuggingState(
+          Streams.concat(directMethods.values().stream(), virtualMethods.values().stream()));
+      return false;
+    }
+
+    // Rewrite generic signatures before we merge a base with a generic signature.
+    rewriteGenericSignatures(target, source, directMethods.values(), virtualMethods.values());
+
+    // Convert out of DefaultInstanceInitializerCode, since this piece of code will require lens
+    // code rewriting.
+    target.forEachProgramInstanceInitializerMatching(
+        method -> method.getCode().isDefaultInstanceInitializerCode(),
+        method -> DefaultInstanceInitializerCode.uncanonicalizeCode(appView, method));
+
+    // Step 2: Merge fields
+    Set<DexString> existingFieldNames = new HashSet<>();
+    for (DexEncodedField field : target.fields()) {
+      existingFieldNames.add(field.getReference().name);
+    }
+
+    // In principle, we could allow multiple fields with the same name, and then only rename the
+    // field in the end when we are done merging all the classes, if it it turns out that the two
+    // fields ended up having the same type. This would not be too expensive, since we visit the
+    // entire program using VerticalClassMerger.TreeFixer anyway.
+    //
+    // For now, we conservatively report that a signature is already taken if there is a field
+    // with the same name. If minification is used with -overloadaggressively, this is solved
+    // later anyway.
+    Predicate<DexField> availableFieldSignatures =
+        field -> !existingFieldNames.contains(field.name);
+
+    DexEncodedField[] mergedInstanceFields =
+        mergeFields(
+            source.instanceFields(),
+            target.instanceFields(),
+            availableFieldSignatures,
+            existingFieldNames);
+
+    DexEncodedField[] mergedStaticFields =
+        mergeFields(
+            source.staticFields(),
+            target.staticFields(),
+            availableFieldSignatures,
+            existingFieldNames);
+
+    // Step 3: Merge interfaces
+    Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
+    // Now destructively update the class.
+    // Step 1: Update supertype or fix interfaces.
+    if (source.isInterface()) {
+      interfaces.remove(source.type);
+    } else {
+      assert !target.isInterface();
+      target.superType = source.superType;
+    }
+    target.interfaces =
+        interfaces.isEmpty()
+            ? DexTypeList.empty()
+            : new DexTypeList(interfaces.toArray(DexType.EMPTY_ARRAY));
+    // Step 2: ensure -if rules cannot target the members that were merged into the target class.
+    directMethods.values().forEach(feedback::markMethodCannotBeKept);
+    virtualMethods.values().forEach(feedback::markMethodCannotBeKept);
+    for (int i = 0; i < source.instanceFields().size(); i++) {
+      feedback.markFieldCannotBeKept(mergedInstanceFields[i]);
+    }
+    for (int i = 0; i < source.staticFields().size(); i++) {
+      feedback.markFieldCannotBeKept(mergedStaticFields[i]);
+    }
+    // Step 3: replace fields and methods.
+    target.addDirectMethods(directMethods.values());
+    target.addVirtualMethods(virtualMethods.values());
+    target.setInstanceFields(mergedInstanceFields);
+    target.setStaticFields(mergedStaticFields);
+    // Step 4: Clear the members of the source class since they have now been moved to the target.
+    source.getMethodCollection().clearDirectMethods();
+    source.getMethodCollection().clearVirtualMethods();
+    source.clearInstanceFields();
+    source.clearStaticFields();
+    // Step 5: Record merging.
+    assert !abortMerge;
+    assert GenericSignatureCorrectnessHelper.createForVerification(
+            appView, GenericSignatureContextBuilder.createForSingleClass(appView, target))
+        .evaluateSignaturesForClass(target)
+        .isValid();
+    return true;
+  }
+
+  /**
+   * The rewriting of generic signatures is pretty simple, but require some bookkeeping. We take the
+   * arguments to the base type:
+   *
+   * <pre>
+   *   class Sub<X> extends Base<X, String>
+   * </pre>
+   *
+   * <p>for
+   *
+   * <pre>
+   *   class Base<T,R> extends OtherBase<T> implements I<R> {
+   *     T t() { ... };
+   *   }
+   * </pre>
+   *
+   * <p>and substitute T -> X and R -> String
+   */
+  private void rewriteGenericSignatures(
+      DexProgramClass target,
+      DexProgramClass source,
+      Collection<DexEncodedMethod> directMethods,
+      Collection<DexEncodedMethod> virtualMethods) {
+    ClassSignature targetSignature = target.getClassSignature();
+    if (targetSignature.hasNoSignature()) {
+      // Null out all source signatures that is moved, but do not clear out the class since this
+      // could be referred to by other generic signatures.
+      // TODO(b/147504070): If merging classes with enclosing/innerclasses, this needs to be
+      //  reconsidered.
+      directMethods.forEach(DexEncodedMethod::clearGenericSignature);
+      virtualMethods.forEach(DexEncodedMethod::clearGenericSignature);
+      source.fields().forEach(DexEncodedMember::clearGenericSignature);
+      return;
+    }
+    GenericSignaturePartialTypeArgumentApplier classApplier =
+        getGenericSignatureArgumentApplier(target, source);
+    if (classApplier == null) {
+      target.clearClassSignature();
+      target.members().forEach(DexEncodedMember::clearGenericSignature);
+      return;
+    }
+    // We could generate a substitution map.
+    ClassSignature rewrittenSource = classApplier.visitClassSignature(source.getClassSignature());
+    // The variables in the class signature is now rewritten to use the targets argument.
+    ClassSignatureBuilder builder = ClassSignature.builder();
+    builder.addFormalTypeParameters(targetSignature.getFormalTypeParameters());
+    if (!source.isInterface()) {
+      if (rewrittenSource.hasSignature()) {
+        builder.setSuperClassSignature(rewrittenSource.getSuperClassSignatureOrNull());
+      } else {
+        builder.setSuperClassSignature(new ClassTypeSignature(source.superType));
+      }
+    } else {
+      builder.setSuperClassSignature(targetSignature.getSuperClassSignatureOrNull());
+    }
+    // Compute the seen set for interfaces to add. This is similar to the merging of interfaces
+    // but allow us to maintain the type arguments.
+    Set<DexType> seenInterfaces = new HashSet<>();
+    if (source.isInterface()) {
+      seenInterfaces.add(source.type);
+    }
+    for (ClassTypeSignature iFace : targetSignature.getSuperInterfaceSignatures()) {
+      if (seenInterfaces.add(iFace.type())) {
+        builder.addSuperInterfaceSignature(iFace);
+      }
+    }
+    if (rewrittenSource.hasSignature()) {
+      for (ClassTypeSignature iFace : rewrittenSource.getSuperInterfaceSignatures()) {
+        if (!seenInterfaces.contains(iFace.type())) {
+          builder.addSuperInterfaceSignature(iFace);
+        }
+      }
+    } else {
+      // Synthesize raw uses of interfaces to align with the actual class
+      for (DexType iFace : source.interfaces) {
+        if (!seenInterfaces.contains(iFace)) {
+          builder.addSuperInterfaceSignature(new ClassTypeSignature(iFace));
+        }
+      }
+    }
+    target.setClassSignature(builder.build(dexItemFactory));
+
+    // Go through all type-variable references for members and update them.
+    CollectionUtils.forEach(
+        method -> {
+          MethodTypeSignature methodSignature = method.getGenericSignature();
+          if (methodSignature.hasNoSignature()) {
+            return;
+          }
+          method.setGenericSignature(
+              classApplier
+                  .buildForMethod(methodSignature.getFormalTypeParameters())
+                  .visitMethodSignature(methodSignature));
+        },
+        directMethods,
+        virtualMethods);
+
+    source.forEachField(
+        field -> {
+          if (field.getGenericSignature().hasNoSignature()) {
+            return;
+          }
+          field.setGenericSignature(
+              classApplier.visitFieldTypeSignature(field.getGenericSignature()));
+        });
+  }
+
+  private GenericSignaturePartialTypeArgumentApplier getGenericSignatureArgumentApplier(
+      DexProgramClass target, DexProgramClass source) {
+    assert target.getClassSignature().hasSignature();
+    // We can assert proper structure below because the generic signature validator has run
+    // before and pruned invalid signatures.
+    List<FieldTypeSignature> genericArgumentsToSuperType =
+        target.getClassSignature().getGenericArgumentsToSuperType(source.type, dexItemFactory);
+    if (genericArgumentsToSuperType == null) {
+      assert false : "Type should be present in generic signature";
+      return null;
+    }
+    Map<String, FieldTypeSignature> substitutionMap = new HashMap<>();
+    List<FormalTypeParameter> formals = source.getClassSignature().getFormalTypeParameters();
+    if (genericArgumentsToSuperType.size() != formals.size()) {
+      if (!genericArgumentsToSuperType.isEmpty()) {
+        assert false : "Invalid argument count to formals";
+        return null;
+      }
+    } else {
+      for (int i = 0; i < formals.size(); i++) {
+        // It is OK to override a generic type variable so we just use put.
+        substitutionMap.put(formals.get(i).getName(), genericArgumentsToSuperType.get(i));
+      }
+    }
+    return GenericSignaturePartialTypeArgumentApplier.build(
+        appView,
+        TypeParameterContext.empty().addPrunedSubstitutions(substitutionMap),
+        (type1, type2) -> true,
+        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, DexEncodedMethod newTarget) {
+    DexMethod oldTargetReference = oldTarget.getReference();
+    DexMethod newTargetReference = newTarget.getReference();
+    InvokeType newTargetType = newTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT;
+    if (source.accessFlags.isInterface()) {
+      // If we merge a default interface method from interface I to its subtype C, then we need
+      // to rewrite invocations on the form "invoke-super I.m()" to "invoke-direct C.m$I()".
+      //
+      // Unlike when we merge a class into its subclass (the else-branch below), we should *not*
+      // 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,
+          prototypeChanges ->
+              new MethodLookupResult(newTargetReference, null, STATIC, prototypeChanges),
+          target.type);
+    } 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
+      // method C.m$B denotes the direct/virtual method that has been created in C for B.m). In
+      // particular, there might be an instruction "invoke-super A.m()" in C that resolves to B.m
+      // at runtime (A is a superclass of B), which also needs to be rewritten to
+      // "invoke-{direct,virtual} C.m$B()".
+      //
+      // We handle this by adding a mapping for [target] and all of its supertypes.
+      DexProgramClass holder = target;
+      while (holder != null && holder.isProgramClass()) {
+        DexMethod signatureInHolder = oldTargetReference.withHolder(holder, dexItemFactory);
+        // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
+        boolean resolutionSucceeds =
+            holder.lookupVirtualMethod(signatureInHolder) != null
+                || appView.appInfo().lookupSuperTarget(signatureInHolder, holder, appView) != null;
+        if (resolutionSucceeds) {
+          deferredRenamings.mapVirtualMethodToDirectInType(
+              signatureInHolder,
+              prototypeChanges ->
+                  new MethodLookupResult(newTargetReference, null, newTargetType, prototypeChanges),
+              target.type);
+        } else {
+          break;
+        }
+
+        // Consider that A gets merged into B and B's subclass C gets merged into D. Instructions
+        // on the form "invoke-super {B,C,D}.m()" in D are changed into "invoke-direct D.m$C()" by
+        // the code above. However, instructions on the form "invoke-super A.m()" should also be
+        // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
+        // that have been merged into [holder].
+        Set<DexType> mergedTypes = mergedClasses.getKeys(holder.getType());
+        for (DexType type : mergedTypes) {
+          DexMethod signatureInType = oldTargetReference.withHolder(type, dexItemFactory);
+          // Resolution would have succeeded if the method used to be in [type], or if one of
+          // its super classes declared the method.
+          boolean resolutionSucceededBeforeMerge =
+              lensBuilder.hasMappingForSignatureInContext(holder, signatureInType)
+                  || appView.appInfo().lookupSuperTarget(signatureInHolder, holder, appView)
+                      != null;
+          if (resolutionSucceededBeforeMerge) {
+            deferredRenamings.mapVirtualMethodToDirectInType(
+                signatureInType,
+                prototypeChanges ->
+                    new MethodLookupResult(
+                        newTargetReference, null, newTargetType, prototypeChanges),
+                target.type);
+          }
+        }
+        holder =
+            holder.hasSuperType()
+                ? asProgramClassOrNull(appView.definitionFor(holder.getSuperType()))
+                : null;
+      }
+    }
+  }
+
+  private void blockRedirectionOfSuperCalls(DexMethod method) {
+    // We are merging a class B into C. The methods from B are being moved into C, and then we
+    // subsequently rewrite the invoke-super instructions in C that hit a method in B, such that
+    // they use an invoke-direct instruction instead. In this process, we need to avoid rewriting
+    // the invoke-super instructions that originally was in the superclass B.
+    //
+    // Example:
+    //   class A {
+    //     public void m() {}
+    //   }
+    //   class B extends A {
+    //     public void m() { super.m(); } <- invoke must not be rewritten to invoke-direct
+    //                                       (this would lead to an infinite loop)
+    //   }
+    //   class C extends B {
+    //     public void m() { super.m(); } <- invoke needs to be rewritten to invoke-direct
+    //   }
+    deferredRenamings.markMethodAsMerged(method);
+  }
+
+  private DexEncodedMethod buildBridgeMethod(
+      DexEncodedMethod method, DexEncodedMethod invocationTarget) {
+    DexMethod newMethod = method.getReference().withHolder(target, dexItemFactory);
+    MethodAccessFlags accessFlags = method.getAccessFlags().copy();
+    accessFlags.setBridge();
+    accessFlags.setSynthetic();
+    accessFlags.unsetAbstract();
+
+    assert invocationTarget.isStatic()
+        || invocationTarget.isNonPrivateVirtualMethod()
+        || invocationTarget.isNonStaticPrivateMethod();
+    SynthesizedBridgeCode code =
+        new SynthesizedBridgeCode(
+            newMethod,
+            invocationTarget.getReference(),
+            invocationTarget.isStatic()
+                ? STATIC
+                : (invocationTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT),
+            target.isInterface());
+
+    // Add the bridge to the list of synthesized bridges such that the method signatures will
+    // be updated by the end of vertical class merging.
+    synthesizedBridges.add(code);
+
+    CfVersion classFileVersion = method.hasClassFileVersion() ? method.getClassFileVersion() : null;
+    DexEncodedMethod bridge =
+        DexEncodedMethod.syntheticBuilder()
+            .setMethod(newMethod)
+            .setAccessFlags(accessFlags)
+            .setCode(code)
+            .setClassFileVersion(classFileVersion)
+            .setApiLevelForDefinition(method.getApiLevelForDefinition())
+            .setApiLevelForCode(method.getApiLevelForDefinition())
+            .setIsLibraryMethodOverride(method.isLibraryMethodOverride())
+            .setGenericSignature(method.getGenericSignature())
+            .build();
+    // The bridge is now the public method serving the role of the original method, and should
+    // reflect that this method was publicized.
+    assert !method.getAccessFlags().isPromotedToPublic()
+        || bridge.getAccessFlags().isPromotedToPublic();
+    return bridge;
+  }
+
+  // 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 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.
+    return null;
+  }
+
+  private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> void add(
+      Map<Wrapper<R>, D> map, D item, Equivalence<R> equivalence) {
+    map.put(equivalence.wrap(item.getReference()), item);
+  }
+
+  private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> void addAll(
+      Collection<Wrapper<R>> collection, Iterable<D> items, Equivalence<R> equivalence) {
+    for (D item : items) {
+      collection.add(equivalence.wrap(item.getReference()));
+    }
+  }
+
+  private <T> Set<T> mergeArrays(T[] one, T[] other) {
+    Set<T> merged = new LinkedHashSet<>();
+    Collections.addAll(merged, one);
+    Collections.addAll(merged, other);
+    return merged;
+  }
+
+  private DexEncodedField[] mergeFields(
+      Collection<DexEncodedField> sourceFields,
+      Collection<DexEncodedField> targetFields,
+      Predicate<DexField> availableFieldSignatures,
+      Set<DexString> existingFieldNames) {
+    DexEncodedField[] result = new DexEncodedField[sourceFields.size() + targetFields.size()];
+    // Add fields from source
+    int i = 0;
+    for (DexEncodedField field : sourceFields) {
+      DexEncodedField resultingField = renameFieldIfNeeded(field, availableFieldSignatures);
+      existingFieldNames.add(resultingField.getReference().name);
+      deferredRenamings.map(field.getReference(), resultingField.getReference());
+      result[i] = resultingField;
+      i++;
+    }
+    // Add fields from target.
+    for (DexEncodedField field : targetFields) {
+      result[i] = field;
+      i++;
+    }
+    return result;
+  }
+
+  // Note that names returned by this function are not necessarily unique. Clients should
+  // repeatedly try to generate a fresh name until it is unique.
+  private DexString getFreshName(String nameString, int index, DexType holder) {
+    String freshName = nameString + "$" + holder.toSourceString().replace('.', '$');
+    if (index > 1) {
+      freshName += index;
+    }
+    return dexItemFactory.createString(freshName);
+  }
+
+  private DexEncodedMethod renameConstructor(
+      DexEncodedMethod method, Predicate<DexMethod> availableMethodSignatures) {
+    assert method.isInstanceInitializer();
+    DexType oldHolder = method.getHolderType();
+
+    DexMethod newSignature;
+    int count = 1;
+    do {
+      DexString newName = getFreshName(TEMPORARY_INSTANCE_INITIALIZER_PREFIX, count, oldHolder);
+      newSignature = dexItemFactory.createMethod(target.getType(), method.getProto(), newName);
+      count++;
+    } while (!availableMethodSignatures.test(newSignature));
+
+    DexEncodedMethod result =
+        method.toTypeSubstitutedMethodAsInlining(newSignature, dexItemFactory);
+    result.getMutableOptimizationInfo().markForceInline();
+    deferredRenamings.map(method.getReference(), result.getReference());
+    deferredRenamings.recordMove(method.getReference(), result.getReference());
+    // 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();
+    makePrivate(result);
+    return result;
+  }
+
+  private DexEncodedMethod renameMethod(
+      DexEncodedMethod method, Predicate<DexMethod> availableMethodSignatures, Rename strategy) {
+    return renameMethod(method, availableMethodSignatures, strategy, method.getProto());
+  }
+
+  private DexEncodedMethod renameMethod(
+      DexEncodedMethod method,
+      Predicate<DexMethod> availableMethodSignatures,
+      Rename strategy,
+      DexProto newProto) {
+    // We cannot handle renaming static initializers yet and constructors should have been
+    // renamed already.
+    assert !method.accessFlags.isConstructor() || strategy == Rename.NEVER;
+    DexString oldName = method.getName();
+    DexType oldHolder = method.getHolderType();
+
+    DexMethod newSignature;
+    switch (strategy) {
+      case IF_NEEDED:
+        newSignature = dexItemFactory.createMethod(target.getType(), newProto, oldName);
+        if (availableMethodSignatures.test(newSignature)) {
+          break;
+        }
+        // Fall-through to ALWAYS so that we assign a new name.
+
+      case ALWAYS:
+        int count = 1;
+        do {
+          DexString newName = getFreshName(oldName.toSourceString(), count, oldHolder);
+          newSignature = dexItemFactory.createMethod(target.getType(), newProto, newName);
+          count++;
+        } while (!availableMethodSignatures.test(newSignature));
+        break;
+
+      case NEVER:
+        newSignature = dexItemFactory.createMethod(target.getType(), newProto, oldName);
+        assert availableMethodSignatures.test(newSignature);
+        break;
+
+      default:
+        throw new Unreachable();
+    }
+
+    return method.toTypeSubstitutedMethodAsInlining(newSignature, dexItemFactory);
+  }
+
+  private DexEncodedField renameFieldIfNeeded(
+      DexEncodedField field, Predicate<DexField> availableFieldSignatures) {
+    DexString oldName = field.getName();
+    DexType oldHolder = field.getHolderType();
+
+    DexField newSignature = dexItemFactory.createField(target.getType(), field.getType(), oldName);
+    if (!availableFieldSignatures.test(newSignature)) {
+      int count = 1;
+      do {
+        DexString newName = getFreshName(oldName.toSourceString(), count, oldHolder);
+        newSignature = dexItemFactory.createField(target.getType(), field.getType(), newName);
+        count++;
+      } while (!availableFieldSignatures.test(newSignature));
+    }
+
+    return field.toTypeSubstitutedField(appView, newSignature);
+  }
+
+  private static void makePrivate(DexEncodedMethod method) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    assert !accessFlags.isAbstract();
+    accessFlags.unsetPublic();
+    accessFlags.unsetProtected();
+    accessFlags.setPrivate();
+  }
+
+  private static void makePublic(DexEncodedMethod method) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    assert !accessFlags.isAbstract();
+    accessFlags.unsetPrivate();
+    accessFlags.unsetProtected();
+    accessFlags.setPublic();
+  }
+
+  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;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
new file mode 100644
index 0000000..ca22d3d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
@@ -0,0 +1,142 @@
+// 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.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+class CollisionDetector {
+
+  private static final int NOT_FOUND = Integer.MIN_VALUE;
+
+  private final DexItemFactory dexItemFactory;
+  private final Collection<DexMethod> invokes;
+  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
+
+  private final DexType source;
+  private final Reference2IntMap<DexProto> sourceProtoCache;
+
+  private final DexType target;
+  private final Reference2IntMap<DexProto> targetProtoCache;
+
+  private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
+
+  CollisionDetector(
+      AppView<AppInfoWithLiveness> appView,
+      Collection<DexMethod> invokes,
+      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
+      DexType source,
+      DexType target) {
+    this.dexItemFactory = appView.dexItemFactory();
+    this.invokes = invokes;
+    this.mergedClasses = mergedClasses;
+    this.source = source;
+    this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+    this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
+    this.target = target;
+    this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+    this.targetProtoCache.defaultReturnValue(NOT_FOUND);
+  }
+
+  boolean mayCollide(Timing timing) {
+    timing.begin("collision detection");
+    fillSeenPositions();
+    boolean result = false;
+    // If the type is not used in methods at all, there cannot be any conflict.
+    if (!seenPositions.isEmpty()) {
+      for (DexMethod method : invokes) {
+        Int2IntMap positionsMap = seenPositions.get(method.getName());
+        if (positionsMap != null) {
+          int arity = method.getArity();
+          int previous = positionsMap.get(arity);
+          if (previous != NOT_FOUND) {
+            assert previous != 0;
+            int positions = computePositionsFor(method.getProto(), source, sourceProtoCache);
+            if ((positions & previous) != 0) {
+              result = true;
+              break;
+            }
+          }
+        }
+      }
+    }
+    timing.end();
+    return result;
+  }
+
+  private void fillSeenPositions() {
+    for (DexMethod method : invokes) {
+      int arity = method.getArity();
+      int positions = computePositionsFor(method.getProto(), target, targetProtoCache);
+      if (positions != 0) {
+        Int2IntMap positionsMap =
+            seenPositions.computeIfAbsent(
+                method.getName(),
+                k -> {
+                  Int2IntMap result = new Int2IntOpenHashMap();
+                  result.defaultReturnValue(NOT_FOUND);
+                  return result;
+                });
+        int value = 0;
+        int previous = positionsMap.get(arity);
+        if (previous != NOT_FOUND) {
+          value = previous;
+        }
+        value |= positions;
+        positionsMap.put(arity, value);
+      }
+    }
+  }
+
+  // Given a method signature and a type, this method computes a bit vector that denotes the
+  // positions at which the given type is used in the method signature.
+  private int computePositionsFor(DexProto proto, DexType type, Reference2IntMap<DexProto> cache) {
+    int result = cache.getInt(proto);
+    if (result != NOT_FOUND) {
+      return result;
+    }
+    result = 0;
+    int bitsUsed = 0;
+    int accumulator = 0;
+    for (DexType parameterBaseType : proto.getParameterBaseTypes(dexItemFactory)) {
+      // Substitute the type with the already merged class to estimate what it will look like.
+      DexType mappedType = mergedClasses.getOrDefault(parameterBaseType, parameterBaseType);
+      accumulator <<= 1;
+      bitsUsed++;
+      if (mappedType.isIdenticalTo(type)) {
+        accumulator |= 1;
+      }
+      // Handle overflow on 31 bit boundary.
+      if (bitsUsed == Integer.SIZE - 1) {
+        result |= accumulator;
+        accumulator = 0;
+        bitsUsed = 0;
+      }
+    }
+    // We also take the return type into account for potential conflicts.
+    DexType returnBaseType = proto.getReturnType().toBaseType(dexItemFactory);
+    DexType mappedReturnType = mergedClasses.getOrDefault(returnBaseType, returnBaseType);
+    accumulator <<= 1;
+    if (mappedReturnType.isIdenticalTo(type)) {
+      accumulator |= 1;
+    }
+    result |= accumulator;
+    cache.put(proto, result);
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
new file mode 100644
index 0000000..c71a51d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
@@ -0,0 +1,207 @@
+package com.android.tools.r8.verticalclassmerging;
+
+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.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistryWithResult;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.utils.OptionalBool;
+
+// Searches for a reference to a non-private, non-public class, field or method declared in the
+// same package as [source].
+public class IllegalAccessDetector extends UseRegistryWithResult<Boolean, ProgramMethod> {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy;
+
+  public IllegalAccessDetector(
+      AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy,
+      ProgramMethod context) {
+    super(appViewWithClassHierarchy, context, false);
+    this.appViewWithClassHierarchy = appViewWithClassHierarchy;
+  }
+
+  protected boolean checkFoundPackagePrivateAccess() {
+    assert getResult();
+    return true;
+  }
+
+  protected boolean setFoundPackagePrivateAccess() {
+    setResult(true);
+    return true;
+  }
+
+  protected static boolean continueSearchForPackagePrivateAccess() {
+    return false;
+  }
+
+  private boolean checkFieldReference(DexField field) {
+    return checkRewrittenFieldReference(appViewWithClassHierarchy.graphLens().lookupField(field));
+  }
+
+  private boolean checkRewrittenFieldReference(DexField field) {
+    assert field.getHolderType().isClassType();
+    DexType fieldHolder = field.getHolderType();
+    if (fieldHolder.isSamePackage(getContext().getHolderType())) {
+      if (checkRewrittenTypeReference(fieldHolder)) {
+        return checkFoundPackagePrivateAccess();
+      }
+      DexClassAndField resolvedField =
+          appViewWithClassHierarchy.appInfo().resolveField(field).getResolutionPair();
+      if (resolvedField == null) {
+        return setFoundPackagePrivateAccess();
+      }
+      if (resolvedField.getHolder() != getContext().getHolder()
+          && !resolvedField.getAccessFlags().isPublic()) {
+        return setFoundPackagePrivateAccess();
+      }
+      if (checkRewrittenFieldType(resolvedField)) {
+        return checkFoundPackagePrivateAccess();
+      }
+    }
+    return continueSearchForPackagePrivateAccess();
+  }
+
+  protected boolean checkRewrittenFieldType(DexClassAndField field) {
+    return continueSearchForPackagePrivateAccess();
+  }
+
+  private boolean checkRewrittenMethodReference(
+      DexMethod rewrittenMethod, OptionalBool isInterface) {
+    DexType baseType =
+        rewrittenMethod.getHolderType().toBaseType(appViewWithClassHierarchy.dexItemFactory());
+    if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
+      if (checkTypeReference(rewrittenMethod.getHolderType())) {
+        return checkFoundPackagePrivateAccess();
+      }
+      MethodResolutionResult resolutionResult =
+          isInterface.isUnknown()
+              ? appViewWithClassHierarchy
+                  .appInfo()
+                  .unsafeResolveMethodDueToDexFormat(rewrittenMethod)
+              : appViewWithClassHierarchy
+                  .appInfo()
+                  .resolveMethod(rewrittenMethod, isInterface.isTrue());
+      if (!resolutionResult.isSingleResolution()) {
+        return setFoundPackagePrivateAccess();
+      }
+      DexClassAndMethod resolvedMethod = resolutionResult.asSingleResolution().getResolutionPair();
+      if (resolvedMethod.getHolder() != getContext().getHolder()
+          && !resolvedMethod.getAccessFlags().isPublic()) {
+        return setFoundPackagePrivateAccess();
+      }
+    }
+    return continueSearchForPackagePrivateAccess();
+  }
+
+  private boolean checkTypeReference(DexType type) {
+    return internalCheckTypeReference(type, appViewWithClassHierarchy.graphLens());
+  }
+
+  private boolean checkRewrittenTypeReference(DexType type) {
+    return internalCheckTypeReference(type, GraphLens.getIdentityLens());
+  }
+
+  private boolean internalCheckTypeReference(DexType type, GraphLens graphLens) {
+    DexType baseType =
+        graphLens.lookupType(type.toBaseType(appViewWithClassHierarchy.dexItemFactory()));
+    if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
+      DexClass clazz = appViewWithClassHierarchy.definitionFor(baseType);
+      if (clazz == null || !clazz.isPublic()) {
+        return setFoundPackagePrivateAccess();
+      }
+    }
+    return continueSearchForPackagePrivateAccess();
+  }
+
+  @Override
+  public void registerInitClass(DexType clazz) {
+    if (appViewWithClassHierarchy.initClassLens().isFinal()) {
+      // The InitClass lens is always rewritten up until the most recent graph lens, so first map
+      // the class type to the most recent graph lens.
+      DexType rewrittenType = appViewWithClassHierarchy.graphLens().lookupType(clazz);
+      DexField initClassField =
+          appViewWithClassHierarchy.initClassLens().getInitClassField(rewrittenType);
+      checkRewrittenFieldReference(initClassField);
+    } else {
+      checkTypeReference(clazz);
+    }
+  }
+
+  @Override
+  public void registerInvokeVirtual(DexMethod method) {
+    MethodLookupResult lookup =
+        appViewWithClassHierarchy.graphLens().lookupInvokeVirtual(method, getContext());
+    checkRewrittenMethodReference(lookup.getReference(), OptionalBool.FALSE);
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod method) {
+    MethodLookupResult lookup =
+        appViewWithClassHierarchy.graphLens().lookupInvokeDirect(method, getContext());
+    checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
+  }
+
+  @Override
+  public void registerInvokeStatic(DexMethod method) {
+    MethodLookupResult lookup =
+        appViewWithClassHierarchy.graphLens().lookupInvokeStatic(method, getContext());
+    checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod method) {
+    MethodLookupResult lookup =
+        appViewWithClassHierarchy.graphLens().lookupInvokeInterface(method, getContext());
+    checkRewrittenMethodReference(lookup.getReference(), OptionalBool.TRUE);
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    MethodLookupResult lookup =
+        appViewWithClassHierarchy.graphLens().lookupInvokeSuper(method, getContext());
+    checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    checkFieldReference(field);
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    checkFieldReference(field);
+  }
+
+  @Override
+  public void registerNewInstance(DexType type) {
+    checkTypeReference(type);
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    checkFieldReference(field);
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    checkFieldReference(field);
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {
+    checkTypeReference(type);
+  }
+
+  @Override
+  public void registerInstanceOf(DexType type) {
+    checkTypeReference(type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
new file mode 100644
index 0000000..506a2af
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
@@ -0,0 +1,31 @@
+package com.android.tools.r8.verticalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class InvokeSpecialToDefaultLibraryMethodUseRegistry
+    extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
+
+  InvokeSpecialToDefaultLibraryMethodUseRegistry(
+      AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
+    super(appView, context, false);
+    assert context.getHolder().isInterface();
+  }
+
+  @Override
+  public void registerInvokeSpecial(DexMethod method) {
+    ProgramMethod context = getContext();
+    if (!method.getHolderType().isIdenticalTo(context.getHolderType())) {
+      return;
+    }
+
+    DexEncodedMethod definition = context.getHolder().lookupMethod(method);
+    if (definition != null && definition.belongsToVirtualPool()) {
+      setResult(true);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java
new file mode 100644
index 0000000..7042bd5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java
@@ -0,0 +1,140 @@
+// 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 static com.android.tools.r8.ir.code.InvokeType.VIRTUAL;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.lens.FieldLookupResult;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+
+public class SingleTypeMapperGraphLens extends NonIdentityGraphLens {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
+
+  private final DexProgramClass source;
+  private final DexProgramClass target;
+
+  public SingleTypeMapperGraphLens(
+      AppView<AppInfoWithLiveness> appView,
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
+      DexProgramClass source,
+      DexProgramClass target) {
+    super(appView.dexItemFactory(), GraphLens.getIdentityLens());
+    this.appView = appView;
+    this.lensBuilder = lensBuilder;
+    this.mergedClasses = mergedClasses;
+    this.source = source;
+    this.target = target;
+  }
+
+  @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexType getPreviousClassType(DexType type) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public final DexType getNextClassType(DexType type) {
+    return type.isIdenticalTo(source.getType())
+        ? target.getType()
+        : mergedClasses.getOrDefault(type, type);
+  }
+
+  @Override
+  public DexField getPreviousFieldSignature(DexField field) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexField getNextFieldSignature(DexField field) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexMethod getPreviousMethodSignature(DexMethod method) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexMethod getNextMethodSignature(DexMethod method) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public MethodLookupResult lookupMethod(
+      DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
+    // First look up the method using the existing graph lens (for example, the type will have
+    // changed if the method was publicized by ClassAndMemberPublicizer).
+    MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type, codeLens);
+    // Then check if there is a renaming due to the vertical class merger.
+    DexMethod newMethod = lensBuilder.methodMap.get(lookup.getReference());
+    if (newMethod == null) {
+      return lookup;
+    }
+    MethodLookupResult.Builder methodLookupResultBuilder =
+        MethodLookupResult.builder(this)
+            .setReference(newMethod)
+            .setPrototypeChanges(lookup.getPrototypeChanges())
+            .setType(lookup.getType());
+    if (lookup.getType() == InvokeType.INTERFACE) {
+      // If an interface has been merged into a class, invoke-interface needs to be translated
+      // to invoke-virtual.
+      DexClass clazz = appView.definitionFor(newMethod.holder);
+      if (clazz != null && !clazz.accessFlags.isInterface()) {
+        assert appView.definitionFor(method.holder).accessFlags.isInterface();
+        methodLookupResultBuilder.setType(VIRTUAL);
+      }
+    }
+    return methodLookupResultBuilder.build();
+  }
+
+  @Override
+  protected MethodLookupResult internalDescribeLookupMethod(
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    // This is unreachable since we override the implementation of lookupMethod() above.
+    throw new Unreachable();
+  }
+
+  @Override
+  public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
+      DexMethod method, GraphLens codeLens) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexField lookupField(DexField field, GraphLens codeLens) {
+    return lensBuilder.fieldMap.getOrDefault(field, field);
+  }
+
+  @Override
+  protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
+    // This is unreachable since we override the implementation of lookupField() above.
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/SynthesizedBridgeCode.java b/src/main/java/com/android/tools/r8/verticalclassmerging/SynthesizedBridgeCode.java
new file mode 100644
index 0000000..4471ff5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/SynthesizedBridgeCode.java
@@ -0,0 +1,84 @@
+package com.android.tools.r8.verticalclassmerging;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.ir.synthetic.AbstractSynthesizedCode;
+import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class SynthesizedBridgeCode extends AbstractSynthesizedCode {
+
+  private DexMethod method;
+  private DexMethod invocationTarget;
+  private InvokeType type;
+  private final boolean isInterface;
+
+  public SynthesizedBridgeCode(
+      DexMethod method, DexMethod invocationTarget, InvokeType type, boolean isInterface) {
+    this.method = method;
+    this.invocationTarget = invocationTarget;
+    this.type = type;
+    this.isInterface = isInterface;
+  }
+
+  public DexMethod getMethod() {
+    return method;
+  }
+
+  public DexMethod getTarget() {
+    return invocationTarget;
+  }
+
+  // By the time the synthesized code object is created, vertical class merging still has not
+  // finished. Therefore it is possible that the method signatures `method` and `invocationTarget`
+  // will change as a result of additional class merging operations. To deal with this, the
+  // vertical class merger explicitly invokes this method to update `method` and `invocation-
+  // Target` when vertical class merging has finished.
+  //
+  // Note that, without this step, these method signatures might refer to intermediate signatures
+  // that are only present in the middle of vertical class merging, which means that the graph
+  // lens will not work properly (since the graph lens generated by vertical class merging only
+  // expects to be applied to method signatures from *before* vertical class merging or *after*
+  // vertical class merging).
+  public void updateMethodSignatures(Function<DexMethod, DexMethod> transformer) {
+    method = transformer.apply(method);
+    invocationTarget = transformer.apply(invocationTarget);
+  }
+
+  @Override
+  public SourceCodeProvider getSourceCodeProvider() {
+    ForwardMethodSourceCode.Builder forwardSourceCodeBuilder =
+        ForwardMethodSourceCode.builder(method);
+    forwardSourceCodeBuilder
+        .setReceiver(method.holder)
+        .setTargetReceiver(type.isStatic() ? null : method.holder)
+        .setTarget(invocationTarget)
+        .setInvokeType(type)
+        .setIsInterface(isInterface);
+    return forwardSourceCodeBuilder::build;
+  }
+
+  @Override
+  public Consumer<UseRegistry> getRegistryCallback(DexClassAndMethod method) {
+    return registry -> {
+      assert registry.getTraversalContinuation().shouldContinue();
+      switch (type) {
+        case DIRECT:
+          registry.registerInvokeDirect(invocationTarget);
+          break;
+        case STATIC:
+          registry.registerInvokeStatic(invocationTarget);
+          break;
+        case VIRTUAL:
+          registry.registerInvokeVirtual(invocationTarget);
+          break;
+        default:
+          throw new Unreachable("Unexpected invocation type: " + type);
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
new file mode 100644
index 0000000..6cb5e12
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -0,0 +1,848 @@
+// 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 com.android.tools.r8.verticalclassmerging;
+
+import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+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.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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.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.graph.PrunedItems;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
+import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
+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.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Merges Supertypes with a single implementation into their single subtype.
+ *
+ * <p>A common use-case for this is to merge an interface into its single implementation.
+ *
+ * <p>The class merger only fixes the structure of the graph but leaves the actual instructions
+ * untouched. Fixup of instructions is deferred via a {@link GraphLens} to the IR building phase.
+ */
+public class VerticalClassMerger {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final DexItemFactory dexItemFactory;
+  private final InternalOptions options;
+  private Collection<DexMethod> invokes;
+
+  // Set of merge candidates. Note that this must have a deterministic iteration order.
+  private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
+
+  // Map from source class to target class.
+  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
+      BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
+
+  private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedInterfaces =
+      BidirectionalManyToOneHashMap.newIdentityHashMap();
+
+  // Set of types that must not be merged into their subtype.
+  private final Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
+
+  // The resulting graph lens that should be used after class merging.
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+
+  // All the bridge methods that have been synthesized during vertical class merging.
+  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
+
+  private final MainDexInfo mainDexInfo;
+
+  public VerticalClassMerger(AppView<AppInfoWithLiveness> appView) {
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.mainDexInfo = appInfo.getMainDexInfo();
+    this.lensBuilder = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
+  }
+
+  private void initializeMergeCandidates(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    for (DexProgramClass sourceClass : appView.appInfo().classesWithDeterministicOrder()) {
+      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)) {
+        continue;
+      }
+      mergeCandidates.add(sourceClass);
+    }
+  }
+
+  // Returns a set of types that must not be merged into other types.
+  private void initializePinnedTypes() {
+    // For all pinned fields, also pin the type of the field (because changing the type of the field
+    // implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
+    // the return type and the parameter types of the method.
+    // TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
+    List<DexReference> pinnedItems = new ArrayList<>();
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    keepInfo.forEachPinnedType(pinnedItems::add, options);
+    keepInfo.forEachPinnedMethod(pinnedItems::add, options);
+    keepInfo.forEachPinnedField(pinnedItems::add, options);
+    extractPinnedItems(pinnedItems);
+
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (Iterables.any(clazz.methods(), method -> method.getAccessFlags().isNative())) {
+        markClassAsPinned(clazz);
+      }
+    }
+
+    // It is valid to have an invoke-direct instruction in a default interface method that targets
+    // another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
+    // SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
+    // Therefore, we disallow merging such interfaces into their subtypes.
+    for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
+      markTypeAsPinned(signature.getHolderType());
+    }
+
+    // The set of targets that must remain for proper resolution error cases should not be merged.
+    // TODO(b/192821424): Can be removed if handled.
+    extractPinnedItems(appView.appInfo().getFailedMethodResolutionTargets());
+  }
+
+  private <T extends DexReference> void extractPinnedItems(Iterable<T> items) {
+    for (DexReference item : items) {
+      if (item.isDexType()) {
+        markTypeAsPinned(item.asDexType());
+      } else if (item.isDexField()) {
+        // Pin the holder and the type of the field.
+        DexField field = item.asDexField();
+        markTypeAsPinned(field.getHolderType());
+        markTypeAsPinned(field.getType());
+      } else {
+        assert item.isDexMethod();
+        // Pin the holder, the return type and the parameter types of the method. If we were to
+        // merge any of these types into their sub classes, then we would implicitly change the
+        // signature of this method.
+        DexMethod method = item.asDexMethod();
+        markTypeAsPinned(method.getHolderType());
+        markTypeAsPinned(method.getReturnType());
+        for (DexType parameterType : method.getParameters()) {
+          markTypeAsPinned(parameterType);
+        }
+      }
+    }
+  }
+
+  private void markTypeAsPinned(DexType type) {
+    DexType baseType = type.toBaseType(dexItemFactory);
+    if (!baseType.isClassType() || appView.appInfo().isPinnedWithDefinitionLookup(baseType)) {
+      // We check for the case where the type is pinned according to appInfo.isPinned,
+      // so we only need to add it here if it is not the case.
+      return;
+    }
+
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(baseType));
+    if (clazz != null) {
+      markClassAsPinned(clazz);
+    }
+  }
+
+  private void markClassAsPinned(DexProgramClass clazz) {
+    pinnedClasses.add(clazz);
+  }
+
+  // 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).isPinned(options)
+        || pinnedClasses.contains(sourceClass)
+        || appView.appInfo().isNoVerticalClassMergingOfType(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 (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;
+      }
+    }
+    if (sourceClass.getEnclosingMethodAttribute() != null
+        || !sourceClass.getInnerClasses().isEmpty()) {
+      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
+      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;
+      }
+    }
+    return true;
+  }
+
+  // Returns true if [clazz] 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 [clazz] into its subtype.
+  private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    assert isMergeCandidate(sourceClass, targetClass);
+    assert !mergedClasses.containsValue(sourceClass.getType());
+    // 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))) {
+      // TODO(herhut): Handle class initializers.
+      return false;
+    }
+    boolean sourceCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(sourceClass)
+            || sourceClass.hasStaticSynchronizedMethods();
+    boolean targetCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(targetClass)
+            || targetClass.hasStaticSynchronizedMethods();
+    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
+      return false;
+    }
+    if (targetClass.getEnclosingMethodAttribute() != null
+        || !targetClass.getInnerClasses().isEmpty()) {
+      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
+      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 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();
+            });
+    return result.shouldBreak();
+  }
+
+  private Collection<DexMethod> getInvokes(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    if (invokes == null) {
+      invokes = new OverloadedMethodSignaturesRetriever(immediateSubtypingInfo).get();
+    }
+    return invokes;
+  }
+
+  // Collects all potentially overloaded method signatures that reference at least one type that
+  // may be the source or target of a merge operation.
+  private class OverloadedMethodSignaturesRetriever {
+    private final Reference2BooleanOpenHashMap<DexProto> cache =
+        new Reference2BooleanOpenHashMap<>();
+    private final Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+    private final Set<DexType> mergeeCandidates = new HashSet<>();
+
+    public OverloadedMethodSignaturesRetriever(
+        ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+      for (DexProgramClass mergeCandidate : mergeCandidates) {
+        List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
+        if (subclasses.size() == 1) {
+          mergeeCandidates.add(ListUtils.first(subclasses).getType());
+        }
+      }
+    }
+
+    public Collection<DexMethod> get() {
+      Map<DexString, DexProto> overloadingInfo = new HashMap<>();
+
+      // Find all signatures that may reference a type that could be the source or target of a
+      // merge operation.
+      Set<Wrapper<DexMethod>> filteredSignatures = new HashSet<>();
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        for (DexEncodedMethod encodedMethod : clazz.methods()) {
+          DexMethod method = encodedMethod.getReference();
+          DexClass definition = appView.definitionFor(method.getHolderType());
+          if (definition != null
+              && definition.isProgramClass()
+              && protoMayReferenceMergedSourceOrTarget(method.getProto())) {
+            filteredSignatures.add(equivalence.wrap(method));
+
+            // Record that we have seen a method named [signature.name] with the proto
+            // [signature.proto]. If at some point, we find a method with the same name, but a
+            // different proto, it could be the case that a method with the given name is
+            // overloaded.
+            DexProto existing =
+                overloadingInfo.computeIfAbsent(method.getName(), key -> method.getProto());
+            if (existing.isNotIdenticalTo(DexProto.SENTINEL)
+                && !existing.equals(method.getProto())) {
+              // Mark that this signature is overloaded by mapping it to SENTINEL.
+              overloadingInfo.put(method.getName(), DexProto.SENTINEL);
+            }
+          }
+        }
+      }
+
+      List<DexMethod> result = new ArrayList<>();
+      for (Wrapper<DexMethod> wrappedSignature : filteredSignatures) {
+        DexMethod signature = wrappedSignature.get();
+
+        // Ignore those method names that are definitely not overloaded since they cannot lead to
+        // any collisions.
+        if (overloadingInfo.get(signature.getName()).isIdenticalTo(DexProto.SENTINEL)) {
+          result.add(signature);
+        }
+      }
+      return result;
+    }
+
+    private boolean protoMayReferenceMergedSourceOrTarget(DexProto proto) {
+      boolean result;
+      if (cache.containsKey(proto)) {
+        result = cache.getBoolean(proto);
+      } else {
+        result = false;
+        if (typeMayReferenceMergedSourceOrTarget(proto.getReturnType())) {
+          result = true;
+        } else {
+          for (DexType type : proto.getParameters()) {
+            if (typeMayReferenceMergedSourceOrTarget(type)) {
+              result = true;
+              break;
+            }
+          }
+        }
+        cache.put(proto, result);
+      }
+      return result;
+    }
+
+    private boolean typeMayReferenceMergedSourceOrTarget(DexType type) {
+      type = type.toBaseType(dexItemFactory);
+      if (type.isClassType()) {
+        if (mergeeCandidates.contains(type)) {
+          return true;
+        }
+        DexClass clazz = appView.definitionFor(type);
+        if (clazz != null && clazz.isProgramClass()) {
+          return mergeCandidates.contains(clazz.asProgramClass());
+        }
+      }
+      return false;
+    }
+  }
+
+  public static void runIfNecessary(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    timing.begin("VerticalClassMerger");
+    if (shouldRun(appView)) {
+      new VerticalClassMerger(appView).run(executorService, timing);
+    } else {
+      appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
+    }
+    assert appView.hasVerticallyMergedClasses();
+    assert ArtProfileCompletenessChecker.verify(appView);
+    timing.end();
+  }
+
+  private static boolean shouldRun(AppView<AppInfoWithLiveness> appView) {
+    return appView.options().getVerticalClassMergerOptions().isEnabled()
+        && !appView.hasCfByteCodePassThroughMethods();
+  }
+
+  private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    ImmediateProgramSubtypingInfo immediateSubtypingInfo =
+        ImmediateProgramSubtypingInfo.create(appView);
+
+    initializePinnedTypes(); // Must be initialized prior to mergeCandidates.
+    initializeMergeCandidates(immediateSubtypingInfo);
+
+    timing.begin("merge");
+    // Visit the program classes in a top-down order according to the class hierarchy.
+    TopDownClassHierarchyTraversal.forProgramClasses(appView)
+        .visit(
+            mergeCandidates, clazz -> mergeClassIfPossible(clazz, immediateSubtypingInfo, timing));
+    timing.end();
+
+    VerticallyMergedClasses verticallyMergedClasses =
+        new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
+    appView.setVerticallyMergedClasses(verticallyMergedClasses);
+    if (verticallyMergedClasses.isEmpty()) {
+      return;
+    }
+
+    timing.begin("fixup");
+    VerticalClassMergerGraphLens lens =
+        new VerticalClassMergerTreeFixer(
+                appView, lensBuilder, verticallyMergedClasses, synthesizedBridges)
+            .fixupTypeReferences();
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    keepInfo.mutate(
+        mutator ->
+            mutator.removeKeepInfoForMergedClasses(
+                PrunedItems.builder().setRemovedClasses(mergedClasses.keySet()).build()));
+    timing.end();
+
+    assert lens != null;
+    assert verifyGraphLens(lens);
+
+    // Include bridges in art profiles.
+    ProfileCollectionAdditions profileCollectionAdditions =
+        ProfileCollectionAdditions.create(appView);
+    if (!profileCollectionAdditions.isNop()) {
+      for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
+        profileCollectionAdditions.applyIfContextIsInProfile(
+            lens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
+            additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
+      }
+    }
+    profileCollectionAdditions.commit(appView);
+
+    // Rewrite collections using the lens.
+    appView.rewriteWithLens(lens, executorService, timing);
+
+    // Copy keep info to newly synthesized methods.
+    keepInfo.mutate(
+        mutator -> {
+          for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
+            ProgramMethod bridge =
+                asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getMethod()));
+            ProgramMethod target =
+                asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getTarget()));
+            if (bridge != null && target != null) {
+              mutator.joinMethod(bridge, info -> info.merge(appView.getKeepInfo(target).joiner()));
+              continue;
+            }
+            assert false;
+          }
+        });
+
+    appView.notifyOptimizationFinishedForTesting();
+  }
+
+  private boolean verifyGraphLens(VerticalClassMergerGraphLens graphLens) {
+    // Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
+    // getRenamedMethodSignature() instead of lookupField() and lookupMethod(). This is important
+    // for this check to succeed, since it is not guaranteed that calling lookupMethod() with a
+    // pinned method will return the method itself.
+    //
+    // Consider the following example.
+    //
+    //   class A {
+    //     public void method() {}
+    //   }
+    //   class B extends A {
+    //     @Override
+    //     public void method() {}
+    //   }
+    //   class C extends B {
+    //     @Override
+    //     public void method() {}
+    //   }
+    //
+    // If A.method() is pinned, then A cannot be merged into B, but B can still be merged into C.
+    // Now, if there is an invoke-super instruction in C that hits B.method(), then this needs to
+    // be rewritten into an invoke-direct instruction. In particular, there could be an instruction
+    // `invoke-super A.method` in C. This would hit B.method(). Therefore, the graph lens records
+    // that `invoke-super A.method` instructions, which are in one of the methods from C, needs to
+    // be rewritten to `invoke-direct C.method$B`. This is valid even though A.method() is actually
+    // pinned, because this rewriting does not affect A.method() in any way.
+    assert graphLens.assertPinnedNotModified(appView);
+
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      for (DexEncodedMethod encodedMethod : clazz.methods()) {
+        DexMethod method = encodedMethod.getReference();
+        DexMethod originalMethod = graphLens.getOriginalMethodSignature(method);
+        DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod);
+
+        // Must be able to map back and forth.
+        if (encodedMethod.hasCode() && encodedMethod.getCode() instanceof SynthesizedBridgeCode) {
+          // For virtual methods, the vertical class merger creates two methods in the sub class
+          // in order to deal with invoke-super instructions (one that is private and one that is
+          // virtual). Therefore, it is not possible to go back and forth. Instead, we check that
+          // the two methods map back to the same original method, and that the original method
+          // can be mapped to the implementation method.
+          DexMethod implementationMethod =
+              ((SynthesizedBridgeCode) encodedMethod.getCode()).getTarget();
+          DexMethod originalImplementationMethod =
+              graphLens.getOriginalMethodSignature(implementationMethod);
+          assert originalMethod.isIdenticalTo(originalImplementationMethod);
+          assert implementationMethod.isIdenticalTo(renamedMethod);
+        } else {
+          assert method.isIdenticalTo(renamedMethod);
+        }
+
+        // Verify that all types are up-to-date. After vertical class merging, there should be no
+        // more references to types that have been merged into another type.
+        assert !mergedClasses.containsKey(method.getReturnType());
+        assert Arrays.stream(method.getParameters().getBacking())
+            .noneMatch(mergedClasses::containsKey);
+      }
+    }
+    return true;
+  }
+
+  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 void mergeClassIfPossible(
+      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo, Timing timing)
+      throws ExecutionException {
+    if (!mergeCandidates.contains(clazz)) {
+      return;
+    }
+    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(clazz);
+    if (subclasses.size() != 1) {
+      return;
+    }
+    DexProgramClass targetClass = ListUtils.first(subclasses);
+    assert !mergedClasses.containsKey(targetClass.getType());
+    if (mergedClasses.containsValue(clazz.getType())) {
+      return;
+    }
+    assert isMergeCandidate(clazz, targetClass);
+    if (mergedClasses.containsValue(targetClass.getType())) {
+      if (!isStillMergeCandidate(clazz, targetClass)) {
+        return;
+      }
+    } else {
+      assert isStillMergeCandidate(clazz, targetClass);
+    }
+
+    // Guard against the case where we have two methods that may get the same signature
+    // if we replace types. This is rare, so we approximate and err on the safe side here.
+    CollisionDetector collisionDetector =
+        new CollisionDetector(
+            appView,
+            getInvokes(immediateSubtypingInfo),
+            mergedClasses,
+            clazz.getType(),
+            targetClass.getType());
+    if (collisionDetector.mayCollide(timing)) {
+      return;
+    }
+
+    // Check with main dex classes to see if we are allowed to merge.
+    if (!mainDexInfo.canMerge(clazz, targetClass, appView.getSyntheticItems())) {
+      return;
+    }
+
+    ClassMerger merger = new ClassMerger(appView, lensBuilder, mergedClasses, clazz, targetClass);
+    if (merger.merge()) {
+      mergedClasses.put(clazz.getType(), targetClass.getType());
+      if (clazz.isInterface()) {
+        mergedInterfaces.put(clazz.getType(), targetClass.getType());
+      }
+      // Commit the changes to the graph lens.
+      lensBuilder.merge(merger.getRenamings());
+      synthesizedBridges.addAll(merger.getSynthesizedBridges());
+    }
+  }
+
+  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 disallowInlining(ProgramMethod method, DexProgramClass context) {
+    if (appView.options().inlinerOptions().enableInlining) {
+      Code code = method.getDefinition().getCode();
+      if (code.isCfCode()) {
+        CfCode cfCode = code.asCfCode();
+        SingleTypeMapperGraphLens lens =
+            new SingleTypeMapperGraphLens(
+                appView, lensBuilder, mergedClasses, method.getHolder(), context);
+        ConstraintWithTarget constraint =
+            cfCode.computeInliningConstraint(
+                method, appView, lens, context.programInstanceInitializers().iterator().next());
+        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, appView.getSyntheticItems())) {
+          return true;
+        }
+        return false;
+      } else if (code.isDefaultInstanceInitializerCode()) {
+        return false;
+      }
+      // For non-jar/cf code we currently cannot guarantee that markForceInline() will succeed.
+    }
+    return true;
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
rename to src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index d4525f0..e3ad747 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.verticalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
@@ -27,6 +26,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -134,16 +134,23 @@
         }
       }
     }
+    MethodLookupResult lookupResult;
     DexMethod newMethod = methodMap.apply(previous.getReference());
     if (newMethod == null) {
-      return previous;
+      lookupResult = previous;
+    } else {
+      lookupResult =
+          MethodLookupResult.builder(this)
+              .setReference(newMethod)
+              .setPrototypeChanges(
+                  internalDescribePrototypeChanges(previous.getPrototypeChanges(), newMethod))
+              .setType(mapInvocationType(newMethod, previous.getReference(), previous.getType()))
+              .build();
     }
-    return MethodLookupResult.builder(this)
-        .setReference(newMethod)
-        .setPrototypeChanges(
-            internalDescribePrototypeChanges(previous.getPrototypeChanges(), newMethod))
-        .setType(mapInvocationType(newMethod, previous.getReference(), previous.getType()))
-        .build();
+    assert !appView.testing().enableVerticalClassMergerLensAssertion
+        || Streams.stream(lookupResult.getReference().getReferencedBaseTypes(dexItemFactory()))
+            .noneMatch(type -> mergedClasses.hasBeenMergedIntoSubtype(type));
+    return lookupResult;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
new file mode 100644
index 0000000..5205d49
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.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.verticalclassmerging;
+
+import com.android.tools.r8.utils.InternalOptions;
+
+public class VerticalClassMergerOptions {
+
+  private final InternalOptions options;
+
+  private boolean enabled = true;
+
+  public VerticalClassMergerOptions(InternalOptions options) {
+    this.options = options;
+  }
+
+  public void disable() {
+    setEnabled(false);
+  }
+
+  public boolean isDisabled() {
+    return !isEnabled();
+  }
+
+  public boolean isEnabled() {
+    return enabled && options.isOptimizing() && options.isShrinking();
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
new file mode 100644
index 0000000..e544c25
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
@@ -0,0 +1,97 @@
+// 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.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.fixup.TreeFixerBase;
+import com.android.tools.r8.shaking.AnnotationFixer;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.OptionalBool;
+import java.util.List;
+
+class VerticalClassMergerTreeFixer extends TreeFixerBase {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final VerticallyMergedClasses mergedClasses;
+  private final List<SynthesizedBridgeCode> synthesizedBridges;
+
+  VerticalClassMergerTreeFixer(
+      AppView<AppInfoWithLiveness> appView,
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      VerticallyMergedClasses mergedClasses,
+      List<SynthesizedBridgeCode> synthesizedBridges) {
+    super(appView);
+    this.appView = appView;
+    this.lensBuilder =
+        VerticalClassMergerGraphLens.Builder.createBuilderForFixup(lensBuilder, mergedClasses);
+    this.mergedClasses = mergedClasses;
+    this.synthesizedBridges = synthesizedBridges;
+  }
+
+  VerticalClassMergerGraphLens fixupTypeReferences() {
+    // Globally substitute merged class types in protos and holders.
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      clazz.getMethodCollection().replaceMethods(this::fixupMethod);
+      clazz.setStaticFields(fixupFields(clazz.staticFields()));
+      clazz.setInstanceFields(fixupFields(clazz.instanceFields()));
+      clazz.setPermittedSubclassAttributes(
+          fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
+    }
+    for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
+      synthesizedBridge.updateMethodSignatures(this::fixupMethodReference);
+    }
+    VerticalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
+    if (lens != null) {
+      new AnnotationFixer(lens, appView.graphLens()).run(appView.appInfo().classes());
+    }
+    return lens;
+  }
+
+  @Override
+  public DexType mapClassType(DexType type) {
+    while (mergedClasses.hasBeenMergedIntoSubtype(type)) {
+      type = mergedClasses.getTargetFor(type);
+    }
+    return type;
+  }
+
+  @Override
+  public void recordClassChange(DexType from, DexType to) {
+    // Fixup of classes is not used so no class type should change.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void recordFieldChange(DexField from, DexField to) {
+    if (!lensBuilder.hasOriginalSignatureMappingFor(to)) {
+      lensBuilder.map(from, to);
+    }
+  }
+
+  @Override
+  public void recordMethodChange(DexMethod from, DexMethod to) {
+    if (!lensBuilder.hasOriginalSignatureMappingFor(to)) {
+      lensBuilder.map(from, to).recordMove(from, to);
+    }
+  }
+
+  @Override
+  public DexEncodedMethod recordMethodChange(DexEncodedMethod method, DexEncodedMethod newMethod) {
+    recordMethodChange(method.getReference(), newMethod.getReference());
+    if (newMethod.isNonPrivateVirtualMethod()) {
+      // Since we changed the return type or one of the parameters, this method cannot be a
+      // classpath or library method override, since we only class merge program classes.
+      assert !method.isLibraryMethodOverride().isTrue();
+      newMethod.setLibraryMethodOverride(OptionalBool.FALSE);
+    }
+    return newMethod;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
similarity index 96%
rename from src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
rename to src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
index 78c6259..d6f3e64 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
@@ -2,10 +2,11 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.graph.classmerging;
+package com.android.tools.r8.verticalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
index 3e30568..5f3ed0b 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
@@ -174,6 +174,9 @@
         resourceUsageRecorders.add(
                 new ProtoAndroidManifestUsageRecorder(
                         fileSystemProto.getPath(ANDROID_MANIFEST_XML)));
+        for (String rawResource : options.getRawResources()) {
+            resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(Paths.get(rawResource)));
+        }
         // If the apk contains a raw folder, find keep rules in there
         if (new ZipFile(options.getInput())
                 .stream().anyMatch(zipEntry -> zipEntry.getName().startsWith("res/raw"))) {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
index 153f1ce..f49e811 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -243,7 +243,7 @@
         }
         zos.putNextEntry(outEntry)
         if (!entry.isDirectory) {
-            zos.write(ByteStreams.toByteArray(zis))
+            zis.transferTo(zos);
         }
         zos.closeEntry()
     }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index 87158c9..ad7ddfb 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -22,9 +22,11 @@
 import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt;
 import com.android.ide.common.resources.ResourcesUtil;
 import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.tools.r8.FeatureSplit;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.protobuf.InvalidProtocolBufferException;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -40,39 +42,44 @@
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.xml.parsers.ParserConfigurationException;
-import org.jetbrains.annotations.NotNull;
 import org.xml.sax.SAXException;
 
 public class LegacyResourceShrinker {
-  private final Map<Integer, byte[]> dexInputs;
+  private final Map<String, byte[]> dexInputs;
   private final List<PathAndBytes> resFolderInputs;
   private final List<PathAndBytes> xmlInputs;
-  private final PathAndBytes manifest;
-  private final PathAndBytes resourceTable;
+  private final List<PathAndBytes> manifest;
+  private final Map<PathAndBytes, FeatureSplit> resourceTables;
 
   public static class Builder {
 
-    private final Map<Integer, byte[]> dexInputs = new HashMap<>();
+    private final Map<String, byte[]> dexInputs = new HashMap<>();
     private final List<PathAndBytes> resFolderInputs = new ArrayList<>();
     private final List<PathAndBytes> xmlInputs = new ArrayList<>();
 
-    private PathAndBytes manifest;
-    private PathAndBytes resourceTable;
+    private final List<PathAndBytes> manifests = new ArrayList<>();
+    private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
 
     private Builder() {}
 
-    public Builder setManifest(Path path, byte[] bytes) {
-      this.manifest = new PathAndBytes(bytes, path);
+    public Builder addManifest(Path path, byte[] bytes) {
+      manifests.add(new PathAndBytes(bytes, path));
       return this;
     }
 
-    public Builder setResourceTable(Path path, byte[] bytes) {
-      this.resourceTable = new PathAndBytes(bytes, path);
+    public Builder addResourceTable(Path path, byte[] bytes, FeatureSplit featureSplit) {
+      resourceTables.put(new PathAndBytes(bytes, path), featureSplit);
+      try {
+        ResourceTable resourceTable = ResourceTable.parseFrom(bytes);
+        System.currentTimeMillis();
+      } catch (InvalidProtocolBufferException e) {
+        throw new RuntimeException(e);
+      }
       return this;
     }
 
-    public Builder addDexInput(int index, byte[] bytes) {
-      dexInputs.put(index, bytes);
+    public Builder addDexInput(String classesLocation, byte[] bytes) {
+      dexInputs.put(classesLocation, bytes);
       return this;
     }
 
@@ -87,22 +94,22 @@
     }
 
     public LegacyResourceShrinker build() {
-      assert manifest != null && resourceTable != null;
+      assert manifests != null && resourceTables != null;
       return new LegacyResourceShrinker(
-          dexInputs, resFolderInputs, manifest, resourceTable, xmlInputs);
+          dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs);
     }
   }
 
   private LegacyResourceShrinker(
-      Map<Integer, byte[]> dexInputs,
+      Map<String, byte[]> dexInputs,
       List<PathAndBytes> resFolderInputs,
-      PathAndBytes manifest,
-      PathAndBytes resourceTable,
+      List<PathAndBytes> manifests,
+      Map<PathAndBytes, FeatureSplit> resourceTables,
       List<PathAndBytes> xmlInputs) {
     this.dexInputs = dexInputs;
     this.resFolderInputs = resFolderInputs;
-    this.manifest = manifest;
-    this.resourceTable = resourceTable;
+    this.manifest = manifests;
+    this.resourceTables = resourceTables;
     this.xmlInputs = xmlInputs;
   }
 
@@ -111,41 +118,52 @@
   }
 
   public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
-    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, false);
-    ResourceTable loadedResourceTable = ResourceTable.parseFrom(resourceTable.bytes);
-    model.instantiateFromResourceTable(loadedResourceTable);
-    for (Entry<Integer, byte[]> entry : dexInputs.entrySet()) {
+    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+    for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
+      ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
+      model.instantiateFromResourceTable(loadedResourceTable);
+    }
+    return shrinkModel(model);
+  }
+
+  public ShrinkerResult shrinkModel(R8ResourceShrinkerModel model) throws IOException {
+    for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
       // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
-      Path inMemoryR8 = Paths.get("in_memory_r8_classes" + entry.getKey() + ".dex");
+      Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
       R8ResourceShrinker.runResourceShrinkerAnalysis(
           entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
     }
-    ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
-        XmlNode.parseFrom(manifest.bytes), model);
+    for (PathAndBytes pathAndBytes : manifest) {
+      ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
+          XmlNode.parseFrom(pathAndBytes.bytes), model);
+    }
     for (PathAndBytes xmlInput : xmlInputs) {
       if (xmlInput.path.startsWith("res/raw")) {
         ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model);
       }
     }
-    new ProtoResourcesGraphBuilder(
-            new ResFolderFileTree() {
-              Map<String, PathAndBytes> pathToBytes =
-                  new ImmutableMap.Builder<String, PathAndBytes>()
-                      .putAll(
-                          xmlInputs.stream()
-                              .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
-                      .putAll(
-                          resFolderInputs.stream()
-                              .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
-                      .build();
 
-              @Override
-              public byte[] getEntryByName(@NotNull String pathInRes) {
-                return pathToBytes.get(pathInRes).getBytes();
-              }
-            },
-            unused -> loadedResourceTable)
-        .buildGraph(model);
+    ImmutableMap<String, PathAndBytes> resFolderMappings =
+        new ImmutableMap.Builder<String, PathAndBytes>()
+            .putAll(
+                xmlInputs.stream()
+                    .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+            .putAll(
+                resFolderInputs.stream()
+                    .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+            .build();
+    for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
+      ResourceTable resourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
+      new ProtoResourcesGraphBuilder(
+              new ResFolderFileTree() {
+                @Override
+                public byte[] getEntryByName(String pathInRes) {
+                  return resFolderMappings.get(pathInRes).getBytes();
+                }
+              },
+              unused -> resourceTable)
+          .buildGraph(model);
+    }
     ResourceStore resourceStore = model.getResourceStore();
     resourceStore.processToolsAttributes();
     model.keepPossiblyReferencedResources();
@@ -164,9 +182,14 @@
             .filter(r -> !r.isReachable())
             .map(r -> r.value)
             .collect(Collectors.toList());
-    ResourceTable shrunkenResourceTable =
-        ResourceTableUtilKt.nullOutEntriesWithIds(loadedResourceTable, resourceIdsToRemove);
-    return new ShrinkerResult(resEntriesToKeep.build(), shrunkenResourceTable.toByteArray());
+    Map<FeatureSplit, ResourceTable> shrunkenTables = new HashMap<>();
+    for (Entry<PathAndBytes, FeatureSplit> entry : resourceTables.entrySet()) {
+      ResourceTable shrunkenResourceTable =
+          ResourceTableUtilKt.nullOutEntriesWithIds(
+              ResourceTable.parseFrom(entry.getKey().bytes), resourceIdsToRemove);
+      shrunkenTables.put(entry.getValue(), shrunkenResourceTable);
+    }
+    return new ShrinkerResult(resEntriesToKeep.build(), shrunkenTables);
   }
 
   // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency
@@ -236,15 +259,17 @@
 
   public static class ShrinkerResult {
     private final Set<String> resFolderEntriesToKeep;
-    private final byte[] resourceTableInProtoFormat;
+    private final Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat;
 
-    public ShrinkerResult(Set<String> resFolderEntriesToKeep, byte[] resourceTableInProtoFormat) {
+    public ShrinkerResult(
+        Set<String> resFolderEntriesToKeep,
+        Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat) {
       this.resFolderEntriesToKeep = resFolderEntriesToKeep;
       this.resourceTableInProtoFormat = resourceTableInProtoFormat;
     }
 
-    public byte[] getResourceTableInProtoFormat() {
-      return resourceTableInProtoFormat;
+    public byte[] getResourceTableInProtoFormat(FeatureSplit featureSplit) {
+      return resourceTableInProtoFormat.get(featureSplit).toByteArray();
     }
 
     public Set<String> getResFolderEntriesToKeep() {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index d8ac6a9..8273c96 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -19,6 +19,7 @@
 import java.util.List;
 
 public class R8ResourceShrinkerState {
+
   private R8ResourceShrinkerModel r8ResourceShrinkerModel;
 
   public List<String> trace(int id) {
@@ -32,6 +33,10 @@
     r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream);
   }
 
+  public R8ResourceShrinkerModel getR8ResourceShrinkerModel() {
+    return r8ResourceShrinkerModel;
+  }
+
   public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
 
     public R8ResourceShrinkerModel(
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index efbc507..0bc2093 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -634,19 +634,12 @@
           // null to static field.
           .put(
               "134-reg-promotion",
-              TestCondition.or(
-                  TestCondition.match(
-                      compilers(CompilerUnderTest.R8),
-                      TestCondition.runtimes(DexVm.Version.V10_0_0, DexVm.Version.V12_0_0)),
-                  TestCondition.match(
-                      compilers(CompilerUnderTest.D8_AFTER_R8CF, CompilerUnderTest.R8_AFTER_D8),
-                      TestCondition.runtimes(
-                          DexVm.Version.V4_0_4,
-                          DexVm.Version.V8_1_0,
-                          DexVm.Version.V9_0_0,
-                          DexVm.Version.V10_0_0,
-                          DexVm.Version.V12_0_0,
-                          DexVm.Version.DEFAULT))))
+              TestCondition.match(
+                  compilers(
+                      CompilerUnderTest.R8,
+                      CompilerUnderTest.D8_AFTER_R8CF,
+                      CompilerUnderTest.R8_AFTER_D8),
+                  TestCondition.runtimes(DexVm.Version.V10_0_0, DexVm.Version.V12_0_0)))
           .put(
               "461-get-reference-vreg",
               TestCondition.match(
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b24f608..7ae3463 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -51,6 +51,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -83,6 +84,7 @@
   private final List<String> applyMappingMaps = new ArrayList<>();
   private final List<Path> features = new ArrayList<>();
   private Path resourceShrinkerOutput = null;
+  private HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
   private PartitionMapConsumer partitionMapConsumer = null;
 
   @Override
@@ -170,7 +172,8 @@
             getMinApiLevel(),
             features,
             residualArtProfiles,
-            resourceShrinkerOutput);
+            resourceShrinkerOutput,
+            resourceShrinkerOutputForFeatures);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -878,19 +881,33 @@
         testResource, getState().getNewTempFile("resourceshrinkeroutput.zip"));
   }
 
-  public T addAndroidResources(AndroidTestResource testResource, Path output) throws IOException {
-    addResourceShrinkerProviderAndConsumer(testResource.getResourceZip(), output);
+  public T addFeatureSplitAndroidResources(AndroidTestResource testResource, String featureName)
+      throws IOException {
+    Path outputFile = getState().getNewTempFile("resourceshrinkeroutput_" + featureName + ".zip");
+    resourceShrinkerOutputForFeatures.put(featureName, outputFile);
+    getBuilder()
+        .addFeatureSplit(
+            featureSplitGenerator -> {
+              featureSplitGenerator.setAndroidResourceConsumer(
+                  new ArchiveProtoAndroidResourceConsumer(outputFile));
+              Path resourceZip = testResource.getResourceZip();
+              featureSplitGenerator.setAndroidResourceProvider(
+                  new ArchiveProtoAndroidResourceProvider(
+                      resourceZip, new PathOrigin(resourceZip)));
+              return featureSplitGenerator.build();
+            });
     return addProgramClassFileData(testResource.getRClass().getClassFileData());
   }
 
-  private T addResourceShrinkerProviderAndConsumer(Path resources, Path output) throws IOException {
+  public T addAndroidResources(AndroidTestResource testResource, Path output) throws IOException {
+    Path resources = testResource.getResourceZip();
     resourceShrinkerOutput = output;
     getBuilder()
         .setAndroidResourceProvider(
             new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
-    getBuilder()
-        .setAndroidResourceConsumer(
-            new ArchiveProtoAndroidResourceConsumer(resourceShrinkerOutput));
-    return self();
+    getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
+    self();
+    return addProgramClassFileData(testResource.getRClass().getClassFileData());
   }
+
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 82997e9..20b9f2d 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -27,7 +27,9 @@
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
@@ -40,6 +42,7 @@
   private final List<Path> features;
   private final List<ExternalArtProfile> residualArtProfiles;
   private final Path resourceShrinkerOutput;
+  private final Map<String, Path> resourceShrinkerOutputForFeatures;
 
   R8TestCompileResult(
       TestState state,
@@ -53,7 +56,8 @@
       int minApiLevel,
       List<Path> features,
       List<ExternalArtProfile> residualArtProfiles,
-      Path resourceShrinkerOutput) {
+      Path resourceShrinkerOutput,
+      HashMap<String, Path> resourceShrinkerOutputForFeatures) {
     super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
@@ -62,6 +66,7 @@
     this.features = features;
     this.residualArtProfiles = residualArtProfiles;
     this.resourceShrinkerOutput = resourceShrinkerOutput;
+    this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
   }
 
   @Override
@@ -164,6 +169,14 @@
     return self();
   }
 
+  public <E extends Throwable> R8TestCompileResult inspectShrunkenResourcesForFeature(
+      Consumer<ResourceTableInspector> consumer, String featureName) throws IOException {
+    Path path = resourceShrinkerOutputForFeatures.get(featureName);
+    assertNotNull(path);
+    consumer.accept(new ResourceTableInspector(ZipUtils.readSingleEntry(path, "resources.pb")));
+    return self();
+  }
+
   public GraphInspector graphInspector() throws IOException {
     assert graphConsumer != null;
     return new GraphInspector(graphConsumer, inspector());
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 6b4518d..9d47152 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1742,8 +1742,25 @@
     return AndroidApiLevel.O;
   }
 
-  public static boolean canUseNativeRecords(TestParameters parameters) {
-    return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+  public static AndroidApiLevel apiLevelWithRecordSupport() {
+    // TODO(b/293591931): Return something when records are stable in Platform (expecting Android
+    // V).
+    throw new Unreachable();
+  }
+
+  public static boolean isRecordsDesugaredForD8(TestParameters parameters) {
+    assert parameters.getApiLevel() != null;
+    // TODO(b/293591931): Return true for some API level when records are stable in Platform
+    //  (expecting Android V) using TestBase.apiLevelWithRecordSupport().
+    return true;
+  }
+
+  public static boolean isRecordsDesugaredForR8(TestParameters parameters) {
+    assert parameters.getApiLevel() != null;
+    // TODO(b/293591931): Also return true for some API level when records are stable in Platform
+    //  (expecting Android V) using TestBase.apiLevelWithRecordSupport(). Note that R8 with class
+    //  file output never performs desugaring.
+    return !parameters.getRuntime().isCf();
   }
 
   public static boolean canUseJavaUtilObjects(TestParameters parameters) {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 7db5069..5349d73 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.testing.AndroidBuildVersion;
@@ -25,6 +26,8 @@
 import com.android.tools.r8.utils.codeinspector.ArgumentPropagatorCodeScannerResultInspector;
 import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.MinificationInspector;
+import com.android.tools.r8.utils.codeinspector.RepackagingInspector;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableSet;
@@ -43,6 +46,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -195,13 +199,22 @@
 
   public T addHorizontallyMergedClassesInspector(
       ThrowableConsumer<HorizontallyMergedClassesInspector> inspector) {
+    return addHorizontallyMergedClassesInspector(inspector, HorizontalClassMerger.Mode::isFinal);
+  }
+
+  public T addHorizontallyMergedClassesInspector(
+      ThrowableConsumer<HorizontallyMergedClassesInspector> inspector,
+      Predicate<HorizontalClassMerger.Mode> predicate) {
     return addOptionsModification(
         options ->
             options.testing.horizontallyMergedClassesConsumer =
-                ((dexItemFactory, horizontallyMergedClasses) ->
+                ((dexItemFactory, horizontallyMergedClasses, mode) -> {
+                  if (predicate.test(mode)) {
                     inspector.acceptWithRuntimeException(
                         new HorizontallyMergedClassesInspector(
-                            dexItemFactory, horizontallyMergedClasses))));
+                            dexItemFactory, horizontallyMergedClasses));
+                  }
+                }));
   }
 
   public T addHorizontallyMergedClassesInspectorIf(
@@ -212,6 +225,24 @@
     return self();
   }
 
+  public T addMinificationInspector(ThrowableConsumer<MinificationInspector> inspector) {
+    return addOptionsModification(
+        options ->
+            options.testing.namingLensConsumer =
+                ((dexItemFactory, namingLens) ->
+                    inspector.acceptWithRuntimeException(
+                        new MinificationInspector(dexItemFactory, namingLens))));
+  }
+
+  public T addRepackagingInspector(ThrowableConsumer<RepackagingInspector> inspector) {
+    return addOptionsModification(
+        options ->
+            options.testing.repackagingLensConsumer =
+                ((dexItemFactory, repackagingLens) ->
+                    inspector.acceptWithRuntimeException(
+                        new RepackagingInspector(dexItemFactory, repackagingLens))));
+  }
+
   public T addVerticallyMergedClassesInspector(
       Consumer<VerticallyMergedClassesInspector> inspector) {
     return addOptionsModification(
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index c03fadc..2d7a5c0 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -416,6 +416,7 @@
       }
 
       public boolean hasRecordsSupport() {
+        // Records support is present from Android U.
         return isNewerThanOrEqual(V14_0_0);
       }
 
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java
index f345fa5..f2ca6d3 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java
@@ -186,9 +186,9 @@
             .addProgramClasses(mainClass)
             .addProgramClasses(CLASSES)
             .addOptionsModification(
-                o -> {
-                  o.inlinerOptions().enableInlining = false;
-                  o.enableVerticalClassMerging = false;
+                options -> {
+                  options.inlinerOptions().enableInlining = false;
+                  options.getVerticalClassMergerOptions().disable();
                 })
             .addDontObfuscate()
             .addKeepMainRule(mainClass)
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index d41508e..f1cb778 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -155,7 +155,9 @@
         testForR8(parameters.getBackend())
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
             .addKeepMainRule(mainClass)
-            .addOptionsModification(o -> o.enableVerticalClassMerging = enableVerticalClassMerging)
+            .addOptionsModification(
+                options ->
+                    options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging))
             .enableConstantArgumentAnnotations()
             .enableNeverClassInliningAnnotations()
             .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 63de716..0ee66c7 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -14,10 +14,12 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.transformers.MethodTransformer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.StreamUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.MoreCollectors;
 import com.google.protobuf.InvalidProtocolBufferException;
@@ -27,6 +29,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -41,6 +44,7 @@
   enum RClassType {
     STRING,
     DRAWABLE,
+    STYLEABLE,
     XML;
 
     public static RClassType fromClass(Class clazz) {
@@ -141,6 +145,10 @@
       return mapping.containsKey(type) && mapping.get(type).containsValueFor(name);
     }
 
+    public Collection<String> entriesForType(String type) {
+      return mapping.get(type).mapping.keySet();
+    }
+
     public static class ResourceNameToValueMapping {
       private final Map<String, List<ResourceValue>> mapping = new HashMap<>();
 
@@ -198,20 +206,28 @@
     }
 
     public void assertContainsResourceWithName(String type, String name) {
-      Assert.assertTrue(testResourceTable.containsValueFor(type, name));
+      Assert.assertTrue(
+          StringUtils.join(",", entries(type)), testResourceTable.containsValueFor(type, name));
     }
 
     public void assertDoesNotContainResourceWithName(String type, String name) {
-      Assert.assertFalse(testResourceTable.containsValueFor(type, name));
+      Assert.assertFalse(
+          StringUtils.join(",", entries(type)), testResourceTable.containsValueFor(type, name));
+    }
+
+    public Collection<String> entries(String type) {
+      return testResourceTable.entriesForType(type);
     }
   }
 
   public static class AndroidTestResourceBuilder {
     private String manifest;
     private final Map<String, String> stringValues = new TreeMap<>();
+    private final Map<String, Integer> styleables = new TreeMap<>();
     private final Map<String, byte[]> drawables = new TreeMap<>();
     private final Map<String, String> xmlFiles = new TreeMap<>();
     private final List<Class<?>> classesToRemap = new ArrayList<>();
+    private int packageId = 0x7f;
 
     // Create the android resources from the passed in R classes
     // All values will be generated based on the fields in the class.
@@ -230,6 +246,10 @@
           if (rClassType == RClassType.DRAWABLE) {
             addDrawable(name, TINY_PNG);
           }
+          if (rClassType == RClassType.STYLEABLE) {
+            // Add 4 different values, i.e., the array will be 4 integers.
+            addStyleable(name, 4);
+          }
         }
       }
       return this;
@@ -252,11 +272,21 @@
       return this;
     }
 
+    AndroidTestResourceBuilder addStyleable(String name, int numberOfValues) {
+      styleables.put(name, numberOfValues);
+      return this;
+    }
+
     AndroidTestResourceBuilder addStringValue(String name, String value) {
       stringValues.put(name, value);
       return this;
     }
 
+    AndroidTestResourceBuilder setPackageId(int packageId) {
+      this.packageId = packageId;
+      return this;
+    }
+
     AndroidTestResourceBuilder addDrawable(String name, byte[] value) {
       drawables.put(name, value);
       return this;
@@ -266,11 +296,13 @@
       Path manifestPath =
           FileUtils.writeTextFile(temp.newFile("AndroidManifest.xml").toPath(), this.manifest);
       Path resFolder = temp.newFolder("res").toPath();
+      Path valuesFolder = temp.newFolder("res", "values").toPath();
       if (stringValues.size() > 0) {
+        FileUtils.writeTextFile(valuesFolder.resolve("strings.xml"), createStringResourceXml());
+      }
+      if (styleables.size() > 0) {
         FileUtils.writeTextFile(
-            temp.newFolder("res", "values").toPath().resolve("strings.xml"),
-            createStringResourceXml());
-
+            valuesFolder.resolve("styleables.xml"), createStyleableResourceXml());
       }
       if (drawables.size() > 0) {
         File drawableFolder = temp.newFolder("res", "drawable");
@@ -288,7 +320,7 @@
 
       Path output = temp.newFile("resources.zip").toPath();
       Path rClassOutputDir = temp.newFolder("aapt_R_class").toPath();
-      compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp);
+      compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp, packageId);
       Path rClassJavaFile =
           Files.walk(rClassOutputDir)
               .filter(path -> path.endsWith("R.java"))
@@ -349,6 +381,19 @@
                               // Don't make the inner<>outer class connection
                             }
                           })
+                      .addMethodTransformer(
+                          new MethodTransformer() {
+                            @Override
+                            public void visitFieldInsn(
+                                int opcode, String owner, String name, String descriptor) {
+                              String maybeTransformedOwner =
+                                  isInnerRClass(owner)
+                                      ? noNamespaceToProgramMap.getOrDefault(
+                                          rClassWithoutNamespaceAndOuter(owner), owner)
+                                      : owner;
+                              super.visitFieldInsn(opcode, maybeTransformedOwner, name, descriptor);
+                            }
+                          })
                       .transform());
             }
           });
@@ -364,10 +409,36 @@
       stringBuilder.append("</resources>");
       return stringBuilder.toString();
     }
+
+    private String createStyleableResourceXml() {
+      StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+      styleables.forEach(
+          (name, value) -> {
+            stringBuilder.append("<declare-styleable name=\"" + name + "\">\n");
+            // For every entry we add here we will have an additional array entry pointing
+            // at the boolean attr in the resource table. We will also get a name_attri R class
+            // entry to select into the generated array.
+            for (Integer i = 0; i < value; i++) {
+              stringBuilder.append("<attr name=\"attr_" + name + i + "\" format=\"boolean\" />\n");
+            }
+            stringBuilder.append("</declare-styleable>");
+          });
+      stringBuilder.append("</resources>");
+      return stringBuilder.toString();
+    }
+  }
+
+  public static void dumpWithAapt2(Path path) throws IOException {
+    System.out.println(ToolHelper.runAapt2("dump", "resources", path.toString()));
   }
 
   public static void compileWithAapt2(
-      Path resFolder, Path manifest, Path rClassFolder, Path resourceZip, TemporaryFolder temp)
+      Path resFolder,
+      Path manifest,
+      Path rClassFolder,
+      Path resourceZip,
+      TemporaryFolder temp,
+      int packageId)
       throws IOException {
     Path compileOutput = temp.newFile("compiled.zip").toPath();
     ProcessResult compileProcessResult =
@@ -384,8 +455,14 @@
             resourceZip.toString(),
             "--java",
             rClassFolder.toString(),
+            "--non-final-ids",
             "--manifest",
             manifest.toString(),
+            "--package-id",
+            "" + packageId,
+            "--allow-reserved-package-id",
+            "--rename-resources-package",
+            "thepackage" + packageId + ".foobar",
             "--proto-format",
             compileOutput.toString());
     failOnError(linkProcesResult);
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
index c09a5b3..7cb981b 100644
--- a/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.androidresources;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -75,15 +76,15 @@
     CodeInspector inspector = new CodeInspector(resourceApp);
     ClassSubject stringClazz = inspector.clazz(R.string.class);
     // Implicitly added with the manifest
-    ensureIntFieldWithValue(stringClazz, "app_name", 0x7f020000);
+    ensureIntFieldWithValue(stringClazz, "app_name");
 
-    ensureIntFieldWithValue(stringClazz, "bar", 0x7f020001);
-    ensureIntFieldWithValue(stringClazz, "foo", 0x7f020002);
-    ensureIntFieldWithValue(inspector.clazz(R.drawable.class), "foobar", 0x7f010000);
+    ensureIntFieldWithValue(stringClazz, "bar");
+    ensureIntFieldWithValue(stringClazz, "foo");
+    ensureIntFieldWithValue(inspector.clazz(R.drawable.class), "foobar");
   }
 
-  private void ensureIntFieldWithValue(ClassSubject clazz, String name, int value) {
-    assertEquals(clazz.field("int", name).getStaticValue().asDexValueInt().value, value);
+  private void ensureIntFieldWithValue(ClassSubject clazz, String name) {
+    assertTrue(clazz.field("int", name).isPresent());
   }
 
   public static class FooBar {
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
new file mode 100644
index 0000000..2b257b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.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.androidresources;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.androidresources.ResourceShrinkingWithFeatures.FeatureSplit.FeatureSplitMain;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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 ResourceShrinkingWithFeatures extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class)
+        .build(temp);
+  }
+
+  public static AndroidTestResource getFeatureSplitTestResources(TemporaryFolder temp)
+      throws IOException {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .setPackageId(0x7E)
+        .addRClassInitializeWithDefaultValues(FeatureSplit.R.string.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testFailureIfNotResourcesOrCode() throws Exception {
+    try {
+      testForR8(parameters.getBackend())
+          .setMinApi(parameters)
+          .addProgramClasses(Base.class)
+          .addFeatureSplit(builder -> builder.build())
+          .compileWithExpectedDiagnostics(
+              diagnostics -> {
+                diagnostics.assertErrorThatMatches(diagnosticException(AssertionError.class));
+              });
+    } catch (CompilationFailedException e) {
+      return;
+    }
+    fail();
+  }
+
+  @Test
+  public void testR8ReferenceFeatureResourcesFromFeature() throws Exception {
+    // We reference a feature resource from a feature.
+    testR8(false);
+  }
+
+  @Test
+  public void testR8ReferenceFeatureResourcesFromBase() throws Exception {
+    // We reference a feature resource from the base.
+    testR8(true);
+  }
+
+  private void testR8(boolean referenceFromBase) throws Exception {
+    TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest();
+    featureSplitTemp.create();
+    R8FullTestBuilder r8FullTestBuilder =
+        testForR8(parameters.getBackend()).setMinApi(parameters).addProgramClasses(Base.class);
+    if (referenceFromBase) {
+      r8FullTestBuilder.addProgramClasses(FeatureSplit.FeatureSplitMain.class);
+    } else {
+      r8FullTestBuilder.addFeatureSplit(FeatureSplit.FeatureSplitMain.class);
+    }
+    R8TestCompileResult compile =
+        r8FullTestBuilder
+            .addAndroidResources(getTestResources(temp))
+            .addFeatureSplitAndroidResources(
+                getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
+            .addKeepMainRule(Base.class)
+            .addKeepMainRule(FeatureSplitMain.class)
+            .compile();
+    compile
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "base_used");
+              resourceTableInspector.assertDoesNotContainResourceWithName("string", "base_unused");
+            })
+        .inspectShrunkenResourcesForFeature(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "feature_used");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "feature_unused");
+            },
+            FeatureSplit.class.getName())
+        .run(parameters.getRuntime(), Base.class)
+        .assertSuccess();
+  }
+
+  public static class Base {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.string.base_used);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+
+      public static int base_used;
+      public static int base_unused;
+    }
+  }
+
+  public static class FeatureSplit {
+    public static class FeatureSplitMain {
+      public static void main(String[] args) {
+        if (System.currentTimeMillis() == 0) {
+          System.out.println(R.string.feature_used);
+        }
+      }
+    }
+
+    public static class R {
+      public static class string {
+        public static int feature_used;
+        public static int feature_unused;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestOptimizedShrinking.java b/src/test/java/com/android/tools/r8/androidresources/TestOptimizedShrinking.java
new file mode 100644
index 0000000..7a396ef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestOptimizedShrinking.java
@@ -0,0 +1,131 @@
+// 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.androidresources;
+
+import com.android.tools.r8.ResourceShrinkerConfiguration;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.androidresources.TestOptimizedShrinking.R.styleable;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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 TestOptimizedShrinking extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean debug;
+
+  @Parameter(2)
+  public boolean optimized;
+
+  @Parameters(name = "{0}, debug: {1}, optimized: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class, R.styleable.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    AndroidTestResource testResources = getTestResources(temp);
+
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(testResources)
+        .addKeepMainRule(FooBar.class)
+        .applyIf(
+            optimized,
+            builder ->
+                builder.addOptionsModification(
+                    internalOptions ->
+                        internalOptions.resourceShrinkerConfiguration =
+                            ResourceShrinkerConfiguration.builder(null)
+                                .enableOptimizedShrinkingWithR8()
+                                .build()))
+        .applyIf(debug, builder -> builder.debug())
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+              resourceTableInspector.assertContainsResourceWithName("drawable", "foobar");
+              // In debug mode legacy shrinker will not attribute our $R inner class as an R class
+              // (this is only used for testing, _real_ R classes are not inner classes.
+              if (!debug || optimized) {
+                resourceTableInspector.assertDoesNotContainResourceWithName(
+                    "string", "unused_string");
+                resourceTableInspector.assertDoesNotContainResourceWithName(
+                    "drawable", "unused_drawable");
+              }
+              resourceTableInspector.assertContainsResourceWithName("styleable", "our_styleable");
+              // The resource shrinker is currently keeping all styleables,
+              // so we don't remove this even when it is unused.
+              resourceTableInspector.assertContainsResourceWithName(
+                  "styleable", "unused_styleable");
+              // We do remove the attributes pointed at by the unreachable styleable.
+              for (int i = 0; i < 4; i++) {
+                resourceTableInspector.assertContainsResourceWithName(
+                    "attr", "attr_our_styleable" + i);
+                if (!debug || optimized) {
+                  resourceTableInspector.assertDoesNotContainResourceWithName(
+                      "attr", "attr_unused_styleable" + i);
+                }
+              }
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.drawable.foobar);
+        System.out.println(R.string.bar);
+        System.out.println(R.string.foo);
+        System.out.println(styleable.our_styleable);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+      public static int bar;
+      public static int foo;
+      public static int unused_string;
+    }
+
+    public static class styleable {
+      public static int[] our_styleable;
+      public static int[] unused_styleable;
+    }
+
+    public static class drawable {
+
+      public static int foobar;
+      public static int unused_drawable;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java
index 27a1252..1dab3a8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingAfterConstructorShrinkingWithRepackagingTest.java
@@ -148,6 +148,7 @@
 
     @KeepConstantArguments
     @KeepUnusedArguments
+    @NeverInline
     public Parent(Parent parent) {}
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
index c67f07e..f3fe7df 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
@@ -10,9 +10,8 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.google.common.collect.ImmutableSet;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runners.Parameterized;
@@ -39,8 +38,7 @@
         .addKeepMainRule(Main.class)
         .enableNeverClassInliningAnnotations()
         .addKeepAttributeInnerClassesAndEnclosingMethod()
-        .addOptionsModification(
-            options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .setMinApi(parameters)
         .addOptionsModification(
             options ->
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
index 5405ddb..e925268 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
@@ -11,9 +11,8 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
 
 public class LargeConstructorsMergingTest extends HorizontalClassMergingTestBase {
@@ -26,11 +25,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .addOptionsModification(
-            options -> {
-              options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-              options.testing.verificationSizeLimitInBytesOverride = 4;
-            })
+        .addOptionsModification(options -> options.testing.verificationSizeLimitInBytesOverride = 4)
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters)
         .addHorizontallyMergedClassesInspector(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
index def0d9e..215fe76 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
@@ -12,8 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.classmerging.horizontal.testclasses.A;
 import com.android.tools.r8.classmerging.horizontal.testclasses.B;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.google.common.collect.ImmutableSet;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import org.junit.Test;
 
 public class PackagePrivateMemberAccessTest extends HorizontalClassMergingTestBase {
@@ -28,8 +27,7 @@
         .addInnerClasses(getClass())
         .addProgramClasses(A.class, B.class)
         .addKeepMainRule(Main.class)
-        .addOptionsModification(
-            options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
index 6e898e9..46b277b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
@@ -12,9 +12,8 @@
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
 
 public class SynchronizedClassesTest extends HorizontalClassMergingTestBase {
@@ -33,8 +32,7 @@
                     .assertMergedInto(C.class, A.class)
                     .assertMergedInto(D.class, B.class)
                     .assertNoOtherClassesMerged())
-        .addOptionsModification(
-            options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
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 93aad99..8cd641d 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
@@ -4,10 +4,16 @@
 
 package com.android.tools.r8.classmerging.vertical;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -21,22 +27,41 @@
   @Parameter(0)
   public TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameter(1)
+  public boolean verifyLensLookup;
+
+  @Parameters(name = "{0}, verify: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
   @Test
   public void test() throws Exception {
-    testForR8(parameters.getBackend())
-        .addInnerClasses(IncorrectRewritingOfInvokeSuperTest.class)
-        .addKeepMainRule(TestClass.class)
-        .addOptionsModification(options -> options.enableUnusedInterfaceRemoval = false)
-        .enableInliningAnnotations()
-        .addDontObfuscate()
-        .setMinApi(parameters)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccess();
+    Box<R8TestCompileResult> compileResult = new Box<>();
+    AssertUtils.assertFailsCompilationIf(
+        verifyLensLookup,
+        () ->
+            compileResult.set(
+                testForR8(parameters.getBackend())
+                    .addInnerClasses(IncorrectRewritingOfInvokeSuperTest.class)
+                    .addKeepMainRule(TestClass.class)
+                    .addOptionsModification(
+                        options -> {
+                          options.enableUnusedInterfaceRemoval = false;
+                          options.testing.enableVerticalClassMergerLensAssertion = verifyLensLookup;
+                        })
+                    .enableInliningAnnotations()
+                    .addDontObfuscate()
+                    .setMinApi(parameters)
+                    .compile()));
+
+    if (!compileResult.isSet()) {
+      assertTrue(verifyLensLookup);
+      return;
+    }
+
+    compileResult.get().run(parameters.getRuntime(), TestClass.class).assertSuccess();
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceAccessibleAfterVerticalClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceAccessibleAfterVerticalClassMergingTest.java
new file mode 100644
index 0000000..83063ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceAccessibleAfterVerticalClassMergingTest.java
@@ -0,0 +1,59 @@
+// 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.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.classmerging.vertical.testclasses.InterfaceAccessibleAfterVerticalClassMergingTestClasses;
+import com.android.tools.r8.classmerging.vertical.testclasses.InterfaceAccessibleAfterVerticalClassMergingTestClasses.A;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+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 InterfaceAccessibleAfterVerticalClassMergingTest 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(), InterfaceAccessibleAfterVerticalClassMergingTestClasses.class)
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoAccessModificationAnnotationsForClasses()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new B());
+    }
+  }
+
+  public static class B extends A {
+
+    @Override
+    public String toString() {
+      return "B";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/SyntheticBridgeSignaturesTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/SyntheticBridgeSignaturesTest.java
index b7bb268..a16c9fb 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/SyntheticBridgeSignaturesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/SyntheticBridgeSignaturesTest.java
@@ -13,11 +13,10 @@
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
-import com.google.common.collect.ImmutableSet;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -51,12 +50,9 @@
         testForR8(parameters.getBackend())
             .addInnerClasses(getClass())
             .addKeepMainRule(TestClass.class)
-            .addOptionsModification(
-                options -> {
-                  if (!allowInlining) {
-                    options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-                  }
-                })
+            .applyIf(
+                !allowInlining,
+                builder -> builder.addOptionsModification(InlinerOptions::setOnlyForceInlining))
             .addVerticallyMergedClassesInspector(this::inspectVerticallyMergedClasses)
             .enableInliningAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
index b42cd2e..466e9fd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
@@ -12,10 +12,9 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
-import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
@@ -52,9 +51,9 @@
         .addOptionsModification(
             options -> {
               options.forceProguardCompatibility = true;
-              // The initializer is small in this example so only allow FORCE inlining.
-              options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
             })
+        // The initializer is small in this example so only allow FORCE inlining.
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .addVerticallyMergedClassesInspector(
             VerticallyMergedClassesInspector::assertNoClassesMerged)
         .compile()
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 7ec72c7..7db55ae 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
@@ -36,6 +36,7 @@
 import com.android.tools.r8.utils.AndroidApp.Builder;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -271,9 +272,7 @@
                 testForR8(parameters.getBackend())
                     .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                     .addOptionsModification(this::configure)
-                    .addOptionsModification(
-                        options ->
-                            options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+                    .addOptionsModification(InlinerOptions::setOnlyForceInlining)
                     .allowUnusedProguardConfigurationRules(),
                 main,
                 readProgramFiles(programFiles),
@@ -504,10 +503,8 @@
                 .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                 .addOptionsModification(this::configure)
                 .addOptionsModification(
-                    options -> {
-                      options.enableVerticalClassMerging = false;
-                      options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-                    })
+                    options -> options.getVerticalClassMergerOptions().disable())
+                .addOptionsModification(InlinerOptions::setOnlyForceInlining)
                 .allowUnusedProguardConfigurationRules(),
             main,
             readProgramFiles(programFiles),
@@ -527,8 +524,7 @@
             testForR8(parameters.getBackend())
                 .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                 .addOptionsModification(this::configure)
-                .addOptionsModification(
-                    options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+                .addOptionsModification(InlinerOptions::setOnlyForceInlining)
                 .allowUnusedProguardConfigurationRules(),
             main,
             readProgramFiles(programFiles),
@@ -577,10 +573,8 @@
                 .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                 .addOptionsModification(this::configure)
                 .addOptionsModification(
-                    options -> {
-                      options.enableVerticalClassMerging = false;
-                      options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-                    })
+                    options -> options.getVerticalClassMergerOptions().disable())
+                .addOptionsModification(InlinerOptions::setOnlyForceInlining)
                 .allowUnusedProguardConfigurationRules(),
             main,
             readProgramFiles(programFiles),
@@ -608,8 +602,7 @@
             testForR8(parameters.getBackend())
                 .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                 .addOptionsModification(this::configure)
-                .addOptionsModification(
-                    options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
+                .addOptionsModification(InlinerOptions::setOnlyForceInlining)
                 .allowUnusedProguardConfigurationRules(),
             main,
             readProgramFiles(programFiles),
@@ -663,9 +656,8 @@
                 .addOptionsModification(this::configure)
                 .addOptionsModification(
                     options -> {
-                      options.enableVerticalClassMerging = false;
-                      options.testing.validInliningReasons =
-                          ImmutableSet.of(Reason.ALWAYS, Reason.FORCE);
+                      options.getVerticalClassMergerOptions().disable();
+                      options.testing.validInliningReasons = ImmutableSet.of(Reason.ALWAYS);
                     })
                 .allowUnusedProguardConfigurationRules(),
             main,
@@ -703,10 +695,8 @@
                             + " method(); }"))
                 .addOptionsModification(this::configure)
                 .addOptionsModification(
-                    options -> {
-                      options.testing.validInliningReasons =
-                          ImmutableSet.of(Reason.ALWAYS, Reason.FORCE);
-                    })
+                    options ->
+                        options.testing.validInliningReasons = ImmutableSet.of(Reason.ALWAYS))
                 .allowUnusedProguardConfigurationRules(),
             main,
             readProgramFiles(programFiles),
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/InterfaceAccessibleAfterVerticalClassMergingTestClasses.java b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/InterfaceAccessibleAfterVerticalClassMergingTestClasses.java
new file mode 100644
index 0000000..6331ea7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/InterfaceAccessibleAfterVerticalClassMergingTestClasses.java
@@ -0,0 +1,18 @@
+// 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.testclasses;
+
+import com.android.tools.r8.NoAccessModification;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+
+public class InterfaceAccessibleAfterVerticalClassMergingTestClasses {
+
+  @NoAccessModification
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  public static class A implements I {}
+}
diff --git a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
index 7c7a868..abf86b0 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
@@ -9,17 +9,33 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.compatproguard.CompatProguard.CompatProguardOptions;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
-public class CompatProguardTest {
+@RunWith(Parameterized.class)
+public class CompatProguardTest extends TestBase {
 
-  private CompatProguardOptions parseArgs(String... args) {
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public CompatProguardTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  private static CompatProguardOptions parseArgs(String... args) {
     return CompatProguard.CompatProguardOptions.parse(args);
   }
 
   @Test
-  public void testDefaultDataResources() throws Exception {
+  public void testDefaultDataResources() {
     CompatProguardOptions options = parseArgs();
     assertNull(options.output);
     assertEquals(1, options.minApi);
@@ -31,13 +47,13 @@
   }
 
   @Test
-  public void testShortLine() throws Exception {
+  public void testShortLine() {
     CompatProguardOptions options = parseArgs("-");
     assertEquals(1, options.proguardConfig.size());
   }
 
   @Test
-  public void testProguardOptions() throws Exception {
+  public void testProguardOptions() {
     CompatProguardOptions options;
 
     options = parseArgs("-xxx");
@@ -51,7 +67,7 @@
   }
 
   @Test
-  public void testInjarsAndOutput() throws Exception {
+  public void testInjarsAndOutput() {
     CompatProguardOptions options;
     String injars = "input.jar";
     String output = "outputdir";
@@ -63,7 +79,7 @@
   }
 
   @Test
-  public void testMainDexList() throws Exception {
+  public void testMainDexList() {
     CompatProguardOptions options;
     String mainDexList = "maindexlist.txt";
 
@@ -78,30 +94,21 @@
   }
 
   @Test
-  public void testInclude() throws Exception {
+  public void testInclude() {
     CompatProguardOptions options = parseArgs("-include --my-include-file.txt");
     assertEquals(1, options.proguardConfig.size());
     assertEquals("-include --my-include-file.txt", options.proguardConfig.get(0));
   }
 
   @Test
-  public void testNoLocalsOption() throws Exception {
+  public void testNoLocalsOption() {
     CompatProguardOptions options = parseArgs("--no-locals");
     assertEquals(0, options.proguardConfig.size());
   }
 
   @Test
-  public void testNoDataResources() throws Exception {
+  public void testNoDataResources() {
     CompatProguardOptions options = parseArgs("--no-data-resources");
     assertFalse(options.includeDataResources);
   }
-
-  @Test
-  public void testDisableVerticalClassMerging() throws Exception {
-    CompatProguardOptions enabledOptions = parseArgs();
-    assertFalse(enabledOptions.disableVerticalClassMerging);
-
-    CompatProguardOptions disabledOptions = parseArgs("--no-vertical-class-merging");
-    assertTrue(disabledOptions.disableVerticalClassMerging);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java b/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java
new file mode 100644
index 0000000..af5c177
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java
@@ -0,0 +1,161 @@
+// 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.compose;
+
+import static com.google.common.base.Predicates.alwaysTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.android.tools.r8.utils.codeinspector.MinificationInspector;
+import com.android.tools.r8.utils.codeinspector.RepackagingInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.EnumMap;
+import java.util.function.BiFunction;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NestedComposableArgumentPropagationTest extends TestBase {
+
+  enum ComposableFunction {
+    A,
+    B,
+    C
+  }
+
+  static class CodeStats {
+
+    final int numberOfIfInstructions;
+
+    CodeStats(MethodSubject methodSubject) {
+      this.numberOfIfInstructions =
+          (int) methodSubject.streamInstructions().filter(InstructionSubject::isIf).count();
+    }
+  }
+
+  private static Path dump;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @BeforeClass
+  public static void setup() throws IOException {
+    dump = getStaticTemp().newFolder().toPath();
+    ZipUtils.unzip(
+        Paths.get(
+            ToolHelper.THIRD_PARTY_DIR,
+            "opensource-apps/compose-examples/changed-bitwise-value-propagation/dump.zip"),
+        dump);
+  }
+
+  public NestedComposableArgumentPropagationTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void test() throws Exception {
+    EnumMap<ComposableFunction, CodeStats> defaultCodeStats = build(false);
+    EnumMap<ComposableFunction, CodeStats> optimizedCodeStats = build(true);
+    for (ComposableFunction composableFunction : ComposableFunction.values()) {
+      CodeStats defaultCodeStatsForFunction = defaultCodeStats.get(composableFunction);
+      CodeStats optimizedCodeStatsForFunction = optimizedCodeStats.get(composableFunction);
+      assertTrue(
+          composableFunction
+              + ": "
+              + defaultCodeStatsForFunction.numberOfIfInstructions
+              + " vs "
+              + optimizedCodeStatsForFunction.numberOfIfInstructions,
+          defaultCodeStatsForFunction.numberOfIfInstructions
+              > optimizedCodeStatsForFunction.numberOfIfInstructions);
+    }
+  }
+
+  private EnumMap<ComposableFunction, CodeStats> build(boolean enableComposeOptimizations)
+      throws Exception {
+    Box<ClassReference> mainActivityKtClassReference =
+        new Box<>(Reference.classFromTypeName("com.example.MainActivityKt"));
+    R8TestCompileResult compileResult =
+        testForR8(Backend.DEX)
+            .addProgramFiles(dump.resolve("program.jar"))
+            .addClasspathFiles(dump.resolve("classpath.jar"))
+            .addLibraryFiles(dump.resolve("library.jar"))
+            .addKeepRuleFiles(dump.resolve("proguard.config"))
+            .addHorizontallyMergedClassesInspector(
+                updateMainActivityKt(
+                    HorizontallyMergedClassesInspector::getTarget,
+                    mainActivityKtClassReference,
+                    false),
+                alwaysTrue())
+            .addRepackagingInspector(
+                updateMainActivityKt(
+                    RepackagingInspector::getTarget, mainActivityKtClassReference, true))
+            .addMinificationInspector(
+                updateMainActivityKt(
+                    MinificationInspector::getTarget, mainActivityKtClassReference, true))
+            .addOptionsModification(
+                options -> {
+                  options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces();
+                  options.testing.enableComposableOptimizationPass = enableComposeOptimizations;
+                  options.testing.modelUnknownChangedAndDefaultArgumentsToComposableFunctions =
+                      enableComposeOptimizations;
+                })
+            .setMinApi(AndroidApiLevel.N)
+            .allowDiagnosticMessages()
+            .allowUnnecessaryDontWarnWildcards()
+            .allowUnusedDontWarnPatterns()
+            .allowUnusedProguardConfigurationRules()
+            .compile();
+    return createCodeStats(compileResult.inspector().clazz(mainActivityKtClassReference.get()));
+  }
+
+  private EnumMap<ComposableFunction, CodeStats> createCodeStats(
+      ClassSubject mainActivityKtClassSubject) {
+    EnumMap<ComposableFunction, CodeStats> result = new EnumMap<>(ComposableFunction.class);
+    result.put(
+        ComposableFunction.A,
+        new CodeStats(mainActivityKtClassSubject.uniqueMethodWithOriginalName("A")));
+    result.put(
+        ComposableFunction.B,
+        new CodeStats(mainActivityKtClassSubject.uniqueMethodWithOriginalName("B")));
+    result.put(
+        ComposableFunction.C,
+        new CodeStats(mainActivityKtClassSubject.uniqueMethodWithOriginalName("C")));
+    return result;
+  }
+
+  private static <T> ThrowableConsumer<T> updateMainActivityKt(
+      BiFunction<T, ClassReference, ClassReference> targetFn,
+      Box<ClassReference> mainActivityKtClassReference,
+      boolean failIfUnchanged) {
+    return inspector -> {
+      ClassReference targetClass = targetFn.apply(inspector, mainActivityKtClassReference.get());
+      if (failIfUnchanged) {
+        assertNotEquals(mainActivityKtClassReference.get(), targetClass);
+      }
+      mainActivityKtClassReference.set(targetClass);
+    };
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/LongBackportJava9Test.java b/src/test/java/com/android/tools/r8/desugar/backports/LongBackportJava9Test.java
index 7404eed..8ffb905 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/LongBackportJava9Test.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/LongBackportJava9Test.java
@@ -5,17 +5,13 @@
 package com.android.tools.r8.desugar.backports;
 
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static org.hamcrest.CoreMatchers.containsString;
 
-import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
@@ -43,16 +39,4 @@
 
     registerTarget(AndroidApiLevel.T, 17);
   }
-
-  @Test
-  @Override
-  public void testD8() throws Exception {
-    testD8(
-        runResult ->
-            runResult.applyIf(
-                parameters.getDexRuntimeVersion().isEqualTo(Version.V6_0_1)
-                    && parameters.getApiLevel().isGreaterThan(AndroidApiLevel.B),
-                rr -> rr.assertFailureWithErrorThatMatches(containsString("SIGSEGV")),
-                SingleTestRunResult::assertSuccess));
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
index c134dc8..1d3b636 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
@@ -131,10 +131,10 @@
         BackportedMethodRewriter.generateListOfBackportedMethods(libHolder, options);
     Map<DexMethod, Object> failures = new IdentityHashMap<>();
     for (FoundClassSubject clazz : inspector.allClasses()) {
-      if (clazz.toString().startsWith("j$.sun.nio.cs.UTF_8")
+      if (clazz.toString().startsWith("j$.sun.nio.cs.")
           && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O)) {
-        // At high API level, the class UTF_8 is there just for resolution, the field access is
-        // retargeted and the code is unused so it's ok if it does not resolve.
+        // At high API level, the sun.nio.cs classes are there just for resolution, the field
+        // access is retargeted and the code is unused so it's ok if it does not resolve.
         continue;
       }
       for (FoundMethodSubject method : clazz.allMethods()) {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PartialDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PartialDesugaringTest.java
index 7d29fa4..2d01320 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PartialDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PartialDesugaringTest.java
@@ -217,14 +217,10 @@
         && api.isLessThan(AndroidApiLevel.T)) {
       expectedFailures.addAll(FAILURES_FILE_STORE);
     }
-    if (librarySpecification != JDK11_MINIMAL && api.isLessThan(AndroidApiLevel.T)) {
-      if (librarySpecification == JDK8) {
-        if (api.isGreaterThanOrEqualTo(AndroidApiLevel.N)) {
-          expectedFailures.addAll(FAILURES_TO_ARRAY);
-        }
-      } else {
-        expectedFailures.addAll(FAILURES_TO_ARRAY);
-      }
+    if (librarySpecification == JDK8
+        && api.isLessThan(AndroidApiLevel.T)
+        && api.isGreaterThanOrEqualTo(AndroidApiLevel.N)) {
+      expectedFailures.addAll(FAILURES_TO_ARRAY);
     }
     if (jdk11NonMinimal
         && api.isGreaterThanOrEqualTo(AndroidApiLevel.O)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
index bd5554af..6f1b750 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
@@ -18,7 +18,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -54,7 +53,6 @@
     this.compilationSpecification = compilationSpecification;
   }
 
-  @Ignore("b/266401747")
   @Test
   public void test() throws Throwable {
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
index e2c0f74..fd41179 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.specification;
 
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.*;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -64,7 +65,7 @@
   public void testMultiLevelLegacy() throws IOException {
     Assume.assumeTrue(ToolHelper.isLocalDevelopment());
 
-    LibraryDesugaringSpecification legacySpec = LibraryDesugaringSpecification.JDK8;
+    LibraryDesugaringSpecification legacySpec = JDK8;
 
     LegacyToHumanSpecificationConverter converter =
         new LegacyToHumanSpecificationConverter(Timing.empty());
@@ -108,13 +109,13 @@
 
   @Test
   public void testMultiLevelLegacyUsingMain() throws IOException {
-    LibraryDesugaringSpecification legacySpec = LibraryDesugaringSpecification.JDK8;
+    LibraryDesugaringSpecification legacySpec = JDK8;
     testMultiLevelUsingMain(legacySpec);
   }
 
   @Test
   public void testMultiLevelHumanUsingMain() throws IOException {
-    LibraryDesugaringSpecification humanSpec = LibraryDesugaringSpecification.JDK11;
+    LibraryDesugaringSpecification humanSpec = JDK11;
     testMultiLevelUsingMain(humanSpec);
   }
 
@@ -133,12 +134,6 @@
             output,
             options);
 
-    MachineDesugaredLibrarySpecification machineSpecParsed =
-        new MachineDesugaredLibrarySpecificationParser(
-                options.dexItemFactory(), options.reporter, true, AndroidApiLevel.B.getLevel())
-            .parse(StringResource.fromFile(output));
-    assertFalse(machineSpecParsed.getRewriteType().isEmpty());
-
     if (humanSpec == null) {
       return;
     }
@@ -154,24 +149,35 @@
     assertSpecEquals(humanSpec, writtenHumanSpec);
 
     // Validate converted machine spec is identical to the written one.
-    HumanDesugaredLibrarySpecification humanSimpleSpec =
-        new HumanDesugaredLibrarySpecificationParser(
-                options.dexItemFactory(), options.reporter, true, AndroidApiLevel.B.getLevel())
-            .parse(StringResource.fromString(json.get(), Origin.unknown()));
-    HumanToMachineSpecificationConverter converter =
-        new HumanToMachineSpecificationConverter(Timing.empty());
-    DexApplication app = spec.getAppForTesting(options, true);
-    MachineDesugaredLibrarySpecification machineSimpleSpec =
-        converter.convert(humanSimpleSpec, app);
+    for (AndroidApiLevel api :
+        new AndroidApiLevel[] {AndroidApiLevel.B, AndroidApiLevel.N, AndroidApiLevel.O}) {
+      MachineDesugaredLibrarySpecification machineSpecParsed =
+          new MachineDesugaredLibrarySpecificationParser(
+                  options.dexItemFactory(), options.reporter, true, api.getLevel())
+              .parse(StringResource.fromFile(output));
+      assertEquals(
+          api.isGreaterThanOrEqualTo(AndroidApiLevel.O) && spec == JDK8,
+          machineSpecParsed.getRewriteType().isEmpty());
 
-    assertSpecEquals(machineSimpleSpec, machineSpecParsed);
+      HumanDesugaredLibrarySpecification humanSimpleSpec =
+          new HumanDesugaredLibrarySpecificationParser(
+                  options.dexItemFactory(), options.reporter, true, api.getLevel())
+              .parse(StringResource.fromString(json.get(), Origin.unknown()));
+      HumanToMachineSpecificationConverter converter =
+          new HumanToMachineSpecificationConverter(Timing.empty());
+      DexApplication app = spec.getAppForTesting(options, true);
+      MachineDesugaredLibrarySpecification machineSimpleSpec =
+          converter.convert(humanSimpleSpec, app);
+
+      assertSpecEquals(machineSimpleSpec, machineSpecParsed);
+    }
   }
 
   @Test
   public void testSingleLevel() throws IOException {
     Assume.assumeTrue(ToolHelper.isLocalDevelopment());
 
-    LibraryDesugaringSpecification humanSpec = LibraryDesugaringSpecification.JDK11_PATH;
+    LibraryDesugaringSpecification humanSpec = JDK11_PATH;
 
     InternalOptions options = new InternalOptions();
 
@@ -296,6 +302,8 @@
 
     assertEquals(
         humanRewritingFlags1.getAmendLibraryMethod(), humanRewritingFlags2.getAmendLibraryMethod());
+    assertEquals(
+        humanRewritingFlags1.getAmendLibraryField(), humanRewritingFlags2.getAmendLibraryField());
   }
 
   private void assertTopLevelFlagsEquals(
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesNotInDexWithForceNestDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesNotInDexWithForceNestDesugaringTest.java
new file mode 100644
index 0000000..86eff4e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesNotInDexWithForceNestDesugaringTest.java
@@ -0,0 +1,127 @@
+// 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.desugar.nestaccesscontrol;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.nestaccesscontrol.NestAttributesNotInDexWithForceNestDesugaringTest.Host.Member1;
+import com.android.tools.r8.desugar.nestaccesscontrol.NestAttributesNotInDexWithForceNestDesugaringTest.Host.Member2;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+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 NestAttributesNotInDexWithForceNestDesugaringTest extends NestAttributesInDexTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean forceNestDesugaring;
+
+  @Parameters(name = "{0}, forceNestDesugaring: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimesAndAllApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8()
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters)
+        .addOptionsModification(options -> options.emitNestAnnotationsInDex = true)
+        .addOptionsModification(options -> options.forceNestDesugaring = forceNestDesugaring)
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(
+                  inspector
+                      .clazz(Member1.class)
+                      .uniqueMethodWithOriginalName(
+                          SyntheticItemsTestUtils.syntheticNestInstanceMethodAccessor(
+                                  Member1.class.getDeclaredMethod("m1"))
+                              .getMethodName()),
+                  isPresentIf(forceNestDesugaring));
+              assertThat(
+                  inspector
+                      .clazz(Member2.class)
+                      .uniqueMethodWithOriginalName(
+                          SyntheticItemsTestUtils.syntheticNestStaticMethodAccessor(
+                                  Member2.class.getDeclaredMethod("m2"))
+                              .getMethodName()),
+                  isPresentIf(forceNestDesugaring));
+            });
+  }
+
+  @Test
+  public void testD8NoDesugar() {
+    assumeTrue(forceNestDesugaring);
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .disableDesugaring()
+                .setMinApi(parameters)
+                .addOptionsModification(options -> options.emitNestAnnotationsInDex = true)
+                .addOptionsModification(
+                    options -> options.forceNestDesugaring = forceNestDesugaring)
+                .compile());
+  }
+
+  public Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(Host.class)
+            .setAccessFlags(MethodPredicate.onName("h"), MethodAccessFlags::setPrivate)
+            .transform(),
+        withNest(Member1.class)
+            .setAccessFlags(MethodPredicate.onName("m1"), MethodAccessFlags::setPrivate)
+            .transform(),
+        withNest(Member2.class)
+            .setAccessFlags(MethodPredicate.onName("m2"), MethodAccessFlags::setPrivate)
+            .transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    return transformer(clazz).setNest(Host.class, Member1.class, Member2.class);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {}
+  }
+
+  static class Host {
+    static class Member1 {
+      void m1() { // Will be private.
+        Member2.m2();
+      }
+    }
+
+    static class Member2 {
+      static void m2() { // Will be private.
+      }
+    }
+
+    void h() { // Will be private.
+      new Member1().m1();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestMethodInlinedTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestMethodInlinedTest.java
index 4dbc4c3e..65df54e 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestMethodInlinedTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestMethodInlinedTest.java
@@ -60,7 +60,7 @@
         .addOptionsModification(
             options -> {
               options.enableClassInlining = false;
-              options.enableVerticalClassMerging = false;
+              options.getVerticalClassMergerOptions().disable();
             })
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
index c2846c5..5c08537 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
@@ -59,15 +59,14 @@
         .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            canUseNativeRecords(parameters),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_RECORD),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD));
+            isRecordsDesugaredForD8(parameters),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_RECORD));
   }
 
   @Test
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
-    boolean willDesugarRecords = parameters.isDexRuntime() && !canUseNativeRecords(parameters);
     testForR8(parameters.getBackend())
         .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
         .addProgramClassFileData(PROGRAM_DATA)
@@ -77,13 +76,15 @@
         .addKeepRules("-keep class records.EmptyRecordAnnotation$Empty")
         .addKeepMainRule(MAIN_TYPE)
         // This is used to avoid renaming com.android.tools.r8.RecordTag.
-        .applyIf(willDesugarRecords, b -> b.addKeepRules("-keep class java.lang.Record"))
+        .applyIf(
+            isRecordsDesugaredForR8(parameters),
+            b -> b.addKeepRules("-keep class java.lang.Record"))
         .compile()
         .applyIf(parameters.isCfRuntime(), r -> r.inspect(RecordTestUtils::assertRecordsAreRecords))
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            !willDesugarRecords,
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_RECORD),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD));
+            isRecordsDesugaredForR8(parameters),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_RECORD));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordBlogTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordBlogTest.java
index 0fcb6da..05fdb8d 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordBlogTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordBlogTest.java
@@ -83,9 +83,10 @@
         .setMinApi(parameters)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
-            r -> r.assertSuccessWithOutput(computeOutput(REFERENCE_OUTPUT_FORMAT)));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(computeOutput(REFERENCE_OUTPUT_FORMAT)),
+            r -> r.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
index 8ded32a..d2c05af 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
@@ -16,8 +16,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.TestShrinkerBuilder;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringUtils;
@@ -36,7 +34,7 @@
   private static final String RECORD_NAME = "RecordWithAnnotations";
   private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
   private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
-  private static final String JVM_EXPECTED_RESULT =
+  private static final String JVM_UNTIL_20_EXPECTED_RESULT =
       StringUtils.lines(
           "Jane Doe",
           "42",
@@ -63,6 +61,9 @@
           "2",
           "@records.RecordWithAnnotations$Annotation(\"a\")",
           "@records.RecordWithAnnotations$AnnotationFieldOnly(\"b\")");
+  private static final String JVM_FROM_21_EXPECTED_RESULT =
+      JVM_UNTIL_20_EXPECTED_RESULT.replaceAll(
+          "RecordWithAnnotations\\$Annotation", "RecordWithAnnotations.Annotation");
   private static final String ART_EXPECTED_RESULT =
       StringUtils.lines(
           "Jane Doe",
@@ -219,29 +220,28 @@
     testForJvm(parameters)
         .addProgramClassFileData(PROGRAM_DATA)
         .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(JVM_EXPECTED_RESULT);
+        .assertSuccessWithOutput(
+            parameters.getRuntime().asCf().getVm().isLessThanOrEqualTo(CfVm.JDK20)
+                ? JVM_UNTIL_20_EXPECTED_RESULT
+                : JVM_FROM_21_EXPECTED_RESULT);
   }
 
   @Test
   public void testDesugaring() throws Exception {
     parameters.assumeDexRuntime();
     assumeTrue(keepAnnotations);
-    // Android U will support records.
-    boolean compilingForNativeRecordSupport =
-        parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
-    boolean runtimeWithNativeRecordSupport =
-        parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForDesugaring(parameters)
         .addProgramClassFileData(PROGRAM_DATA)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            compilingForNativeRecordSupport,
-            r -> r.assertSuccessWithOutput(ART_EXPECTED_RESULT),
+            isRecordsDesugaredForD8(parameters),
             r ->
                 r.assertSuccessWithOutput(
-                        !runtimeWithNativeRecordSupport
-                            ? EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT
-                            : EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT)
+                    runtimeWithRecordsSupport(parameters.getRuntime())
+                        ? EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT
+                        : EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT),
+            r ->
+                r.assertSuccessWithOutput(ART_EXPECTED_RESULT)
                     .inspect(
                         inspector -> {
                           ClassSubject person =
@@ -250,74 +250,60 @@
                           assertThat(name, isPresentAndNotRenamed());
                           FieldSubject age = person.uniqueFieldWithOriginalName("age");
                           assertThat(age, isPresentAndNotRenamed());
-                          if (compilingForNativeRecordSupport) {
-                            assertEquals(2, person.getFinalRecordComponents().size());
+                          assertEquals(2, person.getFinalRecordComponents().size());
 
-                            assertEquals(
-                                name.getFinalName(),
-                                person.getFinalRecordComponents().get(0).getName());
-                            assertTrue(
-                                person
-                                    .getFinalRecordComponents()
-                                    .get(0)
-                                    .getType()
-                                    .is("java.lang.String"));
-                            assertNull(person.getFinalRecordComponents().get(0).getSignature());
-                            assertEquals(
-                                2,
-                                person.getFinalRecordComponents().get(0).getAnnotations().size());
-                            assertThat(
-                                person.getFinalRecordComponents().get(0).getAnnotations(),
-                                hasAnnotationTypes(
-                                    inspector.getTypeSubject(
-                                        "records.RecordWithAnnotations$Annotation"),
-                                    inspector.getTypeSubject(
-                                        "records.RecordWithAnnotations$AnnotationRecordComponentOnly")));
-                            assertThat(
-                                person.getFinalRecordComponents().get(0).getAnnotations().get(0),
-                                hasElements(new Pair<>("value", "a")));
-                            assertThat(
-                                person.getFinalRecordComponents().get(0).getAnnotations().get(1),
-                                hasElements(new Pair<>("value", "c")));
+                          assertEquals(
+                              name.getFinalName(),
+                              person.getFinalRecordComponents().get(0).getName());
+                          assertTrue(
+                              person
+                                  .getFinalRecordComponents()
+                                  .get(0)
+                                  .getType()
+                                  .is("java.lang.String"));
+                          assertNull(person.getFinalRecordComponents().get(0).getSignature());
+                          assertEquals(
+                              2, person.getFinalRecordComponents().get(0).getAnnotations().size());
+                          assertThat(
+                              person.getFinalRecordComponents().get(0).getAnnotations(),
+                              hasAnnotationTypes(
+                                  inspector.getTypeSubject(
+                                      "records.RecordWithAnnotations$Annotation"),
+                                  inspector.getTypeSubject(
+                                      "records.RecordWithAnnotations$AnnotationRecordComponentOnly")));
+                          assertThat(
+                              person.getFinalRecordComponents().get(0).getAnnotations().get(0),
+                              hasElements(new Pair<>("value", "a")));
+                          assertThat(
+                              person.getFinalRecordComponents().get(0).getAnnotations().get(1),
+                              hasElements(new Pair<>("value", "c")));
 
-                            assertEquals(
-                                age.getFinalName(),
-                                person.getFinalRecordComponents().get(1).getName());
-                            assertTrue(
-                                person.getFinalRecordComponents().get(1).getType().is("int"));
-                            assertNull(person.getFinalRecordComponents().get(1).getSignature());
-                            assertEquals(
-                                2,
-                                person.getFinalRecordComponents().get(1).getAnnotations().size());
-                            assertThat(
-                                person.getFinalRecordComponents().get(1).getAnnotations(),
-                                hasAnnotationTypes(
-                                    inspector.getTypeSubject(
-                                        "records.RecordWithAnnotations$Annotation"),
-                                    inspector.getTypeSubject(
-                                        "records.RecordWithAnnotations$AnnotationRecordComponentOnly")));
-                            assertThat(
-                                person.getFinalRecordComponents().get(1).getAnnotations().get(0),
-                                hasElements(new Pair<>("value", "x")));
-                            assertThat(
-                                person.getFinalRecordComponents().get(1).getAnnotations().get(1),
-                                hasElements(new Pair<>("value", "z")));
-                          } else {
-                            assertEquals(0, person.getFinalRecordComponents().size());
-                          }
+                          assertEquals(
+                              age.getFinalName(),
+                              person.getFinalRecordComponents().get(1).getName());
+                          assertTrue(person.getFinalRecordComponents().get(1).getType().is("int"));
+                          assertNull(person.getFinalRecordComponents().get(1).getSignature());
+                          assertEquals(
+                              2, person.getFinalRecordComponents().get(1).getAnnotations().size());
+                          assertThat(
+                              person.getFinalRecordComponents().get(1).getAnnotations(),
+                              hasAnnotationTypes(
+                                  inspector.getTypeSubject(
+                                      "records.RecordWithAnnotations$Annotation"),
+                                  inspector.getTypeSubject(
+                                      "records.RecordWithAnnotations$AnnotationRecordComponentOnly")));
+                          assertThat(
+                              person.getFinalRecordComponents().get(1).getAnnotations().get(0),
+                              hasElements(new Pair<>("value", "x")));
+                          assertThat(
+                              person.getFinalRecordComponents().get(1).getAnnotations().get(1),
+                              hasElements(new Pair<>("value", "z")));
                         }));
   }
 
   @Test
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
-    // Android U will support records.
-    boolean compilingForNativeRecordSupport =
-        parameters.isCfRuntime()
-            || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
-    boolean runtimeWithNativeRecordSupport =
-        parameters.isCfRuntime()
-            || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForR8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         // TODO(b/231930852): Change to android.jar for Androud U when that contains
@@ -337,7 +323,7 @@
               ClassSubject person = inspector.clazz("records.RecordWithAnnotations$Person");
               FieldSubject name = person.uniqueFieldWithOriginalName("name");
               FieldSubject age = person.uniqueFieldWithOriginalName("age");
-              if (compilingForNativeRecordSupport) {
+              if (!isRecordsDesugaredForR8(parameters)) {
                 assertEquals(2, person.getFinalRecordComponents().size());
 
                 assertEquals(
@@ -390,7 +376,7 @@
                     keepAnnotations
                         ? JVM_EXPECTED_RESULT_R8
                         : JVM_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
-            compilingForNativeRecordSupport,
+            !isRecordsDesugaredForR8(parameters),
             r ->
                 r.assertSuccessWithOutput(
                     keepAnnotations
@@ -398,7 +384,7 @@
                         : ART_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
             r ->
                 r.assertSuccessWithOutput(
-                    runtimeWithNativeRecordSupport
+                    runtimeWithRecordsSupport(parameters.getRuntime())
                         ? EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT
                         : EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT));
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
index 9004d73..58f0da0 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
@@ -13,8 +13,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.TestShrinkerBuilder;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -50,9 +48,9 @@
           "0");
   private static final String EXPECTED_RESULT_R8 =
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "true", "0");
-  private static final String EXPECTED_RESULT_DESUGARED =
+  private static final String EXPECTED_RESULT_DESUGARED_NO_NATIVE_RECORDS_SUPPORT =
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
-  private static final String EXPECTED_RESULT_DESUGARED_ART_14 =
+  private static final String EXPECTED_RESULT_DESUGARED_NATIVE_RECORD_SUPPORT =
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "false");
 
   @Parameter(0)
@@ -86,29 +84,23 @@
   public void testDesugaring() throws Exception {
     parameters.assumeDexRuntime();
     assumeTrue(keepSignatures);
-    // Android U will support records.
-    boolean compilingForNativeRecordSupport =
-        parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
-    boolean runningWithNativeRecordSupport =
-        parameters.getRuntime().isDex()
-            && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForDesugaring(parameters)
         .addProgramClassFileData(PROGRAM_DATA)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            compilingForNativeRecordSupport,
+            !isRecordsDesugaredForD8(parameters),
             // Current Art 14 build does not support the java.lang.Record class.
             r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
             r ->
                 r.assertSuccessWithOutput(
-                        runningWithNativeRecordSupport
-                            ? EXPECTED_RESULT_DESUGARED_ART_14
-                            : EXPECTED_RESULT_DESUGARED)
+                        runtimeWithRecordsSupport(parameters.getRuntime())
+                            ? EXPECTED_RESULT_DESUGARED_NATIVE_RECORD_SUPPORT
+                            : EXPECTED_RESULT_DESUGARED_NO_NATIVE_RECORDS_SUPPORT)
                     .inspect(
                         inspector -> {
                           ClassSubject person =
                               inspector.clazz("records.RecordWithSignature$Person");
-                          if (compilingForNativeRecordSupport) {
+                          if (!isRecordsDesugaredForD8(parameters)) {
                             assertEquals(2, person.getFinalRecordComponents().size());
 
                             assertEquals(
@@ -146,16 +138,10 @@
   @Test
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
-    boolean compilingForNativeRecordSupport =
-        parameters.isCfRuntime()
-            || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
-    boolean runningWithNativeRecordSupport =
-        parameters.getRuntime().isDex()
-            && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForR8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         // TODO(b/231930852): Change to android.jar for Android U when that contains
-        //  java.lang.Record.
+        // java.lang.Record.
         .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
         .addKeepMainRule(MAIN_TYPE)
         .applyIf(keepSignatures, TestShrinkerBuilder::addKeepAttributeSignature)
@@ -169,14 +155,13 @@
               assertEquals(0, person.getFinalRecordComponents().size());
             })
         .run(parameters.getRuntime(), MAIN_TYPE)
-        // No Art VM actually supports the java.lang.Record class.
         .applyIf(
-            compilingForNativeRecordSupport,
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_R8),
+            runtimeWithRecordsSupport(parameters.getRuntime()),
             r ->
                 r.assertSuccessWithOutput(
-                    runningWithNativeRecordSupport
-                        ? EXPECTED_RESULT_DESUGARED_ART_14
-                        : EXPECTED_RESULT_DESUGARED));
+                    isRecordsDesugaredForR8(parameters)
+                        ? EXPECTED_RESULT_DESUGARED_NATIVE_RECORD_SUPPORT
+                        : EXPECTED_RESULT_R8),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_NO_NATIVE_RECORDS_SUPPORT));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java
index 0c63278..7e1d2c7 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java
@@ -66,9 +66,10 @@
         .setMinApi(parameters)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
   }
 
   @Test
@@ -80,11 +81,11 @@
     testForD8()
         .addProgramFiles(path)
         .applyIf(
-            canUseNativeRecords(parameters),
-            b -> assertFalse(globals.hasGlobals()),
+            isRecordsDesugaredForD8(parameters),
             b ->
                 b.getBuilder()
-                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()),
+            b -> assertFalse(globals.hasGlobals()))
         .apply(b -> b.getBuilder().setDesugarGraphConsumer(consumer))
         .setMinApi(parameters)
         .setIncludeClassesChecksum(true)
@@ -104,11 +105,11 @@
     testForD8()
         .addProgramFiles(path)
         .applyIf(
-            canUseNativeRecords(parameters),
-            b -> assertFalse(globals.hasGlobals()),
+            isRecordsDesugaredForD8(parameters),
             b ->
                 b.getBuilder()
-                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()),
+            b -> assertFalse(globals.hasGlobals()))
         .apply(b -> b.getBuilder().setDesugarGraphConsumer(consumer))
         .setMinApi(parameters)
         .setIncludeClassesChecksum(true)
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
index 214afea..fa22262 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
@@ -4,12 +4,20 @@
 
 package com.android.tools.r8.desugar.records;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.DuplicateTypeInProgramAndLibraryDiagnostic;
+import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import java.nio.file.Path;
@@ -41,8 +49,6 @@
       String.format(EXPECTED_RESULT, "Empty", "Person", "name", "age");
   private static final String EXPECTED_RESULT_R8 =
       String.format(EXPECTED_RESULT, "a", "b", "name", "age");
-  private static final String EXPECTED_RESULT_R8_2 =
-      String.format(EXPECTED_RESULT, "a", "b", "a", "b");
 
   private final TestParameters parameters;
 
@@ -52,7 +58,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withAllApiLevelsAlsoForCf().build();
+    return getTestParameters().withDexRuntimesAndAllApiLevels().build();
   }
 
   @Test
@@ -80,17 +86,37 @@
             .setMinApi(parameters)
             .compile()
             .writeToZip();
-    if (canUseNativeRecords(parameters)) {
-      assertFalse(ZipUtils.containsEntry(desugared, "com/android/tools/r8/RecordTag.class"));
-    } else {
+    if (isRecordsDesugaredForD8(parameters)) {
       assertTrue(ZipUtils.containsEntry(desugared, "com/android/tools/r8/RecordTag.class"));
+    } else {
+      assertFalse(ZipUtils.containsEntry(desugared, "com/android/tools/r8/RecordTag.class"));
     }
     testForR8(parameters.getBackend())
         .addProgramFiles(desugared)
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_TYPE)
+        .allowDiagnosticMessages()
+        .compileWithExpectedDiagnostics(
+            // Class com.android.tools.r8.RecordTag in desugared input is seen as java.lang.Record
+            // when reading causing the duplicate class.
+            diagnostics -> {
+              if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)) {
+                diagnostics
+                    .assertNoErrors()
+                    .assertInfosMatch(
+                        allOf(
+                            diagnosticType(DuplicateTypesDiagnostic.class),
+                            diagnosticType(DuplicateTypeInProgramAndLibraryDiagnostic.class),
+                            diagnosticMessage(containsString("java.lang.Record"))))
+                    .assertWarningsMatch(
+                        allOf(
+                            diagnosticType(StringDiagnostic.class),
+                            diagnosticMessage(containsString("java.lang.Record"))));
+              } else {
+                diagnostics.assertNoMessages();
+              }
+            })
         .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(
-            canUseNativeRecords(parameters) ? EXPECTED_RESULT_R8_2 : EXPECTED_RESULT_R8);
+        .assertSuccessWithOutput(EXPECTED_RESULT_R8);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
index 38954bf..a46c400 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -74,9 +74,7 @@
             .addProgramClassFileData(PROGRAM_DATA_1)
             .setMinApi(parameters)
             .setIntermediate(true);
-    if (canUseNativeRecords(parameters)) {
-      builder.compile();
-    } else {
+    if (isRecordsDesugaredForD8(parameters)) {
       assertThrows(
           CompilationFailedException.class,
           () ->
@@ -86,6 +84,8 @@
                           .assertOnlyErrors()
                           .assertErrorsMatch(
                               diagnosticType(MissingGlobalSyntheticsConsumerDiagnostic.class))));
+    } else {
+      builder.compile();
     }
   }
 
@@ -129,8 +129,8 @@
             .inspect(this::assertDoesNotHaveRecordTag)
             .writeToZip();
 
-    assertTrue(canUseNativeRecords(parameters) ^ globals1.hasGlobals());
-    assertTrue(canUseNativeRecords(parameters) ^ globals2.hasGlobals());
+    assertTrue(isRecordsDesugaredForD8(parameters) ^ !globals1.hasGlobals());
+    assertTrue(isRecordsDesugaredForD8(parameters) ^ !globals2.hasGlobals());
 
     D8TestCompileResult result =
         testForD8(parameters.getBackend())
@@ -147,15 +147,17 @@
     result
         .run(parameters.getRuntime(), MAIN_TYPE_1)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
     result
         .run(parameters.getRuntime(), MAIN_TYPE_2)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
   }
 
   @Test
@@ -181,15 +183,17 @@
     result
         .run(parameters.getRuntime(), MAIN_TYPE_1)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
     result
         .run(parameters.getRuntime(), MAIN_TYPE_2)
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
   }
 
   @Test
@@ -210,7 +214,7 @@
             .inspect(this::assertHasRecordTag)
             .writeToZip();
 
-    if (canUseNativeRecords(parameters)) {
+    if (!isRecordsDesugaredForD8(parameters)) {
       D8TestCompileResult result =
           testForD8(parameters.getBackend())
               .addProgramFiles(output1, output2)
@@ -219,17 +223,17 @@
       result
           .run(parameters.getRuntime(), MAIN_TYPE_1)
           .applyIf(
-              canUseNativeRecords(parameters)
-                  && !runtimeWithRecordsSupport(parameters.getRuntime()),
-              r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-              r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1));
+              isRecordsDesugaredForD8(parameters)
+                  || runtimeWithRecordsSupport(parameters.getRuntime()),
+              r -> r.assertSuccessWithOutput(EXPECTED_RESULT_1),
+              r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
       result
           .run(parameters.getRuntime(), MAIN_TYPE_2)
           .applyIf(
-              canUseNativeRecords(parameters)
-                  && !runtimeWithRecordsSupport(parameters.getRuntime()),
-              r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-              r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2));
+              isRecordsDesugaredForD8(parameters)
+                  || runtimeWithRecordsSupport(parameters.getRuntime()),
+              r -> r.assertSuccessWithOutput(EXPECTED_RESULT_2),
+              r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
     } else {
       assertThrows(
           CompilationFailedException.class,
@@ -247,7 +251,8 @@
 
   private void assertHasRecordTag(CodeInspector inspector) {
     // Note: this should be asserting on record tag.
-    assertThat(inspector.clazz("java.lang.Record"), isPresentIf(!canUseNativeRecords(parameters)));
+    assertThat(
+        inspector.clazz("java.lang.Record"), isPresentIf(isRecordsDesugaredForD8(parameters)));
   }
 
   private void assertDoesNotHaveRecordTag(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
index 5ed96ab..24757af 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
@@ -77,7 +76,6 @@
   @Test
   public void testD8() throws Exception {
     assumeFalse(forceInvokeRangeForInvokeCustom);
-    boolean runningWithNativeRecordSupport = runtimeWithRecordsSupport(parameters.getRuntime());
     testForD8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         .setMinApi(parameters)
@@ -87,9 +85,10 @@
             options -> options.testing.disableRecordApplicationReaderMap = true)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            canUseNativeRecords(parameters) && !runningWithNativeRecordSupport,
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
+            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
     ;
   }
 
@@ -101,9 +100,7 @@
     Path path = compileIntermediate(globals);
     testForD8()
         .addProgramFiles(path)
-        .applyIf(
-            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U),
-            b -> assertFalse(globals.hasGlobals()),
+        .apply(
             b ->
                 b.getBuilder()
                     .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
@@ -122,9 +119,7 @@
     // In Android Studio they disable desugaring at this point to improve build speed.
     testForD8()
         .addProgramFiles(path)
-        .applyIf(
-            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U),
-            b -> assertFalse(globals.hasGlobals()),
+        .apply(
             b ->
                 b.getBuilder()
                     .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
index c833f8a..6b9d3f9 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
@@ -72,9 +72,7 @@
         .addKeepPermittedSubclasses(Super.class, Sub2.class, UnrelatedSuper.class)
         .addKeepMainRule(TestClass.class)
         .addVerticallyMergedClassesInspector(
-            inspector -> {
-              inspector.assertMergedIntoSubtype(Sub1.class);
-            })
+            inspector -> inspector.assertMergedIntoSubtype(Sub1.class))
         .addHorizontallyMergedClassesInspector(
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .compile()
@@ -99,8 +97,7 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      new SubSub();
-      System.out.println("Success!");
+      System.out.println(new SubSub());
     }
   }
 
@@ -110,7 +107,13 @@
 
   static class Sub2 extends Super {}
 
-  static class SubSub extends Sub1 {}
+  static class SubSub extends Sub1 {
+
+    @Override
+    public String toString() {
+      return "Success!";
+    }
+  }
 
   abstract static class UnrelatedSuper /* permits Sub1, Sub2 */ {}
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
index 3f218f0..1b9c9a1 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
@@ -108,8 +108,7 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      new SubSub();
-      System.out.println("Success!");
+      System.out.println(new SubSub());
     }
   }
 
@@ -128,5 +127,11 @@
 
   static class Sub2 extends Super implements Iface1 {}
 
-  static class SubSub extends Sub1 {}
+  static class SubSub extends Sub1 {
+
+    @Override
+    public String toString() {
+      return "Success!";
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/dex/TryCatchRangeOverflowTest.java b/src/test/java/com/android/tools/r8/dex/TryCatchRangeOverflowTest.java
index 6e9d85c..c59fc1c 100644
--- a/src/test/java/com/android/tools/r8/dex/TryCatchRangeOverflowTest.java
+++ b/src/test/java/com/android/tools/r8/dex/TryCatchRangeOverflowTest.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.Arrays;
@@ -80,10 +79,7 @@
       compile(addCount)
           .run(parameters.getRuntime(), TestClass.class)
           .assertSuccessWithOutputLines("" + addCount)
-          .inspect(
-              inspector ->
-                  checkTryCatchHandlers(
-                      1 + BooleanUtils.intValue(addCount > UNSPLIT_LIMIT + 1), inspector));
+          .inspect(inspector -> checkTryCatchHandlers(2, inspector));
     }
   }
 
@@ -115,7 +111,7 @@
     compile(addCount)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("" + addCount)
-        .inspect(inspector -> checkTryCatchHandlers(2, inspector));
+        .inspect(inspector -> checkTryCatchHandlers(3, inspector));
   }
 
   private D8TestBuilder compile(int addCount) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java b/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java
index e706afa..6876898 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java
@@ -120,7 +120,8 @@
             .addProgramClassFileData(programClassFileData)
             .addKeepMainRule("TestClass")
             .addOptionsModification(
-                options -> options.enableVerticalClassMerging = enableVerticalClassMerging)
+                options ->
+                    options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging))
             .allowDiagnosticWarningMessages()
             .setMinApi(parameters)
             .compile();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index b00cc7e..4817266 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -102,7 +103,9 @@
   }
 
   private void fixInliningNullabilityClass(
-      DexItemFactory dexItemFactory, HorizontallyMergedClasses horizontallyMergedClasses) {
+      DexItemFactory dexItemFactory,
+      HorizontallyMergedClasses horizontallyMergedClasses,
+      HorizontalClassMerger.Mode mode) {
     DexType originalType =
         dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptor("inlining.Nullability"));
     nullabilityClass =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/BoxedPrimitiveFromGenericUnboxingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/BoxedPrimitiveFromGenericUnboxingTest.java
new file mode 100644
index 0000000..5a23263
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/BoxedPrimitiveFromGenericUnboxingTest.java
@@ -0,0 +1,159 @@
+// 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.boxedprimitives;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 BoxedPrimitiveFromGenericUnboxingTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableBridgeHoistingToSharedSyntheticSuperclass;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, opt: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    boolean optimize =
+        enableBridgeHoistingToSharedSyntheticSuperclass
+            && parameters.canHaveNonReboundConstructorInvoke();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options.testing.enableBridgeHoistingToSharedSyntheticSuperclass =
+                    enableBridgeHoistingToSharedSyntheticSuperclass)
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              // Function should be removed as a result of bridge hoisting + inlining when adding a
+              // shared superclass to Increment and Decrement, and another shared superclass to
+              // StdoutPrinter and StderrPrinter.
+              ClassSubject functionClassSubject = inspector.clazz(Function.class);
+              assertThat(functionClassSubject, isAbsentIf(optimize));
+
+              // Check that the cast to java.lang.Integer in Increment.apply has been removed as a
+              // result of devirtualization.
+              ClassSubject incrementClassSubject = inspector.clazz(Increment.class);
+              assertThat(incrementClassSubject, isPresent());
+
+              MethodSubject incrementApplyMethodSubject =
+                  incrementClassSubject.uniqueMethodWithOriginalName("apply");
+              assertThat(incrementApplyMethodSubject, isPresent());
+              assertEquals(
+                  optimize,
+                  incrementApplyMethodSubject
+                      .streamInstructions()
+                      .noneMatch(
+                          instruction -> instruction.isCheckCast(Integer.class.getTypeName())));
+
+              // Check that the cast to java.lang.String in StdoutPrinter.apply has been removed as
+              // result of devirtualization (in fact the `Void apply(String)` method has been
+              // optimized to `void apply()` as a result of constant propagation).
+              ClassSubject stdoutPrinterClassSubject = inspector.clazz(StdoutPrinter.class);
+              assertThat(stdoutPrinterClassSubject, isPresent());
+
+              MethodSubject stdoutPrinterApplyMethodSubject =
+                  stdoutPrinterClassSubject.uniqueMethodWithOriginalName("apply");
+              assertThat(stdoutPrinterApplyMethodSubject, isPresent());
+              assertEquals(
+                  optimize,
+                  stdoutPrinterApplyMethodSubject.getProgramMethod().getReturnType().isVoidType());
+              assertEquals(
+                  optimize ? 0 : 1, stdoutPrinterApplyMethodSubject.getParameters().size());
+              assertEquals(
+                  optimize,
+                  stdoutPrinterApplyMethodSubject
+                      .streamInstructions()
+                      .noneMatch(
+                          instruction -> instruction.isCheckCast(String.class.getTypeName())));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("42", "42", "42");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Function<Integer, Integer> inc =
+          System.currentTimeMillis() > 0 ? new Increment() : new Decrement();
+      Function<Integer, Integer> dec =
+          System.currentTimeMillis() > 0 ? new Decrement() : new Increment();
+      Function<String, Void> printer =
+          System.currentTimeMillis() > 0 ? new StdoutPrinter() : new StderrPrinter();
+      System.out.println(inc.apply(41));
+      System.out.println(dec.apply(43));
+      printer.apply("42");
+    }
+  }
+
+  interface Function<S, T> {
+
+    T apply(S s);
+  }
+
+  @NoHorizontalClassMerging
+  static class Increment implements Function<Integer, Integer> {
+
+    @Override
+    public Integer apply(Integer i) {
+      return i + 1;
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class Decrement implements Function<Integer, Integer> {
+
+    @Override
+    public Integer apply(Integer i) {
+      return i - 1;
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class StdoutPrinter implements Function<String, Void> {
+
+    @Override
+    public Void apply(String obj) {
+      System.out.println(obj);
+      return null;
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class StderrPrinter implements Function<String, Void> {
+
+    @Override
+    public Void apply(String obj) {
+      System.err.println(obj);
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java
new file mode 100644
index 0000000..e19798b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java
@@ -0,0 +1,345 @@
+// 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.boxedprimitives;
+
+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.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ReverseBoxingOperationsTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testUnboxingRemoved() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertBoxOperationsRemoved)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpectedResult());
+  }
+
+  private void assertBoxOperationsRemoved(CodeInspector codeInspector) {
+    DexItemFactory factory = codeInspector.getFactory();
+
+    Set<DexMethod> unboxMethods = Sets.newIdentityHashSet();
+    Set<DexMethod> boxMethods = Sets.newIdentityHashSet();
+    for (DexType primitiveType : factory.unboxPrimitiveMethod.keySet()) {
+      unboxMethods.add(factory.getUnboxPrimitiveMethod(primitiveType));
+      boxMethods.add(factory.getBoxPrimitiveMethod(primitiveType));
+    }
+
+    // All box operations should have been removed, except for the unbox methods that act as null
+    // checks, which may or may not be replaced by null-checks depending if they are reprocessed.
+    assertEquals(
+        24,
+        codeInspector
+            .clazz(Main.class)
+            .allMethods(m -> !m.getOriginalName().equals("main"))
+            .size());
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(m -> !m.getOriginalName().equals("main"))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && boxMethods.contains(i.getMethod()))));
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(
+            m ->
+                !m.getOriginalName().equals("main")
+                    && (m.getOriginalName().contains("Unbox")
+                        || m.getOriginalName().contains("NonNull")))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && unboxMethods.contains(i.getMethod()))));
+  }
+
+  private String getExpectedResult() {
+    String[] resultItems = {"1", "1", "1.0", "1.0", "1", "1", "c", "true"};
+    String[] resultItems2 = {"2", "2", "2.0", "2.0", "2", "2", "e", "false"};
+    List<String> result = new ArrayList<>();
+    for (int i = 0; i < resultItems.length; i++) {
+      String item = resultItems[i];
+      String item2 = resultItems2[i];
+      result.add(">" + item);
+      result.add(">" + item);
+      result.add(">npe failure");
+      result.add(">" + item);
+      result.add(">" + item2);
+    }
+    return StringUtils.lines(result);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      int i = System.currentTimeMillis() > 0 ? 1 : 0;
+      System.out.println(intUnboxTest(i));
+      System.out.println(intTest(i));
+      try {
+        System.out.println(intTest(null));
+      } catch (NullPointerException npe7) {
+        System.out.println("npe failure");
+      }
+      System.out.println(intTestNonNull(i));
+      System.out.println(intTestNonNull(i + 1));
+
+      long l = System.currentTimeMillis() > 0 ? 1L : 0L;
+      System.out.println(longUnboxTest(l));
+      System.out.println(longTest(l));
+      try {
+        System.out.println(longTest(null));
+      } catch (NullPointerException npe6) {
+        System.out.println("npe failure");
+      }
+      System.out.println(longTestNonNull(l));
+      System.out.println(longTestNonNull(l + 1));
+
+      double d = System.currentTimeMillis() > 0 ? 1.0 : 0.0;
+      System.out.println(doubleUnboxTest(d));
+      System.out.println(doubleTest(d));
+      try {
+        System.out.println(doubleTest(null));
+      } catch (NullPointerException npe5) {
+        System.out.println("npe failure");
+      }
+      System.out.println(doubleTestNonNull(d));
+      System.out.println(doubleTestNonNull(d + 1));
+
+      float f = System.currentTimeMillis() > 0 ? 1.0f : 0.0f;
+      System.out.println(floatUnboxTest(f));
+      System.out.println(floatTest(f));
+      try {
+        System.out.println(floatTest(null));
+      } catch (NullPointerException npe4) {
+        System.out.println("npe failure");
+      }
+      System.out.println(floatTestNonNull(f));
+      System.out.println(floatTestNonNull(f + 1));
+
+      byte b = (byte) (System.currentTimeMillis() > 0 ? 1 : 0);
+      System.out.println(byteUnboxTest(b));
+      System.out.println(byteTest(b));
+      try {
+        System.out.println(byteTest(null));
+      } catch (NullPointerException npe3) {
+        System.out.println("npe failure");
+      }
+      System.out.println(byteTestNonNull(b));
+      System.out.println(byteTestNonNull((byte) (b + 1)));
+
+      short s = (short) (System.currentTimeMillis() > 0 ? 1 : 0);
+      System.out.println(shortUnboxTest(s));
+      System.out.println(shortTest(s));
+      try {
+        System.out.println(shortTest(null));
+      } catch (NullPointerException npe2) {
+        System.out.println("npe failure");
+      }
+      System.out.println(shortTestNonNull(s));
+      System.out.println(shortTestNonNull((short) (s + 1)));
+
+      char c = System.currentTimeMillis() > 0 ? 'c' : 'd';
+      System.out.println(charUnboxTest(c));
+      System.out.println(charTest(c));
+      try {
+        System.out.println(charTest(null));
+      } catch (NullPointerException npe1) {
+        System.out.println("npe failure");
+      }
+      System.out.println(charTestNonNull(c));
+      System.out.println(charTestNonNull('e'));
+
+      boolean bool = System.currentTimeMillis() > 0;
+      System.out.println(booleanUnboxTest(bool));
+      System.out.println(booleanTest(bool));
+      try {
+        System.out.println(booleanTest(null));
+      } catch (NullPointerException npe) {
+        System.out.println("npe failure");
+      }
+      System.out.println(booleanTestNonNull(bool));
+      System.out.println(booleanTestNonNull(false));
+    }
+
+    @NeverInline
+    public static int intUnboxTest(int i) {
+      System.out.print(">");
+      return Integer.valueOf(i).intValue();
+    }
+
+    @NeverInline
+    public static Integer intTest(Integer i) {
+      System.out.print(">");
+      return Integer.valueOf(i.intValue());
+    }
+
+    @NeverInline
+    public static Integer intTestNonNull(Integer i) {
+      System.out.print(">");
+      return Integer.valueOf(i.intValue());
+    }
+
+    @NeverInline
+    public static double doubleUnboxTest(double d) {
+      System.out.print(">");
+      return Double.valueOf(d).doubleValue();
+    }
+
+    @NeverInline
+    public static Double doubleTest(Double d) {
+      System.out.print(">");
+      return Double.valueOf(d.doubleValue());
+    }
+
+    @NeverInline
+    public static Double doubleTestNonNull(Double d) {
+      System.out.print(">");
+      return Double.valueOf(d.doubleValue());
+    }
+
+    @NeverInline
+    public static long longUnboxTest(long l) {
+      System.out.print(">");
+      return Long.valueOf(l).longValue();
+    }
+
+    @NeverInline
+    public static Long longTest(Long l) {
+      System.out.print(">");
+      return Long.valueOf(l.longValue());
+    }
+
+    @NeverInline
+    public static Long longTestNonNull(Long l) {
+      System.out.print(">");
+      return Long.valueOf(l.longValue());
+    }
+
+    @NeverInline
+    public static float floatUnboxTest(float f) {
+      System.out.print(">");
+      return Float.valueOf(f).floatValue();
+    }
+
+    @NeverInline
+    public static Float floatTest(Float f) {
+      System.out.print(">");
+      return Float.valueOf(f.floatValue());
+    }
+
+    @NeverInline
+    public static Float floatTestNonNull(Float f) {
+      System.out.print(">");
+      return Float.valueOf(f.floatValue());
+    }
+
+    @NeverInline
+    public static short shortUnboxTest(short s) {
+      System.out.print(">");
+      return Short.valueOf(s).shortValue();
+    }
+
+    @NeverInline
+    public static Short shortTest(Short s) {
+      System.out.print(">");
+      return Short.valueOf(s.shortValue());
+    }
+
+    @NeverInline
+    public static Short shortTestNonNull(Short s) {
+      System.out.print(">");
+      return Short.valueOf(s.shortValue());
+    }
+
+    @NeverInline
+    public static char charUnboxTest(char c) {
+      System.out.print(">");
+      return Character.valueOf(c).charValue();
+    }
+
+    @NeverInline
+    public static Character charTest(Character c) {
+      System.out.print(">");
+      return Character.valueOf(c.charValue());
+    }
+
+    @NeverInline
+    public static Character charTestNonNull(Character c) {
+      System.out.print(">");
+      return Character.valueOf(c.charValue());
+    }
+
+    @NeverInline
+    public static byte byteUnboxTest(byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b).byteValue();
+    }
+
+    @NeverInline
+    public static Byte byteTest(Byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b.byteValue());
+    }
+
+    @NeverInline
+    public static Byte byteTestNonNull(Byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b.byteValue());
+    }
+
+    @NeverInline
+    public static boolean booleanUnboxTest(boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b).booleanValue();
+    }
+
+    @NeverInline
+    public static Boolean booleanTest(Boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b.booleanValue());
+    }
+
+    @NeverInline
+    public static Boolean booleanTestNonNull(Boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b.booleanValue());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java
new file mode 100644
index 0000000..2001277
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java
@@ -0,0 +1,163 @@
+// 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.boxedprimitives;
+
+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.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnboxToCheckNotNullTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testValue() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertCheckNotNull)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpectedResult());
+  }
+
+  private void assertCheckNotNull(CodeInspector codeInspector) {
+    DexItemFactory factory = codeInspector.getFactory();
+
+    Set<DexMethod> unboxMethods = Sets.newIdentityHashSet();
+    unboxMethods.addAll(factory.unboxPrimitiveMethod.values());
+    Set<DexType> boxedTypes = Sets.newIdentityHashSet();
+    boxedTypes.addAll(factory.unboxPrimitiveMethod.keySet());
+
+    // All unbox operations should have been replaced by checkNotNull operations.
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(
+            m ->
+                !m.getParameters().isEmpty()
+                    && boxedTypes.contains(
+                        factory.createType(
+                            DescriptorUtils.javaTypeToDescriptor(m.getParameter(0).getTypeName()))))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && unboxMethods.contains(i.getMethod()))));
+  }
+
+  private String getExpectedResult() {
+    List<String> result = new ArrayList<>();
+    for (int i = 0; i < 8; i++) {
+      result.add("run succeeded");
+      result.add("npe failure");
+    }
+    return StringUtils.lines(result);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      testCheckingNPE(() -> intTest(1));
+      testCheckingNPE(() -> intTest(null));
+
+      testCheckingNPE(() -> longTest(1L));
+      testCheckingNPE(() -> longTest(null));
+
+      testCheckingNPE(() -> doubleTest(1.0));
+      testCheckingNPE(() -> doubleTest(null));
+
+      testCheckingNPE(() -> floatTest(1.0f));
+      testCheckingNPE(() -> floatTest(null));
+
+      testCheckingNPE(() -> byteTest((byte) 1));
+      testCheckingNPE(() -> byteTest(null));
+
+      testCheckingNPE(() -> shortTest((short) 1));
+      testCheckingNPE(() -> shortTest(null));
+
+      testCheckingNPE(() -> charTest('c'));
+      testCheckingNPE(() -> charTest(null));
+
+      testCheckingNPE(() -> booleanTest(true));
+      testCheckingNPE(() -> booleanTest(null));
+    }
+
+    public static void testCheckingNPE(Runnable runnable) {
+      try {
+        runnable.run();
+        System.out.println("run succeeded");
+      } catch (NullPointerException npe) {
+        System.out.println("npe failure");
+      }
+    }
+
+    @NeverInline
+    public static void intTest(Integer i) {
+      i.intValue();
+    }
+
+    @NeverInline
+    public static void doubleTest(Double d) {
+      d.doubleValue();
+    }
+
+    @NeverInline
+    public static void longTest(Long l) {
+      l.longValue();
+    }
+
+    @NeverInline
+    public static void floatTest(Float f) {
+      f.floatValue();
+    }
+
+    @NeverInline
+    public static void shortTest(Short s) {
+      s.shortValue();
+    }
+
+    @NeverInline
+    public static void charTest(Character c) {
+      c.charValue();
+    }
+
+    @NeverInline
+    public static void byteTest(Byte b) {
+      b.byteValue();
+    }
+
+    @NeverInline
+    public static void booleanTest(Boolean b) {
+      b.booleanValue();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastDebugTestRunner.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastDebugTestRunner.java
index 5a65791..41dc6bb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastDebugTestRunner.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastDebugTestRunner.java
@@ -41,7 +41,7 @@
     return testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(A.class, B.class, C.class, MAIN)
         .addKeepMainRule(MAIN)
-        .addOptionsModification(options -> options.enableVerticalClassMerging = false)
+        .addOptionsModification(options -> options.getVerticalClassMergerOptions().disable())
         .debug()
         .enableInliningAnnotations()
         .addDontObfuscate()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
index ec1f304..269b2f9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -57,11 +58,11 @@
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> {
-              options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
               // Here we modify the IR when it is processed normally.
               options.testing.irModifier = this::modifyIr;
               options.enableClassInlining = false;
             })
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines(EXPECTED);
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java
new file mode 100644
index 0000000..cf1b311
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java
@@ -0,0 +1,136 @@
+// 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.inliner;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 BridgeWithCastsInliningTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableSimpleInliningInstructionLimitIncrement;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, enable increment: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // Disable multi caller inlining.
+        .addOptionsModification(
+            options -> {
+              InlinerOptions inlinerOptions = options.inlinerOptions();
+              inlinerOptions.enableSimpleInliningInstructionLimitIncrement =
+                  enableSimpleInliningInstructionLimitIncrement;
+              inlinerOptions.multiCallerInliningInstructionLimits = new int[0];
+            })
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(StringUtils.times(StringUtils.lines("A"), 10));
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(Main.class);
+    assertThat(classSubject, isPresent());
+
+    MethodSubject barMethodSubject = classSubject.uniqueMethodWithOriginalName("bar");
+    assertThat(barMethodSubject, isPresent());
+
+    MethodSubject fooMethodSubject = classSubject.uniqueMethodWithOriginalName("foo");
+    assertThat(fooMethodSubject, isPresent());
+    assertThat(fooMethodSubject, invokesMethod(barMethodSubject));
+
+    MethodSubject invokeWithATestMethodSubject =
+        classSubject.uniqueMethodWithOriginalName("invokeWithATest");
+    assertThat(invokeWithATestMethodSubject, isPresent());
+    assertThat(
+        invokeWithATestMethodSubject,
+        invokesMethod(
+            enableSimpleInliningInstructionLimitIncrement ? barMethodSubject : fooMethodSubject));
+
+    MethodSubject invokeWithObjectTestMethodSubject =
+        classSubject.uniqueMethodWithOriginalName("invokeWithObjectTest");
+    assertThat(invokeWithObjectTestMethodSubject, isPresent());
+    assertThat(invokeWithObjectTestMethodSubject, invokesMethod(fooMethodSubject));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      invokeWithATest();
+      invokeWithObjectTest();
+    }
+
+    @NeverInline
+    static void invokeWithATest() {
+      A a = new A();
+      foo(a, a, a, a, a);
+    }
+
+    @NeverInline
+    static void invokeWithObjectTest() {
+      Object o = System.currentTimeMillis() > 0 ? new A() : new Object();
+      foo(o, o, o, o, o);
+    }
+
+    static void foo(Object o1, Object o2, Object o3, Object o4, Object o5) {
+      A a1 = (A) o1;
+      A a2 = (A) o2;
+      A a3 = (A) o3;
+      A a4 = (A) o4;
+      A a5 = (A) o5;
+      bar(a1, a2, a3, a4, a5);
+    }
+
+    @NeverInline
+    static void bar(A a1, A a2, A a3, A a4, A a5) {
+      System.out.println(a1);
+      System.out.println(a2);
+      System.out.println(a3);
+      System.out.println(a4);
+      System.out.println(a5);
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java
new file mode 100644
index 0000000..2623bc0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java
@@ -0,0 +1,144 @@
+// 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.inliner;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 BridgeWithUnboxingInliningTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableSimpleInliningInstructionLimitIncrement;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, enable increment: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // Disable multi caller inlining.
+        .addOptionsModification(
+            options -> {
+              InlinerOptions inlinerOptions = options.inlinerOptions();
+              inlinerOptions.enableSimpleInliningInstructionLimitIncrement =
+                  enableSimpleInliningInstructionLimitIncrement;
+              inlinerOptions.multiCallerInliningInstructionLimits = new int[0];
+            })
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(StringUtils.times(StringUtils.lines("1", "2", "3", "4", "5"), 2));
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(Main.class);
+    assertThat(classSubject, isPresent());
+
+    MethodSubject barMethodSubject = classSubject.uniqueMethodWithOriginalName("bar");
+    assertThat(barMethodSubject, isPresent());
+
+    MethodSubject fooMethodSubject = classSubject.uniqueMethodWithOriginalName("foo");
+    assertThat(fooMethodSubject, isPresent());
+    assertThat(fooMethodSubject, invokesMethod(barMethodSubject));
+
+    MethodSubject invokeWithATestMethodSubject =
+        classSubject.uniqueMethodWithOriginalName("invokeWithPrimitiveTest");
+    assertThat(invokeWithATestMethodSubject, isPresent());
+    assertThat(
+        invokeWithATestMethodSubject,
+        invokesMethod(
+            enableSimpleInliningInstructionLimitIncrement ? barMethodSubject : fooMethodSubject));
+
+    MethodSubject invokeWithBoxedPrimitiveTestMethodSubject =
+        classSubject.uniqueMethodWithOriginalName("invokeWithBoxedPrimitiveTest");
+    assertThat(invokeWithBoxedPrimitiveTestMethodSubject, isPresent());
+    assertThat(invokeWithBoxedPrimitiveTestMethodSubject, invokesMethod(fooMethodSubject));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      invokeWithPrimitiveTest();
+      invokeWithBoxedPrimitiveTest();
+    }
+
+    @NeverInline
+    static void invokeWithPrimitiveTest() {
+      Integer o1 = 1;
+      Integer o2 = 2;
+      Integer o3 = 3;
+      Integer o4 = 4;
+      Integer o5 = 5;
+      foo(o1, o2, o3, o4, o5);
+    }
+
+    @NeverInline
+    static void invokeWithBoxedPrimitiveTest() {
+      Integer o1 = System.currentTimeMillis() > 0 ? 1 : null;
+      Integer o2 = System.currentTimeMillis() > 0 ? 2 : null;
+      Integer o3 = System.currentTimeMillis() > 0 ? 3 : null;
+      Integer o4 = System.currentTimeMillis() > 0 ? 4 : null;
+      Integer o5 = System.currentTimeMillis() > 0 ? 5 : null;
+      foo(o1, o2, o3, o4, o5);
+    }
+
+    static void foo(Integer o1, Integer o2, Integer o3, Integer o4, Integer o5) {
+      int i1 = o1;
+      int i2 = o2;
+      int i3 = o3;
+      int i4 = o4;
+      int i5 = o5;
+      bar(i1, i2, i3, i4, i5);
+    }
+
+    @NeverInline
+    static void bar(int i1, int i2, int i3, int i4, int i5) {
+      System.out.println(i1);
+      System.out.println(i2);
+      System.out.println(i3);
+      System.out.println(i4);
+      System.out.println(i5);
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java
new file mode 100644
index 0000000..0f49f15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java
@@ -0,0 +1,405 @@
+// 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.string;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.UnsupportedEncodingException;
+import java.util.Formattable;
+import java.util.Formatter;
+import java.util.IllegalFormatCodePointException;
+import java.util.IllegalFormatConversionException;
+import java.util.Locale;
+import java.util.MissingFormatArgumentException;
+import java.util.UnknownFormatConversionException;
+import java.util.function.Predicate;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StringFormatTest extends TestBase {
+
+  private static final String JAVA_OUTPUT =
+      StringUtils.lines(
+          "No % Place %",
+          "X",
+          "X after",
+          "before 0",
+          "before null after",
+          "phiNull S null",
+          "catch 1 true",
+          "might throw 1 2",
+          "reuse 1 null null",
+          "reuse 2 null null",
+          "reuse 3 A null",
+          "extra",
+          "extra",
+          "int0a 0",
+          "int0b 0",
+          "int1 0",
+          "int2 1",
+          "int3 9223372036854775807",
+          "int4 null",
+          "bool0 true",
+          "bool1 false",
+          "bool2 false",
+          "bool3 true",
+          "nobool0 false",
+          "nobool1 true",
+          "nobool2 true",
+          "nobool3 true",
+          "Fancy 0 0 0 0 0.00 @",
+          "Formattable",
+          "NLS",
+          "en_CA",
+          "123456");
+
+  private static final Class<?> MAIN = TestClass.class;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private final TestParameters parameters;
+
+  public StringFormatTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvmOutput() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testReleaseR8() throws Exception {
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(MAIN, TestFormattable.class)
+            .addOptionsModification(options -> options.enableStringFormatOptimization = true)
+            .enableInliningAnnotations()
+            .addKeepMainRule(MAIN)
+            .setMinApi(parameters)
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, false);
+  }
+
+  @Test
+  public void testDebugR8() throws Exception {
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(MAIN, TestFormattable.class)
+            .addOptionsModification(options -> options.enableStringFormatOptimization = true)
+            .enableInliningAnnotations()
+            .addKeepMainRule(MAIN)
+            .setMinApi(parameters)
+            .debug()
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, true);
+  }
+
+  private static Predicate<InstructionSubject> invokeMatcher(String qualifiedName) {
+    return ins -> {
+      if (!ins.isInvoke()) {
+        return false;
+      }
+      InvokeInstructionSubject invoke = (InvokeInstructionSubject) ins;
+      return qualifiedName.equals(invoke.invokedMethod().qualifiedName());
+    };
+  }
+
+  private void test(SingleTestRunResult<?> result, boolean isDebug) throws Exception {
+    CodeInspector codeInspector = result.inspector();
+    ClassSubject mainClass = codeInspector.clazz(MAIN);
+
+    Predicate<InstructionSubject> stringFormatMatcher = invokeMatcher("java.lang.String.format");
+    Predicate<InstructionSubject> stringBuilderMatcher =
+        invokeMatcher("java.lang.StringBuilder.toString")
+            .or(invokeMatcher("java.lang.StringBuilder.append"));
+    Predicate<InstructionSubject> valueOfMatcher =
+        invokeMatcher("java.lang.String.valueOf")
+            .or(invokeMatcher("java.lang.Integer.valueOf"))
+            .or(invokeMatcher("java.lang.Long.valueOf"))
+            .or(invokeMatcher("java.lang.Boolean.valueOf"));
+    Predicate<InstructionSubject> arrayInstructionsMatcher =
+        ((Predicate<InstructionSubject>) InstructionSubject::isNewArray)
+            .or(InstructionSubject::isArrayPut)
+            .or(InstructionSubject::isFilledNewArray);
+
+    for (MethodSubject method : mainClass.allMethods()) {
+      String methodName = method.getOriginalName();
+      if (!methodName.contains("Should")) {
+        continue;
+      }
+      boolean shouldOptimize = !isDebug && methodName.contains("ShouldOptimize");
+      assertEquals(
+          methodName, shouldOptimize, method.streamInstructions().noneMatch(stringFormatMatcher));
+      assertEquals(
+          methodName,
+          shouldOptimize,
+          method.streamInstructions().noneMatch(arrayInstructionsMatcher));
+      if (shouldOptimize) {
+        // All Integer.valueOf() should be gone in non-phi tests.
+        if (!"intPlaceholderWithLocaleShouldOptimize".equals(methodName)) {
+          assertTrue(methodName, method.streamInstructions().noneMatch(valueOfMatcher));
+        }
+      }
+
+      if (shouldOptimize && methodName.endsWith("Fully")) {
+        // Should simplify into a single ConstString.
+        assertTrue(method.streamInstructions().noneMatch(stringBuilderMatcher));
+      } else if (shouldOptimize) {
+        assertEquals(
+            methodName,
+            !shouldOptimize,
+            method.streamInstructions().noneMatch(stringBuilderMatcher));
+      }
+    }
+  }
+
+  public static class TestFormattable implements Formattable {
+    @Override
+    public void formatTo(Formatter formatter, int flags, int width, int precision) {
+      formatter.format("Formattable");
+    }
+  }
+
+  public static class TestClass {
+    private static final boolean ALWAYS_TRUE = System.currentTimeMillis() > 0;
+
+    @NeverInline
+    private static void noPlaceholdersShouldOptimizeFully() {
+      System.out.println(String.format("No %% Place %%") + String.format("", (Object[]) null));
+      // Test no outValue().
+      // Non-english locale should still optimize when it's just %s.
+      String.format(Locale.FRENCH, "Just %s.", "percent s");
+    }
+
+    @NeverInline
+    private static void stringPlaceholderShouldOptimizeFully() {
+      System.out.println(String.format("%s", "X"));
+      System.out.println(String.format("%s after", "X"));
+      System.out.println(String.format("before %s", 0));
+      System.out.println(String.format("before %s after", (Object) null));
+    }
+
+    @NeverInline
+    private static void phiAndNullShouldOptimize() {
+      Object[] args = new Object[2];
+      while (!ALWAYS_TRUE) {}
+      args[0] = ALWAYS_TRUE ? "S" : null;
+      System.out.println(String.format("phiNull %s %s", args));
+    }
+
+    @NeverInline
+    private static void catchHandlersShouldOptimize() {
+      try {
+        Object[] args = new Object[2];
+        args[0] = 1;
+        try {
+          do {
+            // Excess conditionals to ensure coverage of non-fast paths in
+            // ValueUtils.computeSimpleCaseDominatorBlocks().
+            args[1] = ALWAYS_TRUE ? Boolean.TRUE : Boolean.FALSE;
+          } while (!ALWAYS_TRUE && System.currentTimeMillis() > 0
+              || System.currentTimeMillis() < 0);
+          System.out.println(String.format("catch %s %s", args));
+        } catch (RuntimeException e) {
+          System.out.println("threw");
+        }
+      } catch (AssertionError e) {
+        throw e;
+      }
+    }
+
+    @NeverInline
+    private static void catchHandlersShouldNotOptimize() {
+      Object[] args = new Object[2];
+      try {
+        args[0] = 1;
+        System.out.print("might throw ");
+        args[1] = 2;
+      } catch (RuntimeException e) {
+      }
+      System.out.println(String.format("%s %s", args));
+    }
+
+    @NeverInline
+    private static void arrayReuseShouldNotOptimize() {
+      Object[] args = new Object[2];
+      System.out.println(String.format("reuse 1 %s %s", args));
+      System.out.println(String.format("reuse 2 %s %s", args));
+
+      // Also tests array-put after usage while in the same block.
+      args = new Object[2];
+      args[0] = "A";
+      System.out.println(String.format("reuse 3 %s %s", args));
+      args[1] = "B";
+    }
+
+    @NeverInline
+    private static void tooManyArgsShouldOptimizeFully() {
+      // We could remove excess args, but it's very rare, so we don't bother.
+      System.out.println(String.format("extra", 1));
+      System.out.println(String.format("extra", 1, 2));
+    }
+
+    @NeverInline
+    private static void exceptionalCallsShouldNotOptimize() {
+      try {
+        String.format(null);
+        throw new AssertionError("Expect to raise NPE");
+      } catch (NullPointerException npe) {
+        // expected
+        System.out.print("1");
+      }
+      try {
+        String.format("%d", "str");
+        throw new AssertionError("Expected to raise IllegalFormatConversionException");
+      } catch (IllegalFormatConversionException e) {
+        // expected
+        System.out.print("2");
+      }
+      try {
+        String.format("%s");
+        throw new AssertionError("Expected to raise MissingFormatArgumentException");
+      } catch (MissingFormatArgumentException e) {
+        // expected
+        System.out.print("3");
+      }
+      try {
+        String.format("%s %s", "");
+        throw new AssertionError("Expected to raise MissingFormatArgumentException");
+      } catch (MissingFormatArgumentException e) {
+        // expected
+        System.out.print("4");
+      }
+      try {
+        String.format("%c", 0x1200000);
+        throw new AssertionError("Expected to raise IllegalFormatCodePointException");
+      } catch (IllegalFormatCodePointException e) {
+        // expected
+        System.out.print("5");
+      }
+      try {
+        String.format("trailing %");
+        throw new AssertionError("Expected to raise UnknownFormatConversionException");
+      } catch (UnknownFormatConversionException e) {
+        // expected
+        System.out.println("6");
+      }
+    }
+
+    @NeverInline
+    private static void intPlaceholderWithoutLocaleShouldNotOptimize() {
+      System.out.println(String.format("int0a %d", 0));
+      // new Locale("ar") produces different results on different ART versions.
+      System.out.println(String.format(Locale.FRENCH, "int0b %d", 0));
+    }
+
+    @NeverInline
+    private static Integer returnsInteger() {
+      return ALWAYS_TRUE ? null : 1;
+    }
+
+    @NeverInline
+    private static void intPlaceholderWithLocaleShouldOptimize() {
+      System.out.println(String.format((Locale) null, "int1 %d", 0));
+      System.out.println(String.format(Locale.US, "int2 %d", ALWAYS_TRUE ? 1 : 0));
+      System.out.println(String.format(Locale.ROOT, "int3 %d", Long.MAX_VALUE));
+      System.out.println(String.format(Locale.ENGLISH, "int4 %d", returnsInteger()));
+    }
+
+    @NeverInline
+    private static void booleanPlaceholderShouldOptimize() {
+      // Boolean.valueOf()
+      System.out.println(String.format("bool0 %b", true));
+      // isDefinitelyNull() -> "false"
+      System.out.println(String.format("bool1 %b", (Object) null));
+      // null param --> "false"
+      System.out.println(String.format("bool2 %b", new Object[1]));
+      // Type == Boolean without Boolean.valueOf().
+      System.out.println(String.format("bool3 %b", Boolean.TRUE));
+    }
+
+    @NeverInline
+    private static Boolean returnsBoolean() {
+      return ALWAYS_TRUE ? true : null;
+    }
+
+    @NeverInline
+    private static void booleanPlaceholderShouldNotOptimize() {
+      Boolean maybeNull = ALWAYS_TRUE ? null : Boolean.TRUE;
+      // Might be null, so cannot optimize.
+      System.out.println(String.format("nobool0 %b", maybeNull));
+      // Not using Boolean.valueOf(), so don't optimize.
+      System.out.println(String.format("nobool1 %b", 0));
+      // Not the correct type, so don't optimize.
+      System.out.println(String.format("nobool2 %b", ""));
+      System.out.println(String.format("nobool3 %b", returnsBoolean()));
+    }
+
+    @NeverInline
+    private static void fancyPlaceholdersShouldNotOptimize() {
+      System.out.print("Fancy");
+      System.out.print(String.format(" %1$d", 0));
+      System.out.print(String.format(" %1s", 0));
+      System.out.print(String.format(" %o", 0));
+      System.out.print(String.format(" %x", 0));
+      System.out.print(String.format(" %(,.2f", 0f));
+      System.out.println(String.format(" %c", 64));
+    }
+
+    @NeverInline
+    private static void formattableShouldNotOptimize() {
+      System.out.println(String.format("%s", new TestFormattable()));
+    }
+
+    @NeverInline
+    private static void nonLiteralSpecShouldNotOptimize() {
+      String spec = ALWAYS_TRUE ? "NLS" : null;
+      System.out.println(String.format(spec));
+      System.out.println(String.format(Locale.CANADA.toString()));
+    }
+
+    public static void main(String[] args) throws UnsupportedEncodingException {
+      noPlaceholdersShouldOptimizeFully();
+      stringPlaceholderShouldOptimizeFully();
+      phiAndNullShouldOptimize();
+      catchHandlersShouldOptimize();
+      catchHandlersShouldNotOptimize();
+      arrayReuseShouldNotOptimize();
+      tooManyArgsShouldOptimizeFully();
+      intPlaceholderWithoutLocaleShouldNotOptimize();
+      intPlaceholderWithLocaleShouldOptimize();
+      booleanPlaceholderShouldOptimize();
+      booleanPlaceholderShouldNotOptimize();
+      fancyPlaceholdersShouldNotOptimize();
+      formattableShouldNotOptimize();
+      nonLiteralSpecShouldNotOptimize();
+      exceptionalCallsShouldNotOptimize();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/B140588497.java b/src/test/java/com/android/tools/r8/ir/regalloc/B140588497.java
index 82653f9..e2bad4a 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/B140588497.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/B140588497.java
@@ -54,7 +54,7 @@
               while (it.hasNext()) {
                 numbers.add(it.next().getConstNumber());
               }
-              assertEquals(new LongArrayList(new long[] {0, 1, 2, 3, 4, 5}), numbers);
+              assertEquals(new LongArrayList(new long[] {4, 5, 0, 1, 2, 3}), numbers);
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index c579123..4759e94 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
@@ -94,7 +95,11 @@
 
     @Override
     public void replaceCurrentInstructionWithConstClass(
-        AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+        AppView<?> appView,
+        IRCode code,
+        DexType type,
+        DebugLocalInfo localInfo,
+        AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
@@ -105,7 +110,7 @@
 
     @Override
     public void replaceCurrentInstructionWithConstString(
-        AppView<?> appView, IRCode code, DexString value) {
+        AppView<?> appView, IRCode code, DexString value, AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
@@ -200,7 +205,7 @@
     public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
         IRCode code,
         BasicBlockIterator blockIterator,
-        Instruction[] instructions,
+        Collection<Instruction> instructionsToAdd,
         InternalOptions options) {
       throw new Unimplemented();
     }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
deleted file mode 100644
index fd8154f..0000000
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
+++ /dev/null
@@ -1,255 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno;
-
-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.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.JavaCompilerTool;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter.AnnotationVisitorInterface;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
-import com.android.tools.r8.keepanno.ast.KeepDeclaration;
-import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.processor.KeepEdgeProcessor;
-import com.android.tools.r8.keepanno.testsource.KeepClassAndDefaultConstructorSource;
-import com.android.tools.r8.keepanno.testsource.KeepDependentFieldSource;
-import com.android.tools.r8.keepanno.testsource.KeepFieldSource;
-import com.android.tools.r8.keepanno.testsource.KeepSourceEdges;
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.transformers.ClassTransformer;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.ZipUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.objectweb.asm.AnnotationVisitor;
-
-@Ignore("b/248408342: These test break on r8lib builds because of src&test using ASM classes.")
-@RunWith(Parameterized.class)
-public class KeepEdgeAnnotationsTest extends TestBase {
-
-  private static class ParamWrapper {
-    private final Class<?> clazz;
-    private final TestParameters params;
-
-    public ParamWrapper(Class<?> clazz, TestParameters params) {
-      this.clazz = clazz;
-      this.params = params;
-    }
-
-    @Override
-    public String toString() {
-      return clazz.getSimpleName() + ", " + params.toString();
-    }
-  }
-
-  private static List<Class<?>> getTestClasses() {
-    return ImmutableList.of(
-        KeepClassAndDefaultConstructorSource.class,
-        KeepFieldSource.class,
-        KeepDependentFieldSource.class);
-  }
-
-  private final TestParameters parameters;
-  private final Class<?> source;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static List<ParamWrapper> data() {
-    TestParametersCollection params =
-        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
-    return getTestClasses().stream()
-        .flatMap(c -> params.stream().map(p -> new ParamWrapper(c, p)))
-        .collect(Collectors.toList());
-  }
-
-  public KeepEdgeAnnotationsTest(ParamWrapper wrapper) {
-    this.parameters = wrapper.params;
-    this.source = wrapper.clazz;
-  }
-
-  private String getExpected() {
-    return KeepSourceEdges.getExpected(source);
-  }
-
-  @Test
-  public void testProcessorClassfiles() throws Exception {
-    assumeTrue(parameters.isCfRuntime());
-    Path out =
-        JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
-            .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
-            .addClasspathFiles(ToolHelper.getKeepAnnoPath())
-            .addClassNames(Collections.singletonList(typeName(source)))
-            .addClasspathFiles(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "test"))
-            .addClasspathFiles(ToolHelper.getR8WithRelocatedDeps())
-            .compile();
-
-    CodeInspector inspector = new CodeInspector(out);
-    checkSynthesizedKeepEdgeClass(inspector, out);
-    // The source is added as a classpath name but not part of the compilation unit output.
-    assertThat(inspector.clazz(source), isAbsent());
-
-    testForJvm(parameters)
-        .addProgramClassesAndInnerClasses(source)
-        .addProgramFiles(out)
-        .run(parameters.getRuntime(), source)
-        .assertSuccessWithOutput(getExpected());
-  }
-
-  @Test
-  public void testProcessorJavaSource() throws Exception {
-    assumeTrue(parameters.isCfRuntime());
-    Path out =
-        JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
-            .addSourceFiles(ToolHelper.getSourceFileForTestClass(source))
-            .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
-            .addClasspathFiles(ToolHelper.getKeepAnnoPath())
-            .addClasspathFiles(ToolHelper.getDeps())
-            .compile();
-    testForJvm(parameters)
-        .addProgramFiles(out)
-        .run(parameters.getRuntime(), source)
-        .assertSuccessWithOutput(getExpected())
-        .inspect(
-            inspector -> {
-              assertThat(inspector.clazz(source), isPresent());
-              checkSynthesizedKeepEdgeClass(inspector, out);
-            });
-  }
-
-  public static List<byte[]> getInputClassesWithoutKeepAnnotations(Collection<Class<?>> classes)
-      throws Exception {
-    List<byte[]> transformed = new ArrayList<>(classes.size());
-    for (Class<?> clazz : classes) {
-      transformed.add(
-          transformer(clazz).removeAnnotations(AnnotationConstants::isKeepAnnotation).transform());
-    }
-    return transformed;
-  }
-
-  /** Wrapper to bridge ASM visitors when using the r8lib compiled version of the keepanno lib. */
-  private AnnotationVisitorInterface wrap(AnnotationVisitor visitor) {
-    if (visitor == null) {
-      return null;
-    }
-    return new AnnotationVisitorInterface() {
-      @Override
-      public int version() {
-        return KeepEdgeReader.ASM_VERSION;
-      }
-
-      @Override
-      public void visit(String name, Object value) {
-        visitor.visit(name, value);
-      }
-
-      @Override
-      public void visitEnum(String name, String descriptor, String value) {
-        visitor.visitEnum(name, descriptor, value);
-      }
-
-      @Override
-      public AnnotationVisitorInterface visitAnnotation(String name, String descriptor) {
-        AnnotationVisitor v = visitor.visitAnnotation(name, descriptor);
-        return v == visitor ? this : wrap(v);
-      }
-
-      @Override
-      public AnnotationVisitorInterface visitArray(String name) {
-        AnnotationVisitor v = visitor.visitArray(name);
-        return v == visitor ? this : wrap(v);
-      }
-
-      @Override
-      public void visitEnd() {
-        visitor.visitEnd();
-      }
-    };
-  }
-
-  @Test
-  public void testAsmReader() throws Exception {
-    assumeTrue(parameters.isCfRuntime());
-    List<KeepEdge> expectedEdges = KeepSourceEdges.getExpectedEdges(source);
-    ClassReference clazz = Reference.classFromClass(source);
-    // Original bytes of the test class.
-    byte[] original = ToolHelper.getClassAsBytes(source);
-    // Strip out all the annotations to ensure they are actually added again.
-    byte[] stripped =
-        getInputClassesWithoutKeepAnnotations(Collections.singletonList(source)).get(0);
-    // Manually add in the expected edges again.
-    byte[] readded =
-        transformer(stripped, clazz)
-            .addClassTransformer(
-                new ClassTransformer() {
-
-                  @Override
-                  public void visitEnd() {
-                    for (KeepEdge edge : expectedEdges) {
-                      KeepEdgeWriter.writeEdge(
-                          edge, (desc, visible) -> wrap(super.visitAnnotation(desc, visible)));
-                    }
-                    super.visitEnd();
-                  }
-                })
-            .transform();
-
-    // Read the edges from each version.
-    List<KeepDeclaration> originalEdges = KeepEdgeReader.readKeepEdges(original);
-    List<KeepDeclaration> strippedEdges = KeepEdgeReader.readKeepEdges(stripped);
-    List<KeepDeclaration> readdedEdges = KeepEdgeReader.readKeepEdges(readded);
-
-    // The edges are compared to the "expected" ast to ensure we don't hide failures in reading or
-    // writing.
-    assertEquals(Collections.emptyList(), strippedEdges);
-    assertEquals(expectedEdges, originalEdges);
-    assertEquals(expectedEdges, readdedEdges);
-  }
-
-  @Test
-  public void testExtractAndRun() throws Exception {
-    testForR8(parameters.getBackend())
-        .enableExperimentalKeepAnnotations()
-        .addProgramClassesAndInnerClasses(source)
-        .addKeepMainRule(source)
-        .setMinApi(parameters)
-        .run(parameters.getRuntime(), source)
-        .assertSuccessWithOutput(getExpected());
-  }
-
-  private void checkSynthesizedKeepEdgeClass(CodeInspector inspector, Path data)
-      throws IOException {
-    String synthesizedEdgesClassName =
-        KeepEdgeProcessor.getClassTypeNameForSynthesizedEdges(source.getTypeName());
-    ClassSubject synthesizedEdgesClass = inspector.clazz(synthesizedEdgesClassName);
-    assertThat(synthesizedEdgesClass, isPresent());
-    assertThat(synthesizedEdgesClass.annotation(Edge.CLASS.getTypeName()), isPresent());
-    String entry = ZipUtils.zipEntryNameForClass(synthesizedEdgesClass.getFinalReference());
-    byte[] bytes = ZipUtils.readSingleEntry(data, entry);
-    List<KeepDeclaration> keepEdges = KeepEdgeReader.readKeepEdges(bytes);
-    assertEquals(KeepSourceEdges.getExpectedEdges(source), keepEdges);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java
new file mode 100644
index 0000000..5991554
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java
@@ -0,0 +1,89 @@
+// 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;
+
+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.KeepTarget;
+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.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepInclusiveInstanceOfTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepInclusiveInstanceOfTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, Base.class, Sub.class, A.class);
+  }
+
+  static class Base {
+    static void hiddenMethod() {
+      System.out.println("on Base");
+    }
+  }
+
+  static class Sub extends Base {
+    static void hiddenMethod() {
+      System.out.println("on Sub");
+    }
+  }
+
+  static class A {
+
+    @UsesReflection({
+      // Because the method is static, this works whereas `classConstant = Base.class` won't
+      // keep the method on `Sub`.
+      @KeepTarget(instanceOfClassConstant = Base.class, methodName = "hiddenMethod")
+    })
+    public void foo(Base base) throws Exception {
+      base.getClass().getDeclaredMethod("hiddenMethod").invoke(null);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo(new Base());
+      new A().foo(new Sub());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
index b95f700..eb5791a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
@@ -80,6 +80,22 @@
   }
 
   @Test
+  public void testInvalidClassDeclWithBinding() {
+    assertThrowsWith(
+        () -> extractRuleForClass(BindingAndClassDeclarations.class),
+        allOf(containsString("class binding"), containsString("class patterns")));
+  }
+
+  static class BindingAndClassDeclarations {
+
+    // Both properties are using the "default" value of an empty string, but should still fail.
+    @UsesReflection({@KeepTarget(classFromBinding = "", className = "")})
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+
+  @Test
   public void testInvalidExtendsDecl() {
     assertThrowsWith(
         () -> extractRuleForClass(MultipleExtendsDeclarations.class),
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java
new file mode 100644
index 0000000..19505ca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java
@@ -0,0 +1,102 @@
+// 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;
+
+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.KeepTarget;
+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.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepNameAndInstanceOfTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+  static final String EXPECTED_R8 = StringUtils.lines("on Base", "No method on Sub");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepNameAndInstanceOfTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_R8);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, Base.class, Sub.class, A.class);
+  }
+
+  static class Base {
+
+    static void hiddenMethod() {
+      System.out.println("on Base");
+    }
+  }
+
+  static class Sub extends Base {
+
+    static void hiddenMethod() {
+      System.out.println("on Sub");
+    }
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          // Restricting the matching to Base will cause Sub to be stripped.
+          classConstant = Base.class,
+          instanceOfClassConstant = Base.class,
+          methodName = "hiddenMethod")
+    })
+    public void foo(Base base) throws Exception {
+      base.getClass().getDeclaredMethod("hiddenMethod").invoke(null);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      try {
+        new A().foo(new Base());
+      } catch (Exception e) {
+        System.out.println("No method on Base");
+      }
+      try {
+        new A().foo(new Sub());
+      } catch (Exception e) {
+        System.out.println("No method on Sub");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index 602ee63..8baa5ce 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.keepanno.ast.KeepBindings.BindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import com.android.tools.r8.utils.StringUtils;
@@ -137,7 +137,7 @@
                 KeepConsequences.builder()
                     .addTarget(
                         target(
-                            buildClassItem(CLASS)
+                            buildMemberItem(CLASS)
                                 .setMemberPattern(defaultInitializerPattern())
                                 .build()))
                     .build())
@@ -176,7 +176,7 @@
                     .addTarget(target(classItem(CLASS)))
                     .addTarget(
                         target(
-                            buildClassItem(CLASS)
+                            buildMemberItem(CLASS)
                                 .setMemberPattern(defaultInitializerPattern())
                                 .build()))
                     .build())
@@ -191,22 +191,24 @@
   @Test
   public void testKeepInstanceAndInitIfReferencedWithBinding() {
     KeepBindings.Builder bindings = KeepBindings.builder();
-    BindingSymbol classSymbol = bindings.create("CLASS");
+    KeepBindingSymbol classSymbol = bindings.create("CLASS");
     KeepEdge edge =
         KeepEdge.builder()
             .setBindings(bindings.addBinding(classSymbol, classItem(CLASS)).build())
             .setPreconditions(
                 KeepPreconditions.builder()
                     .addCondition(
-                        KeepCondition.builder().setItemReference(itemBinding(classSymbol)).build())
+                        KeepCondition.builder()
+                            .setItemReference(classItemBinding(classSymbol))
+                            .build())
                     .build())
             .setConsequences(
                 KeepConsequences.builder()
-                    .addTarget(target(itemBinding(classSymbol)))
+                    .addTarget(target(classItemBinding(classSymbol)))
                     .addTarget(
                         target(
-                            KeepItemPattern.builder()
-                                .setClassReference(classBinding(classSymbol))
+                            KeepMemberItemPattern.builder()
+                                .setClassReference(classItemBinding(classSymbol))
                                 .setMemberPattern(defaultInitializerPattern())
                                 .build()))
                     .build())
@@ -221,12 +223,8 @@
         extract(edge));
   }
 
-  private KeepItemReference itemBinding(BindingSymbol bindingName) {
-    return KeepItemReference.fromBindingReference(bindingName);
-  }
-
-  private KeepClassReference classBinding(BindingSymbol bindingName) {
-    return KeepClassReference.fromBindingReference(bindingName);
+  private KeepClassItemReference classItemBinding(KeepBindingSymbol bindingName) {
+    return KeepBindingReference.forClass(bindingName).toClassItemReference();
   }
 
   private KeepTarget target(KeepItemPattern item) {
@@ -241,10 +239,15 @@
     return buildClassItem(typeName).build();
   }
 
-  private KeepItemPattern.Builder buildClassItem(String typeName) {
-    return KeepItemPattern.builder().setClassPattern(KeepQualifiedClassNamePattern.exact(typeName));
+  private KeepClassItemPattern.Builder buildClassItem(String typeName) {
+    return KeepClassItemPattern.builder()
+        .setClassNamePattern(KeepQualifiedClassNamePattern.exact(typeName));
   }
 
+  private KeepMemberItemPattern.Builder buildMemberItem(String typeName) {
+    return KeepMemberItemPattern.builder()
+        .setClassReference(buildClassItem(typeName).build().toClassItemReference());
+  }
 
   private KeepMemberPattern defaultInitializerPattern() {
     return KeepMethodPattern.builder()
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
new file mode 100644
index 0000000..0914396
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -0,0 +1,111 @@
+// 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.doctests;
+
+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.KeepTarget;
+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.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UsesReflectionDocumentationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public UsesReflectionDocumentationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, BaseClass.class, SubClass.class, MyClass.class);
+  }
+
+  static class BaseClass {
+    void hiddenMethod() {
+      System.out.println("on Base");
+    }
+  }
+
+  static class SubClass extends BaseClass {
+    void hiddenMethod() {
+      System.out.println("on Sub");
+    }
+  }
+
+  /// DOC START: UsesReflection on virtual method
+  /* DOC TEXT START:
+  <p>If for example, your program is reflectively invoking a virtual method on some base class, you
+  should annotate the method that is performing the reflection with an annotation describing what
+  assumptions the reflective code is making.
+
+  <p>In the following example, the method `foo` is looking up the method with the name
+  `hiddenMethod` on objects that are instances of `BaseClass`. It is then invoking the method with
+   no other arguments than the receiver.
+
+  <p>The minimal requirement for this code to work is therefore that all methods with the name
+  `hiddenMethod` and the empty list of parameters are targeted if they are objects that are
+  instances of the class `BaseClass` or subclasses thereof.
+
+  <p>By placing the `UsesReflection` annotation on the method `foo` the annotation is only in
+  effect if the method `foo` is determined to be used by the shrinker.
+  So, if `foo` turns out to be dead code then the shrinker can remove `foo` and also ignore the
+  keep annotation.
+  DOC TEXT END */
+  static class MyClass {
+
+    @UsesReflection({
+      @KeepTarget(
+          instanceOfClassConstant = BaseClass.class,
+          methodName = "hiddenMethod",
+          methodParameters = {})
+    })
+    public void foo(BaseClass base) throws Exception {
+      base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
+    }
+  }
+
+  // DOC END
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new MyClass().foo(new BaseClass());
+      new MyClass().foo(new SubClass());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
index ac50217..0b2265a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.testsource;
 
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepClassItemReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepConsequences.Builder;
@@ -10,6 +12,7 @@
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
@@ -108,26 +111,36 @@
 
   // Ast helpers.
 
-  static KeepItemPattern mkClass(Class<?> clazz) {
+  static KeepClassItemPattern mkClass(Class<?> clazz) {
     KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
-    return KeepItemPattern.builder().setClassPattern(name).build();
+    return KeepClassItemPattern.builder().setClassNamePattern(name).build();
   }
 
-  static KeepItemPattern mkMethod(Class<?> clazz, String methodName) {
+  static KeepMemberItemPattern mkMethod(Class<?> clazz, String methodName) {
     KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
+    KeepClassItemReference classReference =
+        KeepClassItemPattern.builder().setClassNamePattern(name).build().toClassItemReference();
     KeepMethodPattern methodPattern =
         KeepMethodPattern.builder().setNamePattern(KeepMethodNamePattern.exact(methodName)).build();
-    KeepItemPattern methodItem =
-        KeepItemPattern.builder().setClassPattern(name).setMemberPattern(methodPattern).build();
+    KeepMemberItemPattern methodItem =
+        KeepMemberItemPattern.builder()
+            .setClassReference(classReference)
+            .setMemberPattern(methodPattern)
+            .build();
     return methodItem;
   }
 
   static KeepItemPattern mkField(Class<?> clazz, String fieldName) {
     KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
+    KeepClassItemReference classReference =
+        KeepClassItemPattern.builder().setClassNamePattern(name).build().toClassItemReference();
     KeepFieldPattern fieldPattern =
         KeepFieldPattern.builder().setNamePattern(KeepFieldNamePattern.exact(fieldName)).build();
-    KeepItemPattern fieldItem =
-        KeepItemPattern.builder().setClassPattern(name).setMemberPattern(fieldPattern).build();
+    KeepMemberItemPattern fieldItem =
+        KeepMemberItemPattern.builder()
+            .setClassReference(classReference)
+            .setMemberPattern(fieldPattern)
+            .build();
     return fieldItem;
   }
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinter.java b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinter.java
new file mode 100644
index 0000000..05b2ede
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinter.java
@@ -0,0 +1,17 @@
+// 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.utils;
+
+public class DocPrinter extends DocPrinterBase<DocPrinter> {
+
+  public static DocPrinter printer() {
+    return new DocPrinter();
+  }
+
+  @Override
+  public DocPrinter self() {
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java
new file mode 100644
index 0000000..f193da6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java
@@ -0,0 +1,88 @@
+// 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.utils;
+
+import com.android.tools.r8.examples.sync.Sync.Consumer;
+import com.google.common.html.HtmlEscapers;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class DocPrinterBase<T> {
+
+  private String title = null;
+  private final List<String> additionalLines = new ArrayList<>();
+
+  public abstract T self();
+
+  public T clearDocLines() {
+    additionalLines.clear();
+    return self();
+  }
+
+  public T setDocTitle(String title) {
+    assert this.title == null;
+    assert title.endsWith(".");
+    this.title = title;
+    return self();
+  }
+
+  public T addParagraph(String... lines) {
+    return addParagraph(Arrays.asList(lines));
+  }
+
+  public T addParagraph(List<String> lines) {
+    assert lines.isEmpty() || !lines.get(0).startsWith("<p>");
+    additionalLines.add("<p>");
+    additionalLines.addAll(lines);
+    return self();
+  }
+
+  public T addCodeBlock(String... lines) {
+    return addCodeBlock(Arrays.asList(lines));
+  }
+
+  public T addCodeBlock(List<String> lines) {
+    additionalLines.add("<pre>");
+    for (String line : lines) {
+      additionalLines.add(HtmlEscapers.htmlEscaper().escape(line).replace("@", "&#64;"));
+    }
+    additionalLines.add("</pre>");
+    return self();
+  }
+
+  public T addUnorderedList(String... items) {
+    return addUnorderedList(Arrays.asList(items));
+  }
+
+  public T addUnorderedList(List<String> items) {
+    additionalLines.add("<ul>");
+    for (String item : items) {
+      additionalLines.add("<li>" + item);
+    }
+    additionalLines.add("</ul>");
+    return self();
+  }
+
+  public void printDoc(Consumer<String> println) {
+    assert additionalLines.isEmpty() || title != null;
+    if (title == null) {
+      return;
+    }
+    if (additionalLines.isEmpty()) {
+      println.accept("/** " + title + " */");
+      return;
+    }
+    println.accept("/**");
+    println.accept(" * " + title);
+    for (String line : additionalLines) {
+      if (line.startsWith("<p>")) {
+        println.accept(" *");
+      }
+      println.accept(" * " + line);
+    }
+    println.accept(" */");
+  }
+}
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
new file mode 100644
index 0000000..67a5fe0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -0,0 +1,1263 @@
+// 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.utils;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+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.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepBinding;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepOption;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
+import com.android.tools.r8.keepanno.annotations.MethodAccessFlags;
+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.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class KeepItemAnnotationGenerator {
+
+  public static void main(String[] args) throws IOException {
+    Generator.class.getClassLoader().setDefaultAssertionStatus(true);
+    Generator.run();
+  }
+
+  private static String quote(String str) {
+    return "\"" + str + "\"";
+  }
+
+  private static String simpleName(Class<?> clazz) {
+    return clazz.getSimpleName();
+  }
+
+  private static class GroupMember extends DocPrinterBase<GroupMember> {
+
+    final String name;
+    String valueType = null;
+    String valueDefault = null;
+
+    GroupMember(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public GroupMember self() {
+      return this;
+    }
+
+    void generate(Generator generator) {
+      printDoc(generator::println);
+      if (valueDefault == null) {
+        generator.println(valueType + " " + name + "();");
+      } else {
+        generator.println(valueType + " " + name + "() default " + valueDefault + ";");
+      }
+    }
+
+    public void generateConstants(Generator generator) {
+      generator.println("public static final String " + name + " = " + quote(name) + ";");
+    }
+
+    public GroupMember requiredStringValue() {
+      assert valueDefault == null;
+      return defaultType("String");
+    }
+
+    public GroupMember requiredValueOfType(String type) {
+      assert valueDefault == null;
+      return defaultType(type);
+    }
+
+    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 defaultEmptyString() {
+      return defaultType("String").defaultValue(quote(""));
+    }
+
+    public GroupMember defaultObjectClass() {
+      return defaultType("Class<?>").defaultValue("Object.class");
+    }
+
+    public GroupMember defaultEmptyArray(String valueType) {
+      return defaultType(valueType + "[]").defaultValue("{}");
+    }
+
+    public GroupMember defaultEmptyArray(Class<?> type) {
+      return defaultEmptyArray(simpleName(type));
+    }
+  }
+
+  private static class Group {
+
+    final String name;
+    final List<GroupMember> members = new ArrayList<>();
+    final List<String> footers = new ArrayList<>();
+    final LinkedHashMap<String, Group> mutuallyExclusiveGroups = new LinkedHashMap<>();
+
+    private Group(String name) {
+      this.name = name;
+    }
+
+    Group addMember(GroupMember member) {
+      members.add(member);
+      return this;
+    }
+
+    Group addDocFooterParagraph(String footer) {
+      footers.add(footer);
+      return this;
+    }
+
+    void generate(Generator generator) {
+      assert !members.isEmpty();
+      for (GroupMember member : members) {
+        if (member != members.get(0)) {
+          generator.println();
+        }
+        List<String> mutuallyExclusiveProperties = new ArrayList<>();
+        for (GroupMember other : members) {
+          if (!member.name.equals(other.name)) {
+            mutuallyExclusiveProperties.add(other.name);
+          }
+        }
+        mutuallyExclusiveGroups.forEach(
+            (unused, group) -> {
+              group.members.forEach(m -> mutuallyExclusiveProperties.add(m.name));
+            });
+        if (mutuallyExclusiveProperties.size() == 1) {
+          member.addParagraph(
+              "Mutually exclusive with the property `"
+                  + mutuallyExclusiveProperties.get(0)
+                  + "` also defining "
+                  + name
+                  + ".");
+        } else if (mutuallyExclusiveProperties.size() > 1) {
+          member.addParagraph(
+              "Mutually exclusive with the following other properties defining " + name + ":");
+          member.addUnorderedList(mutuallyExclusiveProperties);
+        }
+        footers.forEach(member::addParagraph);
+        member.generate(generator);
+      }
+    }
+
+    void generateConstants(Generator generator) {
+      for (GroupMember member : members) {
+        member.generateConstants(generator);
+      }
+    }
+
+    public void addMutuallyExclusiveGroups(Group... groups) {
+      for (Group group : groups) {
+        mutuallyExclusiveGroups.computeIfAbsent(
+            group.name,
+            k -> {
+              // Mutually exclusive is bidirectional so link in with other group.
+              group.mutuallyExclusiveGroups.put(name, this);
+              return group;
+            });
+      }
+    }
+  }
+
+  private static class Generator {
+
+    private static final List<Class<?>> ANNOTATION_IMPORTS =
+        ImmutableList.of(ElementType.class, Retention.class, RetentionPolicy.class, Target.class);
+
+    private final PrintStream writer;
+    private int indent = 0;
+
+    public Generator(PrintStream writer) {
+      this.writer = writer;
+    }
+
+    private void withIndent(Runnable fn) {
+      indent += 2;
+      fn.run();
+      indent -= 2;
+    }
+
+    private void println() {
+      // Don't indent empty lines.
+      writer.println();
+    }
+
+    private void println(String line) {
+      assert line.length() > 0;
+      writer.print(Strings.repeat(" ", indent));
+      writer.println(line);
+    }
+
+    private void printCopyRight(int year) {
+      println(
+          CodeGenerationBase.getHeaderString(
+              year, KeepItemAnnotationGenerator.class.getSimpleName()));
+    }
+
+    private void printPackage(String pkg) {
+      println("package com.android.tools.r8.keepanno." + pkg + ";");
+      println();
+    }
+
+    private void printImports(Class<?>... imports) {
+      printImports(Arrays.asList(imports));
+    }
+
+    private void printImports(List<Class<?>> imports) {
+      for (Class<?> clazz : imports) {
+        println("import " + clazz.getCanonicalName() + ";");
+      }
+      println();
+    }
+
+    private static String KIND_GROUP = "kind";
+    private static String OPTIONS_GROUP = "options";
+    private static String CLASS_GROUP = "class";
+    private static String CLASS_NAME_GROUP = "class-name";
+    private static String INSTANCE_OF_GROUP = "instance-of";
+
+    private Group createDescriptionGroup() {
+      return new Group("description")
+          .addMember(
+              new GroupMember("description")
+                  .setDocTitle("Optional description to document the reason for this annotation.")
+                  .defaultEmptyString());
+    }
+
+    private Group createBindingsGroup() {
+      return new Group("bindings")
+          .addMember(new GroupMember("bindings").defaultEmptyArray(KeepBinding.class));
+    }
+
+    private Group createPreconditionsGroup() {
+      return new Group("preconditions")
+          .addMember(
+              new GroupMember("preconditions")
+                  .setDocTitle(
+                      "Conditions that should be satisfied for the annotation to be in effect.")
+                  .addParagraph(
+                      "Defaults to no conditions, thus trivially/unconditionally satisfied.")
+                  .defaultEmptyArray(KeepCondition.class));
+    }
+
+    private Group createConsequencesGroup() {
+      return new Group("consequences")
+          .addMember(
+              new GroupMember("consequences")
+                  .setDocTitle("Consequences that must be kept if the annotation is in effect.")
+                  .requiredValueOfArrayType(KeepTarget.class));
+    }
+
+    private Group createConsequencesAsValueGroup() {
+      return new Group("consequences")
+          .addMember(
+              new GroupMember("value")
+                  .setDocTitle("Consequences that must be kept if the annotation is in effect.")
+                  .requiredValueOfArrayType(KeepTarget.class));
+    }
+
+    private Group createAdditionalPreconditionsGroup() {
+      return new Group("additional-preconditions")
+          .addMember(
+              new GroupMember("additionalPreconditions")
+                  .setDocTitle("Additional preconditions for the annotation to be in effect.")
+                  .addParagraph("Defaults to no additional preconditions.")
+                  .defaultEmptyArray("KeepCondition"));
+    }
+
+    private Group createAdditionalTargetsGroup(String docTitle) {
+      return new Group("additional-targets")
+          .addMember(
+              new GroupMember("additionalTargets")
+                  .setDocTitle(docTitle)
+                  .addParagraph("Defaults to no additional targets.")
+                  .defaultEmptyArray("KeepTarget"));
+    }
+
+    private Group getKindGroup() {
+      return new Group(KIND_GROUP).addMember(getKindMember());
+    }
+
+    private static GroupMember getKindMember() {
+      return new GroupMember("kind")
+          .defaultType("KeepItemKind")
+          .defaultValue("KeepItemKind.DEFAULT")
+          .setDocTitle("Specify the kind of this item pattern.")
+          .addParagraph("Possible values are:")
+          .addUnorderedList(
+              KeepItemKind.ONLY_CLASS.name(),
+              KeepItemKind.ONLY_MEMBERS.name(),
+              KeepItemKind.CLASS_AND_MEMBERS.name())
+          .addParagraph(
+              "If unspecified the default for an item with no member patterns is",
+              KeepItemKind.ONLY_CLASS.name(),
+              "and if it does have member patterns the default is",
+              KeepItemKind.ONLY_MEMBERS.name());
+    }
+
+    private Group getKeepOptionsGroup() {
+      return new Group(OPTIONS_GROUP)
+          .addMember(
+              new GroupMember("allow")
+                  .setDocTitle(
+                      "Define the "
+                          + OPTIONS_GROUP
+                          + " that do not need to be preserved for the target.")
+                  .defaultEmptyArray("KeepOption"))
+          .addMember(
+              new GroupMember("disallow")
+                  .setDocTitle(
+                      "Define the " + OPTIONS_GROUP + " that *must* be preserved for the target.")
+                  .defaultEmptyArray("KeepOption"))
+          .addDocFooterParagraph(
+              "If nothing is specified for "
+                  + OPTIONS_GROUP
+                  + " the default is "
+                  + quote("allow none")
+                  + " / "
+                  + quote("disallow all")
+                  + ".");
+    }
+
+    private GroupMember bindingName() {
+      return new GroupMember("bindingName")
+          .setDocTitle(
+              "Name with which other bindings, conditions or targets "
+                  + "can reference the bound item pattern.")
+          .requiredStringValue();
+    }
+
+    private GroupMember classFromBinding() {
+      return new GroupMember("classFromBinding")
+          .setDocTitle("Define the " + CLASS_GROUP + " pattern by reference to a binding.")
+          .defaultEmptyString();
+    }
+
+    private Group createClassBindingGroup() {
+      return new Group(CLASS_GROUP)
+          .addMember(classFromBinding())
+          .addDocFooterParagraph("If none are specified the default is to match any class.");
+    }
+
+    private GroupMember className() {
+      return new GroupMember("className")
+          .setDocTitle("Define the " + CLASS_NAME_GROUP + " pattern by fully qualified class name.")
+          .defaultEmptyString();
+    }
+
+    private GroupMember classConstant() {
+      return new GroupMember("classConstant")
+          .setDocTitle(
+              "Define the " + CLASS_NAME_GROUP + " pattern by reference to a Class constant.")
+          .defaultObjectClass();
+    }
+
+    private Group createClassNamePatternGroup() {
+      return new Group(CLASS_NAME_GROUP)
+          .addMember(className())
+          .addMember(classConstant())
+          .addDocFooterParagraph("If none are specified the default is to match any class name.");
+    }
+
+    private GroupMember instanceOfClassName() {
+      return new GroupMember("instanceOfClassName")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes that are instances of the fully qualified class name.")
+          .defaultEmptyString();
+    }
+
+    private GroupMember instanceOfClassConstant() {
+      return new GroupMember("instanceOfClassConstant")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes that are instances the referenced Class constant.")
+          .defaultObjectClass();
+    }
+
+    private String getInstanceOfExclusiveDoc() {
+      return "The pattern is exclusive in that it does not match classes that are"
+          + " instances of the pattern, but only those that are instances of classes that"
+          + " are subclasses of the pattern.";
+    }
+
+    private GroupMember instanceOfClassNameExclusive() {
+      return new GroupMember("instanceOfClassNameExclusive")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes that are instances of the fully qualified class name.")
+          .addParagraph(getInstanceOfExclusiveDoc())
+          .defaultEmptyString();
+    }
+
+    private GroupMember instanceOfClassConstantExclusive() {
+      return new GroupMember("instanceOfClassConstantExclusive")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes that are instances the referenced Class constant.")
+          .addParagraph(getInstanceOfExclusiveDoc())
+          .defaultObjectClass();
+    }
+
+    private GroupMember extendsClassName() {
+      return new GroupMember("extendsClassName")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes extending the fully qualified class name.")
+          .addParagraph(getInstanceOfExclusiveDoc())
+          .addParagraph("This property is deprecated, use instanceOfClassName instead.")
+          .defaultEmptyString();
+    }
+
+    private GroupMember extendsClassConstant() {
+      return new GroupMember("extendsClassConstant")
+          .setDocTitle(
+              "Define the "
+                  + INSTANCE_OF_GROUP
+                  + " pattern as classes extending the referenced Class constant.")
+          .addParagraph(getInstanceOfExclusiveDoc())
+          .addParagraph("This property is deprecated, use instanceOfClassConstant instead.")
+          .defaultObjectClass();
+    }
+
+    private Group createClassInstanceOfPatternGroup() {
+      return new Group(INSTANCE_OF_GROUP)
+          .addMember(instanceOfClassName())
+          .addMember(instanceOfClassNameExclusive())
+          .addMember(instanceOfClassConstant())
+          .addMember(instanceOfClassConstantExclusive())
+          .addMember(extendsClassName())
+          .addMember(extendsClassConstant())
+          .addDocFooterParagraph(
+              "If none are specified the default is to match any class instance.");
+    }
+
+    private Group createMemberBindingGroup() {
+      return new Group("member")
+          .addMember(
+              new GroupMember("memberFromBinding")
+                  .setDocTitle("Define the member pattern in full by a reference to a binding.")
+                  .addParagraph(
+                      "Mutually exclusive with all other class and member pattern properties.",
+                      "When a member binding is referenced this item is defined to be that item,",
+                      "including its class and member patterns.")
+                  .defaultEmptyString());
+    }
+
+    private Group createMemberAccessGroup() {
+      return new Group("member-access")
+          .addMember(
+              new GroupMember("memberAccess")
+                  .setDocTitle("Define the member-access pattern by matching on access flags.")
+                  .addParagraph(
+                      "Mutually exclusive with all field and method properties",
+                      "as use restricts the match to both types of members.")
+                  .defaultEmptyArray("MemberAccessFlags"));
+    }
+
+    private String getMutuallyExclusiveForMethodProperties() {
+      return "Mutually exclusive with all field properties.";
+    }
+
+    private String getMutuallyExclusiveForFieldProperties() {
+      return "Mutually exclusive with all method properties.";
+    }
+
+    private String getMethodDefaultDoc(String suffix) {
+      return "If none, and other properties define this item as a method, the default matches "
+          + suffix
+          + ".";
+    }
+
+    private String getFieldDefaultDoc(String suffix) {
+      return "If none, and other properties define this item as a field, the default matches "
+          + suffix
+          + ".";
+    }
+
+    private Group createMethodAccessGroup() {
+      return new Group("method-access")
+          .addMember(
+              new GroupMember("methodAccess")
+                  .setDocTitle("Define the method-access pattern by matching on access flags.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any method-access flags"))
+                  .defaultEmptyArray("MethodAccessFlags"));
+    }
+
+    private Group createMethodNameGroup() {
+      return new Group("method-name")
+          .addMember(
+              new GroupMember("methodName")
+                  .setDocTitle("Define the method-name pattern by an exact method name.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any method name"))
+                  .defaultEmptyString());
+    }
+
+    private Group createMethodReturnTypeGroup() {
+      return new Group("return-type")
+          .addMember(
+              new GroupMember("methodReturnType")
+                  .setDocTitle(
+                      "Define the method return-type pattern by a fully qualified type or 'void'.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any return type"))
+                  .defaultEmptyString());
+    }
+
+    private Group createMethodParametersGroup() {
+      return new Group("parameters")
+          .addMember(
+              new GroupMember("methodParameters")
+                  .setDocTitle(
+                      "Define the method parameters pattern by a list of fully qualified types.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any parameters"))
+                  .defaultType("String[]")
+                  .defaultValue("{\"<default>\"}"));
+    }
+
+    private Group createFieldAccessGroup() {
+      return new Group("field-access")
+          .addMember(
+              new GroupMember("fieldAccess")
+                  .setDocTitle("Define the field-access pattern by matching on access flags.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any field-access flags"))
+                  .defaultEmptyArray("FieldAccessFlags"));
+    }
+
+    private Group createFieldNameGroup() {
+      return new Group("field-name")
+          .addMember(
+              new GroupMember("fieldName")
+                  .setDocTitle("Define the field-name pattern by an exact field name.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any field name"))
+                  .defaultEmptyString());
+    }
+
+    private Group createFieldTypeGroup() {
+      return new Group("field-type")
+          .addMember(
+              new GroupMember("fieldType")
+                  .setDocTitle("Define the field-type pattern by a fully qualified type.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any type"))
+                  .defaultEmptyString());
+    }
+
+    private void generateClassAndMemberPropertiesWithClassAndMemberBinding() {
+      internalGenerateClassAndMemberPropertiesWithBinding(true);
+    }
+
+    private void generateClassAndMemberPropertiesWithClassBinding() {
+      internalGenerateClassAndMemberPropertiesWithBinding(false);
+    }
+
+    private void internalGenerateClassAndMemberPropertiesWithBinding(boolean includeMemberBinding) {
+      // Class properties.
+      {
+        Group bindingGroup = createClassBindingGroup();
+        Group classNameGroup = createClassNamePatternGroup();
+        Group classInstanceOfGroup = createClassInstanceOfPatternGroup();
+        bindingGroup.addMutuallyExclusiveGroups(classNameGroup, classInstanceOfGroup);
+
+        bindingGroup.generate(this);
+        println();
+        classNameGroup.generate(this);
+        println();
+        classInstanceOfGroup.generate(this);
+        println();
+      }
+
+      // Member binding properties.
+      if (includeMemberBinding) {
+        createMemberBindingGroup().generate(this);
+        println();
+      }
+
+      // The remaining member properties.
+      generateMemberPropertiesNoBinding();
+    }
+
+    private void generateMemberPropertiesNoBinding() {
+      // General member properties.
+      createMemberAccessGroup().generate(this);
+      println();
+
+      // Method properties.
+      createMethodAccessGroup().generate(this);
+      println();
+      createMethodNameGroup().generate(this);
+      println();
+      createMethodReturnTypeGroup().generate(this);
+      println();
+      createMethodParametersGroup().generate(this);
+      println();
+
+      // Field properties.
+      createFieldAccessGroup().generate(this);
+      println();
+      createFieldNameGroup().generate(this);
+      println();
+      createFieldTypeGroup().generate(this);
+    }
+
+    private void generateKeepBinding() {
+      printCopyRight(2022);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A binding of a keep item.")
+          .addParagraph(
+              "Bindings allow referencing the exact instance of a match from a condition in other "
+                  + " conditions and/or targets. It can also be used to reduce duplication of"
+                  + " targets by sharing patterns.")
+          .addParagraph("An item can be:")
+          .addUnorderedList(
+              "a pattern on classes;", "a pattern on methods; or", "a pattern on fields.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface KeepBinding {");
+      println();
+      withIndent(
+          () -> {
+            new GroupMember("bindingName")
+                .setDocTitle(
+                    "Name with which other bindings, conditions or targets can reference the bound"
+                        + " item pattern.")
+                .requiredValueOfType("String")
+                .generate(this);
+            println();
+            getKindGroup().generate(this);
+            println();
+            generateClassAndMemberPropertiesWithClassBinding();
+          });
+      println();
+      println("}");
+    }
+
+    private void generateKeepTarget() {
+      printCopyRight(2022);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A target for a keep edge.")
+          .addParagraph(
+              "The target denotes an item along with options for what to keep. An item can be:")
+          .addUnorderedList(
+              "a pattern on classes;", "a pattern on methods; or", "a pattern on fields.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface KeepTarget {");
+      println();
+      withIndent(
+          () -> {
+            getKindGroup().generate(this);
+            println();
+            getKeepOptionsGroup().generate(this);
+            println();
+            generateClassAndMemberPropertiesWithClassAndMemberBinding();
+          });
+      println();
+      println("}");
+    }
+
+    private void generateKeepCondition() {
+      printCopyRight(2022);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A condition for a keep edge.")
+          .addParagraph(
+              "The condition denotes an item used as a precondition of a rule. An item can be:")
+          .addUnorderedList(
+              "a pattern on classes;", "a pattern on methods; or", "a pattern on fields.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface KeepCondition {");
+      println();
+      withIndent(
+          () -> {
+            generateClassAndMemberPropertiesWithClassAndMemberBinding();
+          });
+      println();
+      println("}");
+    }
+
+    private void generateKeepForApi() {
+      printCopyRight(2023);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle(
+              "Annotation to mark a class, field or method as part of a library API surface.")
+          .addParagraph(
+              "When a class is annotated, member patterns can be used to define which members are"
+                  + " to be kept. When no member patterns are specified the default pattern matches"
+                  + " all public and protected members.")
+          .addParagraph(
+              "When a member is annotated, the member patterns cannot be used as the annotated"
+                  + " member itself fully defines the item to be kept (i.e., itself).")
+          .printDoc(this::println);
+      println(
+          "@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,"
+              + " ElementType.CONSTRUCTOR})");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface KeepForApi {");
+      println();
+      withIndent(
+          () -> {
+            createDescriptionGroup().generate(this);
+            println();
+            createAdditionalTargetsGroup(
+                    "Additional targets to be kept as part of the API surface.")
+                .generate(this);
+            println();
+            GroupMember kindProperty = getKindMember();
+            kindProperty
+                .clearDocLines()
+                .addParagraph(
+                    "Default kind is",
+                    KeepItemKind.CLASS_AND_MEMBERS.name(),
+                    ", meaning the annotated class and/or member is to be kept.",
+                    "When annotating a class this can be set to",
+                    KeepItemKind.ONLY_CLASS.name(),
+                    "to avoid patterns on any members.",
+                    "That can be useful when the API members are themselves explicitly annotated.")
+                .addParagraph(
+                    "It is not possible to use",
+                    KeepItemKind.ONLY_CLASS.name(),
+                    "if annotating a member. Also, it is never valid to use kind",
+                    KeepItemKind.ONLY_MEMBERS.name(),
+                    "as the API surface must keep the class if any member is to be accessible.")
+                .generate(this);
+            println();
+            generateMemberPropertiesNoBinding();
+          });
+      println();
+      println("}");
+    }
+
+    private void generateUsesReflection() {
+      printCopyRight(2022);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle(
+              "Annotation to declare the reflective usages made by a class, method or field.")
+          .addParagraph(
+              "The annotation's 'value' is a list of targets to be kept if the annotated item is"
+                  + " used. The annotated item is a precondition for keeping any of the specified"
+                  + " targets. Thus, if an annotated method is determined to be unused by the"
+                  + " program, the annotation itself will not be in effect and the targets will not"
+                  + " be kept (assuming nothing else is otherwise keeping them).")
+          .addParagraph(
+              "The annotation's 'additionalPreconditions' is optional and can specify additional"
+                  + " conditions that should be satisfied for the annotation to be in effect.")
+          .addParagraph(
+              "The translation of the "
+                  + docLink(UsesReflection.class)
+                  + " annotation into a "
+                  + docLink(KeepEdge.class)
+                  + " is as follows:")
+          .addParagraph(
+              "Assume the item of the annotation is denoted by 'CTX' and referred to as its"
+                  + " context.")
+          .addCodeBlock(
+              annoSimpleName(UsesReflection.class)
+                  + "(value = targets, [additionalPreconditions = preconditions])",
+              "==>",
+              annoSimpleName(KeepEdge.class) + "(",
+              "  consequences = targets,",
+              "  preconditions = {createConditionFromContext(CTX)} + preconditions",
+              ")",
+              "",
+              "where",
+              "  KeepCondition createConditionFromContext(ctx) {",
+              "    if (ctx.isClass()) {",
+              "      return new KeepCondition(classTypeName = ctx.getClassTypeName());",
+              "    }",
+              "    if (ctx.isMethod()) {",
+              "      return new KeepCondition(",
+              "        classTypeName = ctx.getClassTypeName(),",
+              "        methodName = ctx.getMethodName(),",
+              "        methodReturnType = ctx.getMethodReturnType(),",
+              "        methodParameterTypes = ctx.getMethodParameterTypes());",
+              "    }",
+              "    if (ctx.isField()) {",
+              "      return new KeepCondition(",
+              "        classTypeName = ctx.getClassTypeName(),",
+              "        fieldName = ctx.getFieldName()",
+              "        fieldType = ctx.getFieldType());",
+              "    }",
+              "    // unreachable",
+              "  }")
+          .printDoc(this::println);
+      println(
+          "@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,"
+              + " ElementType.CONSTRUCTOR})");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + simpleName(UsesReflection.class) + " {");
+      println();
+      withIndent(
+          () -> {
+            createDescriptionGroup().generate(this);
+            println();
+            createConsequencesAsValueGroup().generate(this);
+            println();
+            createAdditionalPreconditionsGroup().generate(this);
+          });
+      println("}");
+    }
+
+    private void generateUsedByX(String annotationClassName, String doc) {
+      printCopyRight(2023);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("Annotation to mark a class, field or method as being " + doc + ".")
+          .addParagraph(
+              "Note: Before using this annotation, consider if instead you can annotate the code"
+                  + " that is doing reflection with "
+                  + docLink(UsesReflection.class)
+                  + ". Annotating the"
+                  + " reflecting code is generally more clear and maintainable, and it also"
+                  + " naturally gives rise to edges that describe just the reflected aspects of the"
+                  + " program. The "
+                  + docLink(UsedByReflection.class)
+                  + " annotation is suitable for cases where"
+                  + " the reflecting code is not under user control, or in migrating away from"
+                  + " rules.")
+          .addParagraph(
+              "When a class is annotated, member patterns can be used to define which members are"
+                  + " to be kept. When no member patterns are specified the default pattern is to"
+                  + " match just the class.")
+          .addParagraph(
+              "When a member is annotated, the member patterns cannot be used as the annotated"
+                  + " member itself fully defines the item to be kept (i.e., itself).")
+          .printDoc(this::println);
+      println(
+          "@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,"
+              + " ElementType.CONSTRUCTOR})");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + annotationClassName + " {");
+      println();
+      withIndent(
+          () -> {
+            createDescriptionGroup().generate(this);
+            println();
+            createPreconditionsGroup().generate(this);
+            println();
+            createAdditionalTargetsGroup(
+                    "Additional targets to be kept in addition to the annotated class/members.")
+                .generate(this);
+            println();
+            GroupMember kindProperty = getKindMember();
+            kindProperty
+                .clearDocLines()
+                .addParagraph(
+                    "When annotating a class without member patterns, the default kind is "
+                        + docLink(KeepItemKind.ONLY_CLASS)
+                        + ".")
+                .addParagraph(
+                    "When annotating a class with member patterns, the default kind is "
+                        + docLink(KeepItemKind.CLASS_AND_MEMBERS)
+                        + ".")
+                .addParagraph(
+                    "When annotating a member, the default kind is "
+                        + docLink(KeepItemKind.ONLY_MEMBERS)
+                        + ".")
+                .addParagraph("It is not possible to use ONLY_CLASS if annotating a member.")
+                .generate(this);
+            println();
+            generateMemberPropertiesNoBinding();
+          });
+      println();
+      println("}");
+    }
+
+    private String annoSimpleName(Class<?> clazz) {
+      return "@" + simpleName(clazz);
+    }
+
+    private String docLink(Class<?> clazz) {
+      return "{@link " + simpleName(clazz) + "}";
+    }
+
+    private String docLink(KeepItemKind kind) {
+      return "{@link KeepItemKind#" + kind.name() + "}";
+    }
+
+    private void generateConstants() {
+      printCopyRight(2023);
+      printPackage("ast");
+      printImports();
+      DocPrinter.printer()
+          .setDocTitle(
+              "Utility class for referencing the various keep annotations and their structure.")
+          .addParagraph(
+              "Use of these references avoids polluting the Java namespace with imports of the java"
+                  + " annotations which overlap in name with the actual semantic AST types.")
+          .printDoc(this::println);
+      println("public final class AnnotationConstants {");
+      withIndent(
+          () -> {
+            // Root annotations.
+            generateKeepEdgeConstants();
+            generateKeepForApiConstants();
+            generateUsesReflectionConstants();
+            generateUsedByReflectionConstants();
+            generateUsedByNativeConstants();
+            generateCheckRemovedConstants();
+            generateCheckOptimizedOutConstants();
+            // Common item fields.
+            generateItemConstants();
+            // Inner annotation classes.
+            generateBindingConstants();
+            generateConditionConstants();
+            generateTargetConstants();
+            generateKindConstants();
+            generateOptionConstants();
+            generateMemberAccessConstants();
+            generateMethodAccessConstants();
+            generateFieldAccessConstants();
+          });
+      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) + ";");
+    }
+
+    private void generateKeepEdgeConstants() {
+      println("public static final class Edge {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepEdge.class);
+            createDescriptionGroup().generateConstants(this);
+            createBindingsGroup().generateConstants(this);
+            createPreconditionsGroup().generateConstants(this);
+            createConsequencesGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateKeepForApiConstants() {
+      println("public static final class ForApi {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepForApi.class);
+            createDescriptionGroup().generateConstants(this);
+            createAdditionalTargetsGroup(".").generateConstants(this);
+            createMemberAccessGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateUsesReflectionConstants() {
+      println("public static final class UsesReflection {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(UsesReflection.class);
+            createDescriptionGroup().generateConstants(this);
+            createConsequencesAsValueGroup().generateConstants(this);
+            createAdditionalPreconditionsGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateUsedByReflectionConstants() {
+      println("public static final class UsedByReflection {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(UsedByReflection.class);
+            createDescriptionGroup().generateConstants(this);
+            createPreconditionsGroup().generateConstants(this);
+            createAdditionalTargetsGroup(".").generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateUsedByNativeConstants() {
+      println("public static final class UsedByNative {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(UsedByNative.class);
+            println("// Content is the same as " + simpleName(UsedByReflection.class) + ".");
+          });
+      println("}");
+      println();
+    }
+
+    private void generateCheckRemovedConstants() {
+      println("public static final class CheckRemoved {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(CheckRemoved.class);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateCheckOptimizedOutConstants() {
+      println("public static final class CheckOptimizedOut {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(CheckOptimizedOut.class);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateItemConstants() {
+      DocPrinter.printer()
+          .setDocTitle("Item properties common to binding items, conditions and targets.")
+          .printDoc(this::println);
+      println("public static final class Item {");
+      withIndent(
+          () -> {
+            // Bindings.
+            createClassBindingGroup().generateConstants(this);
+            createMemberBindingGroup().generateConstants(this);
+            // Classes.
+            createClassNamePatternGroup().generateConstants(this);
+            createClassInstanceOfPatternGroup().generateConstants(this);
+            // Members.
+            createMemberAccessGroup().generateConstants(this);
+            // Methods.
+            createMethodAccessGroup().generateConstants(this);
+            createMethodNameGroup().generateConstants(this);
+            createMethodReturnTypeGroup().generateConstants(this);
+            createMethodParametersGroup().generateConstants(this);
+            // Fields.
+            createFieldAccessGroup().generateConstants(this);
+            createFieldNameGroup().generateConstants(this);
+            createFieldTypeGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateBindingConstants() {
+      println("public static final class Binding {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepBinding.class);
+            bindingName().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateConditionConstants() {
+      println("public static final class Condition {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepCondition.class);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateTargetConstants() {
+      println("public static final class Target {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepTarget.class);
+            getKindGroup().generateConstants(this);
+            getKeepOptionsGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
+    private void generateKindConstants() {
+      println("public static final class Kind {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepItemKind.class);
+            for (KeepItemKind value : KeepItemKind.values()) {
+              if (value != KeepItemKind.DEFAULT) {
+                println(
+                    "public static final String "
+                        + value.name()
+                        + " = "
+                        + quote(value.name())
+                        + ";");
+              }
+            }
+          });
+      println("}");
+      println();
+    }
+
+    private void generateOptionConstants() {
+      println("public static final class Option {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepOption.class);
+            for (KeepOption value : KeepOption.values()) {
+              println(
+                  "public static final String " + value.name() + " = " + quote(value.name()) + ";");
+            }
+          });
+      println("}");
+      println();
+    }
+
+    private boolean isMemberAccessProperty(String name) {
+      for (MemberAccessFlags value : MemberAccessFlags.values()) {
+        if (value.name().equals(name)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    private void generateMemberAccessConstants() {
+      println("public static final class MemberAccess {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(MemberAccessFlags.class);
+            println("public static final String NEGATION_PREFIX = \"NON_\";");
+            for (MemberAccessFlags value : MemberAccessFlags.values()) {
+              if (!value.name().startsWith("NON_")) {
+                println(
+                    "public static final String "
+                        + value.name()
+                        + " = "
+                        + quote(value.name())
+                        + ";");
+              }
+            }
+          });
+      println("}");
+      println();
+    }
+
+    private void generateMethodAccessConstants() {
+      println("public static final class MethodAccess {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(MethodAccessFlags.class);
+            for (MethodAccessFlags value : MethodAccessFlags.values()) {
+              if (value.name().startsWith("NON_") || isMemberAccessProperty(value.name())) {
+                continue;
+              }
+              println(
+                  "public static final String " + value.name() + " = " + quote(value.name()) + ";");
+            }
+          });
+      println("}");
+      println();
+    }
+
+    private void generateFieldAccessConstants() {
+      println("public static final class FieldAccess {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(FieldAccessFlags.class);
+            for (FieldAccessFlags value : FieldAccessFlags.values()) {
+              if (value.name().startsWith("NON_") || isMemberAccessProperty(value.name())) {
+                continue;
+              }
+              println(
+                  "public static final String " + value.name() + " = " + quote(value.name()) + ";");
+            }
+          });
+      println("}");
+      println();
+    }
+
+    private static void writeFile(Path file, Consumer<Generator> fn) throws IOException {
+      ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+      PrintStream printStream = new PrintStream(byteStream);
+      Generator generator = new Generator(printStream);
+      fn.accept(generator);
+      String formatted = CodeGenerationBase.formatRawOutput(byteStream.toString());
+      Files.write(Paths.get(ToolHelper.getProjectRoot()).resolve(file), formatted.getBytes());
+    }
+
+    public static void run() throws IOException {
+      Path keepAnnoRoot = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno");
+
+      Path astPkg = keepAnnoRoot.resolve("ast");
+      writeFile(astPkg.resolve("AnnotationConstants.java"), Generator::generateConstants);
+
+      Path annoPkg = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno/annotations");
+      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(
+          annoPkg.resolve("UsedByReflection.java"),
+          g -> g.generateUsedByX("UsedByReflection", "accessed reflectively"));
+      writeFile(
+          annoPkg.resolve("UsedByNative.java"),
+          g -> g.generateUsedByX("UsedByNative", "accessed from native code via JNI"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index ba11cfa..c7956f8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -89,9 +89,9 @@
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
   private final Consumer<InternalOptions> disableAggressiveClassOptimizations =
-      o -> {
-        o.enableClassInlining = false;
-        o.enableVerticalClassMerging = false;
+      options -> {
+        options.enableClassInlining = false;
+        options.getVerticalClassMergerOptions().disable();
       };
 
   @Parameterized.Parameters(name = "{0}, {1}, allowAccessModification: {2}")
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathBridgeTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathBridgeTest.java
new file mode 100644
index 0000000..8450c93
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathBridgeTest.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.memberrebinding;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.memberrebinding.classpathbridge.Main;
+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 MemberRebindingClasspathBridgeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public void runTest(ThrowableConsumer<R8FullTestBuilder> configure) throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ProgramInterface.class)
+        .addProgramFiles(ToolHelper.getClassFilesForTestPackage(Main.class.getPackage()))
+        .addKeepMainRule(Main.class)
+        .addKeepAllClassesRule()
+        .setMode(CompilationMode.RELEASE)
+        .apply(configure)
+        .run(parameters.getRuntime(), Main.class);
+  }
+
+  @Test
+  public void runTestLibrary() throws Exception {
+    runTest(
+        builder ->
+            builder
+                .addDefaultRuntimeLibrary(parameters)
+                .addLibraryClasses(ClasspathInterface.class));
+  }
+
+  @Test
+  public void runTestClasspath() throws Exception {
+    runTest(builder -> builder.addClasspathClasses(ClasspathInterface.class));
+  }
+
+  @Test
+  public void runTestProgram() throws Exception {
+    runTest(builder -> builder.addProgramClasses(ClasspathInterface.class));
+  }
+
+  /* package private */ interface ClasspathInterface {
+    void m();
+  }
+
+  public interface ProgramInterface extends ClasspathInterface {}
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/Main.java b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/Main.java
new file mode 100644
index 0000000..9e99ecb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/Main.java
@@ -0,0 +1,10 @@
+// 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.memberrebinding.classpathbridge;
+
+public class Main {
+  public static void main(String[] args) {
+    new ProgramInterfaceInvoker().add(new ProgramInterfaceImpl());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceImpl.java b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceImpl.java
new file mode 100644
index 0000000..1f3c2b8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceImpl.java
@@ -0,0 +1,10 @@
+// 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.memberrebinding.classpathbridge;
+
+import com.android.tools.r8.memberrebinding.MemberRebindingClasspathBridgeTest.ProgramInterface;
+
+public class ProgramInterfaceImpl implements ProgramInterface {
+  public void m() {}
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceInvoker.java b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceInvoker.java
new file mode 100644
index 0000000..43ecc24
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/classpathbridge/ProgramInterfaceInvoker.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.memberrebinding.classpathbridge;
+
+import com.android.tools.r8.memberrebinding.MemberRebindingClasspathBridgeTest.ProgramInterface;
+
+/* package private */ class ProgramInterfaceInvoker {
+  public void add(ProgramInterface p) {
+    p.m();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
index cda3c7d..c71f930 100644
--- a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
@@ -53,10 +53,11 @@
     dexItemFactory = appView.dexItemFactory();
     ExecutorService executor = Executors.newSingleThreadExecutor();
     try {
-      return new Minifier(appView).run(executor, Timing.empty());
+      new Minifier(appView).run(executor, Timing.empty());
     } finally {
       executor.shutdown();
     }
+    return appView.getNamingLens();
   }
 
   protected static <T> Collection<Object[]> createTests(
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
index 7517540..a72ff12 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
@@ -108,7 +108,7 @@
         .addOptionsModification(
             options -> {
               options.inlinerOptions().enableInlining = false;
-              options.enableVerticalClassMerging = false;
+              options.getVerticalClassMergerOptions().disable();
               options.enableClassInlining = false;
             })
         .compile();
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
index d4afe52..e040cee 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
@@ -109,7 +109,7 @@
             .addOptionsModification(
                 options -> {
                   options.inlinerOptions().enableInlining = false;
-                  options.enableVerticalClassMerging = false;
+                  options.getVerticalClassMergerOptions().disable();
                 })
             .run(parameters.getRuntime(), noMappingMain)
             .assertSuccessWithOutput(noMappingExpected)
@@ -208,7 +208,7 @@
             .addOptionsModification(
                 options -> {
                   options.inlinerOptions().enableInlining = false;
-                  options.enableVerticalClassMerging = false;
+                  options.getVerticalClassMergerOptions().disable();
                 })
             .compile();
 
diff --git a/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java b/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
index 27320eb..36ee26d 100644
--- a/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
+++ b/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
@@ -12,14 +12,13 @@
 import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -138,12 +137,11 @@
         .addKeepRules(RULES)
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters)
-        .addOptionsModification(o -> o.enableVerticalClassMerging = enableClassMerging)
+        .addOptionsModification(
+            options -> options.getVerticalClassMergerOptions().setEnabled(enableClassMerging))
         .applyIf(
             onlyForceInlining,
-            builder ->
-                builder.addOptionsModification(
-                    o -> o.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE)))
+            builder -> builder.addOptionsModification(InlinerOptions::setOnlyForceInlining))
         .compile()
         .inspect(this::inspect);
   }
diff --git a/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java b/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
index 357b665..eb04750 100644
--- a/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
@@ -13,8 +13,7 @@
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.google.common.collect.ImmutableSet;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -64,11 +63,8 @@
             kotlinJars.getForConfiguration(kotlinParameters), kotlinc.getKotlinAnnotationJar())
         .addKeepMainRule(Main.class)
         .addKeepAllAttributes()
-        .addOptionsModification(
-            options -> {
-              options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-              options.enableClassInlining = false;
-            })
+        .addOptionsModification(options -> options.enableClassInlining = false)
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .addHorizontallyMergedClassesInspector(
             inspector ->
                 inspector.assertIsCompleteMergeGroup(
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
new file mode 100644
index 0000000..755096b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
@@ -0,0 +1,128 @@
+// 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 com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.Objects;
+import org.hamcrest.CoreMatchers;
+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 SimpleNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SimpleNumberUnboxingTest(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)
+        .allowDiagnosticWarningMessages()
+        .compile()
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of arg 0 of void"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.print(java.lang.Integer)"))
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of arg 0 of void"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.forwardToPrint2(java.lang.Integer)"))
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of arg 0 of void"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.directPrintUnbox(java.lang.Integer)"))
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of arg 0 of void"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.forwardToPrint(java.lang.Integer)"))
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of return value of java.lang.Integer"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.get()"))
+        .assertWarningMessageThatMatches(
+            CoreMatchers.containsString(
+                "Unboxing of return value of java.lang.Integer"
+                    + " com.android.tools.r8.numberunboxing.SimpleNumberUnboxingTest$Main.forwardGet()"))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("32", "33", "42", "43", "51", "52", "2");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // The number unboxer should immediately find this method is worth unboxing.
+      directPrintUnbox(31);
+      directPrintUnbox(32);
+
+      // The number unboxer should find the chain of calls is worth unboxing.
+      forwardToPrint(41);
+      forwardToPrint(42);
+
+      // The number unboxer should find this method is *not* worth unboxing.
+      Integer decode1 = Integer.decode("51");
+      Objects.requireNonNull(decode1);
+      directPrintNotUnbox(decode1);
+      Integer decode2 = Integer.decode("52");
+      Objects.requireNonNull(decode2);
+      directPrintNotUnbox(decode2);
+
+      // The number unboxer should unbox the return values.
+      System.out.println(forwardGet() + 1);
+    }
+
+    @NeverInline
+    private static Integer get() {
+      return System.currentTimeMillis() > 0 ? 1 : -1;
+    }
+
+    @NeverInline
+    private static Integer forwardGet() {
+      return get();
+    }
+
+    @NeverInline
+    private static void forwardToPrint(Integer boxed) {
+      forwardToPrint2(boxed);
+    }
+
+    @NeverInline
+    private static void forwardToPrint2(Integer boxed) {
+      print(boxed);
+    }
+
+    @NeverInline
+    private static void print(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+
+    @NeverInline
+    private static void directPrintUnbox(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+
+    @NeverInline
+    private static void directPrintNotUnbox(Integer boxed) {
+      System.out.println(boxed);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
index 31b779c..0704294 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.desugar.records.RecordTestUtils;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
@@ -102,9 +101,10 @@
                     options -> options.testing.disableRecordApplicationReaderMap = true))
         .run(parameters.getRuntime(), MAIN_REFERENCE.getTypeName())
         .applyIf(
-            canUseNativeRecords(parameters) && !runtimeWithRecordsSupport(parameters.getRuntime()),
-            r -> r.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT));
+            isRecordsDesugaredForD8(parameters)
+                || runtimeWithRecordsSupport(parameters.getRuntime()),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
+            r -> r.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
   }
 
   @Test
@@ -166,7 +166,7 @@
         SyntheticItemsTestUtils.syntheticRecordTagClass(),
         false,
         parameters.canUseNestBasedAccessesWhenDesugaring(),
-        canUseNativeRecords(parameters));
+        !isRecordsDesugaredForD8(parameters));
   }
 
   private void inspectR8(ArtProfileInspector profileInspector, CodeInspector inspector) {
@@ -176,7 +176,7 @@
         RECORD_REFERENCE,
         parameters.canHaveNonReboundConstructorInvoke(),
         parameters.canUseNestBasedAccesses(),
-        canUseNativeRecords(parameters) || parameters.isCfRuntime());
+        !isRecordsDesugaredForR8(parameters));
   }
 
   private void inspect(
@@ -211,15 +211,7 @@
             ? inspector.getTypeSubject(RECORD_REFERENCE.getTypeName())
             : recordTagClassSubject.asTypeSubject(),
         personRecordClassSubject.getSuperType());
-    assertEquals(
-        canUseRecords
-            ? (parameters.isCfRuntime()
-                    && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17)
-                    && !canUseNativeRecords(parameters)
-                ? 6
-                : 8)
-            : 10,
-        personRecordClassSubject.allMethods().size());
+    assertEquals(canUseRecords ? 6 : 10, personRecordClassSubject.allMethods().size());
 
     MethodSubject personInstanceInitializerSubject =
         personRecordClassSubject.uniqueInstanceInitializer();
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithoutAnyOptimizationTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithoutAnyOptimizationTest.java
new file mode 100644
index 0000000..4d9ce90
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithoutAnyOptimizationTest.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.repackage;
+
+
+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 RepackageWithoutAnyOptimizationTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(TestClass.class)
+        .addDontOptimize()
+        .addDontObfuscate()
+        .addDontShrink()
+        .addKeepRules("-repackageclasses")
+        .compile();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
index 3cbd047..022698c 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
@@ -180,7 +180,9 @@
         .addProgramClassFileData(DUMP)
         .addKeepMainRule(Main.class)
         .setMinApi(parameters)
-        .addOptionsModification(o -> o.enableVerticalClassMerging = enableVerticalClassMerging)
+        .addOptionsModification(
+            options ->
+                options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging))
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java b/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.java
new file mode 100644
index 0000000..9e45fe5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/InvalidMappingRangesB309080420Test.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.retrace;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class InvalidMappingRangesB309080420Test extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public InvalidMappingRangesB309080420Test(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  private static String MAPPING =
+      StringUtils.unixLines(
+          "a.q -> a.q:",
+          "    1:1:void a(com.example.Foo) -> a",
+          "    2:0:void a() -> a", // Unexpected line range [2:0] - interpreting as [2:2]
+          "    12:21:void a(android.content.Intent) -> a",
+          "a.x -> a.x:",
+          "    1:1:void a(com.example.Foo) -> a",
+          "    11:2:void a() -> a", // Unexpected line range [11:2] - interpreting as [2:11]
+          "    12:21:void a(android.content.Intent) -> a");
+
+  @Test
+  public void test() throws Exception {
+    TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl();
+    ProguardMappingSupplier.builder()
+        .setProguardMapProducer(ProguardMapProducer.fromString(MAPPING))
+        .setLoadAllDefinitions(true)
+        .build()
+        .createRetracer(handler);
+    handler.assertNoMessages();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
index edb6475..95c8b5b 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -7,7 +7,6 @@
 import static junit.framework.TestCase.assertEquals;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -487,19 +486,15 @@
   }
 
   @Test
-  public void testInvalidMinifiedRangeStackTrace() {
+  public void testInvalidMinifiedRangeStackTrace() throws Exception {
     assumeFalse(external);
-    assertThrows(
-        InvalidMappingFileException.class,
-        () -> runRetraceTest(new InvalidMinifiedRangeStackTrace()));
+    runRetraceTest(new InvalidMinifiedRangeStackTrace());
   }
 
   @Test
-  public void testInvalidOriginalRangeStackTrace() {
+  public void testInvalidOriginalRangeStackTrace() throws Exception {
     assumeFalse(external);
-    assertThrows(
-        InvalidMappingFileException.class,
-        () -> runRetraceTest(new InvalidOriginalRangeStackTrace()));
+    runRetraceTest(new InvalidOriginalRangeStackTrace());
   }
 
   private void inspectRetraceTest(
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidMinifiedRangeStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidMinifiedRangeStackTrace.java
index ad16a40..92e6567 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidMinifiedRangeStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidMinifiedRangeStackTrace.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.utils.StringUtils;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 
 public class InvalidMinifiedRangeStackTrace implements StackTraceForTest {
@@ -20,12 +19,16 @@
 
   @Override
   public List<String> retracedStackTrace() {
-    return Collections.emptyList();
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java:3)");
   }
 
   @Override
   public List<String> retraceVerboseStackTrace() {
-    return Collections.emptyList();
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:3)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidOriginalRangeStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidOriginalRangeStackTrace.java
index 3b8fa63..9a6bcde 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidOriginalRangeStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidOriginalRangeStackTrace.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.utils.StringUtils;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 
 public class InvalidOriginalRangeStackTrace implements StackTraceForTest {
@@ -20,12 +19,16 @@
 
   @Override
   public List<String> retracedStackTrace() {
-    return Collections.emptyList();
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java:3)");
   }
 
   @Override
   public List<String> retraceVerboseStackTrace() {
-    return Collections.emptyList();
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:3)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
index 1aaffaf..07f3f2b 100644
--- a/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
@@ -17,14 +17,13 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Objects;
@@ -130,9 +129,12 @@
         .addKeepMainRule(NonVirtualOverrideTestClass.class)
         .addOptionsModification(
             options -> {
-              options.enableVerticalClassMerging = dimensions.enableVerticalClassMerging;
-              options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+              options.enableClassInlining = false;
+              options
+                  .getVerticalClassMergerOptions()
+                  .setEnabled(dimensions.enableVerticalClassMerging);
             })
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
         .setMinApi(AndroidApiLevel.B)
         .compile();
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index d4bcbbd..9d3f2c5 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -131,7 +131,7 @@
                 options -> {
                   options.inlinerOptions().enableInlining = false;
                   options.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
-                  options.enableVerticalClassMerging = enableVerticalClassMerging;
+                  options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging);
                 })
             .enableNoHorizontalClassMergingAnnotations()
             .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking19Test.java b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking19Test.java
index 5b4b206..091dc98 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking19Test.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking19Test.java
@@ -49,7 +49,7 @@
         null,
         ImmutableList.of("src/test/examples/shaking19/keep-rules.txt"),
         // Disable vertical class merging to prevent A from being merged into B.
-        opt -> opt.enableVerticalClassMerging = false);
+        options -> options.getVerticalClassMergerOptions().disable());
   }
 
   private static void unusedRemoved(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index d6ac5bf..f41bf57 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -514,12 +514,12 @@
             .enableGraphInspector()
             .setMinApi(parameters)
             .addOptionsModification(
-                o -> {
-                  o.enableClassInlining = false;
+                options -> {
+                  options.enableClassInlining = false;
 
                   // Prevent InterfaceWithDefaultMethods from being merged into
                   // ClassImplementingInterface.
-                  o.enableVerticalClassMerging = false;
+                  options.getVerticalClassMergerOptions().disable();
                 })
             .compile();
     inspection.accept(compileResult.inspector());
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ExternalizableTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ExternalizableTest.java
index f6e0b49..468ae54 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ExternalizableTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ExternalizableTest.java
@@ -336,8 +336,12 @@
         "}");
 
     AndroidApp processedApp =
-        runShrinker(shrinker, CLASSES_FOR_SERIALIZABLE, config,
-            o -> o.enableVerticalClassMerging = enableVerticalClassMerging);
+        runShrinker(
+            shrinker,
+            CLASSES_FOR_SERIALIZABLE,
+            config,
+            options ->
+                options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging));
     // TODO(b/117302947): Need to update ART binary.
     if (shrinker.generatesCf()) {
       String output = runOnVM(
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
index f2b946e..d1775a6 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
@@ -83,7 +83,7 @@
   }
 
   private void configure(InternalOptions options) {
-    options.enableVerticalClassMerging = enableVerticalClassMerging;
+    options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging);
     // TODO(b/141093535): The precondition set for conditionals is currently based on the syntactic
     // form, when merging is enabled, if the precondition is merged to a differently named type, the
     // rule will still fire, but the reported precondition type is incorrect.
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 da7c397..ed578cd 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
@@ -77,7 +77,8 @@
   public void configure(R8FullTestBuilder builder) {
     builder
         .addOptionsModification(
-            options -> options.enableVerticalClassMerging = enableVerticalClassMerging)
+            options ->
+                options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging))
         .enableNoAccessModificationAnnotationsForClasses();
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/interfacebridge/LambdaAbstractMethodErrorTest.java b/src/test/java/com/android/tools/r8/shaking/interfacebridge/LambdaAbstractMethodErrorTest.java
index 1b5c958..91e2a57 100644
--- a/src/test/java/com/android/tools/r8/shaking/interfacebridge/LambdaAbstractMethodErrorTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/interfacebridge/LambdaAbstractMethodErrorTest.java
@@ -31,10 +31,10 @@
         .addProgramClassesAndInnerClasses(Task.class, OuterClass.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            internalOptions -> {
-              internalOptions.inlinerOptions().enableInlining = false;
-              internalOptions.enableClassInlining = false;
-              internalOptions.enableVerticalClassMerging = false;
+            options -> {
+              options.inlinerOptions().enableInlining = false;
+              options.enableClassInlining = false;
+              options.getVerticalClassMergerOptions().disable();
             })
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class.getTypeName())
diff --git a/src/test/java/com/android/tools/r8/shaking/methods/interfaces/DefaultInterfaceMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/methods/interfaces/DefaultInterfaceMethodsTest.java
index 6f27362..0258428 100644
--- a/src/test/java/com/android/tools/r8/shaking/methods/interfaces/DefaultInterfaceMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/methods/interfaces/DefaultInterfaceMethodsTest.java
@@ -69,8 +69,7 @@
             .addProgramClasses(I.class, J.class)
             .setMinApi(parameters)
             .addKeepMethodRules(J.class, "void foo()")
-            .addOptionsModification(
-                internalOptions -> internalOptions.enableVerticalClassMerging = false)
+            .addOptionsModification(options -> options.getVerticalClassMergerOptions().disable())
             .addDontObfuscate()
             .compile();
     // TODO(b/144269679): We should be able to compile and run this.
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index e6ba0f6..4348acf 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1118,14 +1118,12 @@
               if (object instanceof String) {
                 local[i] = rewriteASMInternalTypeName((String) object);
               }
-              i++;
             }
             for (int i = 0; i < numStack; i++) {
               Object object = stack[i];
               if (object instanceof String) {
                 stack[i] = rewriteASMInternalTypeName((String) object);
               }
-              i++;
             }
             super.visitFrame(type, numLocal, local, numStack, stack);
           }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index 91a9a80..7f74f87 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.ClassReferenceUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Sets;
@@ -61,6 +62,12 @@
     return horizontallyMergedClasses.getSources();
   }
 
+  public ClassReference getTarget(ClassReference classReference) {
+    DexType sourceType = ClassReferenceUtils.toDexType(classReference, dexItemFactory);
+    DexType targetType = getTarget(sourceType);
+    return targetType.asClassReference();
+  }
+
   public DexType getTarget(DexType clazz) {
     return horizontallyMergedClasses.getMergeTargetOrDefault(clazz);
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MinificationInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MinificationInspector.java
new file mode 100644
index 0000000..5f3621a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MinificationInspector.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.codeinspector;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.ClassReferenceUtils;
+
+public class MinificationInspector {
+
+  private final DexItemFactory dexItemFactory;
+  private final NamingLens namingLens;
+
+  public MinificationInspector(DexItemFactory dexItemFactory, NamingLens namingLens) {
+    this.dexItemFactory = dexItemFactory;
+    this.namingLens = namingLens;
+  }
+
+  public ClassReference getTarget(ClassReference classReference) {
+    DexType sourceType = ClassReferenceUtils.toDexType(classReference, dexItemFactory);
+    DexString targetDescriptor = namingLens.lookupClassDescriptor(sourceType);
+    return Reference.classFromDescriptor(targetDescriptor.toString());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/RepackagingInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/RepackagingInspector.java
new file mode 100644
index 0000000..9ee6e1b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/RepackagingInspector.java
@@ -0,0 +1,27 @@
+// 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.codeinspector;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.repackaging.RepackagingLens;
+import com.android.tools.r8.utils.ClassReferenceUtils;
+
+public class RepackagingInspector {
+
+  private final DexItemFactory dexItemFactory;
+  private final RepackagingLens repackagingLens;
+
+  public RepackagingInspector(DexItemFactory dexItemFactory, RepackagingLens repackagingLens) {
+    this.dexItemFactory = dexItemFactory;
+    this.repackagingLens = repackagingLens;
+  }
+
+  public ClassReference getTarget(ClassReference classReference) {
+    DexType sourceType = ClassReferenceUtils.toDexType(classReference, dexItemFactory);
+    DexType targetType = repackagingLens.getNextClassType(sourceType);
+    return targetType.asClassReference();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
index d1f04cb..5b8f002 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
@@ -8,9 +8,9 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 
 public class VerticallyMergedClassesInspector {
 
diff --git a/third_party/dependencies_plugin.tar.gz.sha1 b/third_party/dependencies_plugin.tar.gz.sha1
index fa66acc..ba75d12 100644
--- a/third_party/dependencies_plugin.tar.gz.sha1
+++ b/third_party/dependencies_plugin.tar.gz.sha1
@@ -1 +1 @@
-ebc6c2750dc911f0a974f79395ef28d523c89d09
\ No newline at end of file
+369870d45ae8721ad1379b8bf108e107ad1b5175
\ No newline at end of file
diff --git a/third_party/opensource-apps/compose-examples/changed-bitwise-value-propagation.tar.gz.sha1 b/third_party/opensource-apps/compose-examples/changed-bitwise-value-propagation.tar.gz.sha1
new file mode 100644
index 0000000..7c3bb09
--- /dev/null
+++ b/third_party/opensource-apps/compose-examples/changed-bitwise-value-propagation.tar.gz.sha1
@@ -0,0 +1 @@
+0aac95399bcbeb352820e651c02d422a2ee53a3c
\ No newline at end of file
diff --git a/tools/cherry-pick.py b/tools/cherry-pick.py
index 6e505c5..4a9bca0 100755
--- a/tools/cherry-pick.py
+++ b/tools/cherry-pick.py
@@ -32,17 +32,16 @@
                         metavar='<hash>',
                         nargs='+',
                         help='Hashed to merge')
-
+    parser.add_argument('--remote',
+                        default='origin',
+                        help='The remote name (defaults to "origin")')
     return parser.parse_args()
 
 
 def run(args):
-    # Checkout the branch.
-    subprocess.check_output(['git', 'checkout', args.branch])
-
-    if (args.current_checkout):
+    if args.current_checkout:
         for i in range(len(args.hashes) + 1):
-            branch = 'cherry-%d' % (i + 1)
+            branch = 'cherry-%s-%d' % (args.branch, i + 1)
             print('Deleting branch %s' % branch)
             subprocess.run(['git', 'branch', branch, '-D'])
 
@@ -50,12 +49,12 @@
 
     count = 1
     for hash in args.hashes:
-        branch = 'cherry-%d' % count
+        branch = 'cherry-%s-%d' % (args.branch, count)
         print('Cherry-picking %s in %s' % (hash, branch))
-        if (count == 1):
+        if count == 1:
             subprocess.run([
                 'git', 'new-branch', branch, '--upstream',
-                'origin/%s' % args.branch
+                '%s/%s' % (args.remote, args.branch)
             ])
         else:
             subprocess.run(['git', 'new-branch', branch, '--upstream-current'])
@@ -64,7 +63,7 @@
         confirm_and_upload(branch, args, bugs)
         count = count + 1
 
-    branch = 'cherry-%d' % count
+    branch = 'cherry-%s-%d' % (args.branch, count)
     subprocess.run(['git', 'new-branch', branch, '--upstream-current'])
 
     old_version = 'unknown'
@@ -102,7 +101,7 @@
 
     subprocess.run(['git', 'commit', '-a', '-m', message])
     confirm_and_upload(branch, args, None)
-    if (not args.current_checkout):
+    if not args.current_checkout:
         while True:
             try:
                 answer = input(
@@ -140,12 +139,17 @@
             l.strip() for l in commit_message.decode('UTF-8').split('\n')
         ]
         for line in commit_lines:
+            bug = None
             if line.startswith('Bug: '):
-                normalized = line.replace('Bug: ', '').replace('b/', '')
-                if len(normalized) > 0:
-                    bugs.add(normalized)
+                bug = line.replace('Bug: ', '')
+            elif line.startswith('Fixed: '):
+                bug = line.replace('Fixed: ', '')
+            elif line.startswith('Fixes: '):
+                bug = line.replace('Fixes: ', '')
+            if bug:
+                bugs.add(bug.replace('b/', '').strip())
 
-    if (not args.no_upload):
+    if not args.no_upload:
         subprocess.run(['git', 'cl', 'upload', '--bypass-hooks'])
 
 
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 4f63ef9..05abfb5 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -102,9 +102,10 @@
   '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.2.0.
-  # See commit message for a13217f333cc65fb602502ac446698dd74d10b7f.
-  'org.spdx.sbom:org.spdx.sbom.gradle.plugin:0.4.0-r8-patch01',
+  # 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',
   # See https://github.com/FasterXML/jackson-core/issues/999.
   'ch.randelshofer:fastdoubleparser:0.8.0',
 ]
diff --git a/tools/create_r8lib.py b/tools/create_r8lib.py
index e8bf4f2..c8eb00e 100755
--- a/tools/create_r8lib.py
+++ b/tools/create_r8lib.py
@@ -103,6 +103,7 @@
     cmd.extend(['--map-id-template', map_id_template])
     cmd.extend(['--source-file-template', source_file_template])
     cmd.extend(['--output', args.output])
+    cmd.extend(['--pg-conf-output', args.output + '.config'])
     cmd.extend(['--pg-map-output', args.output + '.map'])
     cmd.extend(['--partition-map-output', args.output + '_map.zip'])
     cmd.extend(['--lib', jdk.GetJdkHome()])
diff --git a/tools/r8_release.py b/tools/r8_release.py
index a5d7a4c..116a52c 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -452,7 +452,7 @@
                 g4_open('METADATA')
                 metadata_path = os.path.join(third_party_r8, 'METADATA')
                 match_count = 0
-                match_count_expected = 11
+                match_count_expected = 10
                 version_match_regexp = r'[1-9]\.[0-9]{1,2}\.[0-9]{1,3}-dev'
                 for line in open(metadata_path, 'r'):
                     result = re.search(version_match_regexp, line)
diff --git a/tools/retrace.py b/tools/retrace.py
index 911c646..9768ff3 100755
--- a/tools/retrace.py
+++ b/tools/retrace.py
@@ -188,7 +188,7 @@
         )
 
     if not r8jar:
-        r8jar = utils.R8_JAR if no_r8lib else utils.R8LIB
+        r8jar = utils.R8_JAR if no_r8lib else utils.R8LIB_JAR
 
     retrace_args += [
         '-cp', r8jar, 'com.android.tools.r8.retrace.Retrace', map_path
diff --git a/tools/test.py b/tools/test.py
index efbe5a1..dcff8c9 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -51,7 +51,7 @@
     'jdk9',
     'jdk11',
     'jdk17',
-    'jdk20',
+    'jdk21',
 ] + ['dex-%s' % dexvm for dexvm in ALL_ART_VMS]