Merge commit 'f6a533d424161b8b606a6e54e01582005dda7766' into dev-release

Change-Id: I707a2ff8fb224417568bf634bf0d4483447eb68b
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index ddb244f..afa21b3 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -327,7 +327,7 @@
              "--map",
              "com.google.protobuf.**->com.android.tools.r8.com.google.protobuf",
              "--map",
-             "android.aapt.**->com.android.tools.r8.android.aaapt"
+             "android.aapt.**->com.android.tools.r8.android.aapt"
       ))
   }
 }
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index 75d6b24..4f7665a 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -21,6 +21,8 @@
 - [Introduction](#introduction)
 - [Build configuration](#build-configuration)
 - [Annotating code using reflection](#using-reflection)
+  - [Invoking methods](#using-reflection-methods)
+  - [Accessing fields](#using-reflection-fields)
 - [Annotating code used by reflection (or via JNI)](#used-by-reflection)
 - [Annotating APIs](#apis)
 - [Migrating rules to annotations](#migrating-rules)
@@ -42,9 +44,10 @@
 using Java annotations. The motivation for using these annotations is foremost
 to place the description of what to keep closer to the program point using
 reflective behavior. Doing so more directly connects the reflective code with
-the keep specification and makes it easier to maintain as the code develops. In
-addition, the annotations are defined independent from keep rules and have a
-hopefully more clear and direct meaning.
+the keep specification and makes it easier to maintain as the code develops.
+Often the keep annotations are only in effect if the annotated method is used,
+allowing more precise shrinking.  In addition, the annotations are defined
+independent from keep rules and have a hopefully more clear and direct meaning.
 
 
 ## Build configuration<a id="build-configuration"></a>
@@ -73,16 +76,22 @@
   # ... the rest of your R8 compilation command here ...
 ```
 
+
 ## Annotating code using reflection<a id="using-reflection"></a>
 
 The keep annotation library defines a family of annotations depending on your
 use case. You should generally prefer [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) where applicable.
+Common uses of reflection are to lookup fields and methods on classes. Examples
+of such use cases are detailed below.
+
+
+### Invoking methods<a id="using-reflection-methods"></a>
 
 For example, if your program is reflectively invoking a method, you
 should annotate the method that is doing the reflection. The annotation must describe the
 assumptions the reflective code makes.
 
-In the following example, the method `foo` is looking up the method with the name
+In the following example, the method `callHiddenMethod` 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.
 
@@ -92,7 +101,7 @@
 
 
 ```
-static class MyClass {
+public class MyHiddenMethodCaller {
 
   @UsesReflection({
     @KeepTarget(
@@ -100,7 +109,7 @@
         methodName = "hiddenMethod",
         methodParameters = {})
   })
-  public void foo(BaseClass base) throws Exception {
+  public void callHiddenMethod(BaseClass base) throws Exception {
     base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
   }
 }
@@ -108,6 +117,41 @@
 
 
 
+### Accessing fields<a id="using-reflection-fields"></a>
+
+For example, if your program is reflectively accessing the fields on a class, you should
+annotate the method that is doing the reflection.
+
+In the following example, the `printFieldValues` method takes in an object of
+type `PrintableFieldInterface` and then looks for all the fields declared on the class
+of the object.
+
+The [@KeepTarget](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html) describes these field targets. Since the printing only cares about preserving
+the fields, the [@KeepTarget.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#kind()) is set to [KeepItemKind.ONLY_FIELDS](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepItemKind.html#ONLY_FIELDS). Also, since printing
+the field names and values only requires looking up the field, printing its name and getting
+its value the [@KeepTarget.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constraints()) are set to just [KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP),
+[KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) and [KeepConstraint.FIELD_GET](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#FIELD_GET).
+
+
+```
+public class MyFieldValuePrinter {
+
+  @UsesReflection({
+    @KeepTarget(
+        instanceOfClassConstant = PrintableFieldInterface.class,
+        kind = KeepItemKind.ONLY_FIELDS,
+        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+  })
+  public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+    for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+      System.out.println(field.getName() + " = " + field.get(objectWithFields));
+    }
+  }
+}
+```
+
+
+
 ## Annotating code used by reflection (or via JNI)<a id="used-by-reflection"></a>
 
 TODO
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index cfa98e2..6e3efde 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -29,9 +29,10 @@
 using Java annotations. The motivation for using these annotations is foremost
 to place the description of what to keep closer to the program point using
 reflective behavior. Doing so more directly connects the reflective code with
-the keep specification and makes it easier to maintain as the code develops. In
-addition, the annotations are defined independent from keep rules and have a
-hopefully more clear and direct meaning.
+the keep specification and makes it easier to maintain as the code develops.
+Often the keep annotations are only in effect if the annotated method is used,
+allowing more precise shrinking.  In addition, the annotations are defined
+independent from keep rules and have a hopefully more clear and direct meaning.
 
 
 ## [Build configuration](build-configuration)
@@ -60,16 +61,29 @@
   # ... the rest of your R8 compilation command here ...
 ```
 
+
 ## [Annotating code using reflection](using-reflection)
 
 The keep annotation library defines a family of annotations depending on your
 use case. You should generally prefer `@UsesReflection` where applicable.
+Common uses of reflection are to lookup fields and methods on classes. Examples
+of such use cases are detailed below.
+
+
+### [Invoking methods](using-reflection-methods)
 
 [[[INCLUDE DOC:UsesReflectionOnVirtualMethod]]]
 
 [[[INCLUDE CODE:UsesReflectionOnVirtualMethod]]]
 
 
+### [Accessing fields](using-reflection-fields)
+
+[[[INCLUDE DOC:UsesReflectionFieldPrinter]]]
+
+[[[INCLUDE CODE:UsesReflectionFieldPrinter]]]
+
+
 ## [Annotating code used by reflection (or via JNI)](used-by-reflection)
 
 TODO
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 e97d2b5..579d46f 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
@@ -44,13 +44,23 @@
    * <p>Possible values are:
    *
    * <ul>
-   *   <li>ONLY_CLASS
-   *   <li>ONLY_MEMBERS
-   *   <li>CLASS_AND_MEMBERS
+   *   <li>{@link KeepItemKind#ONLY_CLASS}
+   *   <li>{@link KeepItemKind#ONLY_MEMBERS}
+   *   <li>{@link KeepItemKind#ONLY_METHODS}
+   *   <li>{@link KeepItemKind#ONLY_FIELDS}
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}
+   *   <li>{@link KeepItemKind#CLASS_AND_METHODS}
+   *   <li>{@link KeepItemKind#CLASS_AND_FIELDS}
    * </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
+   * <p>If unspecified the default kind for an item depends on its member patterns:
+   *
+   * <ul>
+   *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_METHODS} if method patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_FIELDS} if field patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_MEMBERS} otherwise.
+   * </ul>
    *
    * @return The kind for this pattern.
    */
@@ -288,11 +298,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -300,9 +355,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -335,7 +406,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
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 e539cc4..470266b 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
@@ -270,11 +270,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -282,9 +327,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -317,7 +378,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
new file mode 100644
index 0000000..7f10457
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
@@ -0,0 +1,210 @@
+// 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.annotations;
+
+/**
+ * Usage constraints for how a target is used and what must be kept.
+ *
+ * <p>Providing the constraints on how an item is being used allows shrinkers to optimize or remove
+ * aspects of the item that do not change that usage constraint.
+ *
+ * <p>For example, invoking a method reflectively does not use any annotations on that method, and
+ * it is safe to remove the annotations. However, it would not be safe to remove parameters from the
+ * method.
+ */
+public enum KeepConstraint {
+  /**
+   * Indicates that the target item is being looked up reflectively.
+   *
+   * <p>Looking up an item reflectively requires that it remains on its expected context, which for
+   * a method or field means it must remain on its defining class. In other words, the item cannot
+   * be removed or moved.
+   *
+   * <p>Note that looking up a member does not imply that the holder class of the member can be
+   * looked up. If both the class and the member need to be looked, make sure to have a target for
+   * both.
+   *
+   * <p>Note also that the item can be looked up within its context but no constraint is placed on
+   * its name, accessibility or any other properties of the item.
+   *
+   * <p>If assumptions are being made about other aspects, additional constraints and targets should
+   * be added to the keep annotation.
+   */
+  LOOKUP,
+
+  /**
+   * Indicates that the name of the target item is being used.
+   *
+   * <p>This usage constraint is needed if the target is being looked up reflectively by using its
+   * name. Setting it will prohibit renaming of the target item.
+   *
+   * <p>Note that preserving the name of a member does not imply that the holder class of the member
+   * will preserve its qualified or simple name. If both the class and the member need to preserve
+   * their names, make sure to have a target for both.
+   *
+   * <p>Note that preserving the name of a member does not preserve the types of its parameters or
+   * its return type for methods or the type for fields.
+   */
+  NAME,
+
+  /**
+   * Indicates that the visibility of the target must be at least as visible as declared.
+   *
+   * <p>Setting this constraint ensures that any (reflective) access to the target that is allowed
+   * remains valid. In other words, a public class, field or method must remain public. For a
+   * non-public target its visibility may be relaxed in the direction: {@code private ->
+   * package-private -> protected -> public}.
+   *
+   * <p>Note that this constraint does not place any restrictions on any other accesses flags than
+   * visibility. In particular, flags such a static, final and abstract may change.
+   *
+   * <p>Used together with {@link #VISIBILITY_RESTRICT} the visibility will remain invariant.
+   */
+  VISIBILITY_RELAX,
+
+  /**
+   * Indicates that the visibility of the target must be at most as visible as declared.
+   *
+   * <p>Setting this constraint ensures that any (reflective) access to the target that would fail
+   * will continue to fail. In other words, a private class, field or method must remain private.
+   * Concretely the visibility of the target item may be restricted in the direction: {@code public
+   * -> protected -> package-private -> private}.
+   *
+   * <p>Note that this constraint does not place any restrictions on any other accesses flags than
+   * visibility. In particular, flags such a static, final and abstract may change.
+   *
+   * <p>Used together with {@link #VISIBILITY_RELAX} the visibility will remain invariant.
+   */
+  VISIBILITY_RESTRICT,
+
+  /**
+   * Indicates that the class target is being instantiated reflectively.
+   *
+   * <p>This usage constraint is only valid on class targets.
+   *
+   * <p>Being instantiated prohibits reasoning about the class instances at compile time. In other
+   * words, the compiler must assume the class to be possibly instantiated at all times.
+   *
+   * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain a
+   * class. This constraint only implies that if the class is referenced in the program it may be
+   * instantiated.
+   */
+  CLASS_INSTANTIATE,
+
+  /**
+   * Indicates that the method target is being invoked reflectively.
+   *
+   * <p>This usage constraint is only valid on method targets.
+   *
+   * <p>To be invoked reflectively the method must retain the structure of the method. Thus, unused
+   * arguments cannot be removed. However, it does not imply preserving the types of its parameters
+   * or its return type. If the parameter types are being obtained reflectively then those need a
+   * keep target independent of the method.
+   *
+   * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain a
+   * method reference. This constraint only implies that if the method is referenced in the program
+   * it may be reflectively invoked.
+   */
+  METHOD_INVOKE,
+
+  /**
+   * Indicates that the field target is reflectively read from.
+   *
+   * <p>This usage constraint is only valid on field targets.
+   *
+   * <p>A field that has its value read from, requires that the field value must remain. Thus, if
+   * field remains, its value cannot be replaced. It can still be removed in full, be inlined and
+   * its value reasoned about at compile time.
+   *
+   * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain
+   * access to the field. This constraint only implies that if a field is referenced then it may be
+   * reflectively read.
+   */
+  FIELD_GET,
+
+  /**
+   * Indicates that the field target is reflectively written to.
+   *
+   * <p>This usage constraint is only valid on field targets.
+   *
+   * <p>A field that has its value written to, requires that the field value must be treated as
+   * unknown.
+   *
+   * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain
+   * access to the field. This constraint only implies that if a field is referenced then it may be
+   * reflectively written.
+   */
+  FIELD_SET,
+
+  /**
+   * Indicates that the target method can be replaced by an alternative definition at runtime.
+   *
+   * <p>This usage constraint is only valid on method targets.
+   *
+   * <p>Replacing a method implies that the concrete implementation of the target cannot be known at
+   * compile time. Thus, it cannot be moved or otherwise assumed to have particular properties.
+   * Being able to replace the method still allows the compiler to fully remove it if it is
+   * statically found to be unused.
+   *
+   * <p>Note also that no restriction is placed on the method name. To ensure the same name add
+   * {@link #NAME} to the constraint set.
+   */
+  METHOD_REPLACE,
+
+  /**
+   * Indicates that the target field can be replaced by an alternative definition at runtime.
+   *
+   * <p>This usage constraint is only valid on field targets.
+   *
+   * <p>Replacing a field implies that the concrete implementation of the target cannot be known at
+   * compile time. Thus, it cannot be moved or otherwise assumed to have particular properties.
+   * Being able to replace the method still allows the compiler to fully remove it if it is
+   * statically found to be unused.
+   *
+   * <p>Note also that no restriction is placed on the field name. To ensure the same name add
+   * {@link #NAME} to the constraint set.
+   */
+  FIELD_REPLACE,
+
+  /**
+   * Indicates that the target item must never be inlined or merged.
+   *
+   * <p>This ensures that if the item is actually used in the program it will remain in some form.
+   * For example, a method may still be renamed, but it will be present as a frame in stack traces
+   * produced by the runtime (before potentially being retraced). For classes, they too may be
+   * renamed, but will not have been merged with other classes or have their allocations fully
+   * eliminated (aka class inlining).
+   *
+   * <p>For members this also ensures that the field value or method body cannot be reasoned about
+   * outside the item itself. For example, a field value cannot be assumed to be a particular value,
+   * and a method cannot be assumed to have particular properties for callers, such as always
+   * throwing or a constant return value.
+   */
+  NEVER_INLINE,
+
+  /**
+   * Indicates that the class hierarchy below the target class may be extended at runtime.
+   *
+   * <p>This ensures that new subtypes of the target class can later be linked and/or class loaded
+   * at runtime.
+   *
+   * <p>This does not ensure that the class remains if it is otherwise dead code and can be fully
+   * removed.
+   *
+   * <p>Note that this constraint does not ensure that particular methods remain on the target
+   * class. If methods or fields of the target class are being targeted by a subclass that was
+   * classloaded or linked later, then keep annotations are needed for those targets too. Such
+   * non-visible uses requires the same annotations to preserve as for reflective uses.
+   */
+  CLASS_OPEN_HIERARCHY,
+
+  /**
+   * Indicates that the annotations on the target item are being accessed reflectively.
+   *
+   * <p>If only a particular set of annotations is accessed, you should set the TBD property on the
+   * target item.
+   */
+  ANNOTATIONS,
+}
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 cd3559c..5ece578 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
@@ -97,11 +97,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -109,9 +154,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -144,7 +205,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
index 7530bf1..cb08d19 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
@@ -6,6 +6,10 @@
 public enum KeepItemKind {
   ONLY_CLASS,
   ONLY_MEMBERS,
+  ONLY_METHODS,
+  ONLY_FIELDS,
   CLASS_AND_MEMBERS,
+  CLASS_AND_METHODS,
+  CLASS_AND_FIELDS,
   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 8ad5098..7bbc2f0 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
@@ -34,42 +34,95 @@
    * <p>Possible values are:
    *
    * <ul>
-   *   <li>ONLY_CLASS
-   *   <li>ONLY_MEMBERS
-   *   <li>CLASS_AND_MEMBERS
+   *   <li>{@link KeepItemKind#ONLY_CLASS}
+   *   <li>{@link KeepItemKind#ONLY_MEMBERS}
+   *   <li>{@link KeepItemKind#ONLY_METHODS}
+   *   <li>{@link KeepItemKind#ONLY_FIELDS}
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}
+   *   <li>{@link KeepItemKind#CLASS_AND_METHODS}
+   *   <li>{@link KeepItemKind#CLASS_AND_FIELDS}
    * </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
+   * <p>If unspecified the default kind for an item depends on its member patterns:
+   *
+   * <ul>
+   *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_METHODS} if method patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_FIELDS} if field patterns are defined
+   *   <li>{@link KeepItemKind#ONLY_MEMBERS} otherwise.
+   * </ul>
    *
    * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
   /**
-   * Define the options that are allowed to be modified.
+   * Define the usage constraints of the target.
    *
-   * <p>The specified options do not need to be preserved for the target.
+   * <p>The specified constraints must remain valid for the target.
    *
-   * <p>Mutually exclusive with the property `disallow` also defining options.
+   * <p>The default constraints depend on the type of the target.
    *
-   * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+   * <ul>
+   *   <li>For classes, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#CLASS_INSTANTIATE}}
+   *   <li>For methods, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#METHOD_INVOKE}}
+   *   <li>For fields, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#FIELD_GET}, {@link KeepConstraint#FIELD_SET}}
+   * </ul>
    *
-   * @return Options allowed to be modified for the target.
+   * <p>Mutually exclusive with the following other properties defining constraints:
+   *
+   * <ul>
+   *   <li>allow
+   *   <li>disallow
+   * </ul>
+   *
+   * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+   *
+   * @return Usage constraints for the target.
    */
+  KeepConstraint[] constraints() default {};
+
+  /**
+   * Define the constraints that are allowed to be modified.
+   *
+   * <p>The specified option constraints do not need to be preserved for the target.
+   *
+   * <p>Mutually exclusive with the following other properties defining constraints:
+   *
+   * <ul>
+   *   <li>constraints
+   *   <li>disallow
+   * </ul>
+   *
+   * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+   *
+   * @return Option constraints allowed to be modified for the target.
+   * @deprecated Use {@link #constraints} instead.
+   */
+  @Deprecated
   KeepOption[] allow() default {};
 
   /**
-   * Define the options that are not allowed to be modified.
+   * Define the constraints that are not allowed to be modified.
    *
-   * <p>The specified options *must* be preserved for the target.
+   * <p>The specified option constraints *must* be preserved for the target.
    *
-   * <p>Mutually exclusive with the property `allow` also defining options.
+   * <p>Mutually exclusive with the following other properties defining constraints:
    *
-   * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+   * <ul>
+   *   <li>constraints
+   *   <li>allow
+   * </ul>
    *
-   * @return Options not allowed to be modified for the target.
+   * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+   *
+   * @return Option constraints not allowed to be modified for the target.
+   * @deprecated Use {@link #constraints} instead.
    */
+  @Deprecated
   KeepOption[] disallow() default {};
 
   /**
@@ -314,11 +367,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -326,9 +424,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -361,7 +475,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
new file mode 100644
index 0000000..1d7e6c6
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
@@ -0,0 +1,44 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A pattern structure for matching types.
+ *
+ * <p>If no properties are set, the default pattern matches any type.
+ *
+ * <p>All properties on this annotation are mutually exclusive.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface TypePattern {
+
+  /**
+   * Exact type name as a string.
+   *
+   * <p>For example, {@code "long"} or {@code "java.lang.String"}.
+   *
+   * <p>Mutually exclusive with the property `constant` also defining type-pattern.
+   */
+  String name() default "";
+
+  /**
+   * Exact type from a class constant.
+   *
+   * <p>For example, {@code String.class}.
+   *
+   * <p>Mutually exclusive with the property `name` also defining type-pattern.
+   */
+  Class<?> constant() default Object.class;
+}
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 beba152..149e51c 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
@@ -57,21 +57,48 @@
   /**
    * Specify the kind of this item pattern.
    *
-   * <p>When annotating a class without member patterns, the default kind is {@link
-   * KeepItemKind#ONLY_CLASS}.
+   * <p>If unspecified the default kind depends on the annotated item.
    *
-   * <p>When annotating a class with member patterns, the default kind is {@link
-   * KeepItemKind#CLASS_AND_MEMBERS}.
+   * <p>When annotating a class the default kind is:
    *
-   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   * <ul>
+   *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_METHODS} if method patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_FIELDS} if field patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}otherwise.
+   * </ul>
    *
-   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   * <p>When annotating a method the default kind is: {@link KeepItemKind#ONLY_METHODS}
+   *
+   * <p>When annotating a field the default kind is: {@link KeepItemKind#ONLY_FIELDS}
+   *
+   * <p>It is not possible to use {@link KeepItemKind#ONLY_CLASS} if annotating a member.
    *
    * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
   /**
+   * Define the usage constraints of the target.
+   *
+   * <p>The specified constraints must remain valid for the target.
+   *
+   * <p>The default constraints depend on the type of the target.
+   *
+   * <ul>
+   *   <li>For classes, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#CLASS_INSTANTIATE}}
+   *   <li>For methods, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#METHOD_INVOKE}}
+   *   <li>For fields, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#FIELD_GET}, {@link KeepConstraint#FIELD_SET}}
+   * </ul>
+   *
+   * @return Usage constraints for the target.
+   */
+  KeepConstraint[] constraints() 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
@@ -113,11 +140,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -125,9 +197,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -160,7 +248,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
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 8c6e153..24073af 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
@@ -57,21 +57,48 @@
   /**
    * Specify the kind of this item pattern.
    *
-   * <p>When annotating a class without member patterns, the default kind is {@link
-   * KeepItemKind#ONLY_CLASS}.
+   * <p>If unspecified the default kind depends on the annotated item.
    *
-   * <p>When annotating a class with member patterns, the default kind is {@link
-   * KeepItemKind#CLASS_AND_MEMBERS}.
+   * <p>When annotating a class the default kind is:
    *
-   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   * <ul>
+   *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_METHODS} if method patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_FIELDS} if field patterns are defined;
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}otherwise.
+   * </ul>
    *
-   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   * <p>When annotating a method the default kind is: {@link KeepItemKind#ONLY_METHODS}
+   *
+   * <p>When annotating a field the default kind is: {@link KeepItemKind#ONLY_FIELDS}
+   *
+   * <p>It is not possible to use {@link KeepItemKind#ONLY_CLASS} if annotating a member.
    *
    * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
   /**
+   * Define the usage constraints of the target.
+   *
+   * <p>The specified constraints must remain valid for the target.
+   *
+   * <p>The default constraints depend on the type of the target.
+   *
+   * <ul>
+   *   <li>For classes, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#CLASS_INSTANTIATE}}
+   *   <li>For methods, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#METHOD_INVOKE}}
+   *   <li>For fields, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+   *       {@link KeepConstraint#FIELD_GET}, {@link KeepConstraint#FIELD_SET}}
+   * </ul>
+   *
+   * @return Usage constraints for the target.
+   */
+  KeepConstraint[] constraints() 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
@@ -113,11 +140,56 @@
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
    *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnTypeConstant
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
    * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
   /**
+   * Define the method return-type pattern by a class constant.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypePattern
+   * </ul>
+   *
+   * @return A class constant denoting the type of the method return type.
+   */
+  Class<?> methodReturnTypeConstant() default Object.class;
+
+  /**
+   * Define the method return-type pattern by a type pattern.
+   *
+   * <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.
+   *
+   * <p>Mutually exclusive with the following other properties defining return-type:
+   *
+   * <ul>
+   *   <li>methodReturnType
+   *   <li>methodReturnTypeConstant
+   * </ul>
+   *
+   * @return The pattern of the method return type.
+   */
+  TypePattern methodReturnTypePattern() default @TypePattern(name = "");
+
+  /**
    * Define the method parameters pattern by a list of fully qualified types.
    *
    * <p>Mutually exclusive with all field properties.
@@ -125,9 +197,25 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
    *
+   * <p>Mutually exclusive with the property `methodParameterTypePatterns` also defining parameters.
+   *
    * @return The list of qualified type names of the method parameters.
    */
-  String[] methodParameters() default {"<default>"};
+  String[] methodParameters() default {""};
+
+  /**
+   * Define the method parameters pattern by a list of patterns on 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.
+   *
+   * <p>Mutually exclusive with the property `methodParameters` also defining parameters.
+   *
+   * @return The list of type patterns for the method parameters.
+   */
+  TypePattern[] methodParameterTypePatterns() default {@TypePattern(name = "")};
 
   /**
    * Define the field-access pattern by matching on access flags.
@@ -160,7 +248,50 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
    *
-   * @return The qualified type name of the field type.
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldTypeConstant
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The qualified type name for the field type.
    */
   String fieldType() default "";
+
+  /**
+   * Define the field-type pattern by a class constant.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypePattern
+   * </ul>
+   *
+   * @return The class constant for the field type.
+   */
+  Class<?> fieldTypeConstant() default Object.class;
+
+  /**
+   * Define the field-type pattern by a pattern on types.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-type:
+   *
+   * <ul>
+   *   <li>fieldType
+   *   <li>fieldTypeConstant
+   * </ul>
+   *
+   * @return The type pattern for the field type.
+   */
+  TypePattern fieldTypePattern() default @TypePattern(name = "");
 }
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 bc466c5..35beddf 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
@@ -7,6 +7,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.Constraints;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.FieldAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.ForApi;
@@ -16,6 +17,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Option;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflection;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
@@ -88,6 +90,102 @@
         KeepQualifiedClassNamePattern.exact(className));
   }
 
+  private static KeepOptions getKeepOptionsOrDefault(KeepOptions options) {
+    // TODO(b/248408342): These should be constraints computed/filtered based on the item type but
+    //   currently the constraints default to the same set of options.
+    if (options != null) {
+      return options;
+    }
+    return KeepOptions.disallowBuilder()
+        // LOOKUP (same for any type of item).
+        .add(KeepOption.SHRINKING)
+        // NAME (same for any type of item).
+        .add(KeepOption.OBFUSCATING)
+        // CLASS_INSTANTIATE / METHOD_INVOKE / FIELD_GET & FIELD_SET:
+        .add(KeepOption.OPTIMIZING)
+        .add(KeepOption.ACCESS_MODIFICATION)
+        // ACCESS_ALLOW - currently no options needed.
+        .build();
+  }
+
+  /** Internal copy of the user-facing KeepItemKind */
+  public enum ItemKind {
+    ONLY_CLASS,
+    ONLY_MEMBERS,
+    ONLY_METHODS,
+    ONLY_FIELDS,
+    CLASS_AND_MEMBERS,
+    CLASS_AND_METHODS,
+    CLASS_AND_FIELDS;
+
+    private static ItemKind fromString(String name) {
+      switch (name) {
+        case Kind.ONLY_CLASS:
+          return ONLY_CLASS;
+        case Kind.ONLY_MEMBERS:
+          return ONLY_MEMBERS;
+        case Kind.ONLY_METHODS:
+          return ONLY_METHODS;
+        case Kind.ONLY_FIELDS:
+          return ONLY_FIELDS;
+        case Kind.CLASS_AND_MEMBERS:
+          return CLASS_AND_MEMBERS;
+        case Kind.CLASS_AND_METHODS:
+          return CLASS_AND_METHODS;
+        case Kind.CLASS_AND_FIELDS:
+          return CLASS_AND_FIELDS;
+        default:
+          return null;
+      }
+    }
+
+    private boolean isOnlyClass() {
+      return equals(ONLY_CLASS);
+    }
+
+    private boolean requiresMembers() {
+      // If requiring members it is fine to have the more specific methods or fields.
+      return includesMembers();
+    }
+
+    private boolean requiresMethods() {
+      return equals(ONLY_METHODS) || equals(CLASS_AND_METHODS);
+    }
+
+    private boolean requiresFields() {
+      return equals(ONLY_FIELDS) || equals(CLASS_AND_FIELDS);
+    }
+
+    private boolean includesClassAndMembers() {
+      return includesClass() && includesMembers();
+    }
+
+    private boolean includesClass() {
+      return equals(ONLY_CLASS)
+          || equals(CLASS_AND_MEMBERS)
+          || equals(CLASS_AND_METHODS)
+          || equals(CLASS_AND_FIELDS);
+    }
+
+    private boolean includesMembers() {
+      return !equals(ONLY_CLASS);
+    }
+
+    private boolean includesMethod() {
+      return equals(ONLY_MEMBERS)
+          || equals(ONLY_METHODS)
+          || equals(CLASS_AND_MEMBERS)
+          || equals(CLASS_AND_METHODS);
+    }
+
+    private boolean includesField() {
+      return equals(ONLY_MEMBERS)
+          || equals(ONLY_FIELDS)
+          || equals(CLASS_AND_MEMBERS)
+          || equals(CLASS_AND_FIELDS);
+    }
+  }
+
   private static class KeepEdgeClassVisitor extends ClassVisitor {
     private final Parent<KeepDeclaration> parent;
     private String className;
@@ -366,6 +464,10 @@
       return symbol;
     }
 
+    public KeepItemPattern getItemForBinding(KeepBindingReference bindingReference) {
+      return builder.getItemForBinding(bindingReference.getName());
+    }
+
     public KeepBindings build() {
       return builder.build();
     }
@@ -479,7 +581,7 @@
 
     @Override
     public void visitEnd() {
-      if (!getKind().equals(Kind.ONLY_CLASS) && isDefaultMemberDeclaration()) {
+      if (!getKind().isOnlyClass() && isDefaultMemberDeclaration()) {
         // If no member declarations have been made, set public & protected as the default.
         AnnotationVisitor v = visitArray(Item.memberAccess);
         v.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PUBLIC);
@@ -605,6 +707,7 @@
     private final KeepConsequences.Builder consequences = KeepConsequences.builder();
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
+    private final OptionsDeclaration optionsDeclaration;
 
     UsedByReflectionClassVisitor(
         String annotationDescriptor,
@@ -617,6 +720,7 @@
       addContext.accept(metaInfoBuilder);
       // The class context/holder is the annotated class.
       visit(Item.className, className);
+      optionsDeclaration = new OptionsDeclaration(getAnnotationName());
     }
 
     @Override
@@ -653,6 +757,10 @@
             },
             bindingsHelper);
       }
+      AnnotationVisitor visitor = optionsDeclaration.tryParseArray(name);
+      if (visitor != null) {
+        return visitor;
+      }
       return super.visitArray(name);
     }
 
@@ -687,7 +795,11 @@
           throw new KeepEdgeException(
               "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
         }
-        consequences.addTarget(KeepTarget.builder().setItemPattern(itemPattern).build());
+        consequences.addTarget(
+            KeepTarget.builder()
+                .setItemPattern(itemPattern)
+                .setOptions(getKeepOptionsOrDefault(optionsDeclaration.getValue()))
+                .build());
       }
       parent.accept(
           builder
@@ -711,7 +823,8 @@
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
     private final KeepConsequences.Builder consequences = KeepConsequences.builder();
-    private String kind = Kind.ONLY_MEMBERS;
+    private ItemKind kind = KeepEdgeReader.ItemKind.ONLY_MEMBERS;
+    private final OptionsDeclaration optionsDeclaration;
 
     UsedByReflectionMemberVisitor(
         String annotationDescriptor,
@@ -722,6 +835,7 @@
       this.parent = parent;
       this.context = context;
       addContext.accept(metaInfoBuilder);
+      optionsDeclaration = new OptionsDeclaration(getAnnotationName());
     }
 
     @Override
@@ -744,14 +858,11 @@
       if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
         super.visitEnum(name, descriptor, value);
       }
-      switch (value) {
-        case Kind.ONLY_CLASS:
-        case Kind.ONLY_MEMBERS:
-        case Kind.CLASS_AND_MEMBERS:
-          kind = value;
-          break;
-        default:
-          super.visitEnum(name, descriptor, value);
+      KeepEdgeReader.ItemKind kind = KeepEdgeReader.ItemKind.fromString(value);
+      if (kind != null) {
+        this.kind = kind;
+      } else {
+        super.visitEnum(name, descriptor, value);
       }
     }
 
@@ -769,21 +880,30 @@
             },
             bindingsHelper);
       }
+      AnnotationVisitor visitor = optionsDeclaration.tryParseArray(name);
+      if (visitor != null) {
+        return visitor;
+      }
       return super.visitArray(name);
     }
 
     @Override
     public void visitEnd() {
-      if (Kind.ONLY_CLASS.equals(kind)) {
+      if (kind.isOnlyClass()) {
         throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
       }
       assert context.isMemberItemPattern();
       KeepMemberItemPattern memberContext = context.asMemberItemPattern();
-      if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
+      if (kind.includesClass()) {
         consequences.addTarget(
             KeepTarget.builder().setItemReference(memberContext.getClassReference()).build());
       }
-      consequences.addTarget(KeepTarget.builder().setItemPattern(context).build());
+      validateConsistentKind(memberContext.getMemberPattern());
+      consequences.addTarget(
+          KeepTarget.builder()
+              .setOptions(getKeepOptionsOrDefault(optionsDeclaration.getValue()))
+              .setItemPattern(context)
+              .build());
       parent.accept(
           builder
               .setMetaInfo(metaInfoBuilder.build())
@@ -791,6 +911,18 @@
               .setConsequences(consequences.build())
               .build());
     }
+
+    private void validateConsistentKind(KeepMemberPattern memberPattern) {
+      if (memberPattern.isGeneralMember()) {
+        throw new KeepEdgeException("Unexpected general pattern for context.");
+      }
+      if (memberPattern.isMethod() && !kind.includesMethod()) {
+        throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a method");
+      }
+      if (memberPattern.isField() && !kind.includesField()) {
+        throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a field");
+      }
+    }
   }
 
   private static class UsesReflectionVisitor extends AnnotationVisitorBase {
@@ -1055,15 +1187,48 @@
   abstract static class Declaration<T> {
     abstract String kind();
 
-    abstract boolean isDefault();
+    boolean isDefault() {
+      for (Declaration<?> declaration : declarations()) {
+        if (!declaration.isDefault()) {
+          return false;
+        }
+      }
+      return true;
+    }
+    ;
 
     abstract T getValue();
 
+    List<Declaration<?>> declarations() {
+      return Collections.emptyList();
+    }
+
     boolean tryParse(String name, Object value) {
+      for (Declaration<?> declaration : declarations()) {
+        if (declaration.tryParse(name, value)) {
+          return true;
+        }
+      }
       return false;
     }
 
-    AnnotationVisitor tryParseArray(String name, Consumer<T> onValue) {
+    AnnotationVisitor tryParseArray(String name) {
+      for (Declaration<?> declaration : declarations()) {
+        AnnotationVisitor visitor = declaration.tryParseArray(name);
+        if (visitor != null) {
+          return visitor;
+        }
+      }
+      return null;
+    }
+
+    AnnotationVisitor tryParseAnnotation(String name, String descriptor) {
+      for (Declaration<?> declaration : declarations()) {
+        AnnotationVisitor visitor = declaration.tryParseAnnotation(name, descriptor);
+        if (visitor != null) {
+          return visitor;
+        }
+      }
       return null;
     }
   }
@@ -1081,6 +1246,10 @@
       return null;
     }
 
+    AnnotationVisitor parseAnnotation(String name, String descriptor, Consumer<T> setValue) {
+      return null;
+    }
+
     @Override
     boolean isDefault() {
       return !hasDeclaration();
@@ -1121,8 +1290,22 @@
     }
 
     @Override
-    AnnotationVisitor tryParseArray(String name, Consumer<T> setValue) {
-      AnnotationVisitor visitor = parseArray(name, setValue.andThen(v -> declarationValue = v));
+    final AnnotationVisitor tryParseArray(String name) {
+      AnnotationVisitor visitor = parseArray(name, v -> declarationValue = v);
+      if (visitor != null) {
+        if (hasDeclaration()) {
+          error(name);
+        }
+        declarationName = name;
+        declarationVisitor = visitor;
+        return visitor;
+      }
+      return null;
+    }
+
+    @Override
+    final AnnotationVisitor tryParseAnnotation(String name, String descriptor) {
+      AnnotationVisitor visitor = parseAnnotation(name, descriptor, v -> declarationValue = v);
       if (visitor != null) {
         if (hasDeclaration()) {
           error(name);
@@ -1292,13 +1475,130 @@
     }
   }
 
+  private static class MethodReturnTypeDeclaration
+      extends SingleDeclaration<KeepMethodReturnTypePattern> {
+
+    private final Supplier<String> annotationName;
+
+    private MethodReturnTypeDeclaration(Supplier<String> annotationName) {
+      this.annotationName = annotationName;
+    }
+
+    @Override
+    String kind() {
+      return "return type";
+    }
+
+    @Override
+    KeepMethodReturnTypePattern getDefaultValue() {
+      return KeepMethodReturnTypePattern.any();
+    }
+
+    @Override
+    KeepMethodReturnTypePattern parse(String name, Object value) {
+      if (name.equals(Item.methodReturnType) && value instanceof String) {
+        return KeepEdgeReaderUtils.methodReturnTypeFromTypeName((String) value);
+      }
+      if (name.equals(Item.methodReturnTypeConstant) && value instanceof Type) {
+        Type type = (Type) value;
+        return KeepEdgeReaderUtils.methodReturnTypeFromTypeDescriptor(type.getDescriptor());
+      }
+      return null;
+    }
+
+    @Override
+    AnnotationVisitor parseAnnotation(
+        String name, String descriptor, Consumer<KeepMethodReturnTypePattern> setValue) {
+      if (name.equals(Item.methodReturnTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
+        return new TypePatternVisitor(
+            annotationName, t -> setValue.accept(KeepMethodReturnTypePattern.fromType(t)));
+      }
+      return super.parseAnnotation(name, descriptor, setValue);
+    }
+  }
+
+  private static class MethodParametersDeclaration
+      extends SingleDeclaration<KeepMethodParametersPattern> {
+
+    private final Supplier<String> annotationName;
+    private KeepMethodParametersPattern pattern = null;
+
+    public MethodParametersDeclaration(Supplier<String> annotationName) {
+      this.annotationName = annotationName;
+    }
+
+    private void setPattern(
+        KeepMethodParametersPattern pattern, Consumer<KeepMethodParametersPattern> setValue) {
+      assert setValue != null;
+      if (this.pattern != null) {
+        throw new KeepEdgeException("Cannot declare multiple patterns for the parameter list");
+      }
+      setValue.accept(pattern);
+      this.pattern = pattern;
+    }
+
+    @Override
+    String kind() {
+      return "parameters";
+    }
+
+    @Override
+    KeepMethodParametersPattern getDefaultValue() {
+      return KeepMethodParametersPattern.any();
+    }
+
+    @Override
+    KeepMethodParametersPattern parse(String name, Object value) {
+      return null;
+    }
+
+    @Override
+    AnnotationVisitor parseArray(String name, Consumer<KeepMethodParametersPattern> setValue) {
+      if (name.equals(Item.methodParameters)) {
+        return new StringArrayVisitor(
+            annotationName,
+            params -> {
+              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+              for (String param : params) {
+                builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
+              }
+              setPattern(builder.build(), setValue);
+            });
+      }
+      if (name.equals(Item.methodParameterTypePatterns)) {
+        return new TypePatternsArrayVisitor(
+            annotationName,
+            params -> {
+              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+              for (KeepTypePattern param : params) {
+                builder.addParameterTypePattern(param);
+              }
+              setPattern(builder.build(), setValue);
+            });
+      }
+      return super.parseArray(name, setValue);
+    }
+  }
+
   private static class MethodDeclaration extends Declaration<KeepMethodPattern> {
     private final Supplier<String> annotationName;
     private KeepMethodAccessPattern.Builder accessBuilder = null;
     private KeepMethodPattern.Builder builder = null;
+    private final MethodReturnTypeDeclaration returnTypeDeclaration;
+    private final MethodParametersDeclaration parametersDeclaration;
+
+    private final List<Declaration<?>> declarations;
 
     private MethodDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
+      returnTypeDeclaration = new MethodReturnTypeDeclaration(annotationName);
+      parametersDeclaration = new MethodParametersDeclaration(annotationName);
+      declarations = ImmutableList.of(returnTypeDeclaration, parametersDeclaration);
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
     }
 
     private KeepMethodPattern.Builder getBuilder() {
@@ -1315,7 +1615,7 @@
 
     @Override
     boolean isDefault() {
-      return accessBuilder == null && builder == null;
+      return accessBuilder == null && builder == null && super.isDefault();
     }
 
     @Override
@@ -1323,6 +1623,12 @@
       if (accessBuilder != null) {
         getBuilder().setAccessPattern(accessBuilder.build());
       }
+      if (!returnTypeDeclaration.isDefault()) {
+        getBuilder().setReturnTypePattern(returnTypeDeclaration.getValue());
+      }
+      if (!parametersDeclaration.isDefault()) {
+        getBuilder().setParametersPattern(parametersDeclaration.getValue());
+      }
       return builder != null ? builder.build() : null;
     }
 
@@ -1332,43 +1638,77 @@
         getBuilder().setNamePattern(KeepMethodNamePattern.exact((String) value));
         return true;
       }
-      if (name.equals(Item.methodReturnType) && value instanceof String) {
-        getBuilder()
-            .setReturnTypePattern(KeepEdgeReaderUtils.methodReturnTypeFromString((String) value));
-        return true;
-      }
-      return false;
+      return super.tryParse(name, value);
     }
 
     @Override
-    AnnotationVisitor tryParseArray(String name, Consumer<KeepMethodPattern> ignored) {
+    AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.methodAccess)) {
         accessBuilder = KeepMethodAccessPattern.builder();
         return new MethodAccessVisitor(annotationName, accessBuilder);
       }
-      if (name.equals(Item.methodParameters)) {
-        return new StringArrayVisitor(
-            annotationName,
-            params -> {
-              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
-              for (String param : params) {
-                builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
-              }
-              KeepMethodParametersPattern result = builder.build();
-              getBuilder().setParametersPattern(result);
-            });
+      return super.tryParseArray(name);
+    }
+  }
+
+  private static class FieldTypeDeclaration extends SingleDeclaration<KeepFieldTypePattern> {
+
+    private final Supplier<String> annotationName;
+
+    private FieldTypeDeclaration(Supplier<String> annotationName) {
+      this.annotationName = annotationName;
+    }
+
+    @Override
+    String kind() {
+      return "field type";
+    }
+
+    @Override
+    KeepFieldTypePattern getDefaultValue() {
+      return KeepFieldTypePattern.any();
+    }
+
+    @Override
+    KeepFieldTypePattern parse(String name, Object value) {
+      if (name.equals(Item.fieldType) && value instanceof String) {
+        return KeepFieldTypePattern.fromType(
+            KeepEdgeReaderUtils.typePatternFromString((String) value));
+      }
+      if (name.equals(Item.fieldTypeConstant) && value instanceof Type) {
+        String descriptor = ((Type) value).getDescriptor();
+        return KeepFieldTypePattern.fromType(KeepTypePattern.fromDescriptor(descriptor));
       }
       return null;
     }
+
+    @Override
+    AnnotationVisitor parseAnnotation(
+        String name, String descriptor, Consumer<KeepFieldTypePattern> setValue) {
+      if (name.equals(Item.fieldTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
+        return new TypePatternVisitor(
+            annotationName, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
+      }
+      return super.parseAnnotation(name, descriptor, setValue);
+    }
   }
 
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
     private final Supplier<String> annotationName;
+    private final FieldTypeDeclaration typeDeclaration;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
+    private final List<Declaration<?>> declarations;
 
     public FieldDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
+      typeDeclaration = new FieldTypeDeclaration(annotationName);
+      declarations = Collections.singletonList(typeDeclaration);
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
     }
 
     private KeepFieldPattern.Builder getBuilder() {
@@ -1393,6 +1733,9 @@
       if (accessBuilder != null) {
         getBuilder().setAccessPattern(accessBuilder.build());
       }
+      if (!typeDeclaration.isDefault()) {
+        getBuilder().setTypePattern(typeDeclaration.getValue());
+      }
       return builder != null ? builder.build() : null;
     }
 
@@ -1402,23 +1745,16 @@
         getBuilder().setNamePattern(KeepFieldNamePattern.exact((String) value));
         return true;
       }
-      if (name.equals(Item.fieldType) && value instanceof String) {
-        getBuilder()
-            .setTypePattern(
-                KeepFieldTypePattern.fromType(
-                    KeepEdgeReaderUtils.typePatternFromString((String) value)));
-        return true;
-      }
-      return false;
+      return super.tryParse(name, value);
     }
 
     @Override
-    AnnotationVisitor tryParseArray(String name, Consumer<KeepFieldPattern> onValue) {
+    AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.fieldAccess)) {
         accessBuilder = KeepFieldAccessPattern.builder();
         return new FieldAccessVisitor(annotationName, accessBuilder);
       }
-      return super.tryParseArray(name, onValue);
+      return super.tryParseArray(name);
     }
   }
 
@@ -1427,11 +1763,18 @@
     private KeepMemberAccessPattern.Builder accessBuilder = null;
     private final MethodDeclaration methodDeclaration;
     private final FieldDeclaration fieldDeclaration;
+    private final List<Declaration<?>> declarations;
 
     MemberDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
       methodDeclaration = new MethodDeclaration(annotationName);
       fieldDeclaration = new FieldDeclaration(annotationName);
+      declarations = ImmutableList.of(methodDeclaration, fieldDeclaration);
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
     }
 
     @Override
@@ -1468,27 +1811,18 @@
     }
 
     @Override
-    boolean tryParse(String name, Object value) {
-      return methodDeclaration.tryParse(name, value) || fieldDeclaration.tryParse(name, value);
-    }
-
-    @Override
-    AnnotationVisitor tryParseArray(String name, Consumer<KeepMemberPattern> ignored) {
+    AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.memberAccess)) {
         accessBuilder = KeepMemberAccessPattern.memberBuilder();
         return new MemberAccessVisitor(annotationName, accessBuilder);
       }
-      AnnotationVisitor visitor = methodDeclaration.tryParseArray(name, v -> {});
-      if (visitor != null) {
-        return visitor;
-      }
-      return fieldDeclaration.tryParseArray(name, v -> {});
+      return super.tryParseArray(name);
     }
   }
 
   private abstract static class KeepItemVisitorBase extends AnnotationVisitorBase {
     private String memberBindingReference = null;
-    private String kind = null;
+    private ItemKind kind = null;
     private final ClassDeclaration classDeclaration = new ClassDeclaration(this::getBindingsHelper);
     private final MemberDeclaration memberDeclaration;
 
@@ -1505,8 +1839,14 @@
       if (itemReference == null) {
         throw new KeepEdgeException("Item reference not finalized. Missing call to visitEnd()");
       }
-      if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
-        // If kind is set then visitEnd ensures that this cannot be a binding reference.
+      if (itemReference.isBindingReference()) {
+        return Collections.singletonList(itemReference);
+      }
+      // Kind is only null if item is a "binding reference".
+      if (kind == null) {
+        throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+      }
+      if (kind.includesClassAndMembers()) {
         assert !itemReference.isBindingReference();
         KeepItemPattern itemPattern = itemReference.asItemPattern();
         KeepClassItemReference classReference;
@@ -1536,8 +1876,10 @@
         return Collections.singletonList(itemReference);
       }
       // Kind is only null if item is a "binding reference".
-      assert kind != null;
-      if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
+      if (kind == null) {
+        throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+      }
+      if (kind.includesClassAndMembers()) {
         KeepItemPattern itemPattern = itemReference.asItemPattern();
         // Ensure we have a member item linked to the correct class.
         KeepMemberItemPattern memberItemPattern;
@@ -1577,7 +1919,7 @@
       return itemReference;
     }
 
-    public String getKind() {
+    public ItemKind getKind() {
       return kind;
     }
 
@@ -1590,14 +1932,11 @@
       if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
         super.visitEnum(name, descriptor, value);
       }
-      switch (value) {
-        case Kind.ONLY_CLASS:
-        case Kind.ONLY_MEMBERS:
-        case Kind.CLASS_AND_MEMBERS:
-          kind = value;
-          break;
-        default:
-          super.visitEnum(name, descriptor, value);
+      ItemKind kind = ItemKind.fromString(value);
+      if (kind != null) {
+        this.kind = kind;
+      } else {
+        super.visitEnum(name, descriptor, value);
       }
     }
 
@@ -1615,8 +1954,21 @@
     }
 
     @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      AnnotationVisitor visitor = classDeclaration.tryParseAnnotation(name, descriptor);
+      if (visitor != null) {
+        return visitor;
+      }
+      visitor = memberDeclaration.tryParseAnnotation(name, descriptor);
+      if (visitor != null) {
+        return visitor;
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
     public AnnotationVisitor visitArray(String name) {
-      AnnotationVisitor visitor = memberDeclaration.tryParseArray(name, v -> {});
+      AnnotationVisitor visitor = memberDeclaration.tryParseArray(name);
       if (visitor != null) {
         return visitor;
       }
@@ -1636,20 +1988,73 @@
         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 no explicit kind is set, extract it based on the member pattern.
         if (kind == null) {
-          kind = memberPattern.isNone() ? Kind.ONLY_CLASS : Kind.ONLY_MEMBERS;
+          if (memberPattern.isMethod()) {
+            kind = ItemKind.ONLY_METHODS;
+          } else if (memberPattern.isField()) {
+            kind = ItemKind.ONLY_FIELDS;
+          } else if (memberPattern.isGeneralMember()) {
+            kind = ItemKind.ONLY_MEMBERS;
+          } else {
+            assert memberPattern.isNone();
+            kind = ItemKind.ONLY_CLASS;
+          }
+        }
+
+        if (kind.isOnlyClass() && !memberPattern.isNone()) {
+          throw new KeepEdgeException("Item pattern for members is incompatible with kind " + kind);
+        }
+
+        // Refine the member pattern to be as precise as the specified kind.
+        if (kind.requiresMethods() && !memberPattern.isMethod()) {
+          if (memberPattern.isGeneralMember()) {
+            memberPattern =
+                KeepMethodPattern.builder()
+                    .setAccessPattern(
+                        KeepMethodAccessPattern.builder()
+                            .copyOfMemberAccess(memberPattern.getAccessPattern())
+                            .build())
+                    .build();
+          } else if (memberPattern.isNone()) {
+            memberPattern = KeepMethodPattern.allMethods();
+          } else {
+            assert memberPattern.isField();
+            throw new KeepEdgeException(
+                "Item pattern for fields is incompatible with kind " + kind);
+          }
+        }
+
+        if (kind.requiresFields() && !memberPattern.isField()) {
+          if (memberPattern.isGeneralMember()) {
+            memberPattern =
+                KeepFieldPattern.builder()
+                    .setAccessPattern(
+                        KeepFieldAccessPattern.builder()
+                            .copyOfMemberAccess(memberPattern.getAccessPattern())
+                            .build())
+                    .build();
+          } else if (memberPattern.isNone()) {
+            memberPattern = KeepFieldPattern.allFields();
+          } else {
+            assert memberPattern.isMethod();
+            throw new KeepEdgeException(
+                "Item pattern for methods is incompatible with kind " + kind);
+          }
+        }
+
+        if (kind.requiresMembers() && memberPattern.isNone()) {
+          memberPattern = KeepMemberPattern.allMembers();
         }
 
         KeepClassItemReference classReference = classDeclaration.getValue();
-        if (kind.equals(Kind.ONLY_CLASS)) {
+        if (kind.isOnlyClass()) {
           itemReference = classReference;
         } else {
           KeepItemPattern itemPattern =
               KeepMemberItemPattern.builder()
                   .setClassReference(classReference)
-                  .setMemberPattern(
-                      memberPattern.isNone() ? KeepMemberPattern.allMembers() : memberPattern)
+                  .setMemberPattern(memberPattern)
                   .build();
           itemReference = itemPattern.toItemReference();
         }
@@ -1734,6 +2139,80 @@
     }
   }
 
+  private static class TypePatternVisitor extends AnnotationVisitorBase {
+    private final Supplier<String> annotationName;
+    private final Consumer<KeepTypePattern> consumer;
+    private KeepTypePattern result = null;
+
+    private TypePatternVisitor(
+        Supplier<String> annotationName, Consumer<KeepTypePattern> consumer) {
+      this.annotationName = annotationName;
+      this.consumer = consumer;
+    }
+
+    @Override
+    public String getAnnotationName() {
+      return annotationName.get();
+    }
+
+    private void setResult(KeepTypePattern result) {
+      if (this.result != null) {
+        throw new KeepEdgeException("Invalid type annotation defining multiple properties.");
+      }
+      this.result = result;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (TypePattern.name.equals(name) && value instanceof String) {
+        setResult(KeepEdgeReaderUtils.typePatternFromString((String) value));
+        return;
+      }
+      if (TypePattern.constant.equals(name) && value instanceof Type) {
+        Type type = (Type) value;
+        setResult(KeepTypePattern.fromDescriptor(type.getDescriptor()));
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnd() {
+      consumer.accept(result != null ? result : KeepTypePattern.any());
+    }
+  }
+
+  private static class TypePatternsArrayVisitor extends AnnotationVisitorBase {
+    private final Supplier<String> annotationName;
+    private final Consumer<List<KeepTypePattern>> fn;
+    private final List<KeepTypePattern> patterns = new ArrayList<>();
+
+    public TypePatternsArrayVisitor(
+        Supplier<String> annotationName, Consumer<List<KeepTypePattern>> fn) {
+      this.annotationName = annotationName;
+      this.fn = fn;
+    }
+
+    @Override
+    public String getAnnotationName() {
+      return annotationName.get();
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
+      if (TypePattern.DESCRIPTOR.equals(descriptor)) {
+        return new TypePatternVisitor(annotationName, patterns::add);
+      }
+      return null;
+    }
+
+    @Override
+    public void visitEnd() {
+      super.visitEnd();
+      fn.accept(patterns);
+    }
+  }
+
   private static class OptionsDeclaration extends SingleDeclaration<KeepOptions> {
 
     private final String annotationName;
@@ -1749,7 +2228,7 @@
 
     @Override
     KeepOptions getDefaultValue() {
-      return KeepOptions.keepAll();
+      return null;
     }
 
     @Override
@@ -1759,6 +2238,11 @@
 
     @Override
     AnnotationVisitor parseArray(String name, Consumer<KeepOptions> setValue) {
+      if (name.equals(AnnotationConstants.Target.constraints)) {
+        return new KeepConstraintsVisitor(
+            annotationName,
+            options -> setValue.accept(KeepOptions.disallowBuilder().addAll(options).build()));
+      }
       if (name.equals(AnnotationConstants.Target.disallow)) {
         return new KeepOptionsVisitor(
             annotationName,
@@ -1802,7 +2286,7 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
-      AnnotationVisitor visitor = optionsDeclaration.tryParseArray(name, builder::setOptions);
+      AnnotationVisitor visitor = optionsDeclaration.tryParseArray(name);
       if (visitor != null) {
         return visitor;
       }
@@ -1812,6 +2296,7 @@
     @Override
     public void visitEnd() {
       super.visitEnd();
+      builder.setOptions(getKeepOptionsOrDefault(optionsDeclaration.getValue()));
       for (KeepItemReference item : getItemsWithBinding()) {
         parent.accept(builder.setItemReference(item).build());
       }
@@ -1845,6 +2330,74 @@
     }
   }
 
+  private static class KeepConstraintsVisitor extends AnnotationVisitorBase {
+
+    private final String annotationName;
+    private final Parent<Collection<KeepOption>> parent;
+    private final Set<KeepOption> options = new HashSet<>();
+
+    public KeepConstraintsVisitor(String annotationName, Parent<Collection<KeepOption>> parent) {
+      this.annotationName = annotationName;
+      this.parent = parent;
+    }
+
+    @Override
+    public String getAnnotationName() {
+      return annotationName;
+    }
+
+    @Override
+    public void visitEnum(String ignore, String descriptor, String value) {
+      if (!descriptor.equals(AnnotationConstants.Constraints.DESCRIPTOR)) {
+        super.visitEnum(ignore, descriptor, value);
+      }
+      switch (value) {
+        case Constraints.LOOKUP:
+          options.add(KeepOption.SHRINKING);
+          break;
+        case Constraints.NAME:
+          options.add(KeepOption.OBFUSCATING);
+          break;
+        case Constraints.VISIBILITY_RELAX:
+          // The compiler currently satisfies that access is never restricted.
+          break;
+        case Constraints.VISIBILITY_RESTRICT:
+          // We don't have directional rules so this prohibits any modification.
+          options.add(KeepOption.ACCESS_MODIFICATION);
+          break;
+        case Constraints.CLASS_INSTANTIATE:
+        case Constraints.METHOD_INVOKE:
+        case Constraints.FIELD_GET:
+        case Constraints.FIELD_SET:
+          // These options are the item-specific actual uses of the items.
+          // Allocating, invoking and read/writing all imply that the item cannot be "optimized"
+          // at compile time. It would be natural to refine the field specific uses but that is
+          // not expressible as keep options.
+          options.add(KeepOption.OPTIMIZING);
+          break;
+        case Constraints.METHOD_REPLACE:
+        case Constraints.FIELD_REPLACE:
+        case Constraints.NEVER_INLINE:
+        case Constraints.CLASS_OPEN_HIERARCHY:
+          options.add(KeepOption.OPTIMIZING);
+          break;
+        case Constraints.ANNOTATIONS:
+          // The annotation constrain only implies that annotations should remain, no restrictions
+          // are on the item otherwise.
+          options.add(KeepOption.ANNOTATION_REMOVAL);
+          break;
+        default:
+          super.visitEnum(ignore, descriptor, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(options);
+      super.visitEnd();
+    }
+  }
+
   private static class KeepOptionsVisitor extends AnnotationVisitorBase {
 
     private final String annotationName;
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 7b0cf28..d88b010 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
@@ -68,10 +68,18 @@
     }
   }
 
-  public static KeepMethodReturnTypePattern methodReturnTypeFromString(String returnType) {
+  public static KeepMethodReturnTypePattern methodReturnTypeFromTypeName(String returnType) {
     if ("void".equals(returnType)) {
       return KeepMethodReturnTypePattern.voidType();
     }
     return KeepMethodReturnTypePattern.fromType(typePatternFromString(returnType));
   }
+
+  public static KeepMethodReturnTypePattern methodReturnTypeFromTypeDescriptor(
+      String returnTypeDesc) {
+    if ("V".equals(returnTypeDesc)) {
+      return KeepMethodReturnTypePattern.voidType();
+    }
+    return KeepMethodReturnTypePattern.fromType(KeepTypePattern.fromDescriptor(returnTypeDesc));
+  }
 }
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 7ca4306..1ab1561 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
@@ -87,10 +87,15 @@
     public static final String methodAccess = "methodAccess";
     public static final String methodName = "methodName";
     public static final String methodReturnType = "methodReturnType";
+    public static final String methodReturnTypeConstant = "methodReturnTypeConstant";
+    public static final String methodReturnTypePattern = "methodReturnTypePattern";
     public static final String methodParameters = "methodParameters";
+    public static final String methodParameterTypePatterns = "methodParameterTypePatterns";
     public static final String fieldAccess = "fieldAccess";
     public static final String fieldName = "fieldName";
     public static final String fieldType = "fieldType";
+    public static final String fieldTypeConstant = "fieldTypeConstant";
+    public static final String fieldTypePattern = "fieldTypePattern";
   }
 
   public static final class Binding {
@@ -111,6 +116,7 @@
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/KeepTarget;";
     public static final String kind = "kind";
+    public static final String constraints = "constraints";
     public static final String allow = "allow";
     public static final String disallow = "disallow";
   }
@@ -121,7 +127,30 @@
         "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 ONLY_METHODS = "ONLY_METHODS";
+    public static final String ONLY_FIELDS = "ONLY_FIELDS";
     public static final String CLASS_AND_MEMBERS = "CLASS_AND_MEMBERS";
+    public static final String CLASS_AND_METHODS = "CLASS_AND_METHODS";
+    public static final String CLASS_AND_FIELDS = "CLASS_AND_FIELDS";
+  }
+
+  public static final class Constraints {
+    public static final String SIMPLE_NAME = "KeepConstraint";
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/KeepConstraint;";
+    public static final String LOOKUP = "LOOKUP";
+    public static final String NAME = "NAME";
+    public static final String VISIBILITY_RELAX = "VISIBILITY_RELAX";
+    public static final String VISIBILITY_RESTRICT = "VISIBILITY_RESTRICT";
+    public static final String CLASS_INSTANTIATE = "CLASS_INSTANTIATE";
+    public static final String METHOD_INVOKE = "METHOD_INVOKE";
+    public static final String FIELD_GET = "FIELD_GET";
+    public static final String FIELD_SET = "FIELD_SET";
+    public static final String METHOD_REPLACE = "METHOD_REPLACE";
+    public static final String FIELD_REPLACE = "FIELD_REPLACE";
+    public static final String NEVER_INLINE = "NEVER_INLINE";
+    public static final String CLASS_OPEN_HIERARCHY = "CLASS_OPEN_HIERARCHY";
+    public static final String ANNOTATIONS = "ANNOTATIONS";
   }
 
   public static final class Option {
@@ -167,4 +196,12 @@
     public static final String VOLATILE = "VOLATILE";
     public static final String TRANSIENT = "TRANSIENT";
   }
+
+  public static final class TypePattern {
+    public static final String SIMPLE_NAME = "TypePattern";
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/TypePattern;";
+    public static final String name = "name";
+    public static final String constant = "constant";
+  }
 }
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 8b759ee..df6a224 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
@@ -187,6 +187,10 @@
       return this;
     }
 
+    public KeepItemPattern getItemForBinding(KeepBindingSymbol symbol) {
+      return bindings.get(symbol);
+    }
+
     @SuppressWarnings("ReferenceEquality")
     public KeepBindings build() {
       if (bindings.isEmpty()) {
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 c88c7c4..c718995 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
@@ -4,6 +4,7 @@
 package com.android.tools.r8.keepanno.ast;
 
 import java.util.Collection;
+import java.util.function.Function;
 
 /**
  * A pattern for matching items in the program.
@@ -44,5 +45,14 @@
   public abstract Collection<KeepBindingReference> getBindingReferences();
 
   public abstract KeepItemReference toItemReference();
+
+  public <T> T match(
+      Function<KeepClassItemPattern, T> onClass, Function<KeepMemberItemPattern, T> onMember) {
+    if (isClassItemPattern()) {
+      return onClass.apply(asClassItemPattern());
+    }
+    assert isMemberItemPattern();
+    return onMember.apply(asMemberItemPattern());
+  }
 }
 
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 2883d5c..e1e0f8a 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,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import java.util.function.Function;
+
 /**
  * A reference to an item pattern.
  *
@@ -61,4 +63,13 @@
   public KeepMemberItemPattern asMemberItemPattern() {
     return null;
   }
+
+  public <T> T match(
+      Function<KeepBindingReference, T> onBinding, Function<KeepItemPattern, T> onItem) {
+    if (isBindingReference()) {
+      return onBinding.apply(asBindingReference());
+    }
+    assert isItemPattern();
+    return onItem.apply(asItemPattern());
+  }
 }
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 aec5fbf..2a7061d 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
@@ -72,12 +72,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object o) {
       if (this == o) {
         return true;
       }
-      if (o == null || getClass() != o.getClass()) {
+      if (!(o instanceof Some)) {
         return false;
       }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
index ba647eb..284a0ac 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
@@ -23,7 +23,7 @@
     OPTIMIZING,
     OBFUSCATING,
     ACCESS_MODIFICATION,
-    ANNOTATION_REMOVAL,
+    ANNOTATION_REMOVAL
   }
 
   public static KeepOptions keepAll() {
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 3669002..5d5c357 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
@@ -81,6 +81,10 @@
 
   public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
     for (KeepOption option : KeepOption.values()) {
+      if (option == KeepOption.ANNOTATION_REMOVAL) {
+        // Annotation removal is a testing option, we can't reliably extract it out into rules.
+        continue;
+      }
       if (options.isAllowed(option)) {
         builder.append(",allow").append(getOptionString(option));
       }
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 08b1657..03f99fa 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -39,19 +39,6 @@
         }
       };
 
-  public static final FeatureSplit BASE_STARTUP =
-      new FeatureSplit(null, null, null, null) {
-        @Override
-        public boolean isBase() {
-          return true;
-        }
-
-        @Override
-        public boolean isStartupBase() {
-          return true;
-        }
-      };
-
   private ProgramConsumer programConsumer;
   private final List<ProgramResourceProvider> programResourceProviders;
   private final AndroidResourceProvider androidResourceProvider;
@@ -72,10 +59,6 @@
     return false;
   }
 
-  public boolean isStartupBase() {
-    return false;
-  }
-
   void internalSetProgramConsumer(ProgramConsumer consumer) {
     this.programConsumer = consumer;
   }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6923215..12a84ab 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.lens.AppliedGraphLens;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -483,7 +482,9 @@
       // should therefore be run after the publicizer.
       new NestReducer(appViewWithLiveness).run(executorService, timing);
 
-      new MemberRebindingAnalysis(appViewWithLiveness).run(executorService);
+      appView.setGraphLens(MemberRebindingIdentityLensFactory.create(appView, executorService));
+
+      new MemberRebindingAnalysis(appViewWithLiveness).run();
       appViewWithLiveness.appInfo().notifyMemberRebindingFinished(appViewWithLiveness);
 
       assert ArtProfileCompletenessChecker.verify(appView);
@@ -540,9 +541,7 @@
       // 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.time(
-          "AppliedGraphLens construction",
-          () -> appView.setGraphLens(new AppliedGraphLens(appView)));
+      timing.time("AppliedGraphLens construction", appView::flattenGraphLenses);
       timing.end();
 
       RuntimeTypeCheckInfo.Builder finalRuntimeTypeCheckInfoBuilder = null;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 07b3ef5..d2647bb 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -137,6 +137,7 @@
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations =
         System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
+    public boolean enableStartupLayoutOptimization = true;
     private SemanticVersion fakeCompilerVersion = null;
     private AndroidResourceProvider androidResourceProvider = null;
     private AndroidResourceConsumer androidResourceConsumer = null;
@@ -290,7 +291,7 @@
       return super.setProguardMapOutputPath(proguardMapOutput);
     }
 
-    /** Set input proguard map used for distribution of classes in multi-dex. */
+    /** Set input proguard map used for distribution of classes in multi-DEX. */
     public Builder setProguardMapInputFile(Path proguardInputMap) {
       getAppBuilder().setProguardMapInputData(proguardInputMap);
       return self();
@@ -507,11 +508,11 @@
 
     /**
      * Add a collection of startup profile providers that should be used for distributing the
-     * program classes in dex. The given startup profiles are also used to disallow optimizations
+     * program classes in DEX. The given startup profiles are also used to disallow optimizations
      * across the startup and post-startup boundary.
      *
      * <p>NOTE: Startup profiles are ignored when compiling to class files or the min-API level does
-     * not support native multidex (API<=20).
+     * not support native multi-DEX (API<=20).
      */
     @Override
     public Builder addStartupProfileProviders(StartupProfileProvider... startupProfileProviders) {
@@ -520,11 +521,12 @@
 
     /**
      * Add a collection of startup profile providers that should be used for distributing the
-     * program classes in dex. The given startup profiles are also used to disallow optimizations
-     * across the startup and post-startup boundary.
+     * program classes in DEX, unless turned off using {@link #setEnableStartupLayoutOptimization}.
+     * The given startup profiles are also used to disallow optimizations across the startup and
+     * post-startup boundary.
      *
      * <p>NOTE: Startup profiles are ignored when compiling to class files or the min-API level does
-     * not support native multidex (API<=20).
+     * not support native multi-DEX (API<=20).
      */
     @Override
     public Builder addStartupProfileProviders(
@@ -533,6 +535,18 @@
     }
 
     /**
+     * API for specifying whether R8 should use the provided startup profiles to layout the DEX.
+     * When this is set to {@code false}, the given startup profiles are then only used to disallow
+     * optimizations across the startup and post-startup boundary.
+     *
+     * <p>Defaults to true.
+     */
+    public Builder setEnableStartupLayoutOptimization(boolean enable) {
+      enableStartupLayoutOptimization = enable;
+      return this;
+    }
+
+    /**
      * Exprimental API for supporting android resource shrinking.
      *
      * <p>Add an android resource provider, providing the resource table, manifest and res table
@@ -717,6 +731,7 @@
               getMapIdProvider(),
               getSourceFileProvider(),
               enableMissingLibraryApiModeling,
+              enableStartupLayoutOptimization,
               getAndroidPlatformBuild(),
               getArtProfilesForRewriting(),
               getStartupProfileProviders(),
@@ -912,6 +927,7 @@
   private final FeatureSplitConfiguration featureSplitConfiguration;
   private final String synthesizedClassPrefix;
   private final boolean enableMissingLibraryApiModeling;
+  private final boolean enableStartupLayoutOptimization;
   private final AndroidResourceProvider androidResourceProvider;
   private final AndroidResourceConsumer androidResourceConsumer;
   private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
@@ -1004,6 +1020,7 @@
       MapIdProvider mapIdProvider,
       SourceFileProvider sourceFileProvider,
       boolean enableMissingLibraryApiModeling,
+      boolean enableStartupLayoutOptimization,
       boolean isAndroidPlatformBuild,
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
@@ -1055,6 +1072,7 @@
     this.featureSplitConfiguration = featureSplitConfiguration;
     this.synthesizedClassPrefix = synthesizedClassPrefix;
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
     this.androidResourceProvider = androidResourceProvider;
     this.androidResourceConsumer = androidResourceConsumer;
     this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
@@ -1081,6 +1099,7 @@
     featureSplitConfiguration = null;
     synthesizedClassPrefix = null;
     enableMissingLibraryApiModeling = false;
+    enableStartupLayoutOptimization = true;
     androidResourceProvider = null;
     androidResourceConsumer = null;
     resourceShrinkerConfiguration = null;
@@ -1199,7 +1218,7 @@
       internal.apiModelingOptions().disableOutliningAndStubbing();
     }
 
-    // Default is to remove all javac generated assertion code when generating dex.
+    // Default is to remove all javac generated assertion code when generating DEX.
     assert internal.assertionsConfiguration == null;
     AssertionsConfiguration.Builder builder = AssertionsConfiguration.builder(getReporter());
     internal.assertionsConfiguration =
@@ -1244,7 +1263,10 @@
 
     internal.getArtProfileOptions().setArtProfilesForRewriting(getArtProfilesForRewriting());
     if (!getStartupProfileProviders().isEmpty()) {
-      internal.getStartupOptions().setStartupProfileProviders(getStartupProfileProviders());
+      internal
+          .getStartupOptions()
+          .setStartupProfileProviders(getStartupProfileProviders())
+          .setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
     }
 
     internal.programClassConflictResolver =
diff --git a/src/main/java/com/android/tools/r8/classmerging/ClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/classmerging/ClassMergerGraphLens.java
new file mode 100644
index 0000000..300d399
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/ClassMergerGraphLens.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging;
+
+import com.android.tools.r8.graph.AppView;
+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.classmerging.MergedClasses;
+import com.android.tools.r8.graph.lens.NestedGraphLens;
+import com.android.tools.r8.ir.conversion.ExtraParameter;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class ClassMergerGraphLens extends NestedGraphLens {
+
+  public ClassMergerGraphLens(
+      AppView<?> appView,
+      BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+      Map<DexMethod, DexMethod> methodMap,
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap,
+      BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
+    super(appView, fieldMap, methodMap, typeMap, newMethodSignatures);
+  }
+
+  public abstract static class BuilderBase<
+      GL extends ClassMergerGraphLens, MC extends MergedClasses> {
+
+    public abstract void addExtraParameters(
+        DexMethod from, DexMethod to, List<? extends ExtraParameter> extraParameters);
+
+    public abstract void commitPendingUpdates();
+
+    public abstract void fixupField(DexField from, DexField to);
+
+    public abstract void fixupMethod(DexMethod from, DexMethod to);
+
+    public abstract Set<DexMethod> getOriginalMethodReferences(DexMethod method);
+
+    public abstract GL build(AppView<?> appView, MC mergedClasses);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
new file mode 100644
index 0000000..5e0445e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
@@ -0,0 +1,468 @@
+// 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;
+
+import com.android.tools.r8.errors.Unreachable;
+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.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.classmerging.MergedClasses;
+import com.android.tools.r8.graph.fixup.TreeFixerBase;
+import com.android.tools.r8.horizontalclassmerging.SubtypingForrestForClasses;
+import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
+import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
+import com.android.tools.r8.shaking.AnnotationFixer;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.DexMethodSignatureBiMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public abstract class ClassMergerTreeFixer<
+        LB extends ClassMergerGraphLens.BuilderBase<GL, MC>,
+        GL extends ClassMergerGraphLens,
+        MC extends MergedClasses>
+    extends TreeFixerBase {
+
+  protected final LB lensBuilder;
+  protected final MC mergedClasses;
+  private final ProfileCollectionAdditions profileCollectionAdditions;
+  private final SyntheticArgumentClass syntheticArgumentClass;
+
+  private final Map<DexProgramClass, DexType> originalSuperTypes = new IdentityHashMap<>();
+  private final DexMethodSignatureBiMap<DexMethodSignature> reservedInterfaceSignatures =
+      new DexMethodSignatureBiMap<>();
+
+  public ClassMergerTreeFixer(
+      AppView<?> appView,
+      LB lensBuilder,
+      MC mergedClasses,
+      ProfileCollectionAdditions profileCollectionAdditions,
+      SyntheticArgumentClass syntheticArgumentClass) {
+    super(appView);
+    this.lensBuilder = lensBuilder;
+    this.mergedClasses = mergedClasses;
+    this.profileCollectionAdditions = profileCollectionAdditions;
+    this.syntheticArgumentClass = syntheticArgumentClass;
+  }
+
+  public GL run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    if (!appView.enableWholeProgramOptimizations()) {
+      return timing.time("Fixup", () -> lensBuilder.build(appView, mergedClasses));
+    }
+    timing.begin("Fixup");
+    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+    Collection<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
+    Iterables.filter(classes, DexProgramClass::isInterface).forEach(this::fixupInterfaceClass);
+    classes.forEach(this::fixupAttributes);
+    classes.forEach(this::fixupProgramClassSuperTypes);
+    SubtypingForrestForClasses subtypingForrest =
+        new SubtypingForrestForClasses(appView.withClassHierarchy());
+    // TODO(b/170078037): parallelize this code segment.
+    for (DexProgramClass root : subtypingForrest.getProgramRoots()) {
+      subtypingForrest.traverseNodeDepthFirst(
+          root, new DexMethodSignatureBiMap<>(), this::fixupProgramClass);
+    }
+    postprocess();
+    GL lens = lensBuilder.build(appViewWithLiveness, mergedClasses);
+    new AnnotationFixer(appView, lens).run(appView.appInfo().classes(), executorService);
+    timing.end();
+    return lens;
+  }
+
+  public abstract boolean isRunningBeforePrimaryOptimizationPass();
+
+  public void postprocess() {
+    // Intentionally empty.
+  }
+
+  public void fixupAttributes(DexProgramClass clazz) {
+    if (clazz.hasEnclosingMethodAttribute()) {
+      EnclosingMethodAttribute enclosingMethodAttribute = clazz.getEnclosingMethodAttribute();
+      if (mergedClasses.isMergeSource(enclosingMethodAttribute.getEnclosingType())) {
+        clazz.clearEnclosingMethodAttribute();
+      } else {
+        clazz.setEnclosingMethodAttribute(fixupEnclosingMethodAttribute(enclosingMethodAttribute));
+      }
+    }
+    clazz.setInnerClasses(fixupInnerClassAttributes(clazz.getInnerClasses()));
+    clazz.setNestHostAttribute(fixupNestHost(clazz.getNestHostClassAttribute()));
+    clazz.setNestMemberAttributes(fixupNestMemberAttributes(clazz.getNestMembersClassAttributes()));
+    clazz.setPermittedSubclassAttributes(
+        fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
+  }
+
+  private void fixupProgramClassSuperTypes(DexProgramClass clazz) {
+    DexType rewrittenSuperType = fixupType(clazz.getSuperType());
+    if (rewrittenSuperType.isNotIdenticalTo(clazz.getSuperType())) {
+      originalSuperTypes.put(clazz, clazz.getSuperType());
+      clazz.superType = rewrittenSuperType;
+    }
+    clazz.setInterfaces(fixupInterfaces(clazz, clazz.getInterfaces()));
+  }
+
+  private DexMethodSignatureBiMap<DexMethodSignature> fixupProgramClass(
+      DexProgramClass clazz, DexMethodSignatureBiMap<DexMethodSignature> remappedVirtualMethods) {
+    assert !clazz.isInterface();
+
+    MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures =
+        createLocallyReservedMethodSignatures(clazz, remappedVirtualMethods);
+    DexMethodSignatureBiMap<DexMethodSignature> remappedClassVirtualMethods =
+        new DexMethodSignatureBiMap<>(remappedVirtualMethods);
+    clazz
+        .getMethodCollection()
+        .replaceAllVirtualMethods(
+            method ->
+                fixupVirtualMethod(
+                    clazz, method, remappedClassVirtualMethods, newMethodSignatures));
+    clazz
+        .getMethodCollection()
+        .replaceAllDirectMethods(
+            method ->
+                fixupDirectMethod(clazz, method, remappedClassVirtualMethods, newMethodSignatures));
+
+    Set<DexField> newFieldReferences = Sets.newIdentityHashSet();
+    DexEncodedField[] instanceFields = clazz.clearInstanceFields();
+    DexEncodedField[] staticFields = clazz.clearStaticFields();
+    clazz.setInstanceFields(fixupFields(instanceFields, newFieldReferences));
+    clazz.setStaticFields(fixupFields(staticFields, newFieldReferences));
+
+    lensBuilder.commitPendingUpdates();
+
+    return remappedClassVirtualMethods;
+  }
+
+  private DexEncodedMethod fixupVirtualInterfaceMethod(DexEncodedMethod method) {
+    DexMethod originalMethodReference = method.getReference();
+
+    // Don't process this method if it does not refer to a merge class type.
+    boolean referencesMergeClass =
+        Iterables.any(
+            originalMethodReference.getReferencedBaseTypes(dexItemFactory),
+            mergedClasses::isMergeSourceOrTarget);
+    if (!referencesMergeClass) {
+      return method;
+    }
+
+    DexMethodSignature originalMethodSignature = originalMethodReference.getSignature();
+    DexMethodSignature newMethodSignature =
+        reservedInterfaceSignatures.get(originalMethodSignature);
+
+    if (newMethodSignature == null) {
+      newMethodSignature = fixupMethodReference(originalMethodReference).getSignature();
+
+      // If the signature is already reserved by another interface, find a fresh one.
+      if (reservedInterfaceSignatures.containsValue(newMethodSignature)) {
+        DexString name =
+            dexItemFactory.createGloballyFreshMemberString(
+                originalMethodReference.getName().toSourceString());
+        newMethodSignature = newMethodSignature.withName(name);
+      }
+
+      assert !reservedInterfaceSignatures.containsValue(newMethodSignature);
+      reservedInterfaceSignatures.put(originalMethodSignature, newMethodSignature);
+    }
+
+    DexMethod newMethodReference =
+        newMethodSignature.withHolder(originalMethodReference, dexItemFactory);
+    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
+    return newMethodReference.isNotIdenticalTo(originalMethodReference)
+        ? method.toTypeSubstitutedMethodAsInlining(newMethodReference, dexItemFactory)
+        : method;
+  }
+
+  private void fixupInterfaceClass(DexProgramClass iface) {
+    DexMethodSignatureBiMap<DexMethodSignature> remappedVirtualMethods =
+        DexMethodSignatureBiMap.empty();
+    MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
+    iface
+        .getMethodCollection()
+        .replaceDirectMethods(
+            method ->
+                fixupDirectMethod(iface, method, remappedVirtualMethods, newMethodSignatures));
+    iface.getMethodCollection().replaceVirtualMethods(this::fixupVirtualInterfaceMethod);
+
+    assert !iface.hasInstanceFields();
+
+    Set<DexField> newFieldReferences = Sets.newIdentityHashSet();
+    DexEncodedField[] staticFields = iface.clearStaticFields();
+    iface.setStaticFields(fixupFields(staticFields, newFieldReferences));
+
+    lensBuilder.commitPendingUpdates();
+  }
+
+  private DexTypeList fixupInterfaces(DexProgramClass clazz, DexTypeList interfaceTypes) {
+    Set<DexType> seen = Sets.newIdentityHashSet();
+    return interfaceTypes.map(
+        interfaceType -> {
+          DexType rewrittenInterfaceType = mapClassType(interfaceType);
+          assert rewrittenInterfaceType.isNotIdenticalTo(clazz.getType());
+          return seen.add(rewrittenInterfaceType) ? rewrittenInterfaceType : null;
+        });
+  }
+
+  private DexEncodedMethod fixupProgramMethod(
+      DexProgramClass clazz, DexEncodedMethod method, DexMethod newMethodReference) {
+    // Convert out of DefaultInstanceInitializerCode, since this piece of code will require lens
+    // code rewriting.
+    if (isRunningBeforePrimaryOptimizationPass()
+        && method.hasCode()
+        && method.getCode().isDefaultInstanceInitializerCode()
+        && mergedClasses.isMergeSourceOrTarget(clazz.getSuperType())) {
+      DexType originalSuperType = originalSuperTypes.getOrDefault(clazz, clazz.getSuperType());
+      DefaultInstanceInitializerCode.uncanonicalizeCode(
+          appView, method.asProgramMethod(clazz), originalSuperType);
+    }
+
+    DexMethod originalMethodReference = method.getReference();
+    if (newMethodReference.isIdenticalTo(originalMethodReference)) {
+      return method;
+    }
+
+    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
+
+    DexEncodedMethod newMethod =
+        method.toTypeSubstitutedMethodAsInlining(newMethodReference, dexItemFactory);
+    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 DexEncodedMethod fixupDirectMethod(
+      DexProgramClass clazz,
+      DexEncodedMethod method,
+      DexMethodSignatureBiMap<DexMethodSignature> remappedVirtualMethods,
+      MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures) {
+    DexMethod originalMethodReference = method.getReference();
+
+    // Fix all type references in the method prototype.
+    DexMethodSignature reservedMethodSignature = newMethodSignatures.get(method);
+    DexMethod newMethodReference;
+    if (reservedMethodSignature != null) {
+      newMethodReference = reservedMethodSignature.withHolder(clazz, dexItemFactory);
+    } else {
+      newMethodReference = fixupMethodReference(originalMethodReference);
+      if (newMethodSignatures.containsValue(newMethodReference.getSignature())) {
+        // If the method collides with a direct method on the same class then rename it to a
+        // globally
+        // fresh name and record the signature.
+        if (method.isInstanceInitializer()) {
+          // If the method is an instance initializer, then add extra nulls.
+          Box<Set<DexType>> usedSyntheticArgumentClasses = new Box<>();
+          newMethodReference =
+              dexItemFactory.createInstanceInitializerWithFreshProto(
+                  newMethodReference,
+                  syntheticArgumentClass.getArgumentClasses(),
+                  tryMethod -> !newMethodSignatures.containsValue(tryMethod.getSignature()),
+                  usedSyntheticArgumentClasses::set);
+          lensBuilder.addExtraParameters(
+              originalMethodReference,
+              newMethodReference,
+              ExtraUnusedNullParameter.computeExtraUnusedNullParameters(
+                  originalMethodReference, newMethodReference));
+
+          // Amend the art profile collection.
+          if (usedSyntheticArgumentClasses.isSet()) {
+            Set<DexMethod> previousMethodReferences =
+                lensBuilder.getOriginalMethodReferences(originalMethodReference);
+            if (previousMethodReferences.isEmpty()) {
+              profileCollectionAdditions.applyIfContextIsInProfile(
+                  originalMethodReference,
+                  additionsBuilder ->
+                      usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
+            } else {
+              for (DexMethod previousMethodReference : previousMethodReferences) {
+                profileCollectionAdditions.applyIfContextIsInProfile(
+                    previousMethodReference,
+                    additionsBuilder ->
+                        usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
+              }
+            }
+          }
+        } else {
+          newMethodReference =
+              dexItemFactory.createFreshMethodNameWithoutHolder(
+                  newMethodReference.getName().toSourceString(),
+                  newMethodReference.getProto(),
+                  newMethodReference.getHolderType(),
+                  tryMethod ->
+                      !reservedInterfaceSignatures.containsValue(tryMethod.getSignature())
+                          && !remappedVirtualMethods.containsValue(tryMethod.getSignature())
+                          && !newMethodSignatures.containsValue(tryMethod.getSignature()));
+        }
+      }
+
+      assert !newMethodSignatures.containsValue(newMethodReference.getSignature());
+      newMethodSignatures.put(method, newMethodReference.getSignature());
+    }
+
+    return fixupProgramMethod(clazz, method, newMethodReference);
+  }
+
+  private MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature>
+      createLocallyReservedMethodSignatures(
+          DexProgramClass clazz,
+          DexMethodSignatureBiMap<DexMethodSignature> remappedVirtualMethods) {
+    MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
+    for (DexEncodedMethod method : clazz.methods()) {
+      if (method.belongsToVirtualPool()) {
+        DexMethodSignature reservedMethodSignature =
+            lookupReservedVirtualName(method, remappedVirtualMethods);
+        if (reservedMethodSignature != null) {
+          newMethodSignatures.put(method, reservedMethodSignature);
+          continue;
+        }
+      }
+      // Reserve the method signature if it is unchanged and not globally reserved.
+      DexMethodSignature newMethodSignature = fixupMethodSignature(method);
+      if (newMethodSignature.equals(method.getName(), method.getProto())
+          && !reservedInterfaceSignatures.containsValue(newMethodSignature)
+          && !remappedVirtualMethods.containsValue(newMethodSignature)) {
+        newMethodSignatures.put(method, newMethodSignature);
+      }
+    }
+    return newMethodSignatures;
+  }
+
+  private DexMethodSignature lookupReservedVirtualName(
+      DexEncodedMethod method,
+      DexMethodSignatureBiMap<DexMethodSignature> renamedClassVirtualMethods) {
+    DexMethodSignature originalSignature = method.getSignature();
+
+    // Determine if the original method has been rewritten by a parent class
+    DexMethodSignature renamedVirtualName = renamedClassVirtualMethods.get(originalSignature);
+    if (renamedVirtualName == null) {
+      // Determine if there is a signature mapping.
+      DexMethodSignature mappedInterfaceSignature =
+          reservedInterfaceSignatures.get(originalSignature);
+      if (mappedInterfaceSignature != null) {
+        renamedVirtualName = mappedInterfaceSignature;
+      }
+    } else {
+      assert !reservedInterfaceSignatures.containsKey(originalSignature);
+    }
+    return renamedVirtualName;
+  }
+
+  private DexEncodedMethod fixupVirtualMethod(
+      DexProgramClass clazz,
+      DexEncodedMethod method,
+      DexMethodSignatureBiMap<DexMethodSignature> renamedClassVirtualMethods,
+      MutableBidirectionalOneToOneMap<DexEncodedMethod, DexMethodSignature> newMethodSignatures) {
+    DexMethodSignature newSignature = newMethodSignatures.get(method);
+    if (newSignature == null) {
+      // Fix all type references in the method prototype.
+      newSignature =
+          dexItemFactory.createFreshMethodSignatureName(
+              method.getName().toSourceString(),
+              null,
+              fixupProto(method.getProto()),
+              trySignature ->
+                  !reservedInterfaceSignatures.containsValue(trySignature)
+                      && !newMethodSignatures.containsValue(trySignature)
+                      && !renamedClassVirtualMethods.containsValue(trySignature));
+      newMethodSignatures.put(method, newSignature);
+    }
+
+    // If any of the parameter types have been merged, record the signature mapping so that
+    // subclasses perform the identical rename.
+    if (!reservedInterfaceSignatures.containsKey(method)
+        && Iterables.any(
+            newSignature.getProto().getParameterBaseTypes(dexItemFactory),
+            mergedClasses::isMergeTarget)) {
+      renamedClassVirtualMethods.put(method.getSignature(), newSignature);
+    }
+
+    DexMethod newMethodReference = newSignature.withHolder(clazz, dexItemFactory);
+    return fixupProgramMethod(clazz, method, newMethodReference);
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private DexEncodedField[] fixupFields(
+      DexEncodedField[] fields, Set<DexField> newFieldReferences) {
+    if (fields == null || ArrayUtils.isEmpty(fields)) {
+      return DexEncodedField.EMPTY_ARRAY;
+    }
+
+    DexEncodedField[] newFields = new DexEncodedField[fields.length];
+    for (int i = 0; i < fields.length; i++) {
+      DexEncodedField oldField = fields[i];
+      DexField oldFieldReference = oldField.getReference();
+      DexField newFieldReference = fixupFieldReference(oldFieldReference);
+
+      // Rename the field if it already exists.
+      if (!newFieldReferences.add(newFieldReference)) {
+        DexField template = newFieldReference;
+        newFieldReference =
+            dexItemFactory.createFreshMember(
+                tryName ->
+                    Optional.of(template.withName(tryName, dexItemFactory))
+                        .filter(tryMethod -> !newFieldReferences.contains(tryMethod)),
+                newFieldReference.name.toSourceString());
+        boolean added = newFieldReferences.add(newFieldReference);
+        assert added;
+      }
+
+      if (newFieldReference != oldFieldReference) {
+        lensBuilder.fixupField(oldFieldReference, newFieldReference);
+        newFields[i] = oldField.toTypeSubstitutedField(appView, newFieldReference);
+      } else {
+        newFields[i] = oldField;
+      }
+    }
+
+    return newFields;
+  }
+
+  @Override
+  public DexType mapClassType(DexType type) {
+    return mergedClasses.getMergeTargetOrDefault(type, type);
+  }
+
+  @Override
+  public void recordClassChange(DexType from, DexType to) {
+    // Classes are not changed but in-place updated.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void recordFieldChange(DexField from, DexField to) {
+    // Fields are manually changed in 'fixupFields' above.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void recordMethodChange(DexMethod from, DexMethod to) {
+    // Methods are manually changed in 'fixupMethods' above.
+    throw new Unreachable();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java b/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
similarity index 78%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
rename to src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
index 1074a5c..a8dcd49 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
+++ b/src/main/java/com/android/tools/r8/classmerging/SyntheticArgumentClass.java
@@ -1,12 +1,13 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.horizontalclassmerging;
+package com.android.tools.r8.classmerging;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.synthesis.SyntheticItems.SyntheticKindSelector;
 import com.google.common.base.Suppliers;
@@ -48,7 +49,7 @@
 
     private final AppView<AppInfoWithLiveness> appView;
 
-    Builder(AppView<AppInfoWithLiveness> appView) {
+    public Builder(AppView<AppInfoWithLiveness> appView) {
       this.appView = appView;
     }
 
@@ -60,22 +61,28 @@
     }
 
     public SyntheticArgumentClass build(Collection<MergeGroup> mergeGroups) {
-      DexProgramClass context = getDeterministicContext(mergeGroups);
+      return build(getDeterministicContext(mergeGroups));
+    }
+
+    public SyntheticArgumentClass build(DexProgramClass deterministicContext) {
       List<Supplier<DexType>> syntheticArgumentTypes = new ArrayList<>();
       syntheticArgumentTypes.add(
           Suppliers.memoize(
               () ->
-                  synthesizeClass(context, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_1)
+                  synthesizeClass(
+                          deterministicContext, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_1)
                       .getType()));
       syntheticArgumentTypes.add(
           Suppliers.memoize(
               () ->
-                  synthesizeClass(context, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_2)
+                  synthesizeClass(
+                          deterministicContext, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_2)
                       .getType()));
       syntheticArgumentTypes.add(
           Suppliers.memoize(
               () ->
-                  synthesizeClass(context, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_3)
+                  synthesizeClass(
+                          deterministicContext, kinds -> kinds.HORIZONTAL_INIT_TYPE_ARGUMENT_3)
                       .getType()));
       return new SyntheticArgumentClass(syntheticArgumentTypes);
     }
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 105ea1c..37a55ce 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -241,10 +241,15 @@
       // Retrieve the startup order for writing the app. In R8, the startup order is created
       // up-front to guide optimizations through-out the compilation. In D8, the startup
       // order is only used for writing the app, so we create it here for the first time.
-      StartupProfile startupProfile =
-          appView.appInfo().hasClassHierarchy()
-              ? appView.getStartupProfile()
-              : StartupProfile.createInitialStartupProfileForD8(appView);
+      StartupProfile startupProfile;
+      if (options.getStartupOptions().isStartupLayoutOptimizationEnabled()) {
+        startupProfile =
+            appView.appInfo().hasClassHierarchy()
+                ? appView.getStartupProfile()
+                : StartupProfile.createInitialStartupProfileForD8(appView);
+      } else {
+        startupProfile = StartupProfile.empty();
+      }
       distributor =
           new VirtualFile.FillFilesDistributor(
               this, classes, options, executorService, startupProfile);
diff --git a/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java b/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
index 158b912..1e5e242 100644
--- a/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
+++ b/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
@@ -28,7 +28,7 @@
     } else {
       assert virtualFile.getId() == 0;
       startupProfileForWriting =
-          appView.options().getStartupOptions().isStartupLayoutOptimizationsEnabled()
+          appView.options().getStartupOptions().isStartupMixedSectionLayoutOptimizationsEnabled()
               ? virtualFile.getStartupProfile().toStartupProfileForWriting(appView)
               : StartupProfile.empty();
     }
diff --git a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
index f4ed7cd..54d98a5 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -12,12 +12,11 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramDefinition;
-import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.profile.startup.profile.StartupProfile;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
@@ -106,18 +105,15 @@
 
   public Map<FeatureSplit, Set<DexProgramClass>> getFeatureSplitClasses(
       Set<DexProgramClass> classes, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return getFeatureSplitClasses(
-        classes, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+    return getFeatureSplitClasses(classes, appView.getSyntheticItems());
   }
 
   public Map<FeatureSplit, Set<DexProgramClass>> getFeatureSplitClasses(
       Set<DexProgramClass> classes,
-      InternalOptions options,
-      StartupProfile startupProfile,
       SyntheticItems syntheticItems) {
     Map<FeatureSplit, Set<DexProgramClass>> result = new IdentityHashMap<>();
     for (DexProgramClass clazz : classes) {
-      FeatureSplit featureSplit = getFeatureSplit(clazz, options, startupProfile, syntheticItems);
+      FeatureSplit featureSplit = getFeatureSplit(clazz, syntheticItems);
       if (featureSplit != null && !featureSplit.isBase()) {
         result.computeIfAbsent(featureSplit, ignore -> Sets.newIdentityHashSet()).add(clazz);
       }
@@ -127,46 +123,31 @@
 
   public FeatureSplit getFeatureSplit(
       ProgramDefinition definition, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return getFeatureSplit(
-        definition, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+    return getFeatureSplit(definition, appView.getSyntheticItems());
   }
 
   public FeatureSplit getFeatureSplit(
       ProgramDefinition definition,
-      InternalOptions options,
-      StartupProfile startupProfile,
       SyntheticItems syntheticItems) {
-    return getFeatureSplit(definition.getContextType(), options, startupProfile, syntheticItems);
+    return getFeatureSplit(definition.getReference(), syntheticItems);
   }
 
   public FeatureSplit getFeatureSplit(
-      DexType type, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return getFeatureSplit(
-        type, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+      DexReference reference, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return getFeatureSplit(reference, appView.getSyntheticItems());
   }
 
-  public FeatureSplit getFeatureSplit(
-      DexType type,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
+  public FeatureSplit getFeatureSplit(DexReference reference, SyntheticItems syntheticItems) {
+    DexType type = reference.getContextType();
     if (syntheticItems == null) {
       // Called from AndroidApp.dumpProgramResources().
-      assert startupProfile.isEmpty();
       return classToFeatureSplitMap.getOrDefault(type, FeatureSplit.BASE);
     }
     FeatureSplit feature;
     boolean isSynthetic = syntheticItems.isSyntheticClass(type);
     if (isSynthetic) {
       if (syntheticItems.isSyntheticOfKind(type, k -> k.ENUM_UNBOXING_SHARED_UTILITY_CLASS)) {
-        // Use the startup base if there is one, such that we don't merge non-startup classes with
-        // the shared utility class in case it is used during startup. The use of base startup
-        // allows for merging startup classes with the shared utility class, however, which could be
-        // bad for startup if the shared utility class is not used during startup.
-        return startupProfile.isEmpty()
-                || options.getStartupOptions().isStartupBoundaryOptimizationsEnabled()
-            ? FeatureSplit.BASE
-            : FeatureSplit.BASE_STARTUP;
+        return FeatureSplit.BASE;
       }
       feature = syntheticItems.getContextualFeatureSplitOrDefault(type, FeatureSplit.BASE);
       // Verify the synthetic is not in the class to feature split map or the synthetic has the same
@@ -176,10 +157,7 @@
       feature = classToFeatureSplitMap.getOrDefault(type, FeatureSplit.BASE);
     }
     if (feature.isBase()) {
-      return !startupProfile.isStartupClass(type)
-              || options.getStartupOptions().isStartupBoundaryOptimizationsEnabled()
-          ? FeatureSplit.BASE
-          : FeatureSplit.BASE_STARTUP;
+      return FeatureSplit.BASE;
     }
     return feature;
   }
@@ -191,40 +169,29 @@
     return classToFeatureSplitMap.isEmpty();
   }
 
-  public boolean isInBase(
-      DexProgramClass clazz, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return isInBase(
-        clazz, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+  public boolean isInBase(ProgramDefinition definition, AppView<?> appView) {
+    return isInBase(definition, appView.getSyntheticItems());
   }
 
-  public boolean isInBase(
-      DexProgramClass clazz,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
-    return getFeatureSplit(clazz, options, startupProfile, syntheticItems).isBase();
+  public boolean isInBase(ProgramDefinition definition, SyntheticItems syntheticItems) {
+    return getFeatureSplit(definition, syntheticItems).isBase();
   }
 
   public boolean isInBaseOrSameFeatureAs(
-      DexProgramClass clazz,
+      ProgramDefinition clazz,
       ProgramDefinition context,
       AppView<? extends AppInfoWithClassHierarchy> appView) {
     return isInBaseOrSameFeatureAs(
         clazz,
         context,
-        appView.options(),
-        appView.getStartupProfile(),
         appView.getSyntheticItems());
   }
 
   public boolean isInBaseOrSameFeatureAs(
-      DexProgramClass clazz,
+      ProgramDefinition clazz,
       ProgramDefinition context,
-      InternalOptions options,
-      StartupProfile startupProfile,
       SyntheticItems syntheticItems) {
-    return isInBaseOrSameFeatureAs(
-        clazz.getContextType(), context, options, startupProfile, syntheticItems);
+    return isInBaseOrSameFeatureAs(clazz.getContextType(), context, syntheticItems);
   }
 
   public boolean isInBaseOrSameFeatureAs(
@@ -234,60 +201,31 @@
     return isInBaseOrSameFeatureAs(
         clazz,
         context,
-        appView.options(),
-        appView.getStartupProfile(),
         appView.getSyntheticItems());
   }
 
   public boolean isInBaseOrSameFeatureAs(
       DexType clazz,
       ProgramDefinition context,
-      InternalOptions options,
-      StartupProfile startupProfile,
       SyntheticItems syntheticItems) {
-    FeatureSplit split = getFeatureSplit(clazz, options, startupProfile, syntheticItems);
-    return split.isBase()
-        || split == getFeatureSplit(context, options, startupProfile, syntheticItems);
+    FeatureSplit split = getFeatureSplit(clazz, syntheticItems);
+    return split.isBase() || split == getFeatureSplit(context, syntheticItems);
   }
 
   public boolean isInFeature(
       DexProgramClass clazz,
-      InternalOptions options,
-      StartupProfile startupProfile,
       SyntheticItems syntheticItems) {
-    return !isInBase(clazz, options, startupProfile, syntheticItems);
+    return !isInBase(clazz, syntheticItems);
   }
 
-  public boolean isInSameFeatureOrBothInSameBase(
-      ProgramMethod a, ProgramMethod b, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return isInSameFeatureOrBothInSameBase(
-        a, b, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+  public boolean isInSameFeature(
+      ProgramDefinition definition, ProgramDefinition other, AppView<?> appView) {
+    return isInSameFeature(definition, other, appView.getSyntheticItems());
   }
 
-  public boolean isInSameFeatureOrBothInSameBase(
-      ProgramMethod a,
-      ProgramMethod b,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
-    return isInSameFeatureOrBothInSameBase(
-        a.getHolder(), b.getHolder(), options, startupProfile, syntheticItems);
-  }
-
-  public boolean isInSameFeatureOrBothInSameBase(
-      DexProgramClass a, DexProgramClass b, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return isInSameFeatureOrBothInSameBase(
-        a, b, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
-  }
-
-  public boolean isInSameFeatureOrBothInSameBase(
-      DexProgramClass a,
-      DexProgramClass b,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
-    return getFeatureSplit(a, options, startupProfile, syntheticItems)
-        == getFeatureSplit(b, options, startupProfile, syntheticItems);
+  public boolean isInSameFeature(
+      ProgramDefinition definition, ProgramDefinition other, SyntheticItems syntheticItems) {
+    return getFeatureSplit(definition, syntheticItems) == getFeatureSplit(other, syntheticItems);
   }
 
   public ClassToFeatureSplitMap rewrittenWithLens(GraphLens lens, Timing timing) {
@@ -298,7 +236,7 @@
     Map<DexType, FeatureSplit> rewrittenClassToFeatureSplitMap = new IdentityHashMap<>();
     classToFeatureSplitMap.forEach(
         (type, featureSplit) -> {
-          DexType rewrittenType = lens.lookupType(type);
+          DexType rewrittenType = lens.lookupType(type, GraphLens.getIdentityLens());
           if (rewrittenType.isIntType()) {
             // The type was removed by enum unboxing.
             return;
@@ -332,8 +270,6 @@
 
   public static boolean isInFeature(
       DexProgramClass clazz, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return getMap(appView)
-        .isInFeature(
-            clazz, appView.options(), appView.getStartupProfile(), appView.getSyntheticItems());
+    return getMap(appView).isInFeature(clazz, appView.getSyntheticItems());
   }
 }
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 ead08b8..4c5c33b 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
@@ -4,98 +4,76 @@
 
 package com.android.tools.r8.features;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.CompilationState.PROCESSED_INLINING_CANDIDATE_ANY;
+
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.Definition;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.profile.startup.profile.StartupProfile;
-import com.android.tools.r8.synthesis.SyntheticItems;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OptionalBool;
 
 public class FeatureSplitBoundaryOptimizationUtils {
 
-  public static ConstraintWithTarget getInliningConstraintForResolvedMember(
-      ProgramMethod method,
-      DexEncodedMember<?, ?> resolvedMember,
-      AppView<? extends AppInfoWithClassHierarchy> appView) {
-    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
-    // We never inline into the base from a feature (calls should never happen) and we never inline
-    // between features, so this check should be sufficient.
-    if (classToFeatureSplitMap.isInBaseOrSameFeatureAs(
-        resolvedMember.getHolderType(), method, appView)) {
-      return ConstraintWithTarget.ALWAYS;
-    }
-    return ConstraintWithTarget.NEVER;
-  }
-
   public static FeatureSplit getMergeKeyForHorizontalClassMerging(
       DexProgramClass clazz, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
-    return classToFeatureSplitMap.getFeatureSplit(clazz, appView);
+    return appView.appInfo().getClassToFeatureSplitMap().getFeatureSplit(clazz, appView);
   }
 
   public static boolean isSafeForAccess(
-      DexProgramClass accessedClass,
-      ProgramDefinition accessor,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
-    return classToFeatureSplitMap.isInBaseOrSameFeatureAs(
-        accessedClass, accessor, options, startupProfile, syntheticItems);
+      Definition definition,
+      ProgramDefinition context,
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return !appView.options().hasFeatureSplitConfiguration()
+        || !definition.isProgramDefinition()
+        || isSafeForAccess(definition.asProgramDefinition(), context, appView);
+  }
+
+  private static boolean isSafeForAccess(
+      ProgramDefinition definition,
+      ProgramDefinition context,
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    assert appView.options().hasFeatureSplitConfiguration();
+    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
+    if (classToFeatureSplitMap.isInSameFeature(definition, context, appView)) {
+      return true;
+    }
+    if (!classToFeatureSplitMap.isInBase(definition, appView)) {
+      return false;
+    }
+    // If isolated splits are enabled then the resolved method must be public.
+    if (appView.options().getFeatureSplitConfiguration().isIsolatedSplitsEnabled()
+        && !definition.getAccessFlags().isPublic()) {
+      return false;
+    }
+    return true;
   }
 
   public static boolean isSafeForInlining(
       ProgramMethod caller,
       ProgramMethod callee,
       AppView<? extends AppInfoWithClassHierarchy> appView) {
-    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
-    FeatureSplit callerFeatureSplit = classToFeatureSplitMap.getFeatureSplit(caller, appView);
-    FeatureSplit calleeFeatureSplit = classToFeatureSplitMap.getFeatureSplit(callee, appView);
-
-    // First guarantee that we don't cross any actual feature split boundaries.
-    if (!calleeFeatureSplit.isBase()) {
-      if (calleeFeatureSplit != callerFeatureSplit) {
-        return false;
-      }
+    if (!appView.options().hasFeatureSplitConfiguration()) {
+      return true;
     }
-
-    // Next perform startup checks.
-    if (!callee.getOptimizationInfo().forceInline()) {
-      StartupProfile startupProfile = appView.getStartupProfile();
-      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.
-        if (isStartupMethod(callee, startupProfile).isFalse()) {
-          return false;
-        }
-      } else if (callerIsStartupMethod.isFalse()) {
-        // If the caller is not a startup method, then only allow inlining if the caller is not a
-        // startup class or the callee is a startup class.
-        if (startupProfile.isStartupClass(caller.getHolderType())
-            && !startupProfile.isStartupClass(callee.getHolderType())) {
-          return false;
-        }
-      }
+    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
+    if (classToFeatureSplitMap.isInSameFeature(caller, callee, appView)) {
+      return true;
+    }
+    // Check that we don't cross any actual feature split boundaries.
+    if (!classToFeatureSplitMap.isInBase(callee, appView)) {
+      return false;
+    }
+    // Check that inlining can't lead to accessing a package-private item in base when using
+    // isolated splits.
+    if (appView.options().getFeatureSplitConfiguration().isIsolatedSplitsEnabled()
+        && callee.getDefinition().getCompilationState() != PROCESSED_INLINING_CANDIDATE_ANY) {
+      return false;
     }
     return true;
   }
 
-  private static OptionalBool isStartupMethod(ProgramMethod method, StartupProfile startupProfile) {
-    if (method.getDefinition().isD8R8Synthesized()) {
-      // Due to inadequate rewriting of the startup list during desugaring, we do not give an
-      // accurate result in this case.
-      return OptionalBool.unknown();
-    }
-    return OptionalBool.of(startupProfile.containsMethodRule(method.getReference()));
-  }
-
   public static boolean isSafeForVerticalClassMerging(
       DexProgramClass sourceClass,
       DexProgramClass targetClass,
@@ -107,19 +85,9 @@
     // First guarantee that we don't cross any actual feature split boundaries.
     if (targetFeatureSplit.isBase()) {
       assert sourceFeatureSplit.isBase() : "Unexpected class in base that inherits from feature";
+      return true;
     } else {
-      if (sourceFeatureSplit != targetFeatureSplit) {
-        return false;
-      }
+      return sourceFeatureSplit == targetFeatureSplit;
     }
-
-    // If the source class is a startup class then require that the target class is also a startup
-    // class.
-    StartupProfile startupProfile = appView.getStartupProfile();
-    if (startupProfile.isStartupClass(sourceClass.getType())
-        && !startupProfile.isStartupClass(targetClass.getType())) {
-      return false;
-    }
-    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/features/IsolatedFeatureSplitsChecker.java b/src/main/java/com/android/tools/r8/features/IsolatedFeatureSplitsChecker.java
new file mode 100644
index 0000000..1043723
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/features/IsolatedFeatureSplitsChecker.java
@@ -0,0 +1,193 @@
+// 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.features;
+
+import com.android.tools.r8.errors.dontwarn.DontWarnConfiguration;
+import com.android.tools.r8.features.diagnostic.IllegalAccessWithIsolatedFeatureSplitsDiagnostic;
+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.DexField;
+import com.android.tools.r8.graph.DexMethod;
+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.MethodResolutionResult;
+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.analysis.EnqueuerFieldAccessAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerInvokeAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerTypeAccessAnalysis;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.EnqueuerWorklist;
+import com.android.tools.r8.utils.InternalOptions;
+
+// TODO(b/300247439): Also trace types referenced from new-array instructions, call sites, etc.
+public class IsolatedFeatureSplitsChecker
+    implements EnqueuerFieldAccessAnalysis, EnqueuerInvokeAnalysis, EnqueuerTypeAccessAnalysis {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final ClassToFeatureSplitMap features;
+
+  private IsolatedFeatureSplitsChecker(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    this.appView = appView;
+    this.features = appView.appInfo().getClassToFeatureSplitMap();
+  }
+
+  public static void register(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    if (enabled(appView)) {
+      IsolatedFeatureSplitsChecker checker = new IsolatedFeatureSplitsChecker(appView);
+      enqueuer
+          .registerFieldAccessAnalysis(checker)
+          .registerInvokeAnalysis(checker)
+          .registerTypeAccessAnalysis(checker);
+    }
+  }
+
+  private static boolean enabled(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    InternalOptions options = appView.options();
+    return options.hasFeatureSplitConfiguration()
+        && options.getFeatureSplitConfiguration().isIsolatedSplitsEnabled();
+  }
+
+  private void traceFieldAccess(FieldResolutionResult resolutionResult, ProgramMethod context) {
+    ProgramField resolvedField = resolutionResult.getSingleProgramField();
+    if (resolvedField != null) {
+      checkAccess(resolvedField, context);
+      checkAccess(resolutionResult.getInitialResolutionHolder().asProgramClass(), context);
+    }
+  }
+
+  private void traceMethodInvoke(MethodResolutionResult resolutionResult, ProgramMethod context) {
+    ProgramMethod resolvedMethod = resolutionResult.getResolvedProgramMethod();
+    if (resolvedMethod != null) {
+      checkAccess(resolvedMethod, context);
+      checkAccess(resolutionResult.getInitialResolutionHolder().asProgramClass(), context);
+    }
+  }
+
+  private void traceTypeAccess(DexClass clazz, ProgramMethod context) {
+    if (clazz != null && clazz.isProgramClass()) {
+      checkAccess(clazz.asProgramClass(), context);
+    }
+  }
+
+  private void checkAccess(ProgramDefinition accessedItem, ProgramMethod context) {
+    if (accessedItem.getAccessFlags().isPublic()
+        || features.isInSameFeature(accessedItem, context, appView)) {
+      return;
+    }
+    DontWarnConfiguration dontWarnConfiguration = appView.getDontWarnConfiguration();
+    if (dontWarnConfiguration.matches(accessedItem) || dontWarnConfiguration.matches(context)) {
+      return;
+    }
+    appView
+        .reporter()
+        .error(new IllegalAccessWithIsolatedFeatureSplitsDiagnostic(accessedItem, context));
+  }
+
+  // Field accesses.
+
+  @Override
+  public void traceInstanceFieldRead(
+      DexField field,
+      FieldResolutionResult resolutionResult,
+      ProgramMethod context,
+      EnqueuerWorklist worklist) {
+    traceFieldAccess(resolutionResult, context);
+  }
+
+  @Override
+  public void traceInstanceFieldWrite(
+      DexField field,
+      FieldResolutionResult resolutionResult,
+      ProgramMethod context,
+      EnqueuerWorklist worklist) {
+    traceFieldAccess(resolutionResult, context);
+  }
+
+  @Override
+  public void traceStaticFieldRead(
+      DexField field,
+      SingleFieldResolutionResult<?> resolutionResult,
+      ProgramMethod context,
+      EnqueuerWorklist worklist) {
+    traceFieldAccess(resolutionResult, context);
+  }
+
+  @Override
+  public void traceStaticFieldWrite(
+      DexField field,
+      FieldResolutionResult resolutionResult,
+      ProgramMethod context,
+      EnqueuerWorklist worklist) {
+    traceFieldAccess(resolutionResult, context);
+  }
+
+  // Method invokes.
+
+  @Override
+  public void traceInvokeStatic(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    traceMethodInvoke(resolutionResult, context);
+  }
+
+  @Override
+  public void traceInvokeDirect(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    traceMethodInvoke(resolutionResult, context);
+  }
+
+  @Override
+  public void traceInvokeInterface(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    traceMethodInvoke(resolutionResult, context);
+  }
+
+  @Override
+  public void traceInvokeSuper(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    traceMethodInvoke(resolutionResult, context);
+  }
+
+  @Override
+  public void traceInvokeVirtual(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    traceMethodInvoke(resolutionResult, context);
+  }
+
+  // Type accesses.
+
+  @Override
+  public void traceCheckCast(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+
+  @Override
+  public void traceSafeCheckCast(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+
+  @Override
+  public void traceConstClass(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+
+  @Override
+  public void traceExceptionGuard(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+
+  @Override
+  public void traceInstanceOf(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+
+  @Override
+  public void traceNewInstance(DexType type, DexClass clazz, ProgramMethod context) {
+    traceTypeAccess(clazz, context);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/features/diagnostic/IllegalAccessWithIsolatedFeatureSplitsDiagnostic.java b/src/main/java/com/android/tools/r8/features/diagnostic/IllegalAccessWithIsolatedFeatureSplitsDiagnostic.java
new file mode 100644
index 0000000..55e56e1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/features/diagnostic/IllegalAccessWithIsolatedFeatureSplitsDiagnostic.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.features.diagnostic;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class IllegalAccessWithIsolatedFeatureSplitsDiagnostic implements Diagnostic {
+
+  private final DexReference accessedItem;
+  private final ProgramMethod context;
+
+  public IllegalAccessWithIsolatedFeatureSplitsDiagnostic(
+      ProgramDefinition accessedItem, ProgramMethod context) {
+    this.accessedItem = accessedItem.getReference();
+    this.context = context;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return context.getOrigin();
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    String kind = accessedItem.apply(clazz -> "class", field -> "field", method -> "method");
+    return "Unexpected illegal access to non-public "
+        + kind
+        + " in another feature split (accessed: "
+        + accessedItem.toSourceString()
+        + ", context: "
+        + context.toSourceString()
+        + ").";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/AccessControl.java b/src/main/java/com/android/tools/r8/graph/AccessControl.java
index 3819be8..3ed6de7 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessControl.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessControl.java
@@ -3,11 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
-import com.android.tools.r8.profile.startup.profile.StartupProfile;
-import com.android.tools.r8.synthesis.SyntheticItems;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 
 /**
@@ -19,37 +15,20 @@
 public class AccessControl {
 
   public static OptionalBool isClassAccessible(
-      DexClass clazz,
-      ProgramDefinition context,
-      AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return isClassAccessible(
-        clazz,
-        context,
-        appView.appInfo().getClassToFeatureSplitMap(),
-        appView.options(),
-        appView.getStartupProfile(),
-        appView.getSyntheticItems());
+      DexClass clazz, Definition context, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return isClassAccessible(clazz, context, appView, appView.appInfo());
   }
 
   public static OptionalBool isClassAccessible(
-      DexClass clazz,
-      Definition context,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      InternalOptions options,
-      StartupProfile startupProfile,
-      SyntheticItems syntheticItems) {
+      DexClass clazz, Definition context, AppView<?> appView, AppInfoWithClassHierarchy appInfo) {
+    assert appInfo != null;
     if (!clazz.isPublic() && !clazz.getType().isSamePackage(context.getContextType())) {
       return OptionalBool.FALSE;
     }
-    if (clazz.isProgramClass()
+    if (appView.hasClassHierarchy()
         && context.isProgramDefinition()
         && !FeatureSplitBoundaryOptimizationUtils.isSafeForAccess(
-            clazz.asProgramClass(),
-            context.asProgramDefinition(),
-            classToFeatureSplitMap,
-            options,
-            startupProfile,
-            syntheticItems)) {
+            clazz, context.asProgramDefinition(), appView.withClassHierarchy())) {
       return OptionalBool.UNKNOWN;
     }
     return OptionalBool.TRUE;
@@ -86,13 +65,7 @@
       AppInfoWithClassHierarchy appInfo) {
     AccessFlags<?> memberFlags = member.getDefinition().getAccessFlags();
     OptionalBool classAccessibility =
-        isClassAccessible(
-            initialResolutionContext.getContextClass(),
-            context,
-            appInfo.getClassToFeatureSplitMap(),
-            appInfo.options(),
-            appView.getStartupProfile(),
-            appInfo.getSyntheticItems());
+        isClassAccessible(initialResolutionContext.getContextClass(), context, appView, appInfo);
     if (classAccessibility.isFalse()) {
       return OptionalBool.FALSE;
     }
@@ -105,6 +78,12 @@
       }
       return classAccessibility;
     }
+    if (appView.hasClassHierarchy()
+        && context.isProgramDefinition()
+        && !FeatureSplitBoundaryOptimizationUtils.isSafeForAccess(
+            member, context.asProgramDefinition(), appView.withClassHierarchy())) {
+      return OptionalBool.UNKNOWN;
+    }
     if (member.getHolderType().isSamePackage(context.getContextType())) {
       return classAccessibility;
     }
@@ -115,7 +94,6 @@
     return OptionalBool.FALSE;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private static boolean isNestMate(DexClass clazz, DexClass context) {
     if (clazz == context) {
       return true;
@@ -127,6 +105,6 @@
     if (!clazz.isInANest() || !context.isInANest()) {
       return false;
     }
-    return clazz.getNestHost() == context.getNestHost();
+    return clazz.getNestHost().isIdenticalTo(context.getNestHost());
   }
 }
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 d93fae1..532abbd 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -14,6 +14,8 @@
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
 import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis.ResourceAnalysisResult;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
+import com.android.tools.r8.graph.lens.AppliedGraphLens;
+import com.android.tools.r8.graph.lens.ClearCodeRewritingGraphLens;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.InitClassLens;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
@@ -37,6 +39,8 @@
 import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.SeedMapper;
+import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
+import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
 import com.android.tools.r8.optimize.compose.ComposeReferences;
 import com.android.tools.r8.optimize.interfaces.collection.OpenClosedInterfacesCollection;
@@ -433,10 +437,18 @@
     allCodeProcessed = true;
   }
 
-  public GraphLens clearCodeRewritings() {
-    GraphLens newLens = graphLens.withCodeRewritingsApplied(dexItemFactory());
-    setGraphLens(newLens);
-    return newLens;
+  public void clearCodeRewritings(ExecutorService executorService) throws ExecutionException {
+    setGraphLens(new ClearCodeRewritingGraphLens(withClassHierarchy()));
+
+    MemberRebindingIdentityLens memberRebindingIdentityLens =
+        MemberRebindingIdentityLensFactory.rebuild(withClassHierarchy(), executorService);
+    setGraphLens(memberRebindingIdentityLens);
+  }
+
+  public void flattenGraphLenses() {
+    GraphLens graphLens = graphLens();
+    setGraphLens(GraphLens.getIdentityLens());
+    setGraphLens(new AppliedGraphLens(withClassHierarchy(), graphLens));
   }
 
   public AppServices appServices() {
@@ -1087,6 +1099,17 @@
                 @Override
                 public void onJoin() {
                   appView.withClassHierarchy().setAppInfo(result);
+                  assert verifyLensRewriting();
+                }
+
+                private boolean verifyLensRewriting() {
+                  if (appView.hasLiveness()) {
+                    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+                    MethodAccessInfoCollection methodAccessInfoCollection =
+                        appViewWithLiveness.appInfo().getMethodAccessInfoCollection();
+                    methodAccessInfoCollection.modifier().commit(appViewWithLiveness);
+                  }
+                  return true;
                 }
               },
               new ThreadTask() {
@@ -1247,19 +1270,13 @@
     if (!firstUnappliedLens.isMemberRebindingLens()
         && !firstUnappliedLens.isMemberRebindingIdentityLens()) {
       NonIdentityGraphLens appliedMemberRebindingLens =
-          firstUnappliedLens.findPrevious(
-              previous ->
-                  previous.isMemberRebindingLens() || previous.isMemberRebindingIdentityLens());
+          firstUnappliedLens.findPrevious(GraphLens::isMemberRebindingIdentityLens);
       if (appliedMemberRebindingLens != null) {
         newMemberRebindingLens =
-            appliedMemberRebindingLens.isMemberRebindingLens()
-                ? appliedMemberRebindingLens
-                    .asMemberRebindingLens()
-                    .toRewrittenFieldRebindingLens(appView, appliedLens, appliedMemberRebindingLens)
-                : appliedMemberRebindingLens
-                    .asMemberRebindingIdentityLens()
-                    .toRewrittenMemberRebindingIdentityLens(
-                        appView, appliedLens, appliedMemberRebindingLens);
+            appliedMemberRebindingLens
+                .asMemberRebindingIdentityLens()
+                .toRewrittenMemberRebindingIdentityLens(
+                    appView, appliedLens, appliedMemberRebindingLens);
       }
     }
     timing.end();
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 e730dee..3b8ba07 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -850,40 +850,20 @@
 
   @SuppressWarnings("ReferenceEquality")
   public ConstraintWithTarget computeInliningConstraint(
-      ProgramMethod method,
       AppView<AppInfoWithLiveness> appView,
       GraphLens graphLens,
       ProgramMethod context) {
     InliningConstraints inliningConstraints = new InliningConstraints(appView, graphLens);
-    if (appView.options().isInterfaceMethodDesugaringEnabled()) {
-      // TODO(b/120130831): Conservatively need to say "no" at this point if there are invocations
-      // to static interface methods. This should be fixed by making sure that the desugared
-      // versions of default and static interface methods are present in the application during
-      // IR processing.
-      inliningConstraints.disallowStaticInterfaceMethodCalls();
-    }
-    // Model a synchronized method as having a monitor instruction.
-    ConstraintWithTarget constraint =
-        method.getDefinition().isSynchronized()
-            ? inliningConstraints.forMonitor()
-            : ConstraintWithTarget.ALWAYS;
-
-    if (constraint == ConstraintWithTarget.NEVER) {
-      return constraint;
-    }
+    ConstraintWithTarget constraint = ConstraintWithTarget.ALWAYS;
+    assert inliningConstraints.forMonitor().isAlways();
     for (CfInstruction insn : instructions) {
       constraint =
           ConstraintWithTarget.meet(
               constraint, insn.inliningConstraint(inliningConstraints, this, context), appView);
-      if (constraint == ConstraintWithTarget.NEVER) {
+      if (constraint.isNever()) {
         return constraint;
       }
     }
-    if (!tryCatchRanges.isEmpty()) {
-      // Model a try-catch as a move-exception instruction.
-      constraint =
-          ConstraintWithTarget.meet(constraint, inliningConstraints.forMoveException(), appView);
-    }
     return constraint;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index dba763b..888f1e8 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -184,6 +184,10 @@
     set(Constants.ACC_ENUM);
   }
 
+  public void unsetEnum() {
+    unset(Constants.ACC_ENUM);
+  }
+
   public boolean isRecord() {
     return isSet(Constants.ACC_RECORD);
   }
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 e61ec53..14c4975 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -558,7 +558,7 @@
   @SuppressWarnings("ReferenceEquality")
   public DexEncodedMethod lookupSignaturePolymorphicMethod(
       DexString methodName, DexItemFactory factory) {
-    if (type != factory.methodHandleType && type != factory.varHandleType) {
+    if (!isClassWithSignaturePolymorphicMethods(factory)) {
       return null;
     }
     DexEncodedMethod matchingName = null;
@@ -579,15 +579,17 @@
     return signaturePolymorphicMethod;
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  public static boolean isSignaturePolymorphicMethod(
-      DexEncodedMethod method, DexItemFactory factory) {
-    assert method.getHolderType() == factory.methodHandleType
-        || method.getHolderType() == factory.varHandleType;
-    return method.accessFlags.isVarargs()
-        && method.accessFlags.isNative()
-        && method.getReference().proto.parameters.size() == 1
-        && method.getReference().proto.parameters.values[0] == factory.objectArrayType;
+  public boolean isClassWithSignaturePolymorphicMethods(DexItemFactory dexItemFactory) {
+    return type.isIdenticalTo(dexItemFactory.methodHandleType)
+        || type.isIdenticalTo(dexItemFactory.varHandleType);
+  }
+
+  public boolean isSignaturePolymorphicMethod(DexEncodedMethod method, DexItemFactory factory) {
+    assert isClassWithSignaturePolymorphicMethods(factory);
+    return method.isVarargs()
+        && method.isNative()
+        && method.getParameters().size() == 1
+        && method.getParameter(0).isIdenticalTo(factory.objectArrayType);
   }
 
   public boolean canBeInstantiatedByNewInstance() {
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 40bdbe2..7465b0a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -450,6 +450,10 @@
     return accessFlags.isSynchronized();
   }
 
+  public boolean isVarargs() {
+    return accessFlags.isVarargs();
+  }
+
   public boolean isInitializer() {
     checkIfObsolete();
     return isInstanceInitializer() || isClassInitializer();
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 939c529..bd110a3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -31,6 +31,10 @@
     return identical(this, other);
   }
 
+  public final boolean isNotIdenticalTo(DexField other) {
+    return !isIdenticalTo(other);
+  }
+
   public final DexType type;
 
   DexField(DexType holder, DexType type, DexString name, boolean skipNameValidationForTesting) {
@@ -224,8 +228,8 @@
   }
 
   @Override
-  public DexField withHolder(DexType holder, DexItemFactory dexItemFactory) {
-    return dexItemFactory.createField(holder, type, name);
+  public DexField withHolder(DexReference reference, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createField(reference.getContextType(), type, name);
   }
 
   public DexField withName(DexString name, DexItemFactory dexItemFactory) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMember.java b/src/main/java/com/android/tools/r8/graph/DexMember.java
index cd13cc7..88e91d8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMember.java
@@ -4,7 +4,9 @@
 package com.android.tools.r8.graph;
 
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 public abstract class DexMember<D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
     extends DexReference implements NamingLensComparable<R> {
@@ -70,5 +72,11 @@
     return Iterables.transform(getReferencedTypes(), type -> type.toBaseType(dexItemFactory));
   }
 
-  public abstract DexMember<D, R> withHolder(DexType holder, DexItemFactory dexItemFactory);
+  public boolean verifyReferencedBaseTypesMatches(
+      Predicate<DexType> predicate, DexItemFactory dexItemFactory) {
+    assert Streams.stream(getReferencedBaseTypes(dexItemFactory)).allMatch(predicate);
+    return true;
+  }
+
+  public abstract DexMember<D, R> withHolder(DexReference holder, DexItemFactory dexItemFactory);
 }
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 a58d4a9..65ff14d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -360,7 +360,7 @@
   }
 
   @Override
-  public DexMethod withHolder(DexType reference, DexItemFactory dexItemFactory) {
+  public DexMethod withHolder(DexReference reference, DexItemFactory dexItemFactory) {
     return dexItemFactory.createMethod(reference.getContextType(), proto, name);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java b/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
index e441972..4c1e197 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
@@ -71,12 +71,19 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public boolean equals(Object o) {
-    if (this == o) return true;
-    if (!(o instanceof DexMethodSignature)) return false;
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof DexMethodSignature)) {
+      return false;
+    }
     DexMethodSignature that = (DexMethodSignature) o;
-    return getName() == that.getName() && getProto() == that.getProto();
+    return equals(that.getName(), that.getProto());
+  }
+
+  public boolean equals(DexString name, DexProto proto) {
+    return getName().isIdenticalTo(name) && getProto().isIdenticalTo(proto);
   }
 
   @Override
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 beb2fb3..542a558 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -20,6 +22,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 public class MethodAccessInfoCollection {
@@ -334,6 +337,66 @@
       }
     }
 
+    public void removeIf(Predicate<DexMethod> predicate) {
+      removeIf(predicate, directInvokes);
+      removeIf(predicate, interfaceInvokes);
+      removeIf(predicate, staticInvokes);
+      removeIf(predicate, superInvokes);
+      removeIf(predicate, virtualInvokes);
+    }
+
+    private static void removeIf(Predicate<DexMethod> predicate, Map<DexMethod, ?> invokes) {
+      invokes.keySet().removeIf(predicate);
+    }
+
+    public void removeNonResolving(
+        AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+      // TODO(b/313365881): Should use non-legacy resolution, but this fails.
+      removeIf(
+          method -> {
+            MethodResolutionResult result =
+                appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(method);
+            if (result.isFailedResolution()
+                || result.isSignaturePolymorphicResolution(method, appView.dexItemFactory())) {
+              return true;
+            }
+            if (result.hasProgramResult()) {
+              ProgramMethod resolvedMethod = result.getResolvedProgramMethod();
+              // Guard against an unusual case where the invoke resolves to a program method but the
+              // invoke is invalid (e.g., invoke-interface to a non-interface), such that the
+              // resolved method is not retained after all.
+              if (!enqueuer.isMethodLive(resolvedMethod)
+                  && !enqueuer.isMethodTargeted(resolvedMethod)) {
+                return true;
+              }
+            }
+            return false;
+          });
+    }
+
+    public boolean verifyNoNonResolving(AppView<AppInfoWithLiveness> appView) {
+      verifyNoNonResolving(appView, directInvokes);
+      verifyNoNonResolving(appView, interfaceInvokes);
+      verifyNoNonResolving(appView, staticInvokes);
+      verifyNoNonResolving(appView, superInvokes);
+      verifyNoNonResolving(appView, virtualInvokes);
+      return true;
+    }
+
+    private void verifyNoNonResolving(
+        AppView<AppInfoWithLiveness> appView, Map<DexMethod, ?> invokes) {
+      if (!isThrowingMap(invokes)) {
+        for (DexMethod method : invokes.keySet()) {
+          MethodResolutionResult result =
+              appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(method);
+          assert !result.isFailedResolution()
+              : "Unexpected method that does not resolve: " + method.toSourceString();
+          assert !result.isSignaturePolymorphicResolution(method, appView.dexItemFactory())
+              : "Unexpected signature polymorphic resolution: " + method.toSourceString();
+        }
+      }
+    }
+
     public MethodAccessInfoCollection build() {
       return new MethodAccessInfoCollection(
           directInvokes, interfaceInvokes, staticInvokes, superInvokes, virtualInvokes);
@@ -374,5 +437,9 @@
       collection.forEachSuperInvoke(this::registerInvokeSuperInContexts);
       collection.forEachVirtualInvoke(this::registerInvokeVirtualInContexts);
     }
+
+    public void commit(AppView<AppInfoWithLiveness> appView) {
+      assert verifyNoNonResolving(appView);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
index 8e04491..ab17919 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
@@ -403,22 +403,18 @@
   public void replaceAllDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
     DexEncodedMethod[] oldMethods = directMethods;
     clearDirectMethods();
-    DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length];
-    for (int i = 0; i < oldMethods.length; i++) {
-      newMethods[i] = replacement.apply(oldMethods[i]);
-    }
-    directMethods = newMethods;
+    directMethods =
+        ArrayUtils.initialize(
+            new DexEncodedMethod[oldMethods.length], i -> replacement.apply(oldMethods[i]));
   }
 
   @Override
   public void replaceAllVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
     DexEncodedMethod[] oldMethods = virtualMethods;
     clearVirtualMethods();
-    DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length];
-    for (int i = 0; i < oldMethods.length; i++) {
-      newMethods[i] = replacement.apply(oldMethods[i]);
-    }
-    virtualMethods = newMethods;
+    virtualMethods =
+        ArrayUtils.initialize(
+            new DexEncodedMethod[oldMethods.length], i -> replacement.apply(oldMethods[i]));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
index 6dbdc35..bdc3a2a 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
@@ -53,6 +53,15 @@
     return this;
   }
 
+  public boolean isSignaturePolymorphicResolution(DexMethod method, DexItemFactory dexItemFactory) {
+    if (isSingleResolution() && !method.match(getResolvedMethod())) {
+      assert getResolvedHolder().isClassWithSignaturePolymorphicMethods(dexItemFactory);
+      assert getResolvedHolder().isSignaturePolymorphicMethod(getResolvedMethod(), dexItemFactory);
+      return true;
+    }
+    return false;
+  }
+
   /**
    * Returns true if resolution succeeded *and* the resolved method has a known definition.
    *
@@ -179,8 +188,8 @@
       DexProgramClass context, AppView<? extends AppInfoWithClassHierarchy> appView);
 
   public final DexClassAndMethod lookupInvokeSuperTarget(
-      DexProgramClass context, AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return lookupInvokeSuperTarget(context, appView, appView.appInfo());
+      ProgramDefinition context, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return lookupInvokeSuperTarget(context.getContextClass(), appView, appView.appInfo());
   }
 
   /** Lookup the single target of an invoke-super on this resolution result if possible. */
@@ -1471,13 +1480,7 @@
                   .forEachClassResolutionResult(
                       clazz ->
                           seenNoAccess.or(
-                              AccessControl.isClassAccessible(
-                                      clazz,
-                                      context,
-                                      appInfo.getClassToFeatureSplitMap(),
-                                      appView.options(),
-                                      appView.getStartupProfile(),
-                                      appView.getSyntheticItems())
+                              AccessControl.isClassAccessible(clazz, context, appView, appInfo)
                                   .isPossiblyFalse())),
           method -> {
             DexClass holder = appView.definitionFor(method.getHolderType());
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 0eb2d7e..6050c9d 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -490,7 +490,6 @@
           (clazz, allocationSitesForClass) -> {
             DexType type = lens.lookupType(clazz.type);
             if (type.isPrimitiveType()) {
-              assert clazz.isEnum();
               return;
             }
             DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerCheckCastAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerCheckCastAnalysis.java
index ad0dcbf..1ff19a6 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerCheckCastAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerCheckCastAnalysis.java
@@ -4,12 +4,13 @@
 
 package com.android.tools.r8.graph.analysis;
 
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 
 public interface EnqueuerCheckCastAnalysis {
 
-  void traceCheckCast(DexType type, ProgramMethod context);
+  void traceCheckCast(DexType type, DexClass clazz, ProgramMethod context);
 
-  void traceSafeCheckCast(DexType type, ProgramMethod context);
+  void traceSafeCheckCast(DexType type, DexClass clazz, ProgramMethod context);
 }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerConstClassAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerConstClassAnalysis.java
new file mode 100644
index 0000000..61f127a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerConstClassAnalysis.java
@@ -0,0 +1,13 @@
+// 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.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface EnqueuerConstClassAnalysis {
+
+  void traceConstClass(DexType type, DexClass clazz, ProgramMethod context);
+}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerExceptionGuardAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerExceptionGuardAnalysis.java
index 1bf392e..752e4a5 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerExceptionGuardAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerExceptionGuardAnalysis.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.graph.analysis;
 
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 
 public interface EnqueuerExceptionGuardAnalysis {
-  void traceExceptionGuard(DexType guard, ProgramMethod context);
+  void traceExceptionGuard(DexType guard, DexClass clazz, ProgramMethod context);
 }
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 0642216..69eb560 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
@@ -18,28 +18,24 @@
       FieldResolutionResult resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
-  ;
 
   default void traceInstanceFieldWrite(
       DexField field,
       FieldResolutionResult resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
-  ;
 
   default void traceStaticFieldRead(
       DexField field,
       SingleFieldResolutionResult<?> resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
-  ;
 
   default void traceStaticFieldWrite(
       DexField field,
       FieldResolutionResult resolutionResult,
       ProgramMethod context,
       EnqueuerWorklist worklist) {}
-  ;
 
   /**
    * Called when the Enqueuer has reached the final fixpoint. Each analysis may use this callback to
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInstanceOfAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInstanceOfAnalysis.java
index abda4d1..485a311 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInstanceOfAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInstanceOfAnalysis.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.graph.analysis;
 
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 
 public interface EnqueuerInstanceOfAnalysis {
-  void traceInstanceOf(DexType type, ProgramMethod context);
+  void traceInstanceOf(DexType type, DexClass clazz, ProgramMethod context);
 }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInvokeAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInvokeAnalysis.java
index d0a39b3..d2ca27d 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInvokeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerInvokeAnalysis.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph.analysis;
 
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 
 public interface EnqueuerInvokeAnalysis {
@@ -12,13 +13,18 @@
    * Each traceInvokeXX method is called when a corresponding invoke is found while tracing a live
    * method.
    */
-  void traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context);
+  void traceInvokeStatic(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context);
 
-  void traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context);
+  void traceInvokeDirect(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context);
 
-  void traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context);
+  void traceInvokeInterface(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context);
 
-  void traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context);
+  void traceInvokeSuper(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context);
 
-  void traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context);
+  void traceInvokeVirtual(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context);
 }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerNewInstanceAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerNewInstanceAnalysis.java
new file mode 100644
index 0000000..880c91a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerNewInstanceAnalysis.java
@@ -0,0 +1,13 @@
+// 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.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface EnqueuerNewInstanceAnalysis {
+
+  void traceNewInstance(DexType type, DexClass clazz, ProgramMethod context);
+}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerTypeAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerTypeAccessAnalysis.java
new file mode 100644
index 0000000..3d44ef1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerTypeAccessAnalysis.java
@@ -0,0 +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.
+package com.android.tools.r8.graph.analysis;
+
+public interface EnqueuerTypeAccessAnalysis
+    extends EnqueuerCheckCastAnalysis,
+        EnqueuerConstClassAnalysis,
+        EnqueuerExceptionGuardAnalysis,
+        EnqueuerInstanceOfAnalysis,
+        EnqueuerNewInstanceAnalysis {}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
index 94bf7ce..70fca76 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
@@ -8,6 +8,7 @@
 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.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.KeepInfo.Joiner;
@@ -53,7 +54,8 @@
   }
 
   @Override
-  public void traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) {
+  public void traceInvokeVirtual(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     if (isInterfaceInSomeApiLevel(invokedMethod.getHolderType())) {
       enqueuer.getKeepInfo().joinMethod(context, Joiner::disallowOptimization);
     }
@@ -69,22 +71,26 @@
   }
 
   @Override
-  public void traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) {
+  public void traceInvokeDirect(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     // Intentionally empty.
   }
 
   @Override
-  public void traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) {
+  public void traceInvokeInterface(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     // Intentionally empty.
   }
 
   @Override
-  public void traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) {
+  public void traceInvokeStatic(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     // Intentionally empty.
   }
 
   @Override
-  public void traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) {
+  public void traceInvokeSuper(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     // Intentionally empty.
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
index 9aa77c9..c75bc43 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
@@ -14,7 +14,13 @@
 
   void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer);
 
-  boolean hasBeenMergedIntoDifferentType(DexType type);
+  DexType getMergeTargetOrDefault(DexType type, DexType defaultValue);
+
+  boolean isMergeSource(DexType type);
+
+  default boolean isMergeSourceOrTarget(DexType type) {
+    return isMergeSource(type) || isMergeTarget(type);
+  }
 
   boolean isMergeTarget(DexType type);
 
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
index 33bee51..386a256 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.graph.classmerging;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -28,9 +29,14 @@
   }
 
   @Override
-  public boolean hasBeenMergedIntoDifferentType(DexType type) {
+  public DexType getMergeTargetOrDefault(DexType type, DexType defaultValue) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isMergeSource(DexType type) {
     for (MergedClasses mergedClasses : collection) {
-      if (mergedClasses.hasBeenMergedIntoDifferentType(type)) {
+      if (mergedClasses.isMergeSource(type)) {
         return true;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java b/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
index 4810b23..ec65897 100644
--- a/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
+++ b/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
@@ -35,8 +35,8 @@
 
 public abstract class TreeFixerBase {
 
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+  protected final AppView<?> appView;
+  protected final DexItemFactory dexItemFactory;
 
   private final Map<DexType, DexProgramClass> programClassCache = new IdentityHashMap<>();
   private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
@@ -395,7 +395,7 @@
   }
 
   /** Fixup a method signature. */
-  public DexMethodSignature fixupMethodSignature(DexMethodSignature signature) {
-    return signature.withProto(fixupProto(signature.getProto()));
+  public DexMethodSignature fixupMethodSignature(DexEncodedMethod method) {
+    return DexMethodSignature.create(method.getName(), fixupProto(method.getProto()));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
index e6ecef1..2c0c5bc 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
@@ -45,22 +45,23 @@
   private final Map<DexMethod, DexMethod> extraOriginalMethodSignatures = new IdentityHashMap<>();
 
   @SuppressWarnings("ReferenceEquality")
-  public AppliedGraphLens(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    super(appView.dexItemFactory(), GraphLens.getIdentityLens());
+  public AppliedGraphLens(
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens graphLens) {
+    super(appView);
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       // TODO(b/169395592): If merged classes were removed from the application this would not be
       //  necessary.
-      if (appView.graphLens().lookupType(clazz.getType()) != clazz.getType()) {
+      if (graphLens.lookupType(clazz.getType()) != clazz.getType()) {
         continue;
       }
 
       // Record original type names.
-      recordOriginalTypeNames(clazz, appView);
+      recordOriginalTypeNames(clazz, graphLens);
 
       // Record original field signatures.
       for (DexEncodedField encodedField : clazz.fields()) {
         DexField field = encodedField.getReference();
-        DexField original = appView.graphLens().getOriginalFieldSignature(field);
+        DexField original = graphLens.getOriginalFieldSignature(field);
         if (original != field) {
           DexField existing = originalFieldSignatures.forcePut(field, original);
           assert existing == null;
@@ -70,12 +71,12 @@
       // Record original method signatures.
       for (DexEncodedMethod encodedMethod : clazz.methods()) {
         DexMethod method = encodedMethod.getReference();
-        DexMethod original = appView.graphLens().getOriginalMethodSignatureForMapping(method);
+        DexMethod original = graphLens.getOriginalMethodSignatureForMapping(method);
         DexMethod existing = originalMethodSignatures.inverse().get(original);
         if (existing == null) {
           originalMethodSignatures.put(method, original);
         } else {
-          DexMethod renamed = appView.graphLens().getRenamedMethodSignature(original);
+          DexMethod renamed = graphLens.getRenamedMethodSignature(original);
           if (renamed == existing) {
             extraOriginalMethodSignatures.put(method, original);
           } else {
@@ -92,11 +93,10 @@
   }
 
   @SuppressWarnings("ReferenceEquality")
-  private void recordOriginalTypeNames(
-      DexProgramClass clazz, AppView<? extends AppInfoWithClassHierarchy> appView) {
+  private void recordOriginalTypeNames(DexProgramClass clazz, GraphLens graphLens) {
     DexType type = clazz.getType();
 
-    List<DexType> originalTypes = Lists.newArrayList(appView.graphLens().getOriginalTypes(type));
+    List<DexType> originalTypes = Lists.newArrayList(graphLens.getOriginalTypes(type));
     boolean isIdentity = originalTypes.size() == 1 && originalTypes.get(0) == type;
     if (!isIdentity) {
       originalTypes.forEach(
@@ -104,7 +104,7 @@
             assert !renamedTypeNames.containsKey(originalType);
             renamedTypeNames.put(originalType, type);
           });
-      renamedTypeNames.setRepresentative(type, appView.graphLens().getOriginalType(type));
+      renamedTypeNames.setRepresentative(type, graphLens.getOriginalType(type));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
index 0b5348c..f53708a 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
@@ -5,8 +5,9 @@
 package com.android.tools.r8.graph.lens;
 
 import com.android.tools.r8.errors.Unreachable;
+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.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.InvokeType;
@@ -15,8 +16,8 @@
 // relies on the previous lens for names (getRenamed/Original methods).
 public class ClearCodeRewritingGraphLens extends DefaultNonIdentityGraphLens {
 
-  public ClearCodeRewritingGraphLens(DexItemFactory dexItemFactory, GraphLens previousLens) {
-    super(dexItemFactory, previousLens);
+  public ClearCodeRewritingGraphLens(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    super(appView);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
index ffab2e0..26376cd 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.graph.AppView;
 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.DexType;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
@@ -14,11 +13,11 @@
 public class DefaultNonIdentityGraphLens extends NonIdentityGraphLens {
 
   public DefaultNonIdentityGraphLens(AppView<?> appView) {
-    this(appView.dexItemFactory(), appView.graphLens());
+    this(appView, appView.graphLens());
   }
 
-  public DefaultNonIdentityGraphLens(DexItemFactory dexItemFactory, GraphLens previousLens) {
-    super(dexItemFactory, previousLens);
+  public DefaultNonIdentityGraphLens(AppView<?> appView, GraphLens previousLens) {
+    super(appView, previousLens);
   }
 
   @Override
@@ -68,7 +67,7 @@
   @Override
   protected MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    return previous;
+    return previous.verify(this, codeLens);
   }
 
   @Override
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 eff9aa9..d0f23ae 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
@@ -13,7 +13,6 @@
 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.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -37,6 +36,7 @@
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.android.tools.r8.verticalclassmerging.VerticalClassMergerGraphLens;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -226,12 +226,6 @@
 
   public abstract String lookupPackageName(String pkg);
 
-  @Deprecated
-  public final DexType lookupClassType(DexType type) {
-    GraphLens appliedLens = getIdentityLens();
-    return lookupClassType(type, appliedLens);
-  }
-
   public final DexType lookupClassType(DexType type, GraphLens appliedLens) {
     return getRenamedReference(
         type, appliedLens, NonIdentityGraphLens::getNextClassType, DexType::isPrimitiveType);
@@ -245,6 +239,7 @@
 
   public abstract DexType lookupType(DexType type, GraphLens appliedLens);
 
+  @Deprecated
   public final MethodLookupResult lookupInvokeDirect(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.DIRECT);
   }
@@ -254,6 +249,7 @@
     return lookupMethod(method, context.getReference(), InvokeType.DIRECT, codeLens);
   }
 
+  @Deprecated
   public final MethodLookupResult lookupInvokeInterface(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.INTERFACE);
   }
@@ -263,6 +259,7 @@
     return lookupMethod(method, context.getReference(), InvokeType.INTERFACE, codeLens);
   }
 
+  @Deprecated
   public final MethodLookupResult lookupInvokeStatic(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.STATIC);
   }
@@ -272,6 +269,7 @@
     return lookupMethod(method, context.getReference(), InvokeType.STATIC, codeLens);
   }
 
+  @Deprecated
   public final MethodLookupResult lookupInvokeSuper(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.SUPER);
   }
@@ -281,6 +279,7 @@
     return lookupMethod(method, context.getReference(), InvokeType.SUPER, codeLens);
   }
 
+  @Deprecated
   public final MethodLookupResult lookupInvokeVirtual(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.VIRTUAL);
   }
@@ -290,6 +289,8 @@
     return lookupMethod(method, context.getReference(), InvokeType.VIRTUAL, codeLens);
   }
 
+  @Deprecated
+  @SuppressWarnings("InlineMeSuggester")
   public final MethodLookupResult lookupMethod(
       DexMethod method, DexMethod context, InvokeType type) {
     return lookupMethod(method, context, type, null);
@@ -316,7 +317,7 @@
       GraphLens codeLens,
       LookupMethodContinuation continuation);
 
-  interface LookupMethodContinuation {
+  public interface LookupMethodContinuation {
 
     MethodLookupResult lookupMethod(MethodLookupResult previous);
   }
@@ -329,6 +330,8 @@
   public abstract RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
       DexMethod method, GraphLens codeLens);
 
+  @Deprecated
+  @SuppressWarnings("InlineMeSuggester")
   public final DexField lookupField(DexField field) {
     return lookupField(field, null);
   }
@@ -456,11 +459,8 @@
     return false;
   }
 
-  public GraphLens withCodeRewritingsApplied(DexItemFactory dexItemFactory) {
-    if (hasCodeRewritings()) {
-      return new ClearCodeRewritingGraphLens(dexItemFactory, this);
-    }
-    return this;
+  public VerticalClassMergerGraphLens asVerticalClassMergerLens() {
+    return null;
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java
new file mode 100644
index 0000000..b7175d3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLensUtils.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph.lens;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+public class GraphLensUtils {
+
+  public static Deque<NonIdentityGraphLens> extractNonIdentityLenses(GraphLens lens) {
+    Deque<NonIdentityGraphLens> lenses = new ArrayDeque<>();
+    if (lens.isNonIdentityLens()) {
+      lenses.addFirst(lens.asNonIdentityLens());
+      while (true) {
+        GraphLens previous = lenses.getFirst().getPrevious();
+        if (previous.isNonIdentityLens()) {
+          lenses.addFirst(previous.asNonIdentityLens());
+        } else {
+          break;
+        }
+      }
+    }
+    return lenses;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
index e72f128..1497895 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
@@ -55,7 +55,7 @@
   public MethodLookupResult lookupMethod(
       DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
     assert codeLens == null || codeLens.isIdentityLens();
-    return MethodLookupResult.builder(this).setReference(method).setType(type).build();
+    return MethodLookupResult.builder(this, codeLens).setReference(method).setType(type).build();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/lens/MemberLookupResult.java b/src/main/java/com/android/tools/r8/graph/lens/MemberLookupResult.java
index 7ec7c85..9b89ff1 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/MemberLookupResult.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/MemberLookupResult.java
@@ -4,7 +4,11 @@
 
 package com.android.tools.r8.graph.lens;
 
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import java.util.Map;
 import java.util.function.Function;
@@ -31,6 +35,20 @@
     return rewritings.getOrDefault(reference, reference);
   }
 
+  @SuppressWarnings("unchecked")
+  public R getRewrittenReferenceFromRewrittenReboundReference(
+      R rewrittenReboundReference,
+      Function<DexType, DexType> typeRewriter,
+      DexItemFactory dexItemFactory) {
+    R rewrittenReference =
+        ObjectUtils.identical(reference, reboundReference)
+            ? rewrittenReboundReference
+            : (R)
+                rewrittenReboundReference.withHolder(
+                    typeRewriter.apply(reference.getHolderType()), dexItemFactory);
+    return rewrittenReference;
+  }
+
   public boolean hasReboundReference() {
     return reboundReference != null;
   }
@@ -39,7 +57,11 @@
     return reboundReference;
   }
 
-  public R getRewrittenReboundReference(BidirectionalManyToOneRepresentativeMap<R, R> rewritings) {
+  public R getRewrittenReboundReference(BidirectionalManyToManyRepresentativeMap<R, R> rewritings) {
+    return rewritings.getRepresentativeValueOrDefault(reboundReference, reboundReference);
+  }
+
+  public R getRewrittenReboundReference(Map<R, R> rewritings) {
     return rewritings.getOrDefault(reboundReference, reboundReference);
   }
 
@@ -53,11 +75,19 @@
     R reference;
     R reboundReference;
 
+    public R getReference() {
+      return reference;
+    }
+
     public Self setReference(R reference) {
       this.reference = reference;
       return self();
     }
 
+    public R getReboundReference() {
+      return reboundReference;
+    }
+
     public Self setReboundReference(R reboundReference) {
       this.reboundReference = reboundReference;
       return self();
diff --git a/src/main/java/com/android/tools/r8/graph/lens/MethodLookupResult.java b/src/main/java/com/android/tools/r8/graph/lens/MethodLookupResult.java
index d0c3ded..450585d 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/MethodLookupResult.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/MethodLookupResult.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.optimize.bridgehoisting.BridgeHoistingLens;
 
 /**
  * Result of a method lookup in a GraphLens.
@@ -30,29 +31,44 @@
     this.prototypeChanges = prototypeChanges;
   }
 
-  public static Builder builder(GraphLens lens) {
-    return new Builder(lens);
+  public static Builder builder(GraphLens lens, GraphLens codeLens) {
+    return new Builder(lens, codeLens);
   }
 
   public InvokeType getType() {
     return type;
   }
 
-  @SuppressWarnings("UnusedVariable")
   public RewrittenPrototypeDescription getPrototypeChanges() {
     return prototypeChanges;
   }
 
+  public MethodLookupResult verify(GraphLens lens, GraphLens codeLens) {
+    assert getReference() != null;
+    assert lens.isIdentityLens()
+            || lens.isAppliedLens()
+            || lens.asNonIdentityLens().isD8Lens()
+            || getReference().getHolderType().isArrayType()
+            || hasReboundReference()
+            // TODO: Disallow the following.
+            || lens.isEnumUnboxerLens()
+            || lens.isNumberUnboxerLens()
+            || lens instanceof BridgeHoistingLens
+        : lens;
+    return this;
+  }
+
   public static class Builder extends MemberLookupResult.Builder<DexMethod, Builder> {
 
-    @SuppressWarnings("UnusedVariable")
     private final GraphLens lens;
+    private final GraphLens codeLens;
 
     private RewrittenPrototypeDescription prototypeChanges = RewrittenPrototypeDescription.none();
     private InvokeType type;
 
-    private Builder(GraphLens lens) {
+    private Builder(GraphLens lens, GraphLens codeLens) {
       this.lens = lens;
+      this.codeLens = codeLens;
     }
 
     public Builder setPrototypeChanges(RewrittenPrototypeDescription prototypeChanges) {
@@ -66,9 +82,8 @@
     }
 
     public MethodLookupResult build() {
-      assert reference != null;
-      // TODO(b/168282032): All non-identity graph lenses should set the rebound reference.
-      return new MethodLookupResult(reference, reboundReference, type, prototypeChanges);
+      return new MethodLookupResult(reference, reboundReference, type, prototypeChanges)
+          .verify(lens, codeLens);
     }
 
     @Override
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 63aa73a..1186ce2 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
@@ -172,12 +172,14 @@
               : // This assumes that the holder will always be moved in lock-step with the method!
               rewrittenReboundReference.withHolder(
                   getNextClassType(previous.getReference().getHolderType()), dexItemFactory());
-      return MethodLookupResult.builder(this)
+      return MethodLookupResult.builder(this, codeLens)
           .setReference(rewrittenReference)
           .setReboundReference(rewrittenReboundReference)
           .setPrototypeChanges(
               internalDescribePrototypeChanges(
-                  previous.getPrototypeChanges(), rewrittenReboundReference))
+                  previous.getPrototypeChanges(),
+                  previous.getReboundReference(),
+                  rewrittenReboundReference))
           .setType(
               mapInvocationType(
                   rewrittenReboundReference, previous.getReference(), previous.getType()))
@@ -190,14 +192,15 @@
         newMethod = previous.getReference();
       }
       RewrittenPrototypeDescription newPrototypeChanges =
-          internalDescribePrototypeChanges(previous.getPrototypeChanges(), newMethod);
+          internalDescribePrototypeChanges(
+              previous.getPrototypeChanges(), previous.getReference(), newMethod);
       if (newMethod == previous.getReference()
           && newPrototypeChanges == previous.getPrototypeChanges()) {
-        return previous;
+        return previous.verify(this, codeLens);
       }
       // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win
       //  that only subclasses which are known to need it actually do it?
-      return MethodLookupResult.builder(this)
+      return MethodLookupResult.builder(this, codeLens)
           .setReference(newMethod)
           .setPrototypeChanges(newPrototypeChanges)
           .setType(mapInvocationType(newMethod, previous.getReference(), previous.getType()))
@@ -214,11 +217,13 @@
     DexMethod previous = getPreviousMethodSignature(method);
     RewrittenPrototypeDescription lookup =
         getPrevious().lookupPrototypeChangesForMethodDefinition(previous, codeLens);
-    return internalDescribePrototypeChanges(lookup, method);
+    return internalDescribePrototypeChanges(lookup, previous, method);
   }
 
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
-      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+      RewrittenPrototypeDescription prototypeChanges,
+      DexMethod previousMethod,
+      DexMethod newMethod) {
     return prototypeChanges;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
index 33f5aeb..987bfe3 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
@@ -11,23 +11,26 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.utils.ThrowingAction;
+import com.google.common.collect.Streams;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Predicate;
 
 public abstract class NonIdentityGraphLens extends GraphLens {
 
+  protected final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
   private GraphLens previousLens;
 
   private final Map<DexType, DexType> arrayTypeCache = new ConcurrentHashMap<>();
 
   public NonIdentityGraphLens(AppView<?> appView) {
-    this(appView.dexItemFactory(), appView.graphLens());
+    this(appView, appView.graphLens());
   }
 
-  public NonIdentityGraphLens(DexItemFactory dexItemFactory, GraphLens previousLens) {
-    this.dexItemFactory = dexItemFactory;
+  public NonIdentityGraphLens(AppView<?> appView, GraphLens previousLens) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
     this.previousLens = previousLens;
   }
 
@@ -80,20 +83,19 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public MethodLookupResult lookupMethod(
-      DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
+      DexMethod method, DexMethod context, InvokeType invokeType, GraphLens codeLens) {
     if (method.getHolderType().isArrayType()) {
-      assert lookupType(method.getReturnType()) == method.getReturnType();
-      assert method.getParameters().stream()
-          .allMatch(parameterType -> lookupType(parameterType) == parameterType);
-      return MethodLookupResult.builder(this)
-          .setReference(method.withHolder(lookupType(method.getHolderType()), dexItemFactory))
-          .setType(type)
+      assert Streams.stream(method.getReferencedBaseTypes(dexItemFactory))
+          .allMatch(type -> type.isIdenticalTo(lookupClassType(type, codeLens)));
+      return MethodLookupResult.builder(this, codeLens)
+          .setReference(
+              method.withHolder(lookupType(method.getHolderType(), codeLens), dexItemFactory))
+          .setType(invokeType)
           .build();
     }
     assert method.getHolderType().isClassType();
-    return internalLookupMethod(method, context, type, codeLens, result -> result);
+    return internalLookupMethod(method, context, invokeType, codeLens, result -> result);
   }
 
   @Override
@@ -184,6 +186,10 @@
 
   public abstract DexMethod getNextMethodSignature(DexMethod method);
 
+  public final boolean isD8Lens() {
+    return !appView.enableWholeProgramOptimizations();
+  }
+
   @Override
   public final boolean isIdentityLens() {
     return false;
diff --git a/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
index 3f628b9..67659b6 100644
--- a/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
@@ -63,6 +63,16 @@
         : new RewrittenPrototypeDescription(extraParameters, rewrittenReturnInfo, argumentsInfo);
   }
 
+  public static RewrittenPrototypeDescription createForArgumentsInfo(
+      ArgumentInfoCollection argumentsInfo) {
+    return create(Collections.emptyList(), null, argumentsInfo);
+  }
+
+  public static RewrittenPrototypeDescription createForExtraParameters(
+      List<? extends ExtraParameter> extraParameters) {
+    return create(extraParameters, null, ArgumentInfoCollection.empty());
+  }
+
   public static RewrittenPrototypeDescription createForRewrittenTypes(
       RewrittenTypeInfo returnInfo, ArgumentInfoCollection rewrittenArgumentsInfo) {
     return create(Collections.emptyList(), returnInfo, rewrittenArgumentsInfo);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index d6b92e0..ecbf167 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -7,6 +7,7 @@
 import static com.google.common.base.Predicates.not;
 
 import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinition;
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 ab20f7d..57ff66a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
 
+import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -14,9 +15,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
-import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
@@ -170,7 +169,13 @@
 
     HorizontalClassMergerGraphLens horizontalClassMergerGraphLens =
         createLens(
-            mergedClasses, lensBuilder, mode, profileCollectionAdditions, syntheticArgumentClass);
+            mergedClasses,
+            lensBuilder,
+            mode,
+            profileCollectionAdditions,
+            syntheticArgumentClass,
+            executorService,
+            timing);
     profileCollectionAdditions =
         profileCollectionAdditions.rewriteMethodReferences(
             horizontalClassMergerGraphLens::getNextMethodToInvoke);
@@ -250,14 +255,10 @@
               for (VirtuallyMergedMethodsKeepInfo virtuallyMergedMethodsKeepInfo :
                   virtuallyMergedMethodsKeepInfos) {
                 DexMethod representative = virtuallyMergedMethodsKeepInfo.getRepresentative();
-                MethodLookupResult lookupResult =
-                    horizontalClassMergerGraphLens.lookupMethod(
-                        representative,
-                        null,
-                        InvokeType.VIRTUAL,
-                        horizontalClassMergerGraphLens.getPrevious());
+                DexMethod mergedMethodReference =
+                    horizontalClassMergerGraphLens.getNextMethodToInvoke(representative);
                 ProgramMethod mergedMethod =
-                    asProgramMethodOrNull(appView.definitionFor(lookupResult.getReference()));
+                    asProgramMethodOrNull(appView.definitionFor(mergedMethodReference));
                 if (mergedMethod != null) {
                   keepInfo.joinMethod(
                       mergedMethod,
@@ -348,8 +349,7 @@
       return appView
           .app()
           .builder()
-          .removeProgramClasses(
-              clazz -> mergedClasses.hasBeenMergedIntoDifferentType(clazz.getType()))
+          .removeProgramClasses(clazz -> mergedClasses.isMergeSource(clazz.getType()))
           .build();
     }
   }
@@ -413,8 +413,8 @@
   }
 
   /**
-   * Fix all references to merged classes using the {@link TreeFixer}. Construct a graph lens
-   * containing all changes performed by horizontal class merging.
+   * Fix all references to merged classes using the {@link HorizontalClassMergerTreeFixer}.
+   * Construct a graph lens containing all changes performed by horizontal class merging.
    */
   @SuppressWarnings("ReferenceEquality")
   private HorizontalClassMergerGraphLens createLens(
@@ -422,15 +422,18 @@
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode,
       ProfileCollectionAdditions profileCollectionAdditions,
-      SyntheticArgumentClass syntheticArgumentClass) {
-    return new TreeFixer(
+      SyntheticArgumentClass syntheticArgumentClass,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    return new HorizontalClassMergerTreeFixer(
             appView,
             mergedClasses,
             lensBuilder,
             mode,
             profileCollectionAdditions,
             syntheticArgumentClass)
-        .fixupTypeReferences();
+        .run(executorService, timing);
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 394d483..24e3f18 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerGraphLens;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexField;
@@ -14,10 +15,11 @@
 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.NestedGraphLens;
+import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
@@ -30,7 +32,7 @@
 import java.util.Map;
 import java.util.Set;
 
-public class HorizontalClassMergerGraphLens extends NestedGraphLens {
+public class HorizontalClassMergerGraphLens extends ClassMergerGraphLens {
 
   private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters;
   private final HorizontallyMergedClasses mergedClasses;
@@ -40,9 +42,14 @@
       HorizontallyMergedClasses mergedClasses,
       Map<DexMethod, List<ExtraParameter>> methodExtraParameters,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
-      Map<DexMethod, DexMethod> methodMap,
+      BidirectionalManyToOneMap<DexMethod, DexMethod> methodMap,
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
-    super(appView, fieldMap, methodMap, mergedClasses.getBidirectionalMap(), newMethodSignatures);
+    super(
+        appView,
+        fieldMap,
+        methodMap.getForwardMap(),
+        mergedClasses.getBidirectionalMap(),
+        newMethodSignatures);
     this.methodExtraParameters = methodExtraParameters;
     this.mergedClasses = mergedClasses;
   }
@@ -67,6 +74,32 @@
     return IterableUtils.prependSingleton(previous, mergedClasses.getSourcesFor(previous));
   }
 
+  @Override
+  protected MethodLookupResult internalLookupMethod(
+      DexMethod reference,
+      DexMethod context,
+      InvokeType type,
+      GraphLens codeLens,
+      LookupMethodContinuation continuation) {
+    if (this == codeLens) {
+      // We sometimes create code objects that have the HorizontalClassMergerGraphLens as code lens.
+      // When using this lens as a code lens there is no lens that will insert the rebound reference
+      // since the MemberRebindingIdentityLens is an ancestor of the HorizontalClassMergerGraphLens.
+      // We therefore use the reference itself as the rebound reference here, which is safe since
+      // the code objects created during horizontal class merging are guaranteed not to contain
+      // any non-rebound method references.
+      // TODO(b/315284255): Actually guarantee the above!
+      MethodLookupResult lookupResult =
+          MethodLookupResult.builder(this, codeLens)
+              .setReboundReference(reference)
+              .setReference(reference)
+              .setType(type)
+              .build();
+      return continuation.lookupMethod(lookupResult);
+    }
+    return super.internalLookupMethod(reference, context, type, codeLens, continuation);
+  }
+
   /**
    * If an overloaded constructor is requested, add the constructor id as a parameter to the
    * constructor. Otherwise return the lookup on the underlying graph lens.
@@ -74,12 +107,18 @@
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    List<ExtraParameter> extraParameters = methodExtraParameters.get(previous.getReference());
+    if (!previous.hasReboundReference()) {
+      return super.internalDescribeLookupMethod(previous, context, codeLens);
+    }
+    assert previous.hasReboundReference();
+    List<ExtraParameter> extraParameters =
+        methodExtraParameters.get(previous.getReboundReference());
     MethodLookupResult lookup = super.internalDescribeLookupMethod(previous, context, codeLens);
     if (extraParameters == null) {
       return lookup;
     }
-    return MethodLookupResult.builder(this)
+    return MethodLookupResult.builder(this, codeLens)
+        .setReboundReference(lookup.getReboundReference())
         .setReference(lookup.getReference())
         .setPrototypeChanges(lookup.getPrototypeChanges().withExtraParameters(extraParameters))
         .setType(lookup.getType())
@@ -87,24 +126,24 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
     FieldLookupResult lookup = super.internalDescribeLookupField(previous);
-    if (lookup.getReference() == previous.getReference()) {
+    if (lookup.getReference().isIdenticalTo(previous.getReference())) {
       return lookup;
     }
     return FieldLookupResult.builder(this)
         .setReference(lookup.getReference())
         .setReboundReference(lookup.getReboundReference())
         .setReadCastType(
-            lookup.getReference().getType() != previous.getReference().getType()
+            lookup.getReference().getType().isNotIdenticalTo(previous.getReference().getType())
                 ? lookupType(previous.getReference().getType())
                 : null)
         .setWriteCastType(previous.getRewrittenWriteCastType(this::getNextClassType))
         .build();
   }
 
-  public static class Builder {
+  public static class Builder
+      extends BuilderBase<HorizontalClassMergerGraphLens, HorizontallyMergedClasses> {
 
     private final MutableBidirectionalManyToOneRepresentativeMap<DexField, DexField>
         newFieldSignatures = BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
@@ -126,7 +165,8 @@
 
     Builder() {}
 
-    HorizontalClassMergerGraphLens build(
+    @Override
+    public HorizontalClassMergerGraphLens build(
         AppView<?> appView, HorizontallyMergedClasses mergedClasses) {
       assert pendingMethodMapUpdates.isEmpty();
       assert pendingNewFieldSignatureUpdates.isEmpty();
@@ -143,10 +183,15 @@
           mergedClasses,
           methodExtraParameters,
           newFieldSignatures,
-          methodMap.getForwardMap(),
+          methodMap,
           newMethodSignatures);
     }
 
+    @Override
+    public Set<DexMethod> getOriginalMethodReferences(DexMethod method) {
+      return methodMap.getKeys(method);
+    }
+
     DexMethod getRenamedMethodSignature(DexMethod method) {
       assert newMethodSignatures.containsKey(method);
       return newMethodSignatures.get(method);
@@ -156,13 +201,12 @@
       newFieldSignatures.put(oldFieldSignature, newFieldSignature);
     }
 
-    @SuppressWarnings("ReferenceEquality")
     void recordNewFieldSignature(
         Iterable<DexField> oldFieldSignatures,
         DexField newFieldSignature,
         DexField representative) {
       assert Streams.stream(oldFieldSignatures)
-          .anyMatch(oldFieldSignature -> oldFieldSignature != newFieldSignature);
+          .anyMatch(oldFieldSignature -> oldFieldSignature.isNotIdenticalTo(newFieldSignature));
       assert Streams.stream(oldFieldSignatures).noneMatch(newFieldSignatures::containsValue);
       assert Iterables.contains(oldFieldSignatures, representative);
       for (DexField oldFieldSignature : oldFieldSignatures) {
@@ -171,7 +215,8 @@
       newFieldSignatures.setRepresentative(newFieldSignature, representative);
     }
 
-    void fixupField(DexField oldFieldSignature, DexField newFieldSignature) {
+    @Override
+    public void fixupField(DexField oldFieldSignature, DexField newFieldSignature) {
       fixupOriginalMemberSignatures(
           oldFieldSignature,
           newFieldSignature,
@@ -216,7 +261,8 @@
       }
     }
 
-    void fixupMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+    @Override
+    public void fixupMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
       fixupMethodMap(oldMethodSignature, newMethodSignature);
       fixupOriginalMemberSignatures(
           oldMethodSignature,
@@ -230,9 +276,7 @@
       if (originalMethodSignatures.isEmpty()) {
         pendingMethodMapUpdates.put(oldMethodSignature, newMethodSignature);
       } else {
-        for (DexMethod originalMethodSignature : originalMethodSignatures) {
-          pendingMethodMapUpdates.put(originalMethodSignature, newMethodSignature);
-        }
+        pendingMethodMapUpdates.put(originalMethodSignatures, newMethodSignature);
       }
     }
 
@@ -246,9 +290,7 @@
       if (oldMemberSignatures.isEmpty()) {
         pendingNewMemberSignatureUpdates.put(oldMemberSignature, newMemberSignature);
       } else {
-        for (R originalMethodSignature : oldMemberSignatures) {
-          pendingNewMemberSignatureUpdates.put(originalMethodSignature, newMemberSignature);
-        }
+        pendingNewMemberSignatureUpdates.put(oldMemberSignatures, newMemberSignature);
         R representative = newMemberSignatures.getRepresentativeKey(oldMemberSignature);
         if (representative != null) {
           pendingNewMemberSignatureUpdates.setRepresentative(newMemberSignature, representative);
@@ -256,7 +298,8 @@
       }
     }
 
-    void commitPendingUpdates() {
+    @Override
+    public void commitPendingUpdates() {
       // Commit pending method map updates.
       methodMap.removeAll(pendingMethodMapUpdates.keySet());
       pendingMethodMapUpdates.forEachManyToOneMapping(methodMap::put);
@@ -283,17 +326,18 @@
      */
     void mapMergedConstructor(DexMethod from, DexMethod to, List<ExtraParameter> extraParameters) {
       mapMethod(from, to);
-      if (extraParameters.size() > 0) {
+      if (!extraParameters.isEmpty()) {
         methodExtraParameters.put(from, extraParameters);
       }
     }
 
-    void addExtraParameters(
-        DexMethod methodSignature, List<? extends ExtraParameter> extraParameters) {
-      Set<DexMethod> originalMethodSignatures = methodMap.getKeys(methodSignature);
+    @Override
+    public void addExtraParameters(
+        DexMethod from, DexMethod to, List<? extends ExtraParameter> extraParameters) {
+      Set<DexMethod> originalMethodSignatures = methodMap.getKeys(from);
       if (originalMethodSignatures.isEmpty()) {
         methodExtraParameters
-            .computeIfAbsent(methodSignature, ignore -> new ArrayList<>(extraParameters.size()))
+            .computeIfAbsent(from, ignore -> new ArrayList<>(extraParameters.size()))
             .addAll(extraParameters);
       } else {
         for (DexMethod originalMethodSignature : originalMethodSignatures) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java
new file mode 100644
index 0000000..fb27321
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.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.horizontalclassmerging;
+
+import com.android.tools.r8.classmerging.ClassMergerTreeFixer;
+import com.android.tools.r8.classmerging.SyntheticArgumentClass;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
+import com.android.tools.r8.utils.Timing;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * The tree fixer traverses all program classes and finds and fixes references to old classes which
+ * have been remapped to new classes by the class merger. While doing so, all updated changes are
+ * tracked in {@link HorizontalClassMergerTreeFixer#lensBuilder}.
+ */
+class HorizontalClassMergerTreeFixer
+    extends ClassMergerTreeFixer<
+        HorizontalClassMergerGraphLens.Builder,
+        HorizontalClassMergerGraphLens,
+        HorizontallyMergedClasses> {
+
+  private final Mode mode;
+
+  public HorizontalClassMergerTreeFixer(
+      AppView<?> appView,
+      HorizontallyMergedClasses mergedClasses,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      Mode mode,
+      ProfileCollectionAdditions profileCollectionAdditions,
+      SyntheticArgumentClass syntheticArgumentClass) {
+    super(appView, lensBuilder, mergedClasses, profileCollectionAdditions, syntheticArgumentClass);
+    this.mode = mode;
+  }
+
+  /**
+   * Lets assume the following initial classes, where the class B should be merged into A: <code>
+   *   class A {
+   *     public A(A a) { ... }
+   *     public A(A a, int v) { ... }
+   *     public A(B b) { ... }
+   *     public A(B b, int v) { ... }
+   *   }
+   *
+   *   class B {
+   *     public B(A a) { ... }
+   *     public B(B b) { ... }
+   *   }
+   * </code>
+   *
+   * <p>The {@link ClassMerger} merges the constructors {@code A.<init>(B)} and {@code B.<init>(B)}
+   * into the constructor {@code A.<init>(B, int)} to prevent any collisions when merging the
+   * constructor into A. The extra integer argument determines which class' constructor is called.
+   * The SynthArg is used to prevent a collision with the existing {@code A.<init>(B, int)}
+   * constructor. All constructors {@code A.<init>(A, ...)} generate a constructor {@code
+   * A.<init>(A, int, SynthClass)} but are otherwise ignored. During ClassMerging the constructor
+   * produces the following mappings in the graph lens builder:
+   *
+   * <ul>
+   *   <li>{@code B.<init>(B) <--> A.<init>(B, int, SynthArg)}
+   *   <li>{@code A.<init>(B) <--> A.<init>(B, int, SynthArg)} (This mapping is representative)
+   *   <li>{@code A.constructor$B(B) ---> A.constructor$B(B)}
+   *   <li>{@code B.<init>(B) <--- A.constructor$B(B)}
+   * </ul>
+   *
+   * <p>Note: The identity mapping is needed so that the method is remapped in the forward direction
+   * if there are changes in the tree fixer. Otherwise, methods are only remapped in directions they
+   * are already mapped in.
+   *
+   * <p>During the fixup, all type references to B are changed into A. This causes a collision
+   * between {@code A.<init>(A, int, SynthClass)} and {@code A.<init>(B, int, SynthClass)}. This
+   * collision should be fixed by adding an extra argument to {@code A.<init>(B, int, SynthClass)}.
+   * The TreeFixer generates the following mapping of renamed methods:
+   *
+   * <ul>
+   *   <li>{@code A.<init>(B, int, SynthArg) <--> A.<init>(A, int, SynthArg, ExtraArg)}
+   *   <li>{@code A.constructor$B(B) <--> A.constructor$B(A)}
+   * </ul>
+   *
+   * <p>This rewrites the previous method mappings to:
+   *
+   * <ul>
+   *   <li>{@code B.<init>(B) <--- A.constructor$B(A)}
+   *   <li>{@code A.constructor$B(B) ---> A.constructor$B(A)}
+   *   <li>{@code B.<init>(B) <--> A.<init>(A, int, SynthArg, ExtraArg)}
+   *   <li>{@code A.<init>(B) <--> A.<init>(A, int, SynthArg, ExtraArg)} (including represents)
+   * </ul>
+   */
+  @Override
+  public HorizontalClassMergerGraphLens run(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    return super.run(executorService, timing);
+  }
+
+  @Override
+  public boolean isRunningBeforePrimaryOptimizationPass() {
+    return mode.isInitial();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index 11313e4..f832f3a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -43,7 +42,8 @@
     Builder builder = builder();
     forEachMergeGroup(
         (sources, target) -> {
-          DexType rewrittenTarget = newHorizontallyMergedClasses.getMergeTargetOrDefault(target);
+          DexType rewrittenTarget =
+              newHorizontallyMergedClasses.getMergeTargetOrDefault(target, target);
           sources.forEach(source -> builder.add(source, rewrittenTarget));
         });
     newHorizontallyMergedClasses.forEachMergeGroup(
@@ -56,8 +56,9 @@
     mergedClasses.forEachManyToOneMapping(consumer);
   }
 
-  public DexType getMergeTargetOrDefault(DexType type) {
-    return mergedClasses.getOrDefault(type, type);
+  @Override
+  public DexType getMergeTargetOrDefault(DexType type, DexType defaultValue) {
+    return mergedClasses.getOrDefault(type, defaultValue);
   }
 
   public Set<DexType> getSources() {
@@ -72,32 +73,24 @@
     return mergedClasses.values();
   }
 
-  @Override
-  public boolean hasBeenMergedIntoDifferentType(DexType type) {
-    return mergedClasses.containsKey(type);
-  }
-
   public boolean isEmpty() {
     return mergedClasses.isEmpty();
   }
 
   @Override
+  public boolean isMergeSource(DexType type) {
+    return mergedClasses.containsKey(type);
+  }
+
+  @Override
   public boolean isMergeTarget(DexType type) {
     return mergedClasses.containsValue(type);
   }
 
-  public boolean hasBeenMergedOrIsMergeTarget(DexType type) {
-    return this.hasBeenMergedIntoDifferentType(type) || isMergeTarget(type);
-  }
-
   BidirectionalManyToOneRepresentativeMap<DexType, DexType> getBidirectionalMap() {
     return mergedClasses;
   }
 
-  Map<DexType, DexType> getForwardMap() {
-    return mergedClasses.getForwardMap();
-  }
-
   @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
     for (DexType source : mergedClasses.keySet()) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteMergedInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteMergedInstanceInitializerCode.java
index 837157e..e09f988 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteMergedInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteMergedInstanceInitializerCode.java
@@ -102,9 +102,7 @@
       int classIdLocalIndex = maxLocals - 1 - extraNulls;
       instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
       instructionBuilder.add(new CfLoad(ValueType.INT, classIdLocalIndex));
-      instructionBuilder.add(
-          new CfInstanceFieldWrite(
-              lens.getRenamedFieldSignature(classIdField, lens.getPrevious())));
+      instructionBuilder.add(new CfInstanceFieldWrite(lens.getNextFieldSignature(classIdField)));
       maxStack.set(2);
     }
 
@@ -123,7 +121,8 @@
     instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
 
     // Load constructor arguments.
-    MethodLookupResult parentConstructorLookup = lens.lookupInvokeDirect(parentConstructor, method);
+    MethodLookupResult parentConstructorLookup =
+        lens.lookupInvokeDirect(parentConstructor, method, appView.codeLens());
 
     int i = 0;
     for (InstanceFieldInitializationInfo initializationInfo : parentConstructorArguments) {
@@ -195,7 +194,7 @@
           int stackSizeForInitializationInfo =
               addCfInstructionsForInitializationInfo(
                   instructionBuilder, initializationInfo, argumentToLocalIndex, field.getType());
-          DexField rewrittenField = lens.getRenamedFieldSignature(field, lens.getPrevious());
+          DexField rewrittenField = lens.getNextFieldSignature(field);
 
           // Insert a check to ensure the program continues to type check according to Java type
           // checking. Otherwise, instance initializer merging may cause open interfaces. If
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteVirtuallyMergedMethodCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteVirtuallyMergedMethodCode.java
index 6f6a957..6698a71 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteVirtuallyMergedMethodCode.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/IncompleteVirtuallyMergedMethodCode.java
@@ -141,7 +141,7 @@
           lens.lookupInvokeSuper(superMethod.getReboundReference(), method).getReference();
       fallthroughTarget =
           reboundFallthroughTarget.withHolder(
-              lens.lookupClassType(superMethod.getReference().getHolderType()),
+              lens.getNextClassType(superMethod.getReference().getHolderType()),
               appView.dexItemFactory());
     }
     instructions.add(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
index 29db86f..67022ed 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
@@ -73,6 +74,7 @@
     InstanceInitializerDescription.Builder builder =
         InstanceInitializerDescription.builder(appView, instanceInitializer);
     IRCode code = codeProvider.buildIR(instanceInitializer);
+    GraphLens codeLens = instanceInitializer.getDefinition().getCode().getCodeLens(appView);
     WorkList<BasicBlock> workList = WorkList.newIdentityWorkList(code.entryBlock());
     while (workList.hasNext()) {
       BasicBlock block = workList.next();
@@ -105,7 +107,7 @@
               // Check that this writes a field on the enclosing class.
               DexField fieldReference = instancePut.getField();
               DexField lensRewrittenFieldReference =
-                  appView.graphLens().lookupField(fieldReference);
+                  appView.graphLens().lookupField(fieldReference, codeLens);
               if (lensRewrittenFieldReference.getHolderType()
                   != instanceInitializer.getHolderType()) {
                 return invalid();
@@ -143,7 +145,7 @@
               DexMethod lensRewrittenInvokedMethod =
                   appView
                       .graphLens()
-                      .lookupInvokeDirect(invokedMethod, instanceInitializer)
+                      .lookupInvokeDirect(invokedMethod, instanceInitializer, codeLens)
                       .getReference();
 
               // TODO(b/189296638): Consider allowing constructor forwarding.
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
index 38b47fc..54f4898 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
 
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 8d63fb1..da96bcf 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -60,6 +60,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
 import com.android.tools.r8.horizontalclassmerging.policies.SamePackageForNonGlobalMergeSynthetic;
 import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
+import com.android.tools.r8.horizontalclassmerging.policies.SameStartupPartition;
 import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.VerifyMultiClassPolicyAlwaysSatisfied;
 import com.android.tools.r8.horizontalclassmerging.policies.VerifySingleClassPolicyAlwaysSatisfied;
@@ -289,6 +290,7 @@
         new CheckAbstractClasses(appView),
         new NoClassAnnotationCollisions(),
         new SameFeatureSplit(appView),
+        new SameStartupPartition(appView),
         new SameInstanceFields(appView, mode),
         new SameMainDexGroup(appView),
         new SameNestHost(appView),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
deleted file mode 100644
index 538c0d3..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ /dev/null
@@ -1,501 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging;
-
-import com.android.tools.r8.errors.Unreachable;
-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.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.DexMethodSignature;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.graph.EnclosingMethodAttribute;
-import com.android.tools.r8.graph.fixup.TreeFixerBase;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
-import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
-import com.android.tools.r8.shaking.AnnotationFixer;
-import com.android.tools.r8.utils.ArrayUtils;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.OptionalBool;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import java.util.Collection;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * The tree fixer traverses all program classes and finds and fixes references to old classes which
- * have been remapped to new classes by the class merger. While doing so, all updated changes are
- * tracked in {@link TreeFixer#lensBuilder}.
- */
-class TreeFixer extends TreeFixerBase {
-
-  private final AppView<?> appView;
-  private final HorizontallyMergedClasses mergedClasses;
-  private final Mode mode;
-  private final HorizontalClassMergerGraphLens.Builder lensBuilder;
-  private final DexItemFactory dexItemFactory;
-  private final ProfileCollectionAdditions profileCollectionAdditions;
-  private final SyntheticArgumentClass syntheticArgumentClass;
-
-  private final Map<DexProgramClass, DexType> originalSuperTypes = new IdentityHashMap<>();
-  private final BiMap<DexMethodSignature, DexMethodSignature> reservedInterfaceSignatures =
-      HashBiMap.create();
-
-  public TreeFixer(
-      AppView<?> appView,
-      HorizontallyMergedClasses mergedClasses,
-      HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode,
-      ProfileCollectionAdditions profileCollectionAdditions,
-      SyntheticArgumentClass syntheticArgumentClass) {
-    super(appView);
-    this.appView = appView;
-    this.mergedClasses = mergedClasses;
-    this.mode = mode;
-    this.lensBuilder = lensBuilder;
-    this.profileCollectionAdditions = profileCollectionAdditions;
-    this.syntheticArgumentClass = syntheticArgumentClass;
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  /**
-   * Lets assume the following initial classes, where the class B should be merged into A: <code>
-   *   class A {
-   *     public A(A a) { ... }
-   *     public A(A a, int v) { ... }
-   *     public A(B b) { ... }
-   *     public A(B b, int v) { ... }
-   *   }
-   *
-   *   class B {
-   *     public B(A a) { ... }
-   *     public B(B b) { ... }
-   *   }
-   * </code>
-   *
-   * <p>The {@link ClassMerger} merges the constructors {@code A.<init>(B)} and {@code B.<init>(B)}
-   * into the constructor {@code A.<init>(B, int)} to prevent any collisions when merging the
-   * constructor into A. The extra integer argument determines which class' constructor is called.
-   * The SynthArg is used to prevent a collision with the existing {@code A.<init>(B, int)}
-   * constructor. All constructors {@code A.<init>(A, ...)} generate a constructor {@code
-   * A.<init>(A, int, SynthClass)} but are otherwise ignored. During ClassMerging the constructor
-   * produces the following mappings in the graph lens builder:
-   *
-   * <ul>
-   *   <li>{@code B.<init>(B) <--> A.<init>(B, int, SynthArg)}
-   *   <li>{@code A.<init>(B) <--> A.<init>(B, int, SynthArg)} (This mapping is representative)
-   *   <li>{@code A.constructor$B(B) ---> A.constructor$B(B)}
-   *   <li>{@code B.<init>(B) <--- A.constructor$B(B)}
-   * </ul>
-   *
-   * <p>Note: The identity mapping is needed so that the method is remapped in the forward direction
-   * if there are changes in the tree fixer. Otherwise, methods are only remapped in directions they
-   * are already mapped in.
-   *
-   * <p>During the fixup, all type references to B are changed into A. This causes a collision
-   * between {@code A.<init>(A, int, SynthClass)} and {@code A.<init>(B, int, SynthClass)}. This
-   * collision should be fixed by adding an extra argument to {@code A.<init>(B, int, SynthClass)}.
-   * The TreeFixer generates the following mapping of renamed methods:
-   *
-   * <ul>
-   *   <li>{@code A.<init>(B, int, SynthArg) <--> A.<init>(A, int, SynthArg, ExtraArg)}
-   *   <li>{@code A.constructor$B(B) <--> A.constructor$B(A)}
-   * </ul>
-   *
-   * <p>This rewrites the previous method mappings to:
-   *
-   * <ul>
-   *   <li>{@code B.<init>(B) <--- A.constructor$B(A)}
-   *   <li>{@code A.constructor$B(B) ---> A.constructor$B(A)}
-   *   <li>{@code B.<init>(B) <--> A.<init>(A, int, SynthArg, ExtraArg)}
-   *   <li>{@code A.<init>(B) <--> A.<init>(A, int, SynthArg, ExtraArg)} (including represents)
-   * </ul>
-   */
-  public HorizontalClassMergerGraphLens fixupTypeReferences() {
-    HorizontalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
-    if (appView.enableWholeProgramOptimizations()) {
-      Collection<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
-      Iterables.filter(classes, DexProgramClass::isInterface).forEach(this::fixupInterfaceClass);
-      classes.forEach(this::fixupAttributes);
-      classes.forEach(this::fixupProgramClassSuperTypes);
-      SubtypingForrestForClasses subtypingForrest =
-          new SubtypingForrestForClasses(appView.withClassHierarchy());
-      // TODO(b/170078037): parallelize this code segment.
-      for (DexProgramClass root : subtypingForrest.getProgramRoots()) {
-        subtypingForrest.traverseNodeDepthFirst(root, HashBiMap.create(), this::fixupProgramClass);
-      }
-      new AnnotationFixer(lens, appView.graphLens()).run(appView.appInfo().classes());
-    }
-    return lens;
-  }
-
-  private void fixupAttributes(DexProgramClass clazz) {
-    if (clazz.hasEnclosingMethodAttribute()) {
-      EnclosingMethodAttribute enclosingMethodAttribute = clazz.getEnclosingMethodAttribute();
-      if (mergedClasses.hasBeenMergedIntoDifferentType(
-          enclosingMethodAttribute.getEnclosingType())) {
-        clazz.clearEnclosingMethodAttribute();
-      } else {
-        clazz.setEnclosingMethodAttribute(fixupEnclosingMethodAttribute(enclosingMethodAttribute));
-      }
-    }
-    clazz.setInnerClasses(fixupInnerClassAttributes(clazz.getInnerClasses()));
-    clazz.setNestHostAttribute(fixupNestHost(clazz.getNestHostClassAttribute()));
-    clazz.setNestMemberAttributes(fixupNestMemberAttributes(clazz.getNestMembersClassAttributes()));
-    clazz.setPermittedSubclassAttributes(
-        fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private void fixupProgramClassSuperTypes(DexProgramClass clazz) {
-    DexType rewrittenSuperType = fixupType(clazz.getSuperType());
-    if (rewrittenSuperType != clazz.getSuperType()) {
-      originalSuperTypes.put(clazz, clazz.getSuperType());
-      clazz.superType = rewrittenSuperType;
-    }
-    clazz.setInterfaces(fixupInterfaces(clazz, clazz.getInterfaces()));
-  }
-
-  private BiMap<DexMethodSignature, DexMethodSignature> fixupProgramClass(
-      DexProgramClass clazz, BiMap<DexMethodSignature, DexMethodSignature> remappedVirtualMethods) {
-    assert !clazz.isInterface();
-
-    // TODO(b/169395592): ensure merged classes have been removed using:
-    //   assert !mergedClasses.hasBeenMergedIntoDifferentType(clazz.type);
-
-    BiMap<DexMethodSignature, DexMethodSignature> remappedClassVirtualMethods =
-        HashBiMap.create(remappedVirtualMethods);
-
-    Set<DexMethodSignature> newMethodReferences = Sets.newHashSet();
-    clazz
-        .getMethodCollection()
-        .replaceAllVirtualMethods(
-            method -> fixupVirtualMethod(remappedClassVirtualMethods, newMethodReferences, method));
-    clazz
-        .getMethodCollection()
-        .replaceAllDirectMethods(method -> fixupDirectMethod(newMethodReferences, clazz, method));
-
-    Set<DexField> newFieldReferences = Sets.newIdentityHashSet();
-    DexEncodedField[] instanceFields = clazz.clearInstanceFields();
-    DexEncodedField[] staticFields = clazz.clearStaticFields();
-    clazz.setInstanceFields(fixupFields(instanceFields, newFieldReferences));
-    clazz.setStaticFields(fixupFields(staticFields, newFieldReferences));
-
-    lensBuilder.commitPendingUpdates();
-
-    return remappedClassVirtualMethods;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private DexEncodedMethod fixupVirtualInterfaceMethod(DexEncodedMethod method) {
-    DexMethod originalMethodReference = method.getReference();
-
-    // Don't process this method if it does not refer to a merge class type.
-    boolean referencesMergeClass =
-        Iterables.any(
-            originalMethodReference.getProto().getBaseTypes(dexItemFactory),
-            mergedClasses::hasBeenMergedOrIsMergeTarget);
-    if (!referencesMergeClass) {
-      return method;
-    }
-
-    DexMethodSignature originalMethodSignature = originalMethodReference.getSignature();
-    DexMethodSignature newMethodSignature =
-        reservedInterfaceSignatures.get(originalMethodSignature);
-
-    if (newMethodSignature == null) {
-      newMethodSignature = fixupMethodReference(originalMethodReference).getSignature();
-
-      // If the signature is already reserved by another interface, find a fresh one.
-      if (reservedInterfaceSignatures.containsValue(newMethodSignature)) {
-        DexString name =
-            dexItemFactory.createGloballyFreshMemberString(
-                originalMethodReference.getName().toSourceString());
-        newMethodSignature = newMethodSignature.withName(name);
-      }
-
-      assert !reservedInterfaceSignatures.containsValue(newMethodSignature);
-      reservedInterfaceSignatures.put(originalMethodSignature, newMethodSignature);
-    }
-
-    DexMethod newMethodReference =
-        newMethodSignature.withHolder(originalMethodReference, dexItemFactory);
-    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
-    return newMethodReference != originalMethodReference
-        ? method.toTypeSubstitutedMethodAsInlining(newMethodReference, dexItemFactory)
-        : method;
-  }
-
-  private void fixupInterfaceClass(DexProgramClass iface) {
-    Set<DexMethodSignature> newDirectMethods = new LinkedHashSet<>();
-    iface
-        .getMethodCollection()
-        .replaceDirectMethods(method -> fixupDirectMethod(newDirectMethods, iface, method));
-    iface.getMethodCollection().replaceVirtualMethods(this::fixupVirtualInterfaceMethod);
-
-    assert !iface.hasInstanceFields();
-
-    Set<DexField> newFieldReferences = Sets.newIdentityHashSet();
-    DexEncodedField[] staticFields = iface.clearStaticFields();
-    iface.setStaticFields(fixupFields(staticFields, newFieldReferences));
-
-    lensBuilder.commitPendingUpdates();
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private DexTypeList fixupInterfaces(DexProgramClass clazz, DexTypeList interfaceTypes) {
-    Set<DexType> seen = Sets.newIdentityHashSet();
-    return interfaceTypes.map(
-        interfaceType -> {
-          DexType rewrittenInterfaceType = mapClassType(interfaceType);
-          assert rewrittenInterfaceType != clazz.getType();
-          return seen.add(rewrittenInterfaceType) ? rewrittenInterfaceType : null;
-        });
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private DexEncodedMethod fixupProgramMethod(
-      DexMethod newMethodReference, DexEncodedMethod method) {
-    DexMethod originalMethodReference = method.getReference();
-    if (newMethodReference == originalMethodReference) {
-      return method;
-    }
-
-    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
-
-    DexEncodedMethod newMethod =
-        method.toTypeSubstitutedMethodAsInlining(newMethodReference, dexItemFactory);
-    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 DexEncodedMethod fixupDirectMethod(
-      Set<DexMethodSignature> newMethods, DexProgramClass clazz, DexEncodedMethod method) {
-    DexMethod originalMethodReference = method.getReference();
-
-    // Fix all type references in the method prototype.
-    DexMethod newMethodReference = fixupMethodReference(originalMethodReference);
-
-    if (newMethods.contains(newMethodReference.getSignature())) {
-      // If the method collides with a direct method on the same class then rename it to a globally
-      // fresh name and record the signature.
-
-      if (method.isInstanceInitializer()) {
-        // If the method is an instance initializer, then add extra nulls.
-        Box<Set<DexType>> usedSyntheticArgumentClasses = new Box<>();
-        newMethodReference =
-            dexItemFactory.createInstanceInitializerWithFreshProto(
-                newMethodReference,
-                syntheticArgumentClass.getArgumentClasses(),
-                tryMethod -> !newMethods.contains(tryMethod.getSignature()),
-                usedSyntheticArgumentClasses::set);
-        lensBuilder.addExtraParameters(
-            originalMethodReference,
-            ExtraUnusedNullParameter.computeExtraUnusedNullParameters(
-                originalMethodReference, newMethodReference));
-
-        // Amend the art profile collection.
-        if (usedSyntheticArgumentClasses.isSet()) {
-          Set<DexMethod> previousMethodReferences =
-              lensBuilder.methodMap.getKeys(originalMethodReference);
-          if (previousMethodReferences.isEmpty()) {
-            profileCollectionAdditions.applyIfContextIsInProfile(
-                originalMethodReference,
-                additionsBuilder ->
-                    usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
-          } else {
-            for (DexMethod previousMethodReference : previousMethodReferences) {
-              profileCollectionAdditions.applyIfContextIsInProfile(
-                  previousMethodReference,
-                  additionsBuilder ->
-                      usedSyntheticArgumentClasses.get().forEach(additionsBuilder::addRule));
-            }
-          }
-        }
-      } else {
-        newMethodReference =
-            dexItemFactory.createFreshMethodNameWithoutHolder(
-                newMethodReference.getName().toSourceString(),
-                newMethodReference.proto,
-                newMethodReference.holder,
-                tryMethod ->
-                    !reservedInterfaceSignatures.containsValue(tryMethod.getSignature())
-                        && !newMethods.contains(tryMethod.getSignature()));
-      }
-    }
-
-    boolean changed = newMethods.add(newMethodReference.getSignature());
-    assert changed;
-
-    // Convert out of DefaultInstanceInitializerCode, since this piece of code will require lens
-    // code rewriting.
-    if (mode.isInitial()
-        && method.hasCode()
-        && method.getCode().isDefaultInstanceInitializerCode()
-        && mergedClasses.hasBeenMergedOrIsMergeTarget(clazz.getSuperType())) {
-      DexType originalSuperType = originalSuperTypes.getOrDefault(clazz, clazz.getSuperType());
-      DefaultInstanceInitializerCode.uncanonicalizeCode(
-          appView, method.asProgramMethod(clazz), originalSuperType);
-    }
-
-    return fixupProgramMethod(newMethodReference, method);
-  }
-
-  private DexMethodSignature lookupReservedVirtualName(
-      DexMethod originalMethodReference,
-      BiMap<DexMethodSignature, DexMethodSignature> renamedClassVirtualMethods) {
-    DexMethodSignature originalSignature = originalMethodReference.getSignature();
-
-    // Determine if the original method has been rewritten by a parent class
-    DexMethodSignature renamedVirtualName = renamedClassVirtualMethods.get(originalSignature);
-
-    if (renamedVirtualName == null) {
-      // Determine if there is a signature mapping.
-      DexMethodSignature mappedInterfaceSignature =
-          reservedInterfaceSignatures.get(originalSignature);
-      if (mappedInterfaceSignature != null) {
-        renamedVirtualName = mappedInterfaceSignature;
-      }
-    } else {
-      assert !reservedInterfaceSignatures.containsKey(originalSignature);
-    }
-
-    return renamedVirtualName;
-  }
-
-  private DexEncodedMethod fixupVirtualMethod(
-      BiMap<DexMethodSignature, DexMethodSignature> renamedClassVirtualMethods,
-      Set<DexMethodSignature> newMethods,
-      DexEncodedMethod method) {
-    DexMethod originalMethodReference = method.getReference();
-    DexMethodSignature originalSignature = originalMethodReference.getSignature();
-
-    DexMethodSignature renamedVirtualName =
-        lookupReservedVirtualName(originalMethodReference, renamedClassVirtualMethods);
-
-    // Fix all type references in the method prototype.
-    DexMethodSignature newSignature = fixupMethodSignature(originalSignature);
-
-    if (renamedVirtualName != null) {
-      // If the method was renamed in a parent, rename it in the child.
-      newSignature = renamedVirtualName;
-
-      assert !newMethods.contains(newSignature);
-    } else if (reservedInterfaceSignatures.containsValue(newSignature)
-        || newMethods.contains(newSignature)
-        || renamedClassVirtualMethods.containsValue(newSignature)) {
-      // If the method potentially collides with an interface method or with another virtual method
-      // rename it to a globally fresh name and record the name.
-
-      newSignature =
-          dexItemFactory.createFreshMethodSignatureName(
-              originalMethodReference.getName().toSourceString(),
-              null,
-              newSignature.getProto(),
-              trySignature ->
-                  !reservedInterfaceSignatures.containsValue(trySignature)
-                      && !newMethods.contains(trySignature)
-                      && !renamedClassVirtualMethods.containsValue(trySignature));
-
-      // Record signature renaming so that subclasses perform the identical rename.
-      renamedClassVirtualMethods.put(originalSignature, newSignature);
-    } else {
-      // There was no reserved name and the new signature is available.
-
-      if (Iterables.any(
-          newSignature.getProto().getParameterBaseTypes(dexItemFactory),
-          mergedClasses::isMergeTarget)) {
-        // If any of the parameter types have been merged, record the signature mapping.
-        renamedClassVirtualMethods.put(originalSignature, newSignature);
-      }
-    }
-
-    boolean changed = newMethods.add(newSignature);
-    assert changed;
-
-    DexMethod newMethodReference =
-        newSignature.withHolder(fixupType(originalMethodReference.holder), dexItemFactory);
-    return fixupProgramMethod(newMethodReference, method);
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private DexEncodedField[] fixupFields(
-      DexEncodedField[] fields, Set<DexField> newFieldReferences) {
-    if (fields == null || ArrayUtils.isEmpty(fields)) {
-      return DexEncodedField.EMPTY_ARRAY;
-    }
-
-    DexEncodedField[] newFields = new DexEncodedField[fields.length];
-    for (int i = 0; i < fields.length; i++) {
-      DexEncodedField oldField = fields[i];
-      DexField oldFieldReference = oldField.getReference();
-      DexField newFieldReference = fixupFieldReference(oldFieldReference);
-
-      // Rename the field if it already exists.
-      if (!newFieldReferences.add(newFieldReference)) {
-        DexField template = newFieldReference;
-        newFieldReference =
-            dexItemFactory.createFreshMember(
-                tryName ->
-                    Optional.of(template.withName(tryName, dexItemFactory))
-                        .filter(tryMethod -> !newFieldReferences.contains(tryMethod)),
-                newFieldReference.name.toSourceString());
-        boolean added = newFieldReferences.add(newFieldReference);
-        assert added;
-      }
-
-      if (newFieldReference != oldFieldReference) {
-        lensBuilder.fixupField(oldFieldReference, newFieldReference);
-        newFields[i] = oldField.toTypeSubstitutedField(appView, newFieldReference);
-      } else {
-        newFields[i] = oldField;
-      }
-    }
-
-    return newFields;
-  }
-
-  @Override
-  public DexType mapClassType(DexType type) {
-    return mergedClasses.getMergeTargetOrDefault(type);
-  }
-
-  @Override
-  public void recordClassChange(DexType from, DexType to) {
-    // Classes are not changed but in-place updated.
-    throw new Unreachable();
-  }
-
-  @Override
-  public void recordFieldChange(DexField from, DexField to) {
-    // Fields are manually changed in 'fixupFields' above.
-    throw new Unreachable();
-  }
-
-  @Override
-  public void recordMethodChange(DexMethod from, DexMethod to) {
-    // Methods are manually changed in 'fixupMethods' above.
-    throw new Unreachable();
-  }
-}
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 5416af5..bb60289 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
@@ -19,6 +19,7 @@
 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.lens.GraphLens;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.deadlock.SingleCallerInformation;
@@ -408,8 +409,11 @@
 
     class TracerUseRegistry extends DefaultUseRegistry<ProgramMethod> {
 
+      private final GraphLens codeLens;
+
       TracerUseRegistry(ProgramMethod context) {
         super(appView(), context);
+        this.codeLens = context.getDefinition().getCode().getCodeLens(appView);
       }
 
       private void fail() {
@@ -474,14 +478,14 @@
 
       @Override
       public void registerInitClass(DexType type) {
-        DexType rewrittenType = appView.graphLens().lookupType(type);
+        DexType rewrittenType = appView.graphLens().lookupType(type, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenType);
       }
 
       @Override
       public void registerInvokeDirect(DexMethod method) {
         DexMethod rewrittenMethod =
-            appView.graphLens().lookupInvokeDirect(method, getContext()).getReference();
+            appView.graphLens().lookupInvokeDirect(method, getContext(), codeLens).getReference();
         MethodResolutionResult resolutionResult =
             appView().appInfo().resolveMethodOnClassHolderLegacy(rewrittenMethod);
         if (resolutionResult.isSingleResolution()
@@ -493,7 +497,10 @@
       @Override
       public void registerInvokeInterface(DexMethod method) {
         DexMethod rewrittenMethod =
-            appView.graphLens().lookupInvokeInterface(method, getContext()).getReference();
+            appView
+                .graphLens()
+                .lookupInvokeInterface(method, getContext(), codeLens)
+                .getReference();
         DexClassAndMethod resolvedMethod =
             appView()
                 .appInfo()
@@ -507,7 +514,7 @@
       @Override
       public void registerInvokeStatic(DexMethod method) {
         DexMethod rewrittenMethod =
-            appView.graphLens().lookupInvokeStatic(method, getContext()).getReference();
+            appView.graphLens().lookupInvokeStatic(method, getContext(), codeLens).getReference();
         ProgramMethod resolvedMethod =
             appView()
                 .appInfo()
@@ -522,7 +529,7 @@
       @Override
       public void registerInvokeSuper(DexMethod method) {
         DexMethod rewrittenMethod =
-            appView.graphLens().lookupInvokeSuper(method, getContext()).getReference();
+            appView.graphLens().lookupInvokeSuper(method, getContext(), codeLens).getReference();
         ProgramMethod superTarget =
             asProgramMethodOrNull(
                 appView().appInfo().lookupSuperTarget(rewrittenMethod, getContext(), appView()));
@@ -534,7 +541,7 @@
       @Override
       public void registerInvokeVirtual(DexMethod method) {
         DexMethod rewrittenMethod =
-            appView.graphLens().lookupInvokeVirtual(method, getContext()).getReference();
+            appView.graphLens().lookupInvokeVirtual(method, getContext(), codeLens).getReference();
         DexClassAndMethod resolvedMethod =
             appView()
                 .appInfo()
@@ -551,19 +558,19 @@
 
       @Override
       public void registerNewInstance(DexType type) {
-        DexType rewrittenType = appView.graphLens().lookupType(type);
+        DexType rewrittenType = appView.graphLens().lookupType(type, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenType);
       }
 
       @Override
       public void registerStaticFieldRead(DexField field) {
-        DexField rewrittenField = appView.graphLens().lookupField(field);
+        DexField rewrittenField = appView.graphLens().lookupField(field, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
       }
 
       @Override
       public void registerStaticFieldWrite(DexField field) {
-        DexField rewrittenField = appView.graphLens().lookupField(field);
+        DexField rewrittenField = appView.graphLens().lookupField(field, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
       }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
index 361fdeb..872bf09 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
@@ -42,8 +42,8 @@
     if (code.isCfCode()) {
       CfCode cfCode = code.asCfCode();
       ConstraintWithTarget constraint =
-          cfCode.computeInliningConstraint(method, appView, appView.graphLens(), method);
-      return constraint == ConstraintWithTarget.NEVER;
+          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
+      return constraint.isNever();
     } else if (code.isDefaultInstanceInitializerCode()) {
       return false;
     } else {
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 12ef6dc..29a9147 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
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexDefinition;
 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.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
@@ -114,7 +115,8 @@
                 return TraversalContinuation.doBreak();
               }
               return TraversalContinuation.doContinue();
-            });
+            },
+            DexEncodedMethod::hasCode);
     return result.shouldBreak();
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameStartupPartition.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameStartupPartition.java
new file mode 100644
index 0000000..b04ed19
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameStartupPartition.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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.horizontalclassmerging.policies.SameStartupPartition.StartupPartition;
+import com.android.tools.r8.profile.startup.StartupOptions;
+import com.android.tools.r8.profile.startup.profile.StartupProfile;
+
+public class SameStartupPartition extends MultiClassSameReferencePolicy<StartupPartition> {
+
+  public enum StartupPartition {
+    STARTUP,
+    POST_STARTUP
+  }
+
+  private final StartupOptions startupOptions;
+  private final StartupProfile startupProfile;
+
+  public SameStartupPartition(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    this.startupOptions = appView.options().getStartupOptions();
+    this.startupProfile = appView.getStartupProfile();
+  }
+
+  @Override
+  public StartupPartition getMergeKey(DexProgramClass clazz) {
+    return startupProfile.isStartupClass(clazz.getType())
+        ? StartupPartition.STARTUP
+        : StartupPartition.POST_STARTUP;
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return startupProfile.isEmpty() || startupOptions.isStartupBoundaryOptimizationsEnabled();
+  }
+
+  @Override
+  public String getName() {
+    return "SameStartupPartition";
+  }
+}
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 9552f5f..112da4a 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
@@ -12,6 +12,7 @@
 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.lens.GraphLens;
 import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
@@ -73,7 +74,15 @@
 
     public Builder analyze(ExecutorService executorService) throws ExecutionException {
       ThreadUtils.processItems(
-          appView.appInfo()::forEachMethod,
+          consumer ->
+              appView
+                  .appInfo()
+                  .forEachMethod(
+                      method -> {
+                        if (method.getDefinition().hasCode()) {
+                          consumer.accept(method);
+                        }
+                      }),
           this::processMethod,
           appView.options().getThreadingModule(),
           executorService);
@@ -97,12 +106,14 @@
     private class InvokeExtractor extends DefaultUseRegistry<ProgramMethod> {
 
       private final AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierachy;
+      private final GraphLens codeLens;
 
       InvokeExtractor(
           AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierachy,
           ProgramMethod context) {
         super(appViewWithClassHierachy, context);
         this.appViewWithClassHierachy = appViewWithClassHierachy;
+        this.codeLens = context.getDefinition().getCode().getCodeLens(appViewWithClassHierachy);
       }
 
       @SuppressWarnings("ReferenceEquality")
@@ -185,7 +196,7 @@
 
       @Override
       public void registerInitClass(DexType type) {
-        DexType rewrittenType = appViewWithClassHierachy.graphLens().lookupType(type);
+        DexType rewrittenType = appViewWithClassHierachy.graphLens().lookupType(type, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenType);
       }
 
@@ -194,7 +205,7 @@
         DexMethod rewrittenMethod =
             appViewWithClassHierachy
                 .graphLens()
-                .lookupInvokeDirect(method, getContext())
+                .lookupInvokeDirect(method, getContext(), codeLens)
                 .getReference();
         DexProgramClass holder =
             rewrittenMethod.getHolderType().asProgramClass(appViewWithClassHierachy);
@@ -215,7 +226,7 @@
         DexMethod rewrittenMethod =
             appViewWithClassHierachy
                 .graphLens()
-                .lookupInvokeDirect(method, getContext())
+                .lookupInvokeStatic(method, getContext(), codeLens)
                 .getReference();
         ProgramMethod target =
             appViewWithClassHierachy
@@ -242,19 +253,19 @@
 
       @Override
       public void registerNewInstance(DexType type) {
-        DexType rewrittenType = appViewWithClassHierachy.graphLens().lookupType(type);
+        DexType rewrittenType = appViewWithClassHierachy.graphLens().lookupType(type, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenType);
       }
 
       @Override
       public void registerStaticFieldRead(DexField field) {
-        DexField rewrittenField = appViewWithClassHierachy.graphLens().lookupField(field);
+        DexField rewrittenField = appViewWithClassHierachy.graphLens().lookupField(field, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
       }
 
       @Override
       public void registerStaticFieldWrite(DexField field) {
-        DexField rewrittenField = appViewWithClassHierachy.graphLens().lookupField(field);
+        DexField rewrittenField = appViewWithClassHierachy.graphLens().lookupField(field, codeLens);
         triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 834c14c..0b46395 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -628,7 +628,7 @@
     for (Instruction instruction : instructions()) {
       if (instruction.outValue != null && instruction.outValue.getType().isClassType()) {
         ClassTypeElement classTypeLattice = instruction.outValue.getType().asClassType();
-        assert !mergedClasses.hasBeenMergedIntoDifferentType(classTypeLattice.getClassType())
+        assert !mergedClasses.isMergeSource(classTypeLattice.getClassType())
             : "Expected reference to "
                 + classTypeLattice.getClassType().getTypeName()
                 + " to be rewritten at instruction "
@@ -637,7 +637,7 @@
             .getInterfaces()
             .anyMatch(
                 (itf, isKnown) -> {
-                  assert !mergedClasses.hasBeenMergedIntoDifferentType(itf);
+                  assert !mergedClasses.isMergeSource(itf);
                   return false;
                 });
       }
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 4e015d0..8bedc64 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
@@ -99,7 +99,8 @@
           // Invoking a private super method within a nest must use invoke-direct. Invoking a
           // non-private super method within a nest must use invoke-super.
           MethodLookupResult lookupResult =
-              graphLens.lookupMethod(invokedMethod, context.getReference(), InvokeType.DIRECT);
+              graphLens.lookupMethod(
+                  invokedMethod, context.getReference(), InvokeType.DIRECT, codeLens);
           DexEncodedMethod definition = holderType.lookupMethod(lookupResult.getReference());
           return definition != null && definition.isPrivate()
               ? InvokeType.DIRECT
@@ -110,7 +111,7 @@
     }
 
     MethodLookupResult lookupResult =
-        graphLens.lookupMethod(invokedMethod, context.getReference(), InvokeType.DIRECT);
+        graphLens.lookupMethod(invokedMethod, context.getReference(), InvokeType.DIRECT, codeLens);
     if (lookupResult.getType().isStatic()) {
       // This method has been staticized. The original invoke-type is DIRECT.
       return InvokeType.DIRECT;
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 8b4cbce..e87faba 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
@@ -130,7 +130,7 @@
     // All the code has been processed so the rewriting required by the lenses is done everywhere,
     // we clear lens code rewriting so that the lens rewriter can be re-executed in phase 2 if new
     // lenses with code rewriting are added.
-    appView.clearCodeRewritings();
+    appView.clearCodeRewritings(executorService);
 
     // Commit synthetics from the primary optimization pass.
     commitPendingSyntheticItems(appView);
@@ -215,7 +215,7 @@
     // All the code that should be impacted by the lenses inserted between phase 1 and phase 2
     // have now been processed and rewritten, we clear code lens rewriting so that the class
     // staticizer and phase 3 does not perform again the rewriting.
-    appView.clearCodeRewritings();
+    appView.clearCodeRewritings(executorService);
 
     // Commit synthetics before creating a builder (otherwise the builder will not include the
     // synthetics.)
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
index 7e9cc29..0c356a2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
@@ -295,7 +295,6 @@
    * Matches call site for lambda metafactory invocation pattern and returns extracted match
    * information, or MATCH_FAILED if match failed.
    */
-  @SuppressWarnings("ReferenceEquality")
   static LambdaDescriptor infer(
       DexCallSite callSite,
       AppView<?> appView,
@@ -327,7 +326,10 @@
     DexValue.DexValueMethodType funcEnforcedSignature =
         getBootstrapArgument(callSite.bootstrapArgs, 2, DexValue.DexValueMethodType.class);
     if (!isEnforcedSignatureValid(
-        factory, funcEnforcedSignature.value, funcErasedSignature.value)) {
+        factory,
+        funcEnforcedSignature.value,
+        funcErasedSignature.value,
+        lambdaImplMethodHandle.asMethod())) {
       throw new Unreachable(
           "Enforced and erased signatures are inconsistent in " + callSite.toString());
     }
@@ -354,7 +356,7 @@
             mainFuncInterface,
             captures);
 
-    if (bootstrapMethod == factory.metafactoryMethod) {
+    if (factory.metafactoryMethod.isIdenticalTo(bootstrapMethod)) {
       if (callSite.bootstrapArgs.size() != 3) {
         throw new Unreachable(
             "Unexpected number of metafactory method arguments in " + callSite.toString());
@@ -439,8 +441,9 @@
   }
 
   private static boolean isEnforcedSignatureValid(
-      DexItemFactory factory, DexProto enforced, DexProto erased) {
-    if (!isSameOrDerived(factory, enforced.returnType, erased.returnType)) {
+      DexItemFactory factory, DexProto enforced, DexProto erased, DexMethod implMethod) {
+    if (!isSameOrDerivedReturnType(
+        factory, enforced.returnType, erased.returnType, implMethod.getReturnType())) {
       return false;
     }
     DexType[] enforcedValues = enforced.parameters.values;
@@ -457,12 +460,27 @@
     return true;
   }
 
-  @SuppressWarnings("ReferenceEquality")
+  static boolean isSameOrDerivedReturnType(
+      DexItemFactory factory, DexType subType, DexType superType, DexType actualReturnType) {
+    if (isSameOrDerived(factory, subType, superType)) {
+      return true;
+    }
+    // The linking invariants should apply to return types as stated in:
+    // https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html
+    // However, running examples on actual JVMs show they are not enforced for return types.
+    // It appears that a primitive type can appear in the interface type so long as the actual
+    // implementation method return type can be adapted to it.
+    if (subType.isPrimitiveType()) {
+      return LambdaMainMethodSourceCode.isSameOrAdaptableTo(actualReturnType, subType, factory);
+    }
+    return false;
+  }
+
   // Checks if the types are the same OR both types are reference types and
   // `subType` is derived from `b`. Note that in the latter case we only check if
   // both types are class types, for the reasons mentioned in isSameOrAdaptableTo(...).
   static boolean isSameOrDerived(DexItemFactory factory, DexType subType, DexType superType) {
-    if (subType == superType || (subType.isClassType() && superType.isClassType())) {
+    if (subType.isIdenticalTo(superType) || (subType.isClassType() && superType.isClassType())) {
       return true;
     }
 
@@ -472,7 +490,7 @@
         return isSameOrDerived(factory,
             subType.toArrayElementType(factory), superType.toArrayElementType(factory));
       }
-      return superType == factory.objectType; // T[] -> Object.
+      return superType.isIdenticalTo(factory.objectType); // T[] -> Object.
     }
 
     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index f13a1ea..df5b8b4 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -32,7 +32,6 @@
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring.DesugarInvoke;
 import com.android.tools.r8.utils.IntBox;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.Lists;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -87,10 +86,9 @@
     }
   }
 
-  @SuppressWarnings("ReferenceEquality")
   // Checks if the types are the same OR type `a` is adaptable to type `b`.
-  private static boolean isSameOrAdaptableTo(DexType a, DexType b, DexItemFactory factory) {
-    if (a == b) {
+  static boolean isSameOrAdaptableTo(DexType a, DexType b, DexItemFactory factory) {
+    if (a.isIdenticalTo(b)) {
       return true;
     }
 
@@ -98,12 +96,12 @@
       // Arrays are only adaptable to java.lang.Object or other arrays, note that we
       // don't check element type inheritance in the second case since we assume the
       // input code is verifiable.
-      return b == factory.objectType || b.isArrayType();
+      return b.isIdenticalTo(factory.objectType) || b.isArrayType();
     }
 
     if (b.isArrayType()) {
       // If A is typed object it can be convertible to an array type.
-      return a == factory.objectType;
+      return a.isIdenticalTo(factory.objectType);
     }
 
     if (a.isPrimitiveType()) {
@@ -113,19 +111,19 @@
 
       // `a` is primitive and `b` is a supertype of the boxed type `a`.
       DexType boxedPrimitiveType = getBoxedForPrimitiveType(a, factory);
-      if (b == boxedPrimitiveType ||
-          b == factory.objectType ||
-          b == factory.serializableType ||
-          b == factory.comparableType) {
+      if (b.isIdenticalTo(boxedPrimitiveType)
+          || b.isIdenticalTo(factory.objectType)
+          || b.isIdenticalTo(factory.serializableType)
+          || b.isIdenticalTo(factory.comparableType)) {
         return true;
       }
-      return boxedPrimitiveType != factory.boxedCharType
-          && boxedPrimitiveType != factory.boxedBooleanType
-          && b.descriptor == factory.boxedNumberDescriptor;
+      return boxedPrimitiveType.isNotIdenticalTo(factory.boxedCharType)
+          && boxedPrimitiveType.isNotIdenticalTo(factory.boxedBooleanType)
+          && b.descriptor.isIdenticalTo(factory.boxedNumberDescriptor);
     }
 
     if (b.isPrimitiveType()) {
-      if (a == factory.objectType) {
+      if (a.isIdenticalTo(factory.objectType)) {
         // `a` is java.lang.Object in which case we assume it represented by
         // proper boxed type.
         return true;
@@ -171,7 +169,6 @@
     }
   }
 
-  @SuppressWarnings({"BadImport", "ReferenceEquality"})
   public static CfCode build(
       LambdaClass lambda, DexMethod mainMethod, DesugarInvoke desugarInvoke) {
     DexItemFactory factory = lambda.appView.dexItemFactory();
@@ -188,10 +185,17 @@
     DexType erasedReturnType = lambda.descriptor.erasedProto.returnType;
     DexType[] enforcedParams = lambda.descriptor.enforcedProto.parameters.values;
     DexType enforcedReturnType = lambda.descriptor.enforcedProto.returnType;
+    if (enforcedReturnType.isPrimitiveType() && mainMethod.getReturnType().isReferenceType()) {
+      // It is unlikely but runtimes support the case where the return type is unboxed.
+      // In such cases, change the enforced return type to be the reference compatible boxed type.
+      assert LambdaDescriptor.isSameOrDerivedReturnType(
+          factory, enforcedReturnType, erasedReturnType, methodToCall.getReturnType());
+      enforcedReturnType = factory.getBoxedForPrimitiveType(methodToCall.getReturnType());
+    }
 
     // Only constructor call should use direct invoke type since super
     // and private methods require accessor methods.
-    boolean constructorTarget = methodToCall.name == factory.constructorMethodName;
+    boolean constructorTarget = methodToCall.name.isIdenticalTo(factory.constructorMethodName);
     assert !constructorTarget || target.invokeType == InvokeType.DIRECT;
 
     boolean targetWithReceiver =
@@ -218,7 +222,7 @@
         factory);
 
     int maxStack = 0;
-    Builder<CfInstruction> instructions = ImmutableList.builder();
+    ImmutableList.Builder<CfInstruction> instructions = ImmutableList.builder();
 
     // If the target is a constructor, we need to create the instance first.
     // This instance will be the first argument to the call and the dup will be on stack at return.
@@ -335,17 +339,18 @@
 
   // Adds necessary casts and transformations to adjust the value
   // returned by impl-method to expected return type of the method.
-  @SuppressWarnings("BadImport")
   private static int prepareReturnValue(
       DexType erasedType,
       DexType enforcedType,
       DexType actualType,
-      Builder<CfInstruction> instructions,
+      ImmutableList.Builder<CfInstruction> instructions,
       DexItemFactory factory) {
-    // `erasedType` and `enforcedType` may only differ when they both
+    // The `erasedType` and `enforcedType` should only differ when they both
     // are class types and `erasedType` is a base type of `enforcedType`,
-    // so no transformation is actually needed.
-    assert LambdaDescriptor.isSameOrDerived(factory, enforcedType, erasedType);
+    // so no transformation is actually needed. However, it appears primitives can appear in place
+    // of reference types in the `erasedType` signature.
+    assert LambdaDescriptor.isSameOrDerivedReturnType(
+        factory, enforcedType, erasedType, actualType);
     return adjustType(actualType, enforcedType, true, instructions, factory);
   }
 
@@ -356,41 +361,38 @@
   // be converted to enforced parameter type (`enforcedType`), which,
   // in its turn, may need to be adjusted to the parameter type of
   // the impl-method (`expectedType`).
-  @SuppressWarnings("BadImport")
   private static int prepareParameterValue(
       DexType erasedType,
       DexType enforcedType,
       DexType expectedType,
-      Builder<CfInstruction> instructions,
+      ImmutableList.Builder<CfInstruction> instructions,
       DexItemFactory factory) {
     enforceParameterType(erasedType, enforcedType, instructions, factory);
     return adjustType(enforcedType, expectedType, false, instructions, factory);
   }
 
-  @SuppressWarnings({"BadImport", "ReferenceEquality"})
   private static void enforceParameterType(
       DexType paramType,
       DexType enforcedType,
-      Builder<CfInstruction> instructions,
+      ImmutableList.Builder<CfInstruction> instructions,
       DexItemFactory factory) {
     // `paramType` must be either same as `enforcedType` or both must be class
     // types and `enforcedType` must be a subclass of `paramType` in which case
     // a cast need to be inserted.
-    if (paramType != enforcedType) {
+    if (paramType.isNotIdenticalTo(enforcedType)) {
       assert LambdaDescriptor.isSameOrDerived(factory, enforcedType, paramType);
       instructions.add(new CfCheckCast(enforcedType));
     }
   }
 
-  @SuppressWarnings({"BadImport", "ReferenceEquality"})
   private static int adjustType(
       DexType fromType,
       DexType toType,
       boolean returnType,
-      Builder<CfInstruction> instructions,
+      ImmutableList.Builder<CfInstruction> instructions,
       DexItemFactory factory) {
     internalAdjustType(fromType, toType, returnType, instructions, factory);
-    if (fromType == toType) {
+    if (fromType.isIdenticalTo(toType)) {
       return ValueType.fromDexType(fromType).requiredRegisters();
     }
     // Account for the potential unboxing of a wide type.
@@ -404,14 +406,13 @@
             ValueType.fromDexType(toType).requiredRegisters()));
   }
 
-  @SuppressWarnings({"BadImport", "ReferenceEquality"})
   private static void internalAdjustType(
       DexType fromType,
       DexType toType,
       boolean returnType,
-      Builder<CfInstruction> instructions,
+      ImmutableList.Builder<CfInstruction> instructions,
       DexItemFactory factory) {
-    if (fromType == toType) {
+    if (fromType.isIdenticalTo(toType)) {
       return;
     }
 
@@ -429,7 +430,7 @@
     // widening conversion.
     if (toTypePrimitive) {
       DexType boxedType = fromType;
-      if (boxedType == factory.objectType) {
+      if (boxedType.isIdenticalTo(factory.objectType)) {
         // We are in situation when from(=java.lang.Object) is being adjusted to a
         // primitive type, in which case we assume it is of proper box type.
         boxedType = getBoxedForPrimitiveType(toType, factory);
@@ -447,19 +448,20 @@
     // type for this primitive type, just box the value.
     if (fromTypePrimitive) {
       DexType boxedFromType = getBoxedForPrimitiveType(fromType, factory);
-      if (toType == boxedFromType
-          || toType == factory.objectType
-          || toType == factory.serializableType
-          || toType == factory.comparableType
-          || (boxedFromType != factory.booleanType
-              && boxedFromType != factory.charType
-              && toType == factory.boxedNumberType)) {
+      if (toType.isIdenticalTo(boxedFromType)
+          || toType.isIdenticalTo(factory.objectType)
+          || toType.isIdenticalTo(factory.serializableType)
+          || toType.isIdenticalTo(factory.comparableType)
+          || (boxedFromType.isNotIdenticalTo(factory.booleanType)
+              && boxedFromType.isNotIdenticalTo(factory.charType)
+              && toType.isIdenticalTo(factory.boxedNumberType))) {
         addPrimitiveBoxing(boxedFromType, instructions, factory);
         return;
       }
     }
 
-    if (fromType.isArrayType() && (toType == factory.objectType || toType.isArrayType())) {
+    if (fromType.isArrayType()
+        && (toType.isIdenticalTo(factory.objectType) || toType.isArrayType())) {
       // If `fromType` is an array and `toType` is java.lang.Object, no cast is needed.
       // If both `fromType` and `toType` are arrays, no cast is needed since we assume
       // the input code is verifiable.
@@ -467,8 +469,8 @@
     }
 
     if ((fromType.isClassType() && toType.isClassType())
-        || (fromType == factory.objectType && toType.isArrayType())) {
-      if (returnType && toType != factory.objectType) {
+        || (fromType.isIdenticalTo(factory.objectType) && toType.isArrayType())) {
+      if (returnType && toType.isNotIdenticalTo(factory.objectType)) {
         // For return type adjustment in case `fromType` and `toType` are both reference types,
         // `fromType` does NOT have to be deriving from `toType` and we need to add a cast.
         // NOTE: we don't check `toType` for being actually a supertype, since we
@@ -482,11 +484,10 @@
         + fromType.toSourceString() + " to " + toType);
   }
 
-  @SuppressWarnings({"BadImport", "ReferenceEquality"})
   private static void addPrimitiveWideningConversion(
-      DexType fromType, DexType toType, Builder<CfInstruction> instructions) {
+      DexType fromType, DexType toType, ImmutableList.Builder<CfInstruction> instructions) {
     assert fromType.isPrimitiveType() && toType.isPrimitiveType();
-    if (fromType == toType) {
+    if (fromType.isIdenticalTo(toType)) {
       return;
     }
 
@@ -544,16 +545,14 @@
         "converted to " + toType.toSourceString() + " via primitive widening conversion.");
   }
 
-  @SuppressWarnings("BadImport")
   private static void addPrimitiveUnboxing(
-      DexType boxType, Builder<CfInstruction> instructions, DexItemFactory factory) {
+      DexType boxType, ImmutableList.Builder<CfInstruction> instructions, DexItemFactory factory) {
     DexMethod method = factory.getUnboxPrimitiveMethod(boxType);
     instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, method, false));
   }
 
-  @SuppressWarnings("BadImport")
   private static void addPrimitiveBoxing(
-      DexType boxType, Builder<CfInstruction> instructions, DexItemFactory factory) {
+      DexType boxType, ImmutableList.Builder<CfInstruction> instructions, DexItemFactory factory) {
     DexMethod method = factory.getBoxPrimitiveMethod(boxType);
     instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, method, false));
   }
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 3117467..1ed5f9a 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
@@ -48,6 +48,7 @@
 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;
+import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.AssumeInfoCollection;
 import com.android.tools.r8.shaking.MainDexInfo;
@@ -176,6 +177,11 @@
       return false;
     }
 
+    if (!StartupBoundaryOptimizationUtils.isSafeForInlining(method, singleTarget, appView)) {
+      whyAreYouNotInliningReporter.reportInliningAcrossStartupBoundary();
+      return false;
+    }
+
     // Abort inlining attempt if method -> target access is not right.
     if (resolutionResult.isAccessibleFrom(method, appView).isPossiblyFalse()) {
       whyAreYouNotInliningReporter.reportInaccessible();
@@ -184,13 +190,11 @@
 
     // 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 (mainDexInfo.disallowInliningIntoContext(
-        appView, method, singleTarget, appView.getSyntheticItems())) {
+    if (mainDexInfo.disallowInliningIntoContext(appView, method, singleTarget)) {
       whyAreYouNotInliningReporter.reportInlineeRefersToClassesNotInMainDex();
       return false;
     }
-    assert !mainDexInfo.disallowInliningIntoContext(
-        appView, method, singleTarget, appView.getSyntheticItems());
+    assert !mainDexInfo.disallowInliningIntoContext(appView, method, singleTarget);
     return true;
   }
 
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 fd3ec8c..9f25637 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
@@ -131,7 +131,7 @@
   private ConstraintWithTarget instructionAllowedForInlining(
       Instruction instruction, InliningConstraints inliningConstraints, ProgramMethod context) {
     ConstraintWithTarget result = instruction.inliningConstraint(inliningConstraints, context);
-    if (result == ConstraintWithTarget.NEVER && instruction.isDebugInstruction()) {
+    if (result.isNever() && instruction.isDebugInstruction()) {
       return ConstraintWithTarget.ALWAYS;
     }
     return result;
@@ -155,7 +155,7 @@
     for (Instruction instruction : code.instructions()) {
       ConstraintWithTarget state =
           instructionAllowedForInlining(instruction, inliningConstraints, context);
-      if (state == ConstraintWithTarget.NEVER) {
+      if (state.isNever()) {
         result = state;
         break;
       }
@@ -232,6 +232,10 @@
       return otherConstraint;
     }
 
+    public boolean isAlways() {
+      return this == ALWAYS;
+    }
+
     public boolean isNever() {
       return this == NEVER;
     }
@@ -279,6 +283,10 @@
       this.targetHolder = targetHolder;
     }
 
+    public boolean isAlways() {
+      return constraint.isAlways();
+    }
+
     public boolean isNever() {
       return constraint.isNever();
     }
@@ -358,7 +366,7 @@
         return meet(other, one, appView);
       }
       // From now on, one.constraint.ordinal() <= other.constraint.ordinal()
-      if (one == NEVER) {
+      if (one.isNever()) {
         return NEVER;
       }
       if (other == ALWAYS) {
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 8d5d425..a3ea046 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
@@ -6,34 +6,28 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
-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.DexClassAndMember;
 import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexEncodedMember;
 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.DexReference;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.FieldResolutionResult;
-import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.InvokeType;
 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.utils.TriFunction;
-import com.android.tools.r8.verticalclassmerging.SingleTypeMapperGraphLens;
 
 // Computes the inlining constraint for a given instruction.
 public class InliningConstraints {
 
   private AppView<AppInfoWithLiveness> appView;
 
-  private boolean allowStaticInterfaceMethodCalls = true;
-
   // Currently used only by the vertical class merger (in all other cases this is the identity).
   //
   // When merging a type A into its subtype B we need to inline A.<init>() into B.<init>().
@@ -59,14 +53,6 @@
     return graphLens;
   }
 
-  public void disallowStaticInterfaceMethodCalls() {
-    allowStaticInterfaceMethodCalls = false;
-  }
-
-  private boolean isVerticalClassMerging() {
-    return graphLens instanceof SingleTypeMapperGraphLens;
-  }
-
   public ConstraintWithTarget forAlwaysMaterializingUser() {
     return ConstraintWithTarget.ALWAYS;
   }
@@ -175,16 +161,17 @@
     if (lookup.holder.isArrayType()) {
       return ConstraintWithTarget.ALWAYS;
     }
-    MethodResolutionResult resolutionResult =
-        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup);
-    DexClassAndMethod target =
-        singleTargetWhileVerticalClassMerging(
-            resolutionResult, context, MethodResolutionResult::lookupInvokeDirectTarget);
-    if (target != null) {
-      return forResolvedMember(
-          resolutionResult.getInitialResolutionHolder(), context, target.getDefinition());
+    SingleResolutionResult<?> resolutionResult =
+        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup).asSingleResolution();
+    if (resolutionResult == null) {
+      return ConstraintWithTarget.NEVER;
     }
-    return ConstraintWithTarget.NEVER;
+    DexClassAndMethod target =
+        resolutionResult.lookupInvokeDirectTarget(context.getHolder(), appView);
+    if (target == null) {
+      return ConstraintWithTarget.NEVER;
+    }
+    return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
   }
 
   public ConstraintWithTarget forInvokeInterface(DexMethod method, ProgramMethod context) {
@@ -211,56 +198,20 @@
     if (lookup.holder.isArrayType()) {
       return ConstraintWithTarget.ALWAYS;
     }
-    MethodResolutionResult resolutionResult =
-        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup);
+    SingleResolutionResult<?> resolutionResult =
+        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup).asSingleResolution();
+    if (resolutionResult == null) {
+      return ConstraintWithTarget.NEVER;
+    }
     DexClassAndMethod target =
-        singleTargetWhileVerticalClassMerging(
-            resolutionResult, context, MethodResolutionResult::lookupInvokeStaticTarget);
-    if (!allowStaticInterfaceMethodCalls && target != null) {
-      // See b/120121170.
-      DexClass methodClass = appView.definitionFor(graphLens.lookupType(target.getHolderType()));
-      if (methodClass != null && methodClass.isInterface() && target.getDefinition().hasCode()) {
-        return ConstraintWithTarget.NEVER;
-      }
+        resolutionResult.lookupInvokeStaticTarget(context.getHolder(), appView);
+    if (target == null) {
+      return ConstraintWithTarget.NEVER;
     }
     if (target != null) {
-      return forResolvedMember(
-          resolutionResult.getInitialResolutionHolder(), context, target.getDefinition());
+      return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
     }
-    return ConstraintWithTarget.NEVER;
-  }
-
-  @SuppressWarnings({"ConstantConditions", "ReferenceEquality"})
-  private DexClassAndMethod singleTargetWhileVerticalClassMerging(
-      MethodResolutionResult resolutionResult,
-      ProgramMethod context,
-      TriFunction<
-              MethodResolutionResult,
-              DexProgramClass,
-              AppView<? extends AppInfoWithClassHierarchy>,
-              DexClassAndMethod>
-          lookup) {
-    if (!resolutionResult.isSingleResolution()) {
-      return null;
-    }
-    DexClassAndMethod lookupResult = lookup.apply(resolutionResult, context.getHolder(), appView);
-    if (!isVerticalClassMerging() || lookupResult != null) {
-      return lookupResult;
-    }
-    assert isVerticalClassMerging();
-    assert graphLens.lookupType(context.getHolder().superType) == context.getHolderType();
-    DexProgramClass superContext =
-        appView.programDefinitionFor(context.getHolder().superType, context);
-    if (superContext == null) {
-      return null;
-    }
-    DexClassAndMethod alternativeDexEncodedMethod =
-        lookup.apply(resolutionResult, superContext, appView);
-    if (alternativeDexEncodedMethod != null
-        && alternativeDexEncodedMethod.getHolderType() == superContext.type) {
-      return alternativeDexEncodedMethod;
-    }
-    return null;
+    return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
   }
 
   public ConstraintWithTarget forInvokeSuper(DexMethod method, ProgramMethod context) {
@@ -360,14 +311,15 @@
 
   private ConstraintWithTarget forFieldInstruction(DexField field, ProgramMethod context) {
     DexField lookup = graphLens.lookupField(field);
-    FieldResolutionResult fieldResolutionResult = appView.appInfo().resolveField(lookup);
-    if (fieldResolutionResult.isMultiFieldResolutionResult()) {
+    SingleFieldResolutionResult<?> fieldResolutionResult =
+        appView.appInfo().resolveField(lookup).asSingleFieldResolutionResult();
+    if (fieldResolutionResult == null) {
       return ConstraintWithTarget.NEVER;
     }
     return forResolvedMember(
         fieldResolutionResult.getInitialResolutionHolder(),
         context,
-        fieldResolutionResult.getResolvedField());
+        fieldResolutionResult.getResolutionPair());
   }
 
   private ConstraintWithTarget forVirtualInvoke(
@@ -378,34 +330,29 @@
 
     // Perform resolution and derive inlining constraints based on the accessibility of the
     // resolution result.
-    MethodResolutionResult resolutionResult =
-        appView.appInfo().resolveMethodLegacy(method, isInterface);
-    if (!resolutionResult.isVirtualTarget()) {
+    SingleResolutionResult<?> resolutionResult =
+        appView.appInfo().resolveMethodLegacy(method, isInterface).asSingleResolution();
+    if (resolutionResult == null || !resolutionResult.isVirtualTarget()) {
       return ConstraintWithTarget.NEVER;
     }
-
     return forResolvedMember(
-        resolutionResult.getInitialResolutionHolder(), context, resolutionResult.getSingleTarget());
+        resolutionResult.getInitialResolutionHolder(),
+        context,
+        resolutionResult.getResolutionPair());
   }
 
   @SuppressWarnings("ReferenceEquality")
   private ConstraintWithTarget forResolvedMember(
       DexClass initialResolutionHolder,
       ProgramMethod context,
-      DexEncodedMember<?, ?> resolvedMember) {
-    if (resolvedMember == null) {
-      // This will fail at runtime.
+      DexClassAndMember<?, ?> resolvedMember) {
+    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForAccess(
+            initialResolutionHolder, context, appView)
+        || !FeatureSplitBoundaryOptimizationUtils.isSafeForAccess(
+            resolvedMember, context, appView)) {
       return ConstraintWithTarget.NEVER;
     }
-    ConstraintWithTarget featureSplitInliningConstraint =
-        FeatureSplitBoundaryOptimizationUtils.getInliningConstraintForResolvedMember(
-            context, resolvedMember, appView);
-    assert featureSplitInliningConstraint == ConstraintWithTarget.ALWAYS
-        || featureSplitInliningConstraint == ConstraintWithTarget.NEVER;
-    if (featureSplitInliningConstraint == ConstraintWithTarget.NEVER) {
-      return featureSplitInliningConstraint;
-    }
-    DexType resolvedHolder = graphLens.lookupType(resolvedMember.getHolderType());
+    DexType resolvedHolder = resolvedMember.getHolderType();
     assert initialResolutionHolder != null;
     ConstraintWithTarget memberConstraintWithTarget =
         ConstraintWithTarget.deriveConstraint(
@@ -413,7 +360,10 @@
     // We also have to take the constraint of the initial resolution holder into account.
     ConstraintWithTarget classConstraintWithTarget =
         ConstraintWithTarget.deriveConstraint(
-            context, initialResolutionHolder.type, initialResolutionHolder.accessFlags, appView);
+            context,
+            initialResolutionHolder.getType(),
+            initialResolutionHolder.getAccessFlags(),
+            appView);
     return ConstraintWithTarget.meet(
         classConstraintWithTarget, memberConstraintWithTarget, appView);
   }
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 360d241..7ed32343 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
@@ -191,7 +191,7 @@
     // Make sure the target (base) type is visible.
     ConstraintWithTarget constraints =
         ConstraintWithTarget.classIsVisible(context, baseType, appView);
-    if (constraints == ConstraintWithTarget.NEVER) {
+    if (constraints.isNever()) {
       return;
     }
 
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 7935419..cb5bbbe 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
@@ -515,16 +515,17 @@
       inliner.performForcedInlining(
           method, code, methodCallsOnInstance, inliningIRProvider, methodProcessor, Timing.empty());
     } else {
-      assert indirectMethodCallsOnInstance.stream()
-          .filter(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects())
-          .allMatch(
-              method ->
-                  method.getDefinition().isInstanceInitializer()
-                      && !method
-                          .getDefinition()
-                          .getOptimizationInfo()
-                          .getContextInsensitiveInstanceInitializerInfo()
-                          .mayHaveOtherSideEffectsThanInstanceFieldAssignments());
+      // TODO(b/315284776): Diagnose if this should be removed or reenabled.
+      /*assert indirectMethodCallsOnInstance.stream()
+      .filter(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects())
+      .allMatch(
+          method ->
+              method.getDefinition().isInstanceInitializer()
+                  && !method
+                      .getDefinition()
+                      .getOptimizationInfo()
+                      .getContextInsensitiveInstanceInitializerInfo()
+                      .mayHaveOtherSideEffectsThanInstanceFieldAssignments());*/
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 5132141..373c579 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -1417,12 +1417,12 @@
     DexClassAndMethod mostAccurateTarget = singleTarget == null ? resolvedMethod : singleTarget;
 
     if (mostAccurateTarget.isProgramMethod()) {
-      if (mostAccurateTarget.getHolder().isEnum()
-          && resolvedMethod.getDefinition().isInstanceInitializer()) {
+      DexProgramClass candidateOrNull =
+          getEnumUnboxingCandidateOrNull(mostAccurateTarget.getHolderType());
+      if (candidateOrNull != null && resolvedMethod.getDefinition().isInstanceInitializer()) {
         // The enum instance initializer is only allowed to be called from an initializer of the
         // enum itself.
-        if (getEnumUnboxingCandidateOrNull(code.context().getHolderType())
-                != getEnumUnboxingCandidateOrNull(mostAccurateTarget.getHolderType())
+        if (getEnumUnboxingCandidateOrNull(code.context().getHolderType()) != candidateOrNull
             || !context.getDefinition().isInitializer()) {
           return Reason.INVALID_INIT;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 86709f7..c0a7655 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.ir.optimize.enums;
 
-import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -14,16 +13,14 @@
 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.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Set;
+import java.util.List;
 
 class EnumUnboxingCandidateAnalysis {
 
@@ -35,12 +32,9 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final EnumUnboxerImpl enumUnboxer;
   private final DexItemFactory factory;
-  private EnumUnboxingCandidateInfoCollection enumToUnboxCandidates =
+  private final EnumUnboxingCandidateInfoCollection enumToUnboxCandidates =
       new EnumUnboxingCandidateInfoCollection();
 
-  private final Map<DexType, Set<DexProgramClass>> enumSubclasses = new IdentityHashMap<>();
-  private final Set<DexType> ineligibleCandidates = Sets.newIdentityHashSet();
-
   EnumUnboxingCandidateAnalysis(AppView<AppInfoWithLiveness> appView, EnumUnboxerImpl enumUnboxer) {
     this.appView = appView;
     this.enumUnboxer = enumUnboxer;
@@ -54,13 +48,13 @@
       // disables the enum unboxer.
       return enumToUnboxCandidates;
     }
+    ImmediateProgramSubtypingInfo subtypingInfo = ImmediateProgramSubtypingInfo.create(appView);
+    // In recent Kotlin enum subclasses are not flagged as enum, so we cannot rely on the flag.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (clazz.isEnum()) {
-        analyzeEnum(graphLensForPrimaryOptimizationPass, clazz);
+      if (clazz.isEnum() && clazz.superType.isIdenticalTo(factory.enumType)) {
+        analyzeEnum(graphLensForPrimaryOptimizationPass, clazz, subtypingInfo);
       }
     }
-    removeIneligibleCandidates();
-    setEnumSubclassesOnCandidates();
     removeEnumsInAnnotations();
     removePinnedCandidates();
     if (appView.options().protoShrinking().isProtoShrinkingEnabled()) {
@@ -70,42 +64,28 @@
     return enumToUnboxCandidates;
   }
 
-  private void setEnumSubclassesOnCandidates() {
-    enumToUnboxCandidates.forEachCandidateInfo(
-        info -> {
-          DexType type = info.getEnumClass().getType();
-          enumToUnboxCandidates.setEnumSubclasses(
-              type, enumSubclasses.getOrDefault(type, ImmutableSet.of()));
-        });
-  }
-
-  private void removeIneligibleCandidates() {
-    for (DexType ineligibleCandidate : ineligibleCandidates) {
-      enumToUnboxCandidates.removeCandidate(ineligibleCandidate);
+  private void analyzeEnum(
+      GraphLens graphLensForPrimaryOptimizationPass,
+      DexProgramClass clazz,
+      ImmediateProgramSubtypingInfo subtypingInfo) {
+    if (!isSuperEnumUnboxingCandidate(clazz)) {
+      return;
     }
-  }
 
-  @SuppressWarnings("ReferenceEquality")
-  private void analyzeEnum(GraphLens graphLensForPrimaryOptimizationPass, DexProgramClass clazz) {
-    if (clazz.superType == factory.enumType) {
-      if (isSuperEnumUnboxingCandidate(clazz)) {
-        enumToUnboxCandidates.addCandidate(appView, clazz, graphLensForPrimaryOptimizationPass);
+    List<DexProgramClass> subtypes = subtypingInfo.getSubclasses(clazz);
+    ImmutableSet.Builder<DexProgramClass> subEnumClassesBuilder = ImmutableSet.builder();
+    for (DexProgramClass subEnum : subtypes) {
+      if (!isSubEnumUnboxingCandidate(subEnum)) {
+        return;
       }
-    } else {
-      if (isSubEnumUnboxingCandidate(clazz)
-          && appView.options().testing.enableEnumWithSubtypesUnboxing) {
-        enumSubclasses
-            .computeIfAbsent(clazz.superType, ignoreKey(Sets::newIdentityHashSet))
-            .add(clazz);
-      } else {
-        ineligibleCandidates.add(clazz.superType);
-      }
+      subEnumClassesBuilder.add(subEnum);
     }
+    enumToUnboxCandidates.addCandidate(
+        appView, clazz, subEnumClassesBuilder.build(), graphLensForPrimaryOptimizationPass);
   }
 
   @SuppressWarnings("ReferenceEquality")
   private boolean isSubEnumUnboxingCandidate(DexProgramClass clazz) {
-    assert clazz.isEnum();
     boolean result = true;
     // Javac does not seem to generate enums with more than a single subtype level.
     // TODO(b/273910479): Stop using isEffectivelyFinal.
@@ -132,29 +112,14 @@
     return result;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean isSuperEnumUnboxingCandidate(DexProgramClass clazz) {
     assert clazz.isEnum();
-
-    // This is used in debug mode, where we don't do quick returns to log all the reasons an enum
-    // is not unboxed.
-    boolean result = true;
-
-    // TODO(b/271385332): Change this into an assert when legacy is removed.
-    if (clazz.superType != factory.enumType) {
-      if (!enumUnboxer.reportFailure(clazz, Reason.INVALID_LIBRARY_SUPERTYPE)) {
-        return false;
-      }
-      result = false;
-    }
-
+    assert clazz.superType.isIdenticalTo(factory.enumType);
     if (clazz.instanceFields().size() > MAX_INSTANCE_FIELDS_FOR_UNBOXING) {
-      if (!enumUnboxer.reportFailure(clazz, Reason.MANY_INSTANCE_FIELDS)) {
-        return false;
-      }
-      result = false;
+      enumUnboxer.reportFailure(clazz, Reason.MANY_INSTANCE_FIELDS);
+      return false;
     }
-    return result;
+    return true;
   }
 
   private void removeEnumsInAnnotations() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
index aaa428d..c2444a8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
@@ -34,11 +34,16 @@
   public void addCandidate(
       AppView<AppInfoWithLiveness> appView,
       DexProgramClass enumClass,
+      Set<DexProgramClass> subclasses,
       GraphLens graphLensForPrimaryOptimizationPass) {
     assert !enumTypeToInfo.containsKey(enumClass.type);
     enumTypeToInfo.put(
         enumClass.type,
-        new EnumUnboxingCandidateInfo(appView, enumClass, graphLensForPrimaryOptimizationPass));
+        new EnumUnboxingCandidateInfo(
+            appView, enumClass, subclasses, graphLensForPrimaryOptimizationPass));
+    for (DexProgramClass subclass : subclasses) {
+      subEnumToSuperEnumMap.put(subclass.getType(), enumClass.getType());
+    }
   }
 
   public boolean hasSubtypes(DexType enumType) {
@@ -50,13 +55,6 @@
         enumTypeToInfo.get(enumType).getSubclasses(), DexClass::getType);
   }
 
-  public void setEnumSubclasses(DexType superEnum, Set<DexProgramClass> subclasses) {
-    enumTypeToInfo.get(superEnum).setSubclasses(subclasses);
-    for (DexProgramClass subclass : subclasses) {
-      subEnumToSuperEnumMap.put(subclass.getType(), superEnum);
-    }
-  }
-
   public void addPrunedMethod(ProgramMethod method) {
     prunedMethods.add(method.getReference());
   }
@@ -174,10 +172,12 @@
     public EnumUnboxingCandidateInfo(
         AppView<AppInfoWithLiveness> appView,
         DexProgramClass enumClass,
+        Set<DexProgramClass> subclasses,
         GraphLens graphLensForPrimaryOptimizationPass) {
       assert enumClass != null;
       assert appView.graphLens() == graphLensForPrimaryOptimizationPass;
       this.enumClass = enumClass;
+      this.subclasses = subclasses;
       this.methodDependencies =
           LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(
               graphLensForPrimaryOptimizationPass);
@@ -188,10 +188,6 @@
       return subclasses;
     }
 
-    public void setSubclasses(Set<DexProgramClass> subclasses) {
-      this.subclasses = subclasses;
-    }
-
     public DexProgramClass getEnumClass() {
       return enumClass;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 37b6314..1eee403 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -135,7 +135,31 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
+  protected MethodLookupResult internalLookupMethod(
+      DexMethod reference,
+      DexMethod context,
+      InvokeType type,
+      GraphLens codeLens,
+      LookupMethodContinuation continuation) {
+    if (this == codeLens) {
+      // We sometimes create code objects that have the EnumUnboxingLens as code lens.
+      // When using this lens as a code lens there is no lens that will insert the rebound reference
+      // since the MemberRebindingIdentityLens is an ancestor of the EnumUnboxingLens.
+      // We therefore use the reference itself as the rebound reference here, which is safe since
+      // the code objects created during enum unboxing are guaranteed not to contain any non-rebound
+      // method references.
+      MethodLookupResult lookupResult =
+          MethodLookupResult.builder(this, codeLens)
+              .setReboundReference(reference)
+              .setReference(reference)
+              .setType(type)
+              .build();
+      return continuation.lookupMethod(lookupResult);
+    }
+    return super.internalLookupMethod(reference, context, type, codeLens, continuation);
+  }
+
+  @Override
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     assert context != null || verifyIsContextFreeForMethod(previous.getReference(), codeLens);
@@ -146,9 +170,9 @@
       DexMethod previousContext = getPreviousMethodSignature(context);
       DexType superEnum = unboxedEnums.representativeType(previousContext.getHolderType());
       if (unboxedEnums.isUnboxedEnum(superEnum)) {
-        if (superEnum != previousContext.getHolderType()) {
+        if (superEnum.isNotIdenticalTo(previousContext.getHolderType())) {
           DexMethod reference = previous.getReference();
-          if (reference.getHolderType() != superEnum) {
+          if (reference.getHolderType().isNotIdenticalTo(superEnum)) {
             // We are in an enum subtype where super-invokes are rebound differently.
             reference = reference.withHolder(superEnum, dexItemFactory());
           }
@@ -156,7 +180,7 @@
         } else {
           // This is a super-invoke to a library method, not rewritten by the lens.
           // This is rewritten by the EnumUnboxerRewriter.
-          return previous;
+          return previous.verify(this, codeLens);
         }
       } else {
         result = methodMap.apply(previous.getReference());
@@ -165,12 +189,13 @@
       result = methodMap.apply(previous.getReference());
     }
     if (result == null) {
-      return previous;
+      return previous.verify(this, codeLens);
     }
-    return MethodLookupResult.builder(this)
+    return MethodLookupResult.builder(this, codeLens)
         .setReference(result)
         .setPrototypeChanges(
-            internalDescribePrototypeChanges(previous.getPrototypeChanges(), result))
+            internalDescribePrototypeChanges(
+                previous.getPrototypeChanges(), previous.getReference(), result))
         .setType(mapInvocationType(result, previous.getReference(), previous.getType()))
         .build();
   }
@@ -178,7 +203,9 @@
   @Override
   @SuppressWarnings("ReferenceEquality")
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
-      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+      RewrittenPrototypeDescription prototypeChanges,
+      DexMethod previousMethod,
+      DexMethod newMethod) {
     // Rewrite the single value of the given RewrittenPrototypeDescription if it is referring to an
     // unboxed enum field.
     if (prototypeChanges.hasRewrittenReturnInfo()) {
@@ -198,12 +225,8 @@
         }
       }
     }
-
-    // During the second IR processing enum unboxing is the only optimization rewriting
-    // prototype description, if this does not hold, remove the assertion and merge
-    // the two prototype changes.
     RewrittenPrototypeDescription enumUnboxingPrototypeChanges =
-        prototypeChangesPerMethod.getOrDefault(method, RewrittenPrototypeDescription.none());
+        prototypeChangesPerMethod.getOrDefault(newMethod, RewrittenPrototypeDescription.none());
     return prototypeChanges.combine(enumUnboxingPrototypeChanges);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
index 56b05f6..3cf50f1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
@@ -11,18 +11,13 @@
 
 public abstract class Reason {
   public static final Reason ELIGIBLE = new StringReason("ELIGIBLE");
-  public static final Reason ACCESSIBILITY = new StringReason("ACCESSIBILITY");
   public static final Reason ANNOTATION = new StringReason("ANNOTATION");
   public static final Reason PINNED = new StringReason("PINNED");
   public static final Reason DOWN_CAST = new StringReason("DOWN_CAST");
-  public static final Reason INVALID_LIBRARY_SUPERTYPE =
-      new StringReason("INVALID_LIBRARY_SUPERTYPE");
-  public static final Reason SUBTYPES = new StringReason("SUBTYPES");
   public static final Reason SUBENUM_SUBTYPES = new StringReason("SUBENUM_SUBTYPES");
   public static final Reason SUBENUM_INVALID_HIERARCHY =
       new StringReason("SUBENUM_INVALID_HIERARCHY");
   public static final Reason SUBENUM_INSTANCE_FIELDS = new StringReason("SUBENUM_INSTANCE_FIELDS");
-  public static final Reason SUBENUM_STATIC_MEMBER = new StringReason("SUBENUM_STATIC_MEMBER");
   public static final Reason MANY_INSTANCE_FIELDS = new StringReason("MANY_INSTANCE_FIELDS");
   public static final Reason DEFAULT_METHOD_INVOKE = new StringReason("DEFAULT_METHOD_INVOKE");
   public static final Reason UNRESOLVABLE_FIELD = new StringReason("UNRESOLVABLE_FIELD");
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
index d3d3735..1f116b4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
@@ -69,6 +69,9 @@
   public void reportInliningAcrossFeatureSplit() {}
 
   @Override
+  public void reportInliningAcrossStartupBoundary() {}
+
+  @Override
   public void reportInstructionBudgetIsExceeded() {}
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index 4bf931b..159128c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -76,6 +76,8 @@
 
   public abstract void reportInliningAcrossFeatureSplit();
 
+  public abstract void reportInliningAcrossStartupBoundary();
+
   public abstract void reportInstructionBudgetIsExceeded();
 
   public abstract void reportInvalidDoubleInliningCandidate();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
index 05e2cb1..23391b7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
@@ -153,6 +153,11 @@
   }
 
   @Override
+  public void reportInliningAcrossStartupBoundary() {
+    report("cannot inline across startup/non-startup boundary.");
+  }
+
+  @Override
   public void reportInstructionBudgetIsExceeded() {
     report("caller's instruction budget is exceeded.");
   }
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
index 75d2340..6e1f5a7 100644
--- 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
@@ -9,12 +9,14 @@
 public class MethodBoxingStatus {
 
   public static final MethodBoxingStatus NONE_UNBOXABLE = new MethodBoxingStatus(null, null);
+  public static final MethodBoxingStatus UNPROCESSED_CANDIDATE = new MethodBoxingStatus(null, null);
 
   private final ValueBoxingStatus returnStatus;
   private final ValueBoxingStatus[] argStatuses;
 
   public static MethodBoxingStatus create(
       ValueBoxingStatus returnStatus, ValueBoxingStatus[] argStatuses) {
+    assert !ArrayUtils.contains(argStatuses, null);
     if (returnStatus.isNotUnboxable()
         && ArrayUtils.all(argStatuses, ValueBoxingStatus.NOT_UNBOXABLE)) {
       return NONE_UNBOXABLE;
@@ -31,6 +33,12 @@
     if (isNoneUnboxable() || other.isNoneUnboxable()) {
       return NONE_UNBOXABLE;
     }
+    if (isUnprocessedCandidate()) {
+      return other;
+    }
+    if (other.isUnprocessedCandidate()) {
+      return this;
+    }
     assert argStatuses.length == other.argStatuses.length;
     ValueBoxingStatus[] newArgStatuses = new ValueBoxingStatus[argStatuses.length];
     for (int i = 0; i < other.argStatuses.length; i++) {
@@ -43,6 +51,10 @@
     return this == NONE_UNBOXABLE;
   }
 
+  public boolean isUnprocessedCandidate() {
+    return this == UNPROCESSED_CANDIDATE;
+  }
+
   public ValueBoxingStatus getReturnStatus() {
     assert !isNoneUnboxable();
     return returnStatus;
@@ -50,6 +62,7 @@
 
   public ValueBoxingStatus getArgStatus(int i) {
     assert !isNoneUnboxable();
+    assert argStatuses[i] != null;
     return argStatuses[i];
   }
 
@@ -62,7 +75,9 @@
   public String toString() {
     StringBuilder sb = new StringBuilder();
     sb.append("MethodBoxingStatus[");
-    if (this == NONE_UNBOXABLE) {
+    if (isUnprocessedCandidate()) {
+      sb.append("UNPROCESSED_CANDIDATE");
+    } else if (isNoneUnboxable()) {
       sb.append("NONE_UNBOXABLE");
     } else {
       for (int i = 0; i < argStatuses.length; i++) {
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
index 4a9cbfd..a685b00 100644
--- 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
@@ -24,15 +24,16 @@
 
   // TODO(b/307872552): Add threshold to NumberUnboxing options.
   private static final int UNBOX_DELTA_THRESHOLD = 0;
+  private final Map<DexMethod, MethodBoxingStatus> methodBoxingStatus;
   private final Map<DexMethod, MethodBoxingStatusResult> boxingStatusResultMap =
       new IdentityHashMap<>();
 
-  static class MethodBoxingStatusResult {
+  public NumberUnboxerBoxingStatusResolution(
+      Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+    this.methodBoxingStatus = methodBoxingStatus;
+  }
 
-    public static MethodBoxingStatusResult createNonUnboxable(DexMethod method) {
-      // Replace by singleton.
-      return new MethodBoxingStatusResult(method, NO_UNBOX);
-    }
+  static class MethodBoxingStatusResult {
 
     public static MethodBoxingStatusResult create(DexMethod method) {
       return new MethodBoxingStatusResult(method, TO_PROCESS);
@@ -88,18 +89,18 @@
     }
   }
 
-  void markNoneUnboxable(DexMethod method) {
-    boxingStatusResultMap.put(method, MethodBoxingStatusResult.createNonUnboxable(method));
-  }
-
   private MethodBoxingStatusResult getMethodBoxingStatusResult(DexMethod method) {
+    assert methodBoxingStatus.containsKey(method);
     return boxingStatusResultMap.computeIfAbsent(method, MethodBoxingStatusResult::create);
   }
 
   BoxingStatusResult get(TransitiveDependency transitiveDependency) {
     assert transitiveDependency.isMethodDependency();
-    MethodBoxingStatusResult methodBoxingStatusResult =
-        getMethodBoxingStatusResult(transitiveDependency.asMethodDependency().getMethod());
+    DexMethod method = transitiveDependency.asMethodDependency().getMethod();
+    if (!methodBoxingStatus.containsKey(method)) {
+      return NO_UNBOX;
+    }
+    MethodBoxingStatusResult methodBoxingStatusResult = getMethodBoxingStatusResult(method);
     if (transitiveDependency.isMethodRet()) {
       return methodBoxingStatusResult.getRet();
     }
@@ -109,8 +110,14 @@
 
   void register(TransitiveDependency transitiveDependency, BoxingStatusResult boxingStatusResult) {
     assert transitiveDependency.isMethodDependency();
-    MethodBoxingStatusResult methodBoxingStatusResult =
-        getMethodBoxingStatusResult(transitiveDependency.asMethodDependency().getMethod());
+    DexMethod method = transitiveDependency.asMethodDependency().getMethod();
+    if (boxingStatusResult == NO_UNBOX) {
+      if (!methodBoxingStatus.containsKey(method)) {
+        // Nothing to unbox, nothing to register.
+        return;
+      }
+    }
+    MethodBoxingStatusResult methodBoxingStatusResult = getMethodBoxingStatusResult(method);
     if (transitiveDependency.isMethodRet()) {
       methodBoxingStatusResult.setRet(boxingStatusResult);
       return;
@@ -120,21 +127,18 @@
         boxingStatusResult, transitiveDependency.asMethodArg().getParameterIndex());
   }
 
-  public Map<DexMethod, MethodBoxingStatusResult> resolve(
-      Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+  public Map<DexMethod, MethodBoxingStatusResult> resolve() {
+    assert allProcessedAndUnboxable(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;
-      }
+      assert !status.isNoneUnboxable();
       MethodBoxingStatusResult methodBoxingStatusResult = getMethodBoxingStatusResult(method);
       if (status.getReturnStatus().isNotUnboxable()) {
         methodBoxingStatusResult.setRet(NO_UNBOX);
       } else {
         if (methodBoxingStatusResult.getRet() == TO_PROCESS) {
-          resolve(methodBoxingStatus, new MethodRet(method));
+          resolve(new MethodRet(method));
         }
       }
       for (int i = 0; i < status.getArgStatuses().length; i++) {
@@ -143,16 +147,34 @@
           methodBoxingStatusResult.setArg(NO_UNBOX, i);
         } else {
           if (methodBoxingStatusResult.getArg(i) == TO_PROCESS) {
-            resolve(methodBoxingStatus, new MethodArg(i, method));
+            resolve(new MethodArg(i, method));
           }
         }
       }
     }
+    assert noResultForNoneUnboxable();
     assert allProcessed();
     clearNoneUnboxable();
     return boxingStatusResultMap;
   }
 
+  private boolean noResultForNoneUnboxable() {
+    boxingStatusResultMap.forEach(
+        (k, v) -> {
+          assert methodBoxingStatus.containsKey(k);
+        });
+    return true;
+  }
+
+  private boolean allProcessedAndUnboxable(Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+    methodBoxingStatus.forEach(
+        (k, v) -> {
+          assert !v.isNoneUnboxable() : v + " registered for " + k;
+          assert !v.isUnprocessedCandidate() : v + " registered for " + k;
+        });
+    return true;
+  }
+
   private void clearNoneUnboxable() {
     boxingStatusResultMap.values().removeIf(MethodBoxingStatusResult::isNoneUnboxable);
   }
@@ -168,11 +190,14 @@
     return true;
   }
 
-  private ValueBoxingStatus getValueBoxingStatus(
-      TransitiveDependency dep, Map<DexMethod, MethodBoxingStatus> methodBoxingStatus) {
+  private ValueBoxingStatus getValueBoxingStatus(TransitiveDependency dep) {
     // Later we will implement field dependencies.
     assert dep.isMethodDependency();
     MethodBoxingStatus status = methodBoxingStatus.get(dep.asMethodDependency().getMethod());
+    if (status == null) {
+      // Nothing was recorded because nothing was unboxable.
+      return ValueBoxingStatus.NOT_UNBOXABLE;
+    }
     if (dep.isMethodRet()) {
       return status.getReturnStatus();
     }
@@ -180,8 +205,7 @@
     return status.getArgStatus(dep.asMethodArg().getParameterIndex());
   }
 
-  private void resolve(
-      Map<DexMethod, MethodBoxingStatus> methodBoxingStatus, TransitiveDependency dep) {
+  private void resolve(TransitiveDependency dep) {
     WorkList<TransitiveDependency> workList = WorkList.newIdentityWorkList(dep);
     int delta = 0;
     while (workList.hasNext()) {
@@ -191,7 +215,7 @@
         delta++;
         continue;
       }
-      ValueBoxingStatus valueBoxingStatus = getValueBoxingStatus(next, methodBoxingStatus);
+      ValueBoxingStatus valueBoxingStatus = getValueBoxingStatus(next);
       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.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
index 1e623e1..0c471b0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
@@ -4,11 +4,13 @@
 
 package com.android.tools.r8.ir.optimize.numberunboxer;
 
+import static com.android.tools.r8.ir.optimize.numberunboxer.MethodBoxingStatus.UNPROCESSED_CANDIDATE;
 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.DexClassAndMember;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -26,10 +28,12 @@
 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.BooleanUtils;
 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 com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -47,9 +51,10 @@
   private final DexItemFactory factory;
   private final Set<DexType> boxedTypes;
 
-  // Temporarily keep the information here, and not in the MethodOptimizationInfo as the
-  // optimization is developed and unfinished.
-  private final Map<DexMethod, MethodBoxingStatus> methodBoxingStatus = new ConcurrentHashMap<>();
+  // All candidate methods are initialized to UNPROCESSED_CANDIDATE (bottom) and methods not in
+  // this map are not subject to unboxing.
+  private final Map<DexMethod, MethodBoxingStatus> candidateBoxingStatus =
+      new ConcurrentHashMap<>();
   private Map<DexMethod, DexMethod> virtualMethodsRepresentative;
 
   public NumberUnboxerImpl(AppView<AppInfoWithLiveness> appView) {
@@ -88,22 +93,36 @@
   // TODO(b/307872552): Do not store irrelevant representative.
   private Map<DexMethod, DexMethod> computeVirtualMethodRepresentative(
       Set<DexProgramClass> component) {
-    DexMethodSignatureMap<List<DexMethod>> componentVirtualMethods = DexMethodSignatureMap.create();
+    DexMethodSignatureMap<List<ProgramMethod>> componentVirtualMethods =
+        DexMethodSignatureMap.create();
     for (DexProgramClass clazz : component) {
       for (ProgramMethod virtualProgramMethod : clazz.virtualProgramMethods()) {
-        DexMethod reference = virtualProgramMethod.getReference();
-        List<DexMethod> set =
+        List<ProgramMethod> set =
             componentVirtualMethods.computeIfAbsent(virtualProgramMethod, k -> new ArrayList<>());
-        set.add(reference);
+        set.add(virtualProgramMethod);
+      }
+      for (ProgramMethod candidate : clazz.directProgramMethods()) {
+        if (shouldConsiderForUnboxing(candidate)) {
+          candidateBoxingStatus.put(candidate.getReference(), UNPROCESSED_CANDIDATE);
+        }
       }
     }
     Map<DexMethod, DexMethod> vMethodRepresentative = new IdentityHashMap<>();
-    for (List<DexMethod> vMethods : componentVirtualMethods.values()) {
+    for (List<ProgramMethod> 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);
+        if (Iterables.all(vMethods, this::shouldConsiderForUnboxing)) {
+          vMethods.sort(Comparator.comparing(DexClassAndMember::getReference));
+          ProgramMethod representative = vMethods.get(0);
+          for (int i = 1; i < vMethods.size(); i++) {
+            vMethodRepresentative.put(
+                vMethods.get(i).getReference(), representative.getReference());
+          }
+        }
+      } else {
+        assert vMethods.size() == 1;
+        ProgramMethod candidate = vMethods.get(0);
+        if (shouldConsiderForUnboxing(candidate)) {
+          candidateBoxingStatus.put(candidate.getReference(), UNPROCESSED_CANDIDATE);
         }
       }
     }
@@ -112,8 +131,12 @@
 
   private void registerMethodUnboxingStatusIfNeeded(
       ProgramMethod method, ValueBoxingStatus returnStatus, ValueBoxingStatus[] args) {
-    if (args == null && returnStatus == null) {
-      // We don't register anything if nothing unboxable was found.
+    DexMethod representative =
+        virtualMethodsRepresentative.getOrDefault(method.getReference(), method.getReference());
+    if (args == null && (returnStatus == null || returnStatus.isNotUnboxable())) {
+      // Effectively NOT_UNBOXABLE, remove the candidate.
+      // TODO(b/307872552): Do we need to remove at the end of the wave for determinism?
+      candidateBoxingStatus.remove(representative);
       return;
     }
     ValueBoxingStatus nonNullReturnStatus = returnStatus == null ? NOT_UNBOXABLE : returnStatus;
@@ -121,16 +144,13 @@
         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);
-        });
+    MethodBoxingStatus newStatus =
+        candidateBoxingStatus.computeIfPresent(
+            representative, (m, old) -> old.merge(unboxingStatus));
+    if (newStatus != null && newStatus.isNoneUnboxable()) {
+      // TODO(b/307872552): Do we need to remove at the end of the wave for determinism?
+      candidateBoxingStatus.remove(representative);
+    }
   }
 
   /**
@@ -142,14 +162,16 @@
     DexMethod contextReference = code.context().getReference();
     ValueBoxingStatus[] args = null;
     ValueBoxingStatus returnStatus = null;
+    int shift = BooleanUtils.intValue(!code.context().getDefinition().isStatic());
     for (Instruction next : code.instructions()) {
       if (next.isArgument()) {
         ValueBoxingStatus unboxingStatus = analyzeOutput(next.outValue());
         if (unboxingStatus.mayBeUnboxable()) {
           if (args == null) {
             args = new ValueBoxingStatus[contextReference.getArity()];
+            Arrays.fill(args, NOT_UNBOXABLE);
           }
-          args[next.asArgument().getIndex()] = unboxingStatus;
+          args[next.asArgument().getIndex() - shift] = unboxingStatus;
         }
       } else if (next.isReturn()) {
         Return ret = next.asReturn();
@@ -204,12 +226,25 @@
   }
 
   private boolean shouldConsiderForUnboxing(Value value) {
+    return value.getType().isClassType()
+        && shouldConsiderForUnboxing(value.getType().asClassType().getClassType());
+  }
+
+  private boolean shouldConsiderForUnboxing(ProgramMethod method) {
+    if (appView.getKeepInfo().isPinned(method, appView.options())) {
+      return false;
+    }
+    return shouldConsiderForUnboxing(method.getReturnType())
+        || Iterables.any(method.getParameters(), this::shouldConsiderForUnboxing);
+  }
+
+  private boolean shouldConsiderForUnboxing(DexType type) {
     // 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());
+    // Types to consider: Object, Serializable, Comparable, Number.
+    return boxedTypes.contains(type);
   }
 
   // Inputs are values flowing into a method return, an invoke argument or a field write.
@@ -220,11 +255,12 @@
     DexType boxedType = inValue.getType().asClassType().getClassType();
     DexType primitiveType = factory.primitiveToBoxed.inverse().get(boxedType);
     DexMethod boxPrimitiveMethod = factory.getBoxPrimitiveMethod(primitiveType);
-    if (!inValue.isPhi()) {
+    if (!inValue.getAliasedValue().isPhi()) {
       Instruction definition = inValue.getAliasedValue().getDefinition();
       if (definition.isArgument()) {
+        int shift = BooleanUtils.intValue(!context.getDefinition().isStatic());
         return ValueBoxingStatus.with(
-            new MethodArg(definition.asArgument().getIndex(), context.getReference()));
+            new MethodArg(definition.asArgument().getIndex() - shift, context.getReference()));
       }
       if (definition.isInvokeMethod()) {
         if (boxPrimitiveMethod.isIdenticalTo(definition.asInvokeMethod().getInvokedMethod())) {
@@ -304,7 +340,7 @@
       ExecutorService executorService)
       throws ExecutionException {
     Map<DexMethod, MethodBoxingStatusResult> unboxingResult =
-        new NumberUnboxerBoxingStatusResolution().resolve(methodBoxingStatus);
+        new NumberUnboxerBoxingStatusResolution(candidateBoxingStatus).resolve();
     if (unboxingResult.isEmpty()) {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
index eeb1768..8e7c278 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
@@ -35,10 +35,12 @@
 
   @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
-      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
-    RewrittenPrototypeDescription enumUnboxingPrototypeChanges =
-        prototypeChangesPerMethod.getOrDefault(method, RewrittenPrototypeDescription.none());
-    return prototypeChanges.combine(enumUnboxingPrototypeChanges);
+      RewrittenPrototypeDescription previousPrototypeChanges,
+      DexMethod previousMethod,
+      DexMethod newMethod) {
+    RewrittenPrototypeDescription prototypeChanges =
+        prototypeChangesPerMethod.getOrDefault(newMethod, RewrittenPrototypeDescription.none());
+    return previousPrototypeChanges.combine(prototypeChanges);
   }
 
   @Override
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
index f368e52..ff49fe2 100644
--- 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
@@ -113,6 +113,7 @@
     public MethodArg(int parameterIndex, DexMethod method) {
       super(method);
       assert parameterIndex >= 0;
+      assert parameterIndex < method.getArity();
       this.parameterIndex = parameterIndex;
     }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
index 8e60aa7..cc2634a 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 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.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ListUtils;
@@ -380,7 +381,9 @@
     List<KmType> rewrittenSuperTypes = kmClass.getSupertypes();
     for (KotlinTypeInfo superType : superTypes) {
       // Ensure the rewritten super type is not this type.
-      if (clazz.getType() != superType.rewriteType(appView.graphLens())) {
+      DexType rewrittenSuperType =
+          superType.rewriteType(appView.graphLens(), appView.getKotlinMetadataLens());
+      if (clazz.getType() != rewrittenSuperType) {
         rewritten |= superType.rewrite(rewrittenSuperTypes::add, appView);
       } else {
         rewritten = true;
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
index 890d89e..4eb0dca 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
@@ -51,7 +51,7 @@
 
   abstract boolean rewrite(KmTypeVisitor visitor, AppView<?> appView);
 
-  public DexType rewriteType(GraphLens graphLens) {
+  public DexType rewriteType(GraphLens graphLens, GraphLens codeLens) {
     return null;
   }
 
@@ -81,8 +81,8 @@
     }
 
     @Override
-    public DexType rewriteType(GraphLens graphLens) {
-      return type.rewriteType(graphLens);
+    public DexType rewriteType(GraphLens graphLens, GraphLens codeLens) {
+      return type.rewriteType(graphLens, codeLens);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
index ab3b27f..ba82c7d 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
@@ -123,7 +123,7 @@
     forEachApply(annotations, annotation -> annotation::trace, definitionSupplier);
   }
 
-  public DexType rewriteType(GraphLens graphLens) {
-    return classifier.rewriteType(graphLens);
+  public DexType rewriteType(GraphLens graphLens, GraphLens codeLens) {
+    return classifier.rewriteType(graphLens, codeLens);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
index 700b508..92b8c20 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
@@ -162,7 +162,8 @@
     if (!type.isClassType()) {
       return type;
     }
-    DexType rewrittenType = appView.graphLens().lookupClassType(type);
+    DexType rewrittenType =
+        appView.graphLens().lookupClassType(type, appView.getKotlinMetadataLens());
     if (appView.appInfo().hasLiveness()
         && !appView.withLiveness().appInfo().isNonProgramTypeOrLiveProgramType(rewrittenType)) {
       return null;
@@ -183,9 +184,9 @@
     }
   }
 
-  public DexType rewriteType(GraphLens graphLens) {
+  public DexType rewriteType(GraphLens graphLens, GraphLens codeLens) {
     if (known != null && known.isClassType()) {
-      return graphLens.lookupClassType(known);
+      return graphLens.lookupClassType(known, codeLens);
     }
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
deleted file mode 100644
index 2e641f6..0000000
--- a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.optimize;
-
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
-import com.android.tools.r8.graph.lens.FieldLookupResult;
-import com.android.tools.r8.graph.lens.GraphLens;
-import java.util.IdentityHashMap;
-import java.util.Map;
-
-/**
- * This lens is used to populate the rebound field reference during lookup, such that both the
- * non-rebound and rebound field references are available to all descendants of this lens.
- *
- * <p>TODO(b/157616970): All uses of this should be replaced by {@link MemberRebindingIdentityLens}.
- */
-public class FieldRebindingIdentityLens extends DefaultNonIdentityGraphLens {
-
-  private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap;
-
-  private FieldRebindingIdentityLens(
-      Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap,
-      DexItemFactory dexItemFactory,
-      GraphLens previousLens) {
-    super(dexItemFactory, previousLens);
-    this.nonReboundFieldReferenceToDefinitionMap = nonReboundFieldReferenceToDefinitionMap;
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  @Override
-  public boolean hasCodeRewritings() {
-    return false;
-  }
-
-  @Override
-  protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    assert !previous.hasReadCastType();
-    assert !previous.hasReboundReference();
-    return FieldLookupResult.builder(this)
-        .setReference(previous.getReference())
-        .setReboundReference(getReboundFieldReference(previous.getReference()))
-        .build();
-  }
-
-  private DexField getReboundFieldReference(DexField field) {
-    return nonReboundFieldReferenceToDefinitionMap.getOrDefault(field, field);
-  }
-
-  public static class Builder {
-
-    private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap =
-        new IdentityHashMap<>();
-
-    private Builder() {}
-
-    void recordDefinitionForNonReboundFieldReference(
-        DexField nonReboundFieldReference, DexField reboundFieldReference) {
-      nonReboundFieldReferenceToDefinitionMap.put(nonReboundFieldReference, reboundFieldReference);
-    }
-
-    FieldRebindingIdentityLens build(DexItemFactory dexItemFactory) {
-      // This intentionally does not return null when the map is empty. In this case there are no
-      // non-rebound field references, but the member rebinding lens is still needed to populate the
-      // rebound reference during field lookup.
-      return new FieldRebindingIdentityLens(
-          nonReboundFieldReferenceToDefinitionMap, dexItemFactory, GraphLens.getIdentityLens());
-    }
-  }
-}
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 388638c..c177e50 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.AndroidApiLevelUtils.isApiSafeForMemberRebinding;
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
+import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndField;
@@ -16,22 +17,17 @@
 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.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.MethodResolutionResult;
 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.ir.code.InvokeType;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BiForEachable;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Pair;
-import com.android.tools.r8.utils.SetUtils;
-import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.TriConsumer;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Iterables;
@@ -40,9 +36,7 @@
 import java.util.IdentityHashMap;
 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.BiFunction;
 import java.util.function.Function;
 
@@ -64,10 +58,6 @@
     this.lensBuilder = MemberRebindingLens.builder(appView);
   }
 
-  private AppView<AppInfoWithLiveness> appView() {
-    return appView;
-  }
-
   private DexMethod validMemberRebindingTargetForNonProgramMethod(
       DexClassAndMethod resolvedMethod,
       SingleResolutionResult<?> resolutionResult,
@@ -299,8 +289,8 @@
 
     methodsWithContexts.forEach(
         (method, contexts) -> {
-          MethodResolutionResult resolutionResult = resolver.apply(method);
-          if (!resolutionResult.isSingleResolution()) {
+          SingleResolutionResult<?> resolutionResult = resolver.apply(method).asSingleResolution();
+          if (resolutionResult == null) {
             return;
           }
 
@@ -337,7 +327,7 @@
               // If the target class is not public but the targeted method is, we might run into
               // visibility problems when rebinding.
               if (contexts.stream()
-                  .anyMatch(context -> mayNeedBridgeForVisibility(context, resolvedMethod))) {
+                  .anyMatch(context -> mayNeedBridgeForVisibility(context, resolutionResult))) {
                 bridgeMethod =
                     insertBridgeForVisibilityIfNeeded(
                         method, resolvedMethod, initialResolutionHolder, addBridge);
@@ -447,23 +437,11 @@
     return findHolderForInterfaceMethodBridge(superClass.asProgramClass(), iface);
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  private boolean mayNeedBridgeForVisibility(ProgramMethod context, DexClassAndMethod method) {
-    DexType holderType = method.getHolderType();
-    DexClass holder = appView.definitionFor(holderType);
-    if (holder == null) {
-      return false;
-    }
-    ConstraintWithTarget classVisibility =
-        ConstraintWithTarget.deriveConstraint(
-            context, holderType, holder.getAccessFlags(), appView);
-    ConstraintWithTarget methodVisibility =
-        ConstraintWithTarget.deriveConstraint(
-            context, holderType, method.getAccessFlags(), appView);
-    // We may need bridge for visibility if the target class is not visible while the target method
-    // is visible from the calling context.
-    return classVisibility == ConstraintWithTarget.NEVER
-        && methodVisibility != ConstraintWithTarget.NEVER;
+  private boolean mayNeedBridgeForVisibility(
+      ProgramMethod context, SingleResolutionResult<?> resolutionResult) {
+    return resolutionResult.isAccessibleFrom(context, appView).isTrue()
+        && AccessControl.isClassAccessible(resolutionResult.getResolvedHolder(), context, appView)
+            .isPossiblyFalse();
   }
 
   private DexMethod insertBridgeForVisibilityIfNeeded(
@@ -516,141 +494,13 @@
     return null;
   }
 
-  private void recordNonReboundFieldAccesses(ExecutorService executorService)
-      throws ExecutionException {
-    assert verifyFieldAccessCollectionContainsAllNonReboundFieldReferences(executorService);
-    FieldAccessInfoCollection<?> fieldAccessInfoCollection =
-        appView.appInfo().getFieldAccessInfoCollection();
-    fieldAccessInfoCollection.forEach(lensBuilder::recordNonReboundFieldAccesses);
-  }
-
-  public void run(ExecutorService executorService) throws ExecutionException {
+  public void run() throws ExecutionException {
     AppInfoWithLiveness appInfo = appView.appInfo();
     computeMethodRebinding(appInfo.getMethodAccessInfoCollection());
-    recordNonReboundFieldAccesses(executorService);
     appInfo.getFieldAccessInfoCollection().flattenAccessContexts();
     MemberRebindingLens memberRebindingLens = lensBuilder.build();
     appView.setGraphLens(memberRebindingLens);
     eventConsumer.finished(appView, memberRebindingLens);
     appView.notifyOptimizationFinishedForTesting();
   }
-
-  @SuppressWarnings("ReferenceEquality")
-  private boolean verifyFieldAccessCollectionContainsAllNonReboundFieldReferences(
-      ExecutorService executorService) throws ExecutionException {
-    Set<DexField> nonReboundFieldReferences = computeNonReboundFieldReferences(executorService);
-    FieldAccessInfoCollection<?> fieldAccessInfoCollection =
-        appView.appInfo().getFieldAccessInfoCollection();
-    fieldAccessInfoCollection.forEach(
-        info -> {
-          DexField reboundFieldReference = info.getField();
-          info.forEachIndirectAccess(
-              nonReboundFieldReference -> {
-                assert reboundFieldReference != nonReboundFieldReference;
-                assert reboundFieldReference
-                    == appView
-                        .appInfo()
-                        .resolveField(nonReboundFieldReference)
-                        .getResolvedFieldReference();
-                nonReboundFieldReferences.remove(nonReboundFieldReference);
-              });
-        });
-    assert nonReboundFieldReferences.isEmpty();
-    return true;
-  }
-
-  private Set<DexField> computeNonReboundFieldReferences(ExecutorService executorService)
-      throws ExecutionException {
-    Set<DexField> nonReboundFieldReferences = SetUtils.newConcurrentHashSet();
-    ThreadUtils.processItems(
-        appView.appInfo()::forEachMethod,
-        method -> {
-          if (method.getDefinition().hasCode()) {
-            method.registerCodeReferences(
-                new UseRegistry<ProgramMethod>(appView, method) {
-
-                  @Override
-                  public void registerInstanceFieldRead(DexField field) {
-                    registerFieldReference(field);
-                  }
-
-                  @Override
-                  public void registerInstanceFieldWrite(DexField field) {
-                    registerFieldReference(field);
-                  }
-
-                  @Override
-                  public void registerStaticFieldRead(DexField field) {
-                    registerFieldReference(field);
-                  }
-
-                  @Override
-                  public void registerStaticFieldWrite(DexField field) {
-                    registerFieldReference(field);
-                  }
-
-                  @SuppressWarnings("ReferenceEquality")
-                  private void registerFieldReference(DexField field) {
-                    appView()
-                        .appInfo()
-                        .resolveField(field)
-                        .forEachSuccessfulFieldResolutionResult(
-                            resolutionResult -> {
-                              if (resolutionResult.getResolvedField().getReference() != field) {
-                                nonReboundFieldReferences.add(field);
-                              }
-                            });
-                  }
-
-                  @Override
-                  public void registerInitClass(DexType type) {
-                    // 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 registerNewInstance(DexType type) {
-                    // Intentionally empty.
-                  }
-
-                  @Override
-                  public void registerInstanceOf(DexType type) {
-                    // Intentionally empty.
-                  }
-
-                  @Override
-                  public void registerTypeReference(DexType type) {
-                    // Intentionally empty.
-                  }
-                });
-          }
-        },
-        appView.options().getThreadingModule(),
-        executorService);
-    return nonReboundFieldReferences;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 616ac22..0ea0a9a 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -15,8 +15,10 @@
 import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
 import com.android.tools.r8.graph.lens.FieldLookupResult;
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.GraphLensUtils;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.Map;
 
@@ -33,9 +35,9 @@
   private MemberRebindingIdentityLens(
       Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap,
       Map<DexMethod, DexMethod> nonReboundMethodReferenceToDefinitionMap,
-      DexItemFactory dexItemFactory,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
       GraphLens previousLens) {
-    super(dexItemFactory, previousLens);
+    super(appView, previousLens);
     this.nonReboundFieldReferenceToDefinitionMap = nonReboundFieldReferenceToDefinitionMap;
     this.nonReboundMethodReferenceToDefinitionMap = nonReboundMethodReferenceToDefinitionMap;
   }
@@ -77,7 +79,7 @@
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     assert previous.getReboundReference() == null;
-    return MethodLookupResult.builder(this)
+    return MethodLookupResult.builder(this, codeLens)
         .setReference(previous.getReference())
         .setReboundReference(getReboundMethodReference(previous.getReference()))
         .setPrototypeChanges(previous.getPrototypeChanges())
@@ -85,10 +87,9 @@
         .build();
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private DexMethod getReboundMethodReference(DexMethod method) {
     DexMethod rebound = nonReboundMethodReferenceToDefinitionMap.get(method);
-    assert method != rebound;
+    assert method.isNotIdenticalTo(rebound);
     while (rebound != null) {
       method = rebound;
       rebound = nonReboundMethodReferenceToDefinitionMap.get(method);
@@ -130,20 +131,40 @@
                   lens.lookupType(
                       nonReboundFieldReference.getHolderType(), appliedMemberRebindingLens),
                   dexItemFactory);
-          builder.recordNonReboundFieldAccess(
-              rewrittenNonReboundFieldReference, rewrittenReboundFieldReference);
+          if (rewrittenNonReboundFieldReference.isNotIdenticalTo(rewrittenReboundFieldReference)) {
+            builder.recordNonReboundFieldAccess(
+                rewrittenNonReboundFieldReference, rewrittenReboundFieldReference);
+          }
         });
+
+    Deque<NonIdentityGraphLens> lenses = GraphLensUtils.extractNonIdentityLenses(lens);
     nonReboundMethodReferenceToDefinitionMap.forEach(
         (nonReboundMethodReference, reboundMethodReference) -> {
-          DexMethod rewrittenReboundMethodReference =
-              lens.getRenamedMethodSignature(reboundMethodReference, appliedMemberRebindingLens);
+          DexMethod rewrittenReboundMethodReference = reboundMethodReference;
+          for (NonIdentityGraphLens currentLens : lenses) {
+            if (currentLens.isVerticalClassMergerLens()) {
+              // The vertical class merger lens maps merged virtual methods to private methods in
+              // the subclass. Invokes to such virtual methods are mapped to the corresponding
+              // virtual method in the subclass.
+              rewrittenReboundMethodReference =
+                  currentLens
+                      .asVerticalClassMergerLens()
+                      .getNextBridgeMethodSignature(rewrittenReboundMethodReference);
+            } else {
+              rewrittenReboundMethodReference =
+                  currentLens.getNextMethodSignature(rewrittenReboundMethodReference);
+            }
+          }
           DexMethod rewrittenNonReboundMethodReference =
               rewrittenReboundMethodReference.withHolder(
                   lens.lookupType(
                       nonReboundMethodReference.getHolderType(), appliedMemberRebindingLens),
                   dexItemFactory);
-          builder.recordNonReboundMethodAccess(
-              rewrittenNonReboundMethodReference, rewrittenReboundMethodReference);
+          if (rewrittenNonReboundMethodReference.isNotIdenticalTo(
+              rewrittenReboundMethodReference)) {
+            builder.recordNonReboundMethodAccess(
+                rewrittenNonReboundMethodReference, rewrittenReboundMethodReference);
+          }
         });
     return builder.build();
   }
@@ -171,11 +192,13 @@
 
     private void recordNonReboundFieldAccess(
         DexField nonReboundFieldReference, DexField reboundFieldReference) {
+      assert nonReboundFieldReference.isNotIdenticalTo(reboundFieldReference);
       nonReboundFieldReferenceToDefinitionMap.put(nonReboundFieldReference, reboundFieldReference);
     }
 
     private void recordNonReboundMethodAccess(
         DexMethod nonReboundMethodReference, DexMethod reboundMethodReference) {
+      assert nonReboundMethodReference.isNotIdenticalTo(reboundMethodReference);
       nonReboundMethodReferenceToDefinitionMap.put(
           nonReboundMethodReference, reboundMethodReference);
     }
@@ -202,7 +225,7 @@
       return new MemberRebindingIdentityLens(
           nonReboundFieldReferenceToDefinitionMap,
           nonReboundMethodReferenceToDefinitionMap,
-          appView.dexItemFactory(),
+          appView,
           previousLens);
     }
   }
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 331d29e..19e25d2 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLensFactory.java
@@ -37,6 +37,30 @@
    * Otherwise we apply the {@link NonReboundMemberReferencesRegistry} below to all code objects to
    * compute the mapping.
    */
+  public static MemberRebindingIdentityLens rebuild(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService)
+      throws ExecutionException {
+    FieldAccessInfoCollectionImpl mutableFieldAccessInfoCollection =
+        new FieldAccessInfoCollectionImpl(new ConcurrentHashMap<>());
+    MethodAccessInfoCollection.ConcurrentBuilder methodAccessInfoCollectionBuilder =
+        MethodAccessInfoCollection.concurrentBuilder();
+    initializeMemberAccessInfoCollectionsForMemberRebinding(
+        appView,
+        mutableFieldAccessInfoCollection,
+        methodAccessInfoCollectionBuilder,
+        executorService);
+    return create(
+        appView, mutableFieldAccessInfoCollection, methodAccessInfoCollectionBuilder.build());
+  }
+
+  /**
+   * In order to construct an instance of {@link MemberRebindingIdentityLens} we need a mapping from
+   * non-rebound field and method references to their definitions.
+   *
+   * <p>If shrinking or minification is enabled, we retrieve these from {@link AppInfoWithLiveness}.
+   * Otherwise we apply the {@link NonReboundMemberReferencesRegistry} below to all code objects to
+   * compute the mapping.
+   */
   public static MemberRebindingIdentityLens create(
       AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService)
       throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 283581a..6445afd 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -6,17 +6,12 @@
 
 import static com.android.tools.r8.graph.lens.NestedGraphLens.mapVirtualInterfaceInvocationTypes;
 
-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.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
 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.ir.code.InvokeType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collections;
@@ -25,18 +20,12 @@
 
 public class MemberRebindingLens extends DefaultNonIdentityGraphLens {
 
-  private final AppView<AppInfoWithLiveness> appView;
   private final Map<InvokeType, Map<DexMethod, DexMethod>> methodMaps;
-  private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap;
 
   public MemberRebindingLens(
-      AppView<AppInfoWithLiveness> appView,
-      Map<InvokeType, Map<DexMethod, DexMethod>> methodMaps,
-      Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap) {
-    super(appView.dexItemFactory(), appView.graphLens());
-    this.appView = appView;
+      AppView<AppInfoWithLiveness> appView, Map<InvokeType, Map<DexMethod, DexMethod>> methodMaps) {
+    super(appView);
     this.methodMaps = methodMaps;
-    this.nonReboundFieldReferenceToDefinitionMap = nonReboundFieldReferenceToDefinitionMap;
   }
 
   public static Builder builder(AppView<AppInfoWithLiveness> appView) {
@@ -55,16 +44,7 @@
 
   @Override
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    assert !previous.hasReadCastType();
-    assert !previous.hasReboundReference();
-    return FieldLookupResult.builder(this)
-        .setReference(previous.getReference())
-        .setReboundReference(getReboundFieldReference(previous.getReference()))
-        .build();
-  }
-
-  private DexField getReboundFieldReference(DexField field) {
-    return nonReboundFieldReferenceToDefinitionMap.getOrDefault(field, field);
+    return previous;
   }
 
   @Override
@@ -72,16 +52,15 @@
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     Map<DexMethod, DexMethod> methodMap =
         methodMaps.getOrDefault(previous.getType(), Collections.emptyMap());
-    DexMethod newMethod = methodMap.get(previous.getReference());
-    if (newMethod == null) {
-      return previous;
-    }
-    return MethodLookupResult.builder(this)
-        .setReference(newMethod)
+    DexMethod newReboundMethod =
+        methodMap.getOrDefault(previous.getReference(), previous.getReference());
+    return MethodLookupResult.builder(this, codeLens)
+        .setReboundReference(newReboundMethod)
+        .setReference(newReboundMethod)
         .setPrototypeChanges(previous.getPrototypeChanges())
         .setType(
             mapVirtualInterfaceInvocationTypes(
-                appView, newMethod, previous.getReference(), previous.getType()))
+                appView, newReboundMethod, previous.getReference(), previous.getType()))
         .build();
   }
 
@@ -93,33 +72,10 @@
     return getPrevious().isIdentityLensForFields(codeLens);
   }
 
-  public FieldRebindingIdentityLens toRewrittenFieldRebindingLens(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      GraphLens lens,
-      NonIdentityGraphLens appliedMemberRebindingLens) {
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
-    FieldRebindingIdentityLens.Builder builder = FieldRebindingIdentityLens.builder();
-    nonReboundFieldReferenceToDefinitionMap.forEach(
-        (nonReboundFieldReference, reboundFieldReference) -> {
-          DexField rewrittenReboundFieldReference =
-              lens.getRenamedFieldSignature(reboundFieldReference, appliedMemberRebindingLens);
-          DexField rewrittenNonReboundFieldReference =
-              rewrittenReboundFieldReference.withHolder(
-                  lens.lookupType(
-                      nonReboundFieldReference.getHolderType(), appliedMemberRebindingLens),
-                  dexItemFactory);
-          builder.recordDefinitionForNonReboundFieldReference(
-              rewrittenNonReboundFieldReference, rewrittenReboundFieldReference);
-        });
-    return builder.build(dexItemFactory);
-  }
-
   public static class Builder {
 
     private final AppView<AppInfoWithLiveness> appView;
     private final Map<InvokeType, Map<DexMethod, DexMethod>> methodMaps = new IdentityHashMap<>();
-    private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap =
-        new IdentityHashMap<>();
 
     private Builder(AppView<AppInfoWithLiveness> appView) {
       this.appView = appView;
@@ -137,20 +93,8 @@
       methodMap.put(from, to);
     }
 
-    void recordNonReboundFieldAccesses(FieldAccessInfo info) {
-      DexField reboundFieldReference = info.getField();
-      info.forEachIndirectAccess(
-          nonReboundFieldReference ->
-              recordNonReboundFieldAccess(nonReboundFieldReference, reboundFieldReference));
-    }
-
-    private void recordNonReboundFieldAccess(
-        DexField nonReboundFieldReference, DexField reboundFieldReference) {
-      nonReboundFieldReferenceToDefinitionMap.put(nonReboundFieldReference, reboundFieldReference);
-    }
-
     public MemberRebindingLens build() {
-      return new MemberRebindingLens(appView, methodMaps, nonReboundFieldReferenceToDefinitionMap);
+      return new MemberRebindingLens(appView, methodMaps);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
deleted file mode 100644
index 969887e..0000000
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.optimize;
-
-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.DexMethod;
-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.NestedGraphLens;
-import com.android.tools.r8.ir.code.InvokeType;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
-import java.util.Set;
-
-final class PublicizerLens extends NestedGraphLens {
-
-  private final AppView<?> appView;
-  private final Set<DexMethod> publicizedMethods;
-
-  private PublicizerLens(AppView<?> appView, Set<DexMethod> publicizedMethods) {
-    super(appView, EMPTY_FIELD_MAP, EMPTY_METHOD_MAP, EMPTY_TYPE_MAP);
-    this.appView = appView;
-    this.publicizedMethods = publicizedMethods;
-  }
-
-  @Override
-  protected boolean isLegitimateToHaveEmptyMappings() {
-    // This lens does not map any DexItem's at all.
-    // It will just tweak invoke type for publicized methods from invoke-direct to invoke-virtual.
-    return true;
-  }
-
-  @Override
-  public boolean isPublicizerLens() {
-    return true;
-  }
-
-  @Override
-  protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    return previous;
-  }
-
-  @Override
-  public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    if (previous.getType() == InvokeType.DIRECT
-        && publicizedMethods.contains(previous.getReference())) {
-      assert publicizedMethodIsPresentOnHolder(previous.getReference(), context);
-      return MethodLookupResult.builder(this)
-          .setReference(previous.getReference())
-          .setPrototypeChanges(previous.getPrototypeChanges())
-          .setType(InvokeType.VIRTUAL)
-          .build();
-    }
-    return previous;
-  }
-
-  private boolean publicizedMethodIsPresentOnHolder(DexMethod method, DexMethod context) {
-    MethodLookupResult lookup =
-        appView.graphLens().lookupMethod(method, context, InvokeType.VIRTUAL);
-    DexMethod signatureInCurrentWorld = lookup.getReference();
-    DexClass clazz = appView.definitionFor(signatureInCurrentWorld.holder);
-    assert clazz != null;
-    DexEncodedMethod actualEncodedTarget = clazz.lookupVirtualMethod(signatureInCurrentWorld);
-    assert actualEncodedTarget != null;
-    assert actualEncodedTarget.isPublic();
-    return true;
-  }
-
-  static PublicizedLensBuilder createBuilder() {
-    return new PublicizedLensBuilder();
-  }
-
-  static class PublicizedLensBuilder {
-    private final Set<DexMethod> publicizedMethods = Sets.newIdentityHashSet();
-
-    private PublicizedLensBuilder() {}
-
-    public PublicizerLens build(AppView<AppInfoWithLiveness> appView) {
-      if (publicizedMethods.isEmpty()) {
-        return null;
-      }
-      return new PublicizerLens(appView, publicizedMethods);
-    }
-
-    public void add(DexMethod publicizedMethod) {
-      publicizedMethods.add(publicizedMethod);
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
index 8a649e1..81edcbf 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
@@ -56,27 +56,34 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    assert !previous.hasReboundReference();
-    DexMethod newMethod = getNextMethodSignature(previous.getReference());
+    DexMethod newReboundReference = getNextMethodSignature(previous.getReboundReference());
+    assert newReboundReference
+        .getHolderType()
+        .isIdenticalTo(previous.getReboundReference().getHolderType());
+
     InvokeType newInvokeType = previous.getType();
-    if (previous.getType() == InvokeType.DIRECT) {
-      if (publicizedPrivateInterfaceMethods.contains(newMethod)) {
+    if (previous.getType().isDirect()) {
+      if (publicizedPrivateInterfaceMethods.contains(newReboundReference)) {
         newInvokeType = InvokeType.INTERFACE;
-      } else if (publicizedPrivateVirtualMethods.contains(newMethod)) {
+      } else if (publicizedPrivateVirtualMethods.contains(newReboundReference)) {
         newInvokeType = InvokeType.VIRTUAL;
       }
     }
-    if (newInvokeType != previous.getType() || newMethod != previous.getReference()) {
-      return MethodLookupResult.builder(this)
-          .setReference(newMethod)
+
+    if (newInvokeType != previous.getType()
+        || newReboundReference.isNotIdenticalTo(previous.getReboundReference())) {
+      DexMethod newReference =
+          newReboundReference.withHolder(previous.getReference(), dexItemFactory());
+      return MethodLookupResult.builder(this, codeLens)
+          .setReboundReference(newReboundReference)
+          .setReference(newReference)
           .setPrototypeChanges(previous.getPrototypeChanges())
           .setType(newInvokeType)
           .build();
     }
-    return previous;
+    return previous.verify(this, codeLens);
   }
 
   public static class Builder {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
index 06118f6..75c0655 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
@@ -38,13 +38,13 @@
  */
 public class ArgumentPropagatorApplicationFixer extends TreeFixerBase {
 
-  private final AppView<AppInfoWithLiveness> appView;
+  private final AppView<AppInfoWithLiveness> appViewWithLiveness;
   private final ArgumentPropagatorGraphLens graphLens;
 
   public ArgumentPropagatorApplicationFixer(
-      AppView<AppInfoWithLiveness> appView, ArgumentPropagatorGraphLens graphLens) {
-    super(appView);
-    this.appView = appView;
+      AppView<AppInfoWithLiveness> appViewWithLiveness, ArgumentPropagatorGraphLens graphLens) {
+    super(appViewWithLiveness);
+    this.appViewWithLiveness = appViewWithLiveness;
     this.graphLens = graphLens;
   }
 
@@ -105,7 +105,7 @@
           DexEncodedMethod replacement =
               method.toTypeSubstitutedMethodAsInlining(
                   methodReferenceAfterParameterRemoval,
-                  appView.dexItemFactory(),
+                  dexItemFactory,
                   builder -> {
                     if (graphLens.hasPrototypeChanges(methodReferenceAfterParameterRemoval)) {
                       RewrittenPrototypeDescription prototypeChanges =
@@ -138,7 +138,8 @@
               @Override
               public void fixup(
                   DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) {
-                optimizationInfo.fixupAbstractValue(appView, field, graphLens, codeLens);
+                optimizationInfo.fixupAbstractValue(
+                    appViewWithLiveness, field, graphLens, codeLens);
               }
 
               @Override
@@ -147,8 +148,9 @@
                 // Fixup the return value in case the method returns a field that had its signature
                 // changed.
                 optimizationInfo
-                    .fixupAbstractReturnValue(appView, method, graphLens, codeLens)
-                    .fixupInstanceInitializerInfo(appView, graphLens, codeLens, prunedItems);
+                    .fixupAbstractReturnValue(appViewWithLiveness, method, graphLens, codeLens)
+                    .fixupInstanceInitializerInfo(
+                        appViewWithLiveness, graphLens, codeLens, prunedItems);
 
                 // Rewrite the optimization info to account for method signature changes.
                 if (graphLens.hasPrototypeChanges(method.getReference())) {
@@ -156,7 +158,7 @@
                       graphLens.getPrototypeChanges(method.getReference());
                   MethodOptimizationInfoFixer fixer =
                       prototypeChanges.createMethodOptimizationInfoFixer();
-                  optimizationInfo.fixup(appView, method, fixer);
+                  optimizationInfo.fixup(appViewWithLiveness, method, fixer);
                 }
               }
             });
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
index d20c06b..a5a4a61 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
@@ -57,10 +57,9 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
     FieldLookupResult lookupResult = super.internalDescribeLookupField(previous);
-    if (lookupResult.getReference().getType() != previous.getReference().getType()) {
+    if (lookupResult.getReference().getType().isNotIdenticalTo(previous.getReference().getType())) {
       return FieldLookupResult.builder(this)
           .setReboundReference(lookupResult.getReboundReference())
           .setReference(lookupResult.getReference())
@@ -73,15 +72,17 @@
 
   @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
-      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
-    DexMethod previous = getPreviousMethodSignature(method);
-    if (!hasPrototypeChanges(method)) {
+      RewrittenPrototypeDescription prototypeChanges,
+      DexMethod previousMethod,
+      DexMethod newMethod) {
+    DexMethod previous = getPreviousMethodSignature(newMethod);
+    if (!hasPrototypeChanges(newMethod)) {
       return prototypeChanges;
     }
     RewrittenPrototypeDescription newPrototypeChanges =
-        prototypeChanges.combine(getPrototypeChanges(method));
+        prototypeChanges.combine(getPrototypeChanges(newMethod));
     assert previous.getReturnType().isVoidType()
-        || !method.getReturnType().isVoidType()
+        || !newMethod.getReturnType().isVoidType()
         || newPrototypeChanges.hasRewrittenReturnInfo();
     return newPrototypeChanges;
   }
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 5a8e0de..4b4b611 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -114,6 +114,7 @@
                 assert false;
               }
             });
+        methodAccessInfoCollectionModifier.commit(appView);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
index 694096a..166b08e 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import java.util.Set;
 
-class BridgeHoistingLens extends DefaultNonIdentityGraphLens {
+public class BridgeHoistingLens extends DefaultNonIdentityGraphLens {
 
   // Mapping from non-hoisted bridge methods to hoisted bridge methods.
   private final BidirectionalManyToOneMap<DexMethod, DexMethod> bridgeToHoistedBridgeMap;
@@ -18,7 +18,7 @@
   public BridgeHoistingLens(
       AppView<?> appView,
       BidirectionalManyToOneMap<DexMethod, DexMethod> bridgeToHoistedBridgeMap) {
-    super(appView.dexItemFactory(), appView.graphLens());
+    super(appView);
     this.bridgeToHoistedBridgeMap = bridgeToHoistedBridgeMap;
   }
 
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
index 0cbf138..8364488 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraph.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposableCallGraph.java
@@ -87,7 +87,8 @@
     // 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();
+      assert appView.graphLens().isMemberRebindingIdentityLens();
+      assert appView.codeLens() == appView.graphLens().asNonIdentityLens().getPrevious();
 
       for (DexProgramClass clazz : appView.appInfo().classes()) {
         clazz.forEachProgramMethodMatching(
diff --git a/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java b/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
index 74e3046..dd3f5bd 100644
--- a/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
@@ -58,24 +58,28 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   protected MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    DexMethod methodSignature = previous.getReference();
-    DexMethod newMethodSignature = getNextMethodSignature(methodSignature);
-    if (methodSignature == newMethodSignature) {
-      return previous;
+    assert previous.hasReboundReference();
+    DexMethod previousReboundReference = previous.getReboundReference();
+    DexMethod newReboundReference = getNextMethodSignature(previousReboundReference);
+    if (newReboundReference.isIdenticalTo(previousReboundReference)) {
+      return previous.verify(this, codeLens);
     }
-    assert !previous.hasReboundReference()
-        || previous.getReference() == previous.getReboundReference();
-    return MethodLookupResult.builder(this)
+    DexMethod previousReference = previous.getReference();
+    DexMethod newReference =
+        previousReference.isIdenticalTo(previousReboundReference)
+            ? newReboundReference
+            : newReboundReference.withHolder(previousReference.getHolderType(), dexItemFactory());
+    return MethodLookupResult.builder(this, codeLens)
         .setPrototypeChanges(
             previous
                 .getPrototypeChanges()
                 .combine(
                     prototypeChanges.getOrDefault(
-                        newMethodSignature, RewrittenPrototypeDescription.none())))
-        .setReference(newMethodSignature)
+                        newReboundReference, RewrittenPrototypeDescription.none())))
+        .setReboundReference(newReboundReference)
+        .setReference(newReference)
         .setType(previous.getType())
         .build();
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
index 9de23a8..5e40a11 100644
--- a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
@@ -47,11 +47,16 @@
       } while (methodMap.containsKey(newReference));
       boolean holderTypeIsInterface = interfaces.contains(newReference.getHolderType());
       if (previous.getType().isSuper() && holderTypeIsInterface) {
-        return previous;
+        return MethodLookupResult.builder(this, codeLens)
+            .setReboundReference(newReference)
+            .setReference(previous.getReference())
+            .setPrototypeChanges(previous.getPrototypeChanges())
+            .setType(previous.getType())
+            .build();
       }
-      return MethodLookupResult.builder(this)
-          .setReference(newReference)
+      return MethodLookupResult.builder(this, codeLens)
           .setReboundReference(newReference)
+          .setReference(newReference)
           .setPrototypeChanges(previous.getPrototypeChanges())
           .setType(
               holderTypeIsInterface && previous.getType().isVirtual()
@@ -59,7 +64,7 @@
                   : previous.getType())
           .build();
     }
-    return previous;
+    return previous.verify(this, codeLens);
   }
 
   public static class Builder {
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
index e132aed..6f4fb22 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
@@ -17,7 +17,7 @@
 import java.util.Collection;
 import java.util.List;
 
-public abstract class ArtProfileCollection {
+public abstract class ArtProfileCollection implements Iterable<ArtProfile> {
 
   public static ArtProfileCollection createInitialArtProfileCollection(
       AppInfo appInfo, InternalOptions options) {
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 78235d8..77476a2 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
@@ -57,7 +57,7 @@
     assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
     List<DexReference> missing = new ArrayList<>();
     for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
-      if (appView.horizontallyMergedClasses().hasBeenMergedIntoDifferentType(clazz.getType())
+      if (appView.horizontallyMergedClasses().isMergeSource(clazz.getType())
           || (appView.hasVerticallyMergedClasses()
               && appView.getVerticallyMergedClasses().hasBeenMergedIntoSubtype(clazz.getType()))
           || appView.unboxedEnums().isUnboxedEnum(clazz)) {
diff --git a/src/main/java/com/android/tools/r8/profile/art/EmptyArtProfileCollection.java b/src/main/java/com/android/tools/r8/profile/art/EmptyArtProfileCollection.java
index ab478b8..42ab16d 100644
--- a/src/main/java/com/android/tools/r8/profile/art/EmptyArtProfileCollection.java
+++ b/src/main/java/com/android/tools/r8/profile/art/EmptyArtProfileCollection.java
@@ -8,7 +8,9 @@
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.Timing;
+import java.util.Iterator;
 
 public class EmptyArtProfileCollection extends ArtProfileCollection {
 
@@ -31,6 +33,11 @@
   }
 
   @Override
+  public Iterator<ArtProfile> iterator() {
+    return IteratorUtils.empty();
+  }
+
+  @Override
   public NonEmptyArtProfileCollection asNonEmpty() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/profile/art/NonEmptyArtProfileCollection.java b/src/main/java/com/android/tools/r8/profile/art/NonEmptyArtProfileCollection.java
index a4b8450..c98371b 100644
--- a/src/main/java/com/android/tools/r8/profile/art/NonEmptyArtProfileCollection.java
+++ b/src/main/java/com/android/tools/r8/profile/art/NonEmptyArtProfileCollection.java
@@ -17,8 +17,7 @@
 import java.util.List;
 import java.util.function.Function;
 
-public class NonEmptyArtProfileCollection extends ArtProfileCollection
-    implements Iterable<ArtProfile> {
+public class NonEmptyArtProfileCollection extends ArtProfileCollection {
 
   private final List<ArtProfile> artProfiles;
 
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
index b33a217..877df9a 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
@@ -247,7 +247,13 @@
         .forEach(classRuleBuilder -> ruleAdditionsSorted.add(classRuleBuilder.build()));
     methodRuleAdditions
         .values()
-        .forEach(methodRuleBuilder -> ruleAdditionsSorted.add(methodRuleBuilder.build()));
+        .forEach(
+            methodRuleBuilder -> {
+              MethodRule methodRule = methodRuleBuilder.build();
+              if (!methodRuleRemovals.contains(methodRule.getReference())) {
+                ruleAdditionsSorted.add(methodRule);
+              }
+            });
     ruleAdditionsSorted.sort(getRuleComparator());
     ruleAdditionsSorted.forEach(profileBuilder::addRule);
 
diff --git a/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java b/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
index 632fd74..e6406bc 100644
--- a/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
+++ b/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
@@ -42,11 +42,14 @@
   private boolean enableStartupCompletenessCheckForTesting =
       parseSystemPropertyOrDefault("com.android.tools.r8.startup.completenesscheck", false);
 
+  /** When enabled, the startup profile is used to layout the DEX. */
+  private boolean enableStartupLayoutOptimization = true;
+
   /**
-   * When enabled, the layout of the primary dex file will be generated using the startup list,
-   * using {@link com.android.tools.r8.dex.StartupMixedSectionLayoutStrategy}.
+   * When enabled, the mixed section layout of the primary dex file will be generated using the
+   * startup list, using {@link com.android.tools.r8.dex.StartupMixedSectionLayoutStrategy}.
    */
-  private boolean enableStartupLayoutOptimizations =
+  private boolean enableStartupMixedSectionLayoutOptimizations =
       parseSystemPropertyOrDefault("com.android.tools.r8.startup.layout", true);
 
   private String multiStartupDexDistributionStrategyName =
@@ -84,14 +87,18 @@
     return this;
   }
 
-  public boolean isStartupLayoutOptimizationsEnabled() {
-    return enableStartupLayoutOptimizations;
+  public boolean isStartupMixedSectionLayoutOptimizationsEnabled() {
+    return enableStartupMixedSectionLayoutOptimizations;
   }
 
   public boolean isStartupCompletenessCheckForTestingEnabled() {
     return enableStartupCompletenessCheckForTesting;
   }
 
+  public boolean isStartupLayoutOptimizationEnabled() {
+    return enableStartupLayoutOptimization;
+  }
+
   public StartupOptions setEnableStartupCompletenessCheckForTesting() {
     return setEnableStartupCompletenessCheckForTesting(true);
   }
@@ -102,6 +109,12 @@
     return this;
   }
 
+  public StartupOptions setEnableStartupLayoutOptimization(
+      boolean enableStartupLayoutOptimization) {
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
+    return this;
+  }
+
   public String getMultiStartupDexDistributionStrategyName() {
     return multiStartupDexDistributionStrategyName;
   }
diff --git a/src/main/java/com/android/tools/r8/profile/startup/optimization/StartupBoundaryOptimizationUtils.java b/src/main/java/com/android/tools/r8/profile/startup/optimization/StartupBoundaryOptimizationUtils.java
new file mode 100644
index 0000000..e2ffa11
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/profile/startup/optimization/StartupBoundaryOptimizationUtils.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.profile.startup.optimization;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.profile.startup.profile.StartupProfile;
+
+public class StartupBoundaryOptimizationUtils {
+
+  public static boolean isSafeForInlining(
+      ProgramMethod caller,
+      ProgramMethod callee,
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    StartupProfile startupProfile = appView.getStartupProfile();
+    if (startupProfile.isEmpty()
+        || appView.options().getStartupOptions().isStartupBoundaryOptimizationsEnabled()
+        || callee.getOptimizationInfo().forceInline()) {
+      return true;
+    }
+    if (appView.hasLiveness()
+        && appView.withLiveness().appInfo().isAlwaysInlineMethod(callee.getReference())) {
+      return true;
+    }
+    // It is always OK to inline into a non-startup class.
+    if (!startupProfile.isStartupClass(caller.getHolderType())) {
+      return true;
+    }
+    // Otherwise the caller is a startup method or a post-startup method on a non-startup class. In
+    // either case, only allow inlining if the callee is defined on a startup class.
+    return startupProfile.isStartupClass(callee.getHolderType());
+  }
+
+  public static boolean isSafeForVerticalClassMerging(
+      DexProgramClass sourceClass,
+      DexProgramClass targetClass,
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    StartupProfile startupProfile = appView.getStartupProfile();
+    if (startupProfile.isEmpty()
+        || appView.options().getStartupOptions().isStartupBoundaryOptimizationsEnabled()) {
+      return true;
+    }
+    return !startupProfile.isStartupClass(sourceClass.getType())
+        || startupProfile.isStartupClass(targetClass.getType());
+  }
+}
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 f7836d3..3d6fc32 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -164,7 +164,7 @@
             repackagingTreeFixer.fixupClasses(appView.appInfo().classesWithDeterministicOrder()));
     appBuilder.replaceProgramClasses(newProgramClasses);
     RepackagingLens lens = lensBuilder.build(appView, packageMappings);
-    new AnnotationFixer(lens, appView.graphLens()).run(appBuilder.getProgramClasses());
+    new AnnotationFixer(appView, lens).run(appBuilder.getProgramClasses(), executorService);
     return lens;
   }
 
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
index 49eb0a2..59002e8 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
@@ -105,7 +105,7 @@
   }
 
   public void registerFieldAccess(DexField field) {
-    registerMemberAccess(appInfo.resolveField(graphLens.lookupField(field)), false);
+    registerMemberAccess(appInfo.resolveField(graphLens.lookupField(field, codeLens)), false);
   }
 
   public ProgramMethod registerMethodReference(DexMethod method) {
@@ -209,7 +209,8 @@
   @Override
   public void registerInitClass(DexType type) {
     registerMemberAccess(
-        appInfo.resolveField(initClassLens.getInitClassField(graphLens.lookupClassType(type))),
+        appInfo.resolveField(
+            initClassLens.getInitClassField(graphLens.lookupClassType(type, codeLens))),
         false);
   }
 
@@ -262,7 +263,7 @@
 
   @Override
   public void registerNewInstance(DexType type) {
-    registerTypeAccess(graphLens.lookupClassType(type));
+    registerTypeAccess(graphLens.lookupClassType(type, codeLens));
   }
 
   @Override
@@ -286,7 +287,7 @@
 
   @Override
   public void registerInstanceOf(DexType type) {
-    registerTypeAccess(graphLens.lookupType(type));
+    registerTypeAccess(graphLens.lookupType(type, codeLens));
   }
 
   public void registerEnclosingMethodAttribute(EnclosingMethodAttribute enclosingMethodAttribute) {
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
index 65ada3d..3dea441 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.shaking;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
@@ -22,27 +23,37 @@
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
 public class AnnotationFixer {
 
+  private final AppView<?> appView;
   private final GraphLens lens;
   private final GraphLens annotationLens;
 
-  public AnnotationFixer(GraphLens lens, GraphLens annotationLens) {
+  public AnnotationFixer(AppView<?> appView, GraphLens lens) {
+    this.appView = appView;
     this.lens = lens;
-    this.annotationLens = annotationLens;
+    this.annotationLens = appView.graphLens();
   }
 
   private DexType lookupType(DexType type) {
     return lens.lookupType(type, annotationLens);
   }
 
-  public void run(Iterable<DexProgramClass> classes) {
-    for (DexProgramClass clazz : classes) {
-      clazz.setAnnotations(clazz.annotations().rewrite(this::rewriteAnnotation));
-      clazz.forEachMethod(this::processMethod);
-      clazz.forEachField(this::processField);
-    }
+  public void run(Collection<DexProgramClass> classes, ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(
+        classes, this::processClass, appView.options().getThreadingModule(), executorService);
+  }
+
+  private void processClass(DexProgramClass clazz) {
+    clazz.setAnnotations(clazz.annotations().rewrite(this::rewriteAnnotation));
+    clazz.forEachMethod(this::processMethod);
+    clazz.forEachField(this::processField);
   }
 
   private void processMethod(DexEncodedMethod method) {
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 f130874..e222e20 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -59,7 +59,6 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryAPIConverter;
-import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper;
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.repackaging.RepackagingUtils;
 import com.android.tools.r8.shaking.KeepInfo.Joiner;
@@ -608,8 +607,6 @@
     assert definition != null
             || deadProtoTypes.contains(type)
             || getMissingClasses().contains(type)
-            // TODO(b/150693139): Remove these exceptions once fixed.
-            || InterfaceDesugaringSyntheticHelper.isCompanionClassType(type)
             // TODO(b/150736225): Not sure how to remove these.
             || DesugaredLibraryAPIConverter.isVivifiedType(type)
         : "Failed lookup of non-missing type: " + type;
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 30d6e81..4cdfc22 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -27,6 +27,7 @@
 import com.android.tools.r8.errors.InterfaceDesugarMissingTypeDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
+import com.android.tools.r8.features.IsolatedFeatureSplitsChecker;
 import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -90,10 +91,13 @@
 import com.android.tools.r8.graph.analysis.ApiModelAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerCheckCastAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerConstClassAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerExceptionGuardAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerFieldAccessAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerInstanceOfAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerInvokeAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerNewInstanceAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerTypeAccessAnalysis;
 import com.android.tools.r8.graph.analysis.GetArrayOfMissingTypeVerifyErrorWorkaround;
 import com.android.tools.r8.graph.analysis.InvokeVirtualToInterfaceVerifyErrorWorkaround;
 import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis;
@@ -256,6 +260,8 @@
   private final Set<EnqueuerInstanceOfAnalysis> instanceOfAnalyses = new LinkedHashSet<>();
   private final Set<EnqueuerExceptionGuardAnalysis> exceptionGuardAnalyses = new LinkedHashSet<>();
   private final Set<EnqueuerCheckCastAnalysis> checkCastAnalyses = new LinkedHashSet<>();
+  private final Set<EnqueuerConstClassAnalysis> constClassAnalyses = new LinkedHashSet<>();
+  private final Set<EnqueuerNewInstanceAnalysis> newInstanceAnalyses = new LinkedHashSet<>();
 
   // Don't hold a direct pointer to app info (use appView).
   private AppInfoWithClassHierarchy appInfo;
@@ -508,6 +514,7 @@
       }
       appView.withGeneratedMessageLiteBuilderShrinker(
           shrinker -> registerAnalysis(shrinker.createEnqueuerAnalysis()));
+      IsolatedFeatureSplitsChecker.register(appView, this);
       ResourceAccessAnalysis.register(appView, this);
     }
 
@@ -585,11 +592,29 @@
     return this;
   }
 
+  public Enqueuer registerConstClassAnalysis(EnqueuerConstClassAnalysis analysis) {
+    constClassAnalyses.add(analysis);
+    return this;
+  }
+
   public Enqueuer registerExceptionGuardAnalysis(EnqueuerExceptionGuardAnalysis analysis) {
     exceptionGuardAnalyses.add(analysis);
     return this;
   }
 
+  public Enqueuer registerNewInstanceAnalysis(EnqueuerNewInstanceAnalysis analysis) {
+    newInstanceAnalyses.add(analysis);
+    return this;
+  }
+
+  public Enqueuer registerTypeAccessAnalysis(EnqueuerTypeAccessAnalysis analysis) {
+    return registerCheckCastAnalysis(analysis)
+        .registerConstClassAnalysis(analysis)
+        .registerExceptionGuardAnalysis(analysis)
+        .registerInstanceOfAnalysis(analysis)
+        .registerNewInstanceAnalysis(analysis);
+  }
+
   public void setAnnotationRemoverBuilder(AnnotationRemover.Builder annotationRemoverBuilder) {
     this.annotationRemoverBuilder = annotationRemoverBuilder;
   }
@@ -1227,23 +1252,23 @@
   }
 
   void traceCheckCast(DexType type, ProgramMethod currentMethod, boolean ignoreCompatRules) {
-    checkCastAnalyses.forEach(analysis -> analysis.traceCheckCast(type, currentMethod));
-    internalTraceConstClassOrCheckCast(type, currentMethod, ignoreCompatRules);
+    DexClass clazz = internalTraceConstClassOrCheckCast(type, currentMethod, ignoreCompatRules);
+    checkCastAnalyses.forEach(analysis -> analysis.traceCheckCast(type, clazz, currentMethod));
   }
 
   void traceSafeCheckCast(DexType type, ProgramMethod currentMethod) {
-    checkCastAnalyses.forEach(analysis -> analysis.traceSafeCheckCast(type, currentMethod));
-    internalTraceConstClassOrCheckCast(type, currentMethod, true);
+    DexClass clazz = internalTraceConstClassOrCheckCast(type, currentMethod, true);
+    checkCastAnalyses.forEach(analysis -> analysis.traceSafeCheckCast(type, clazz, currentMethod));
   }
 
-  @SuppressWarnings("ReferenceEquality")
   void traceConstClass(
       DexType type,
       ProgramMethod currentMethod,
       ListIterator<? extends CfOrDexInstruction> iterator,
       boolean ignoreCompatRules) {
     handleLockCandidate(type, currentMethod, iterator);
-    internalTraceConstClassOrCheckCast(type, currentMethod, ignoreCompatRules);
+    DexClass clazz = internalTraceConstClassOrCheckCast(type, currentMethod, ignoreCompatRules);
+    constClassAnalyses.forEach(analysis -> analysis.traceConstClass(type, clazz, currentMethod));
   }
 
   private void handleLockCandidate(
@@ -1298,18 +1323,21 @@
     return result;
   }
 
-  private void internalTraceConstClassOrCheckCast(
+  private DexClass internalTraceConstClassOrCheckCast(
       DexType type, ProgramMethod currentMethod, boolean ignoreCompatRules) {
-    DexProgramClass baseClass = resolveBaseType(type, currentMethod);
+    DexClass baseClass = resolveBaseType(type, currentMethod);
     traceTypeReference(type, currentMethod);
     if (!forceProguardCompatibility || ignoreCompatRules) {
-      return;
+      return baseClass;
     }
-    if (baseClass != null) {
+    if (baseClass != null && baseClass.isProgramClass()) {
       // Don't require any constructor, see b/112386012.
+      DexProgramClass baseProgramClass = baseClass.asProgramClass();
       markClassAsInstantiatedWithCompatRule(
-          baseClass, () -> graphReporter.reportCompatInstantiated(baseClass, currentMethod));
+          baseProgramClass,
+          () -> graphReporter.reportCompatInstantiated(baseProgramClass, currentMethod));
     }
+    return baseClass;
   }
 
   void traceRecordFieldValues(DexField[] fields, ProgramMethod currentMethod) {
@@ -1401,14 +1429,16 @@
   }
 
   void traceInstanceOf(DexType type, ProgramMethod currentMethod) {
-    instanceOfAnalyses.forEach(analysis -> analysis.traceInstanceOf(type, currentMethod));
-    resolveBaseType(type, currentMethod);
+    DexClass clazz = resolveBaseType(type, currentMethod);
     traceTypeReference(type, currentMethod);
+    instanceOfAnalyses.forEach(analysis -> analysis.traceInstanceOf(type, clazz, currentMethod));
   }
 
-  void traceExceptionGuard(DexType guard, ProgramMethod currentMethod) {
-    exceptionGuardAnalyses.forEach(analysis -> analysis.traceExceptionGuard(guard, currentMethod));
-    traceTypeReference(guard, currentMethod);
+  void traceExceptionGuard(DexType type, ProgramMethod currentMethod) {
+    DexClass clazz = resolveBaseType(type, currentMethod);
+    traceTypeReference(type, currentMethod);
+    exceptionGuardAnalyses.forEach(
+        analysis -> analysis.traceExceptionGuard(type, clazz, currentMethod));
   }
 
   void traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) {
@@ -1449,8 +1479,10 @@
         methodAccessInfoCollection::registerInvokeDirectInContext, invokedMethod, context)) {
       return;
     }
-    handleInvokeOfDirectTarget(invokedMethod, context, reason);
-    invokeAnalyses.forEach(analysis -> analysis.traceInvokeDirect(invokedMethod, context));
+    MethodResolutionResult resolutionResult =
+        handleInvokeOfDirectTarget(invokedMethod, context, reason);
+    invokeAnalyses.forEach(
+        analysis -> analysis.traceInvokeDirect(invokedMethod, resolutionResult, context));
   }
 
   void traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) {
@@ -1467,8 +1499,8 @@
         methodAccessInfoCollection::registerInvokeInterfaceInContext, method, context)) {
       return;
     }
-    markVirtualMethodAsReachable(method, true, context, keepReason);
-    invokeAnalyses.forEach(analysis -> analysis.traceInvokeInterface(method, context));
+    MethodResolutionResult result = markVirtualMethodAsReachable(method, true, context, keepReason);
+    invokeAnalyses.forEach(analysis -> analysis.traceInvokeInterface(method, result, context));
   }
 
   void traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) {
@@ -1505,21 +1537,20 @@
         methodAccessInfoCollection::registerInvokeStaticInContext, invokedMethod, context)) {
       return;
     }
-    handleInvokeOfStaticTarget(invokedMethod, context, reason);
-    invokeAnalyses.forEach(analysis -> analysis.traceInvokeStatic(invokedMethod, context));
+    MethodResolutionResult resolutionResult =
+        handleInvokeOfStaticTarget(invokedMethod, context, reason);
+    invokeAnalyses.forEach(
+        analysis -> analysis.traceInvokeStatic(invokedMethod, resolutionResult, context));
   }
 
-  @SuppressWarnings("UnusedVariable")
   void traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) {
     // We have to revisit super invokes based on the context they are found in. The same
     // method descriptor will hit different targets, depending on the context it is used in.
-    DexMethod actualTarget = getInvokeSuperTarget(invokedMethod, context);
     if (!registerMethodWithTargetAndContext(
         methodAccessInfoCollection::registerInvokeSuperInContext, invokedMethod, context)) {
       return;
     }
     worklist.enqueueMarkReachableSuperAction(invokedMethod, context);
-    invokeAnalyses.forEach(analysis -> analysis.traceInvokeSuper(invokedMethod, context));
   }
 
   void traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) {
@@ -1546,8 +1577,10 @@
         methodAccessInfoCollection::registerInvokeVirtualInContext, invokedMethod, context)) {
       return;
     }
-    markVirtualMethodAsReachable(invokedMethod, false, context, reason);
-    invokeAnalyses.forEach(analysis -> analysis.traceInvokeVirtual(invokedMethod, context));
+    MethodResolutionResult resolutionResult =
+        markVirtualMethodAsReachable(invokedMethod, false, context, reason);
+    invokeAnalyses.forEach(
+        analysis -> analysis.traceInvokeVirtual(invokedMethod, resolutionResult, context));
   }
 
   void traceNewInstance(DexType type, ProgramMethod context) {
@@ -1559,11 +1592,13 @@
       return;
     }
 
-    traceNewInstance(
-        type,
-        context,
-        InstantiationReason.NEW_INSTANCE_INSTRUCTION,
-        KeepReason.instantiatedIn(context));
+    DexClass clazz =
+        traceNewInstance(
+            type,
+            context,
+            InstantiationReason.NEW_INSTANCE_INSTRUCTION,
+            KeepReason.instantiatedIn(context));
+    newInstanceAnalyses.forEach(analysis -> analysis.traceNewInstance(type, clazz, context));
   }
 
   void traceNewInstanceFromLambda(DexType type, ProgramMethod context) {
@@ -1571,19 +1606,22 @@
         type, context, InstantiationReason.LAMBDA, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private void traceNewInstance(
+  private DexClass traceNewInstance(
       DexType type,
       ProgramMethod context,
       InstantiationReason instantiationReason,
       KeepReason keepReason) {
-    DexProgramClass clazz = getProgramClassOrNull(type, context);
-    if (clazz != null) {
+    DexClass clazz = resolveBaseType(type, context);
+    if (clazz != null && clazz.isProgramClass()) {
+      DexProgramClass programClass = clazz.asProgramClass();
       if (clazz.isAnnotation() || clazz.isInterface()) {
-        markTypeAsLive(clazz, graphReporter.registerClass(clazz, keepReason));
+        markTypeAsLive(programClass, graphReporter.registerClass(programClass, keepReason));
       } else {
-        worklist.enqueueMarkInstantiatedAction(clazz, context, instantiationReason, keepReason);
+        worklist.enqueueMarkInstantiatedAction(
+            programClass, context, instantiationReason, keepReason);
       }
     }
+    return clazz;
   }
 
   void traceInstanceFieldRead(DexField field, ProgramMethod currentMethod) {
@@ -1963,20 +2001,6 @@
         });
   }
 
-  private DexMethod getInvokeSuperTarget(DexMethod method, ProgramMethod currentMethod) {
-    DexClass methodHolderClass = appView.definitionFor(method.holder);
-    if (methodHolderClass != null && methodHolderClass.isInterface()) {
-      return method;
-    }
-    DexProgramClass holderClass = currentMethod.getHolder();
-    if (holderClass.superType == null || holderClass.isInterface()) {
-      // We do not know better or this call is made from an interface.
-      return method;
-    }
-    // Return the invoked method on the supertype.
-    return appView.dexItemFactory().createMethod(holderClass.superType, method.proto, method.name);
-  }
-
   //
   // Actual actions performed.
   //
@@ -2363,13 +2387,12 @@
         appView, annotatedItem, annotation, isLive, annotatedKind, mode);
   }
 
-  private DexProgramClass resolveBaseType(DexType type, ProgramDefinition context) {
+  private DexClass resolveBaseType(DexType type, ProgramDefinition context) {
     if (type.isArrayType()) {
       return resolveBaseType(type.toBaseType(appView.dexItemFactory()), context);
     }
     if (type.isClassType()) {
-      DexProgramClass clazz =
-          asProgramClassOrNull(appView.definitionFor(type, context.getContextClass()));
+      DexClass clazz = appView.definitionFor(type, context.getContextClass());
       if (clazz != null) {
         checkAccess(clazz, context);
       }
@@ -2419,9 +2442,11 @@
         appInfo.resolveMethodLegacy(method, interfaceInvoke);
     methodResolutionResult.visitMethodResolutionResults(
         resolutionResult -> {
-          checkAccess(resolutionResult, context);
-          recordMethodReference(
-              method, resolutionResult.getResolutionPair().asProgramDerivedContext(context));
+          if (!resolutionResult.isArrayCloneMethodResult()) {
+            checkAccess(resolutionResult, context);
+            recordMethodReference(
+                method, resolutionResult.getResolutionPair().asProgramDerivedContext(context));
+          }
         },
         failedResolutionResult -> {
           markFailedMethodResolutionTargets(method, failedResolutionResult, context, reason);
@@ -2430,32 +2455,33 @@
     return methodResolutionResult;
   }
 
-  private void handleInvokeOfStaticTarget(
+  private MethodResolutionResult handleInvokeOfStaticTarget(
       DexMethod reference, ProgramDefinition context, KeepReason reason) {
-    resolveMethod(reference, context, reason)
-        .forEachMethodResolutionResult(
-            resolutionResult -> {
-              if (!resolutionResult.isSingleResolution()) {
-                return;
-              }
-              SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
-              if (resolution.getResolvedHolder().isNotProgramClass()) {
-                return;
-              }
-              DexProgramClass clazz = resolution.getResolvedHolder().asProgramClass();
-              DexEncodedMethod encodedMethod = resolution.getResolvedMethod();
+    MethodResolutionResult resolutionResults = resolveMethod(reference, context, reason);
+    resolutionResults.forEachMethodResolutionResult(
+        resolutionResult -> {
+          if (!resolutionResult.isSingleResolution()) {
+            return;
+          }
+          SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
+          if (resolution.getResolvedHolder().isNotProgramClass()) {
+            return;
+          }
+          DexProgramClass clazz = resolution.getResolvedHolder().asProgramClass();
+          DexEncodedMethod encodedMethod = resolution.getResolvedMethod();
 
-              // We have to mark the resolved method as targeted even if it cannot actually be
-              // invoked to make sure the invocation will keep failing in the appropriate way.
-              ProgramMethod method = new ProgramMethod(clazz, encodedMethod);
-              markMethodAsTargeted(method, reason);
+          // We have to mark the resolved method as targeted even if it cannot actually be
+          // invoked to make sure the invocation will keep failing in the appropriate way.
+          ProgramMethod method = new ProgramMethod(clazz, encodedMethod);
+          markMethodAsTargeted(method, reason);
 
-              // Only mark methods for which invocation will succeed at runtime live.
-              if (encodedMethod.isStatic()) {
-                markDirectAndIndirectClassInitializersAsLive(clazz);
-                markDirectStaticOrConstructorMethodAsLive(method, reason);
-              }
-            });
+          // Only mark methods for which invocation will succeed at runtime live.
+          if (encodedMethod.isStatic()) {
+            markDirectAndIndirectClassInitializersAsLive(clazz);
+            markDirectStaticOrConstructorMethodAsLive(method, reason);
+          }
+        });
+    return resolutionResults;
   }
 
   void markDirectAndIndirectClassInitializersAsLive(DexProgramClass clazz) {
@@ -2559,43 +2585,44 @@
     handleInvokeOfDirectTarget(method, context, reason);
   }
 
-  private void handleInvokeOfDirectTarget(
+  private MethodResolutionResult handleInvokeOfDirectTarget(
       DexMethod reference, ProgramDefinition context, KeepReason reason) {
-    resolveMethod(reference, context, reason)
-        .forEachMethodResolutionResult(
-            resolutionResult -> {
-              if (resolutionResult.isFailedResolution()) {
-                failedMethodResolutionTargets.add(reference);
-                return;
-              }
+    MethodResolutionResult resolutionResults = resolveMethod(reference, context, reason);
+    resolutionResults.forEachMethodResolutionResult(
+        resolutionResult -> {
+          if (resolutionResult.isFailedResolution()) {
+            failedMethodResolutionTargets.add(reference);
+            return;
+          }
 
-              if (!resolutionResult.isSingleResolution()
-                  || !resolutionResult.getResolvedHolder().isProgramClass()) {
-                return;
-              }
+          if (!resolutionResult.isSingleResolution()
+              || !resolutionResult.getResolvedHolder().isProgramClass()) {
+            return;
+          }
 
-              ProgramMethod resolvedMethod =
-                  resolutionResult.asSingleResolution().getResolvedProgramMethod();
+          ProgramMethod resolvedMethod =
+              resolutionResult.asSingleResolution().getResolvedProgramMethod();
 
-              // We have to mark the resolved method as targeted even if it cannot actually be
-              // invoked to make sure the invocation will keep failing in the appropriate way.
-              markMethodAsTargeted(resolvedMethod, reason);
+          // We have to mark the resolved method as targeted even if it cannot actually be
+          // invoked to make sure the invocation will keep failing in the appropriate way.
+          markMethodAsTargeted(resolvedMethod, reason);
 
-              // Only mark methods for which invocation will succeed at runtime live.
-              if (resolvedMethod.getAccessFlags().isStatic()) {
-                return;
-              }
+          // Only mark methods for which invocation will succeed at runtime live.
+          if (resolvedMethod.getAccessFlags().isStatic()) {
+            return;
+          }
 
-              markDirectStaticOrConstructorMethodAsLive(resolvedMethod, reason);
+          markDirectStaticOrConstructorMethodAsLive(resolvedMethod, reason);
 
-              // It is valid to have an invoke-direct instruction in a default interface method that
-              // targets another default method in the same interface. In a class, that would lead
-              // to a verification error. See also testInvokeSpecialToDefaultMethod.
-              if (resolvedMethod.getDefinition().isNonPrivateVirtualMethod()
-                  && virtualMethodsTargetedByInvokeDirect.add(resolvedMethod.getReference())) {
-                worklist.enqueueMarkMethodLiveAction(resolvedMethod, context, reason);
-              }
-            });
+          // It is valid to have an invoke-direct instruction in a default interface method that
+          // targets another default method in the same interface. In a class, that would lead
+          // to a verification error. See also testInvokeSpecialToDefaultMethod.
+          if (resolvedMethod.getDefinition().isNonPrivateVirtualMethod()
+              && virtualMethodsTargetedByInvokeDirect.add(resolvedMethod.getReference())) {
+            worklist.enqueueMarkMethodLiveAction(resolvedMethod, context, reason);
+          }
+        });
+    return resolutionResults;
   }
 
   private void ensureFromLibraryOrThrow(DexType type, DexLibraryClass context) {
@@ -3359,89 +3386,91 @@
     liveTypes.getItems().forEach(consumer);
   }
 
-  private void markVirtualMethodAsReachable(
+  private MethodResolutionResult markVirtualMethodAsReachable(
       DexMethod method, boolean interfaceInvoke, ProgramMethod context, KeepReason reason) {
-    if (method.holder.isArrayType()) {
+    MethodResolutionResult resolutionResults =
+        resolveMethod(method, context, reason, interfaceInvoke);
+    if (method.getHolderType().isArrayType()) {
       // This is an array type, so the actual class will be generated at runtime. We treat this
       // like an invoke on a direct subtype of java.lang.Object that has no further subtypes.
       // As it has no subtypes, it cannot affect liveness of the program we are processing.
       // Ergo, we can ignore it. We need to make sure that the element type is available, though.
-      markTypeAsLive(method.holder, context, reason);
-      return;
+      markTypeAsLive(method.getHolderType(), context, reason);
+      return resolutionResults;
     }
 
-    resolveMethod(method, context, reason, interfaceInvoke)
-        .forEachMethodResolutionResult(
-            resolutionResult -> {
-              if (!resolutionResult.isSingleResolution()) {
-                return;
-              }
-              SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
-              // Note that all virtual methods derived from library methods are kept regardless of
-              // being reachable, so the following only needs to consider reachable targets in the
-              // program.
-              // TODO(b/70160030): Revise this to support tree shaking library methods on
-              //  non-escaping types.
-              DexProgramClass initialResolutionHolder =
-                  resolution.getInitialResolutionHolder().asProgramClass();
-              if (initialResolutionHolder == null) {
-                recordMethodReference(method, context);
-                return;
-              }
+    resolutionResults.forEachMethodResolutionResult(
+        resolutionResult -> {
+          if (!resolutionResult.isSingleResolution()) {
+            return;
+          }
+          SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
+          // Note that all virtual methods derived from library methods are kept regardless of
+          // being reachable, so the following only needs to consider reachable targets in the
+          // program.
+          // TODO(b/70160030): Revise this to support tree shaking library methods on
+          //  non-escaping types.
+          DexProgramClass initialResolutionHolder =
+              resolution.getInitialResolutionHolder().asProgramClass();
+          if (initialResolutionHolder == null) {
+            recordMethodReference(method, context);
+            return;
+          }
 
-              if (resolution.getResolvedHolder().isNotProgramClass()) {
-                // TODO(b/70160030): If the resolution is on a library method, then the keep edge
-                //  needs to go directly to the target method in the program. Thus this method will
-                //  need to ensure that 'reason' is not already reported (eg, must be delayed /
-                //  non-witness) and report that for each possible target edge below.
-                return;
-              }
+          if (resolution.getResolvedHolder().isNotProgramClass()) {
+            // TODO(b/70160030): If the resolution is on a library method, then the keep edge
+            //  needs to go directly to the target method in the program. Thus this method will
+            //  need to ensure that 'reason' is not already reported (eg, must be delayed /
+            //  non-witness) and report that for each possible target edge below.
+            return;
+          }
 
-              DexProgramClass contextHolder = context.getContextClass();
-              // If the method has already been marked, just report the new reason for the resolved
-              // target and save the context to ensure correct lookup of virtual dispatch targets.
-              ResolutionSearchKey resolutionSearchKey =
-                  new ResolutionSearchKey(method, interfaceInvoke);
-              ProgramMethodSet seenContexts =
-                  getReachableVirtualTargets(initialResolutionHolder).get(resolutionSearchKey);
-              if (seenContexts != null) {
-                seenContexts.add(context);
-                graphReporter.registerMethod(resolution.getResolvedMethod(), reason);
-                return;
-              }
+          DexProgramClass contextHolder = context.getContextClass();
+          // If the method has already been marked, just report the new reason for the resolved
+          // target and save the context to ensure correct lookup of virtual dispatch targets.
+          ResolutionSearchKey resolutionSearchKey =
+              new ResolutionSearchKey(method, interfaceInvoke);
+          ProgramMethodSet seenContexts =
+              getReachableVirtualTargets(initialResolutionHolder).get(resolutionSearchKey);
+          if (seenContexts != null) {
+            seenContexts.add(context);
+            graphReporter.registerMethod(resolution.getResolvedMethod(), reason);
+            return;
+          }
 
-              // We have to mark the resolution targeted, even if it does not become live, we
-              // need at least an abstract version of it so that it can be targeted.
-              DexProgramClass resolvedHolder = resolution.getResolvedHolder().asProgramClass();
-              DexEncodedMethod resolvedMethod = resolution.getResolvedMethod();
-              markMethodAsTargeted(new ProgramMethod(resolvedHolder, resolvedMethod), reason);
-              if (resolution.isAccessibleForVirtualDispatchFrom(contextHolder, appView).isFalse()) {
-                // Not accessible from this context, so this call will cause a runtime exception.
-                return;
-              }
+          // We have to mark the resolution targeted, even if it does not become live, we
+          // need at least an abstract version of it so that it can be targeted.
+          DexProgramClass resolvedHolder = resolution.getResolvedHolder().asProgramClass();
+          DexEncodedMethod resolvedMethod = resolution.getResolvedMethod();
+          markMethodAsTargeted(new ProgramMethod(resolvedHolder, resolvedMethod), reason);
+          if (resolution.isAccessibleForVirtualDispatchFrom(contextHolder, appView).isFalse()) {
+            // Not accessible from this context, so this call will cause a runtime exception.
+            return;
+          }
 
-              // The method resolved and is accessible, so currently live overrides become live.
-              reachableVirtualTargets
-                  .computeIfAbsent(initialResolutionHolder, ignoreArgument(HashMap::new))
-                  .computeIfAbsent(resolutionSearchKey, ignoreArgument(ProgramMethodSet::create))
-                  .add(context);
+          // The method resolved and is accessible, so currently live overrides become live.
+          reachableVirtualTargets
+              .computeIfAbsent(initialResolutionHolder, ignoreArgument(HashMap::new))
+              .computeIfAbsent(resolutionSearchKey, ignoreArgument(ProgramMethodSet::create))
+              .add(context);
 
-              resolution
-                  .lookupVirtualDispatchTargets(
-                      contextHolder,
-                      appView,
-                      (type, subTypeConsumer, lambdaConsumer) ->
-                          objectAllocationInfoCollection.forEachInstantiatedSubType(
-                              type, subTypeConsumer, lambdaConsumer, appInfo),
-                      definition -> keepInfo.isPinned(definition, options, appInfo))
-                  .forEach(
-                      target ->
-                          markVirtualDispatchTargetAsLive(
-                              target,
-                              programMethod ->
-                                  graphReporter.reportReachableMethodAsLive(
-                                      resolvedMethod.getReference(), programMethod)));
-            });
+          resolution
+              .lookupVirtualDispatchTargets(
+                  contextHolder,
+                  appView,
+                  (type, subTypeConsumer, lambdaConsumer) ->
+                      objectAllocationInfoCollection.forEachInstantiatedSubType(
+                          type, subTypeConsumer, lambdaConsumer, appInfo),
+                  definition -> keepInfo.isPinned(definition, options, appInfo))
+              .forEach(
+                  target ->
+                      markVirtualDispatchTargetAsLive(
+                          target,
+                          programMethod ->
+                              graphReporter.reportReachableMethodAsLive(
+                                  resolvedMethod.getReference(), programMethod)));
+        });
+    return resolutionResults;
   }
 
   private void markVirtualDispatchTargetAsLive(
@@ -3558,54 +3587,56 @@
   }
 
   // Package protected due to entry point from worklist.
-  void markSuperMethodAsReachable(DexMethod reference, ProgramMethod from) {
-    KeepReason reason = KeepReason.targetedBySuperFrom(from);
-    resolveMethod(reference, from, reason)
-        .forEachMethodResolutionResult(
-            resolutionResult -> {
-              if (!resolutionResult.isSingleResolution()) {
-                return;
-              }
-              SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
-              // If the resolution is in the program, mark it targeted.
-              if (resolution.getResolvedHolder().isProgramClass()) {
-                markMethodAsTargeted(
-                    new ProgramMethod(
-                        resolution.getResolvedHolder().asProgramClass(),
-                        resolution.getResolvedMethod()),
-                    reason);
-              }
-              // If invoke target is invalid (inaccessible or not an instance-method) record it and
-              // stop.
-              DexClassAndMethod target =
-                  resolution.lookupInvokeSuperTarget(from.getHolder(), appView);
-              if (target == null) {
-                failedMethodResolutionTargets.add(resolution.getResolvedMethod().getReference());
-                analyses.forEach(
-                    analyses ->
-                        analyses.notifyFailedMethodResolutionTarget(
-                            resolution.getResolvedMethod(), worklist));
-                return;
-              }
+  void markSuperMethodAsReachable(DexMethod reference, ProgramMethod context) {
+    KeepReason reason = KeepReason.targetedBySuperFrom(context);
+    MethodResolutionResult resolutionResults = resolveMethod(reference, context, reason);
+    resolutionResults.forEachMethodResolutionResult(
+        resolutionResult -> {
+          if (!resolutionResult.isSingleResolution()) {
+            return;
+          }
+          SingleResolutionResult<?> resolution = resolutionResult.asSingleResolution();
+          // If the resolution is in the program, mark it targeted.
+          if (resolution.getResolvedHolder().isProgramClass()) {
+            markMethodAsTargeted(
+                new ProgramMethod(
+                    resolution.getResolvedHolder().asProgramClass(),
+                    resolution.getResolvedMethod()),
+                reason);
+          }
+          // If invoke target is invalid (inaccessible or not an instance-method) record it and
+          // stop.
+          DexClassAndMethod target =
+              resolution.lookupInvokeSuperTarget(context.getHolder(), appView);
+          if (target == null) {
+            failedMethodResolutionTargets.add(resolution.getResolvedMethod().getReference());
+            analyses.forEach(
+                analyses ->
+                    analyses.notifyFailedMethodResolutionTarget(
+                        resolution.getResolvedMethod(), worklist));
+            return;
+          }
 
-              DexProgramClass clazz = target.getHolder().asProgramClass();
-              if (clazz == null) {
-                return;
-              }
+          DexProgramClass clazz = target.getHolder().asProgramClass();
+          if (clazz == null) {
+            return;
+          }
 
-              ProgramMethod method = target.asProgramMethod();
+          ProgramMethod method = target.asProgramMethod();
 
-              if (superInvokeDependencies
-                  .computeIfAbsent(from.getDefinition(), ignore -> ProgramMethodSet.create())
-                  .add(method)) {
-                if (liveMethods.contains(from)) {
-                  markMethodAsTargeted(method, KeepReason.invokedViaSuperFrom(from));
-                  if (!target.getAccessFlags().isAbstract()) {
-                    markVirtualMethodAsLive(method, KeepReason.invokedViaSuperFrom(from));
-                  }
-                }
+          if (superInvokeDependencies
+              .computeIfAbsent(context.getDefinition(), ignore -> ProgramMethodSet.create())
+              .add(method)) {
+            if (liveMethods.contains(context)) {
+              markMethodAsTargeted(method, KeepReason.invokedViaSuperFrom(context));
+              if (!target.getAccessFlags().isAbstract()) {
+                markVirtualMethodAsLive(method, KeepReason.invokedViaSuperFrom(context));
               }
-            });
+            }
+          }
+        });
+    invokeAnalyses.forEach(
+        analysis -> analysis.traceInvokeSuper(reference, resolutionResults, context));
   }
 
   // Returns the set of live types.
@@ -4225,6 +4256,9 @@
     assert fieldAccessInfoCollection.verifyMappingIsOneToOne();
     timing.end();
 
+    // Remove mappings for methods that don't resolve in the method access info collection.
+    methodAccessInfoCollection.removeNonResolving(appView, this);
+
     // Verify all references on the input app before synthesizing definitions.
     assert verifyReferences(appInfo.app());
 
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
index 4a5ffd6..aa6c1f1 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
@@ -202,11 +202,11 @@
   public boolean disallowInliningIntoContext(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       ProgramDefinition context,
-      ProgramMethod method,
-      SyntheticItems synthetics) {
+      ProgramMethod method) {
     if (context.getContextType() == method.getContextType()) {
       return false;
     }
+    SyntheticItems synthetics = appView.getSyntheticItems();
     MainDexGroup mainDexGroupInternal = getMainDexGroupInternal(context, synthetics);
     if (mainDexGroupInternal == MainDexGroup.NOT_IN_MAIN_DEX
         || mainDexGroupInternal == MainDexGroup.MAIN_DEX_DEPENDENCY) {
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 5b6511d..045227d 100644
--- a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.shaking;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -83,22 +84,22 @@
     }
 
     @Override
-    public void traceCheckCast(DexType type, ProgramMethod context) {
+    public void traceCheckCast(DexType type, DexClass clazz, ProgramMethod context) {
       add(type, checkCastTypes);
     }
 
     @Override
-    public void traceSafeCheckCast(DexType type, ProgramMethod context) {
+    public void traceSafeCheckCast(DexType type, DexClass clazz, ProgramMethod context) {
       // Intentionally empty.
     }
 
     @Override
-    public void traceInstanceOf(DexType type, ProgramMethod context) {
+    public void traceInstanceOf(DexType type, DexClass clazz, ProgramMethod context) {
       add(type, instanceOfTypes);
     }
 
     @Override
-    public void traceExceptionGuard(DexType guard, ProgramMethod context) {
+    public void traceExceptionGuard(DexType guard, DexClass clazz, ProgramMethod context) {
       add(guard, exceptionGuardTypes);
     }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 6fe2e59..7c1b22f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -640,7 +640,7 @@
                             equivalences.containsKey(candidateType)
                                 || appView
                                     .horizontallyMergedClasses()
-                                    .hasBeenMergedIntoDifferentType(candidateType));
+                                    .isMergeSource(candidateType));
             equivalences.put(representativeType, group);
           }
         });
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 41c19f5..8222389 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -39,7 +39,6 @@
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.profile.startup.profile.StartupProfile;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
@@ -609,25 +608,17 @@
 
   private SynthesizingContext getSynthesizingContext(
       ProgramDefinition context, AppView<?> appView) {
-    InternalOptions options = appView.options();
     if (appView.hasClassHierarchy()) {
       AppInfoWithClassHierarchy appInfo = appView.appInfoWithClassHierarchy();
-      return getSynthesizingContext(
-          context, appInfo.getClassToFeatureSplitMap(), options, appView.getStartupProfile());
+      return getSynthesizingContext(context, appInfo.getClassToFeatureSplitMap());
     }
     return getSynthesizingContext(
-        context,
-        ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap(),
-        options,
-        StartupProfile.empty());
+        context, ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap());
   }
 
   /** Used to find the synthesizing context for a new synthetic that is about to be created. */
   private SynthesizingContext getSynthesizingContext(
-      ProgramDefinition context,
-      ClassToFeatureSplitMap featureSplits,
-      InternalOptions options,
-      StartupProfile startupProfile) {
+      ProgramDefinition context, ClassToFeatureSplitMap featureSplits) {
     DexType contextType = context.getContextType();
     SyntheticDefinition<?, ?, ?> existingDefinition = pending.definitions.get(contextType);
     if (existingDefinition != null) {
@@ -643,8 +634,7 @@
           .getContext();
     }
     // This context is not nested in an existing synthetic context so create a new "leaf" context.
-    FeatureSplit featureSplit =
-        featureSplits.getFeatureSplit(context, options, startupProfile, this);
+    FeatureSplit featureSplit = featureSplits.getFeatureSplit(context, this);
     return SynthesizingContext.fromNonSyntheticInputContext(context, featureSplit);
   }
 
diff --git a/src/main/java/com/android/tools/r8/threading/ThreadingModule.java b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
index 7e374c4..2ec6382 100644
--- a/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
+++ b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
@@ -5,11 +5,13 @@
 package com.android.tools.r8.threading;
 
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -24,6 +26,7 @@
  * interface they implement must be kept.
  */
 @UsedByReflection(
+    description = "Implementations of this interface are dynamically loaded at runtime",
     kind = KeepItemKind.CLASS_AND_MEMBERS,
     memberAccess = {MemberAccessFlags.PUBLIC})
 public interface ThreadingModule {
@@ -38,29 +41,33 @@
 
   class Loader {
 
-    // Splitting up the names to make reflective identification unlikely.
-    // We explicitly don't want R8 to optimize out the reflective lookup.
-    private static final String PACKAGE = "com.android.tools.r8.threading.providers";
-    private static final String BLOCKING_PROVIDER = "blocking.ThreadingModuleBlockingProvider";
-    private static final String SINGLE_THREADED_PROVIDER =
-        "singlethreaded.ThreadingModuleSingleThreadedProvider";
-    private static final String[] IMPLEMENTATIONS = {BLOCKING_PROVIDER, SINGLE_THREADED_PROVIDER};
+    @UsedByReflection(
+        description = "Prevent analysis any inlining and assumptions of the provider class names",
+        constraints = {KeepConstraint.NEVER_INLINE})
+    private static List<String> getProviderNames() {
+      return ImmutableList.of(
+          "com.android.tools.r8.threading.providers.blocking.ThreadingModuleBlockingProvider",
+          "com.android.tools.r8.threading.providers.singlethreaded.ThreadingModuleSingleThreadedProvider");
+    }
 
     @UsesReflection({
       @KeepTarget(
-          kind = KeepItemKind.CLASS_AND_MEMBERS,
-          className = PACKAGE + "." + "blocking.ThreadingModuleBlockingProvider",
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          className =
+              "com.android.tools.r8.threading.providers."
+                  + "blocking.ThreadingModuleBlockingProvider",
           methodName = "<init>",
           methodParameters = {}),
       @KeepTarget(
-          kind = KeepItemKind.CLASS_AND_MEMBERS,
-          className = PACKAGE + "." + "singlethreaded.ThreadingModuleSingleThreadedProvider",
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          className =
+              "com.android.tools.r8.threading.providers."
+                  + "singlethreaded.ThreadingModuleSingleThreadedProvider",
           methodName = "<init>",
           methodParameters = {})
     })
     public static ThreadingModuleProvider load() {
-      for (String implementation : IMPLEMENTATIONS) {
-        String name = PACKAGE + "." + implementation;
+      for (String name : getProviderNames()) {
         try {
           Class<?> providerClass = Class.forName(name);
           return (ThreadingModuleProvider) providerClass.getDeclaredConstructor().newInstance();
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index f35f32b..8b3a049 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -298,7 +298,7 @@
                     }
                   });
           if (seenMethod.isFalse()) {
-            handleRewrittenMethodReference(rewrittenMethod, (DexClassAndMethod) null);
+            handleRewrittenMethodReference(rewrittenMethod, null);
           }
         }
       }
@@ -368,7 +368,8 @@
                     .forEachFailureDependency(
                         type -> addType(type, referencedFrom),
                         methodCausingFailure ->
-                            handleRewrittenMethodReference(method, methodCausingFailure));
+                            handleRewrittenMethodReference(
+                                method, methodCausingFailure.asDexClassAndMethod(appView)));
                 return;
               }
               seenSingleResult.set();
@@ -379,33 +380,27 @@
               failingResult -> {
                 assert failingResult.isFailedResolution();
                 if (!failingResult.asFailedResolution().hasMethodsCausingError()) {
-                  handleRewrittenMethodReference(method, (DexEncodedMethod) null);
+                  handleRewrittenMethodReference(method, null);
                 }
               });
         }
       }
 
-      private void handleRewrittenMethodReference(
-          DexMethod method, DexClassAndMethod resolvedMethod) {
-        handleRewrittenMethodReference(
-            method, resolvedMethod == null ? null : resolvedMethod.getDefinition());
-      }
-
       @SuppressWarnings("ReferenceEquality")
       private void handleRewrittenMethodReference(
-          DexMethod method, DexEncodedMethod resolvedMethod) {
-        assert resolvedMethod == null
-            || resolvedMethod.getReference().match(method)
-            || DexClass.isSignaturePolymorphicMethod(resolvedMethod, factory);
+          DexMethod method, DexClassAndMethod resolvedMethod) {
         addType(method.getHolderType(), referencedFrom);
         addTypes(method.getParameters(), referencedFrom);
         addType(method.getReturnType(), referencedFrom);
         if (resolvedMethod != null) {
+          DexEncodedMethod definition = resolvedMethod.getDefinition();
+          assert resolvedMethod.getReference().match(method)
+              || resolvedMethod.getHolder().isSignaturePolymorphicMethod(definition, factory);
           if (isTargetType(resolvedMethod.getHolderType())) {
             if (resolvedMethod.getHolderType() != method.getHolderType()) {
               addType(resolvedMethod.getHolderType(), referencedFrom);
             }
-            TracedMethodImpl tracedMethod = new TracedMethodImpl(resolvedMethod, referencedFrom);
+            TracedMethodImpl tracedMethod = new TracedMethodImpl(definition, referencedFrom);
             consumer.acceptMethod(tracedMethod, diagnostics);
             if (resolvedMethod.getAccessFlags().isVisibilityDependingOnPackage()) {
               consumer.acceptPackage(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 8bbf801..adb0992 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -46,7 +46,6 @@
 import com.android.tools.r8.profile.art.ArtProfileProvider;
 import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
 import com.android.tools.r8.profile.startup.StartupProfileProviderUtils;
-import com.android.tools.r8.profile.startup.profile.StartupProfile;
 import com.android.tools.r8.shaking.FilteredClassPath;
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -717,8 +716,7 @@
                           DexType type = options.dexItemFactory().createType(classDescriptor);
                           SyntheticItems syntheticItems = null;
                           FeatureSplit featureSplit =
-                              classToFeatureSplitMap.getFeatureSplit(
-                                  type, options, StartupProfile.empty(), syntheticItems);
+                              classToFeatureSplitMap.getFeatureSplit(type, syntheticItems);
                           if (featureSplit != null && !featureSplit.isBase()) {
                             return featureSplitArchiveOutputStreams.get(featureSplit);
                           }
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 27d7516..176ca67 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.graph.DexType;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -235,4 +236,13 @@
     optionals[ts.length] = Optional.empty();
     return optionals;
   }
+
+  public static boolean any(DexType[] values, Predicate<DexType> predicate) {
+    for (DexType value : values) {
+      if (predicate.test(value)) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
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 b71ad68..e069c30 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -223,6 +223,14 @@
     return false;
   }
 
+  public boolean hasFeatureSplitConfiguration() {
+    return featureSplitConfiguration != null;
+  }
+
+  public FeatureSplitConfiguration getFeatureSplitConfiguration() {
+    return featureSplitConfiguration;
+  }
+
   public boolean hasProguardConfiguration() {
     return proguardConfiguration != null;
   }
@@ -2411,7 +2419,6 @@
     public boolean enableSwitchToIfRewriting = true;
     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;
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 6730a23..00aae47 100644
--- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -44,6 +44,10 @@
     return counter.get();
   }
 
+  public static <T> Iterator<T> empty() {
+    return IterableUtils.<T>empty().iterator();
+  }
+
   public static <T, S extends T> Iterator<S> filter(
       Iterator<? extends T> iterator, Predicate<T> predicate) {
     return new Iterator<S>() {
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 4be2a4e..769905a 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -284,6 +284,15 @@
     return existingMappedRanges == null ? null : last(existingMappedRanges);
   }
 
+  public static <T> boolean all(List<T> items, Predicate<T> predicate) {
+    for (T item : items) {
+      if (predicate.test(item)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public interface ReferenceAndIntConsumer<T> {
     void accept(T item, int index);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
index 0887329..31803ad 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
@@ -36,7 +36,11 @@
   @Override
   public void forEachManyToOneMapping(TriConsumer<? super Set<K>, V, K> consumer) {
     forEachManyToOneMapping(
-        (keys, value) -> consumer.accept(keys, value, getRepresentativeKey(value)));
+        (keys, value) ->
+            consumer.accept(
+                keys,
+                value,
+                hasExplicitRepresentativeKey(value) ? getRepresentativeKey(value) : null));
   }
 
   @Override
@@ -85,7 +89,7 @@
     map.forEachManyToOneMapping(
         (keys, value, representative) -> {
           put(keys, value);
-          if (keys.size() > 1) {
+          if (representative != null) {
             setRepresentative(value, representative);
           }
         });
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
index aca734e..18c7f8b 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
@@ -149,7 +149,7 @@
   }
 
   @Override
-  public void putAll(BidirectionalManyToManyMap<K, V> map) {
+  public void putAll(BidirectionalManyToManyMap<? extends K, ? extends V> map) {
     map.forEach(this::put);
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java
index 569c52e..cb54a3f 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java
@@ -17,6 +17,11 @@
 
   K getKey(V value);
 
+  default K getKeyOrDefault(V value, K defaultValue) {
+    K key = getKey(value);
+    return key != null ? key : defaultValue;
+  }
+
   @Override
   BiMap<K, V> getForwardMap();
 
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureBiMap.java b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureBiMap.java
index 64ce1ec..55a512e 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureBiMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureBiMap.java
@@ -4,11 +4,26 @@
 
 package com.android.tools.r8.utils.collections;
 
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableBiMap;
 
 public class DexMethodSignatureBiMap<T> extends DexMethodSignatureMap<T> {
 
   public DexMethodSignatureBiMap() {
-    super(HashBiMap.create());
+    this(HashBiMap.create());
+  }
+
+  public DexMethodSignatureBiMap(DexMethodSignatureBiMap<T> map) {
+    this(HashBiMap.create(map));
+  }
+
+  public DexMethodSignatureBiMap(BiMap<DexMethodSignature, T> map) {
+    super(map);
+  }
+
+  public static <T> DexMethodSignatureBiMap<T> empty() {
+    return new DexMethodSignatureBiMap<>(ImmutableBiMap.of());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureMap.java b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureMap.java
index 39377e5..8aa569a 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureMap.java
@@ -159,6 +159,10 @@
     return merge(method.getReference(), value, remappingFunction);
   }
 
+  public boolean containsKey(DexEncodedMethod method) {
+    return containsKey(method.getSignature());
+  }
+
   @Override
   public boolean containsKey(Object o) {
     return backing.containsKey(o);
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java
index 05bd5ea..17a59dd 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.utils.collections;
 
+import java.util.Collection;
+import java.util.Map;
 import java.util.Set;
 
 /** Interface that provides mutable access to the implementation of a many-to-one mapping. */
@@ -15,9 +17,21 @@
 
   void put(Iterable<K> key, V value);
 
+  default void putAll(BidirectionalManyToOneMap<K, V> map) {
+    map.forEach(this::put);
+  }
+
+  default void putAll(Map<K, V> map) {
+    map.forEach(this::put);
+  }
+
   V remove(K key);
 
   void removeAll(Iterable<K> keys);
 
   Set<K> removeValue(V value);
+
+  default void removeValues(Collection<V> values) {
+    values.forEach(this::removeValue);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java
index 87a29fa..2732ca4 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java
@@ -4,12 +4,16 @@
 
 package com.android.tools.r8.utils.collections;
 
+import java.util.Map;
+
 /** Interface that provides mutable access to the implementation of a one-to-one mapping. */
 public interface MutableBidirectionalOneToOneMap<K, V> extends BidirectionalOneToOneMap<K, V> {
 
   V put(K key, V value);
 
-  void putAll(BidirectionalManyToManyMap<K, V> map);
+  void putAll(BidirectionalManyToManyMap<? extends K, ? extends V> map);
+
+  void putAll(Map<? extends K, ? extends V> map);
 
   V remove(Object key);
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
index 44b2d2c..bf8a075 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -39,15 +39,12 @@
 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;
@@ -79,7 +76,7 @@
   private final VerticalClassMergerGraphLens.Builder deferredRenamings;
   private final DexItemFactory dexItemFactory;
   private final VerticalClassMergerGraphLens.Builder lensBuilder;
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
+  private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
 
   private final DexProgramClass source;
   private final DexProgramClass target;
@@ -91,15 +88,14 @@
   ClassMerger(
       AppView<AppInfoWithLiveness> appView,
       VerticalClassMergerGraphLens.Builder lensBuilder,
-      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
+      VerticallyMergedClasses.Builder verticallyMergedClassesBuilder,
       DexProgramClass source,
       DexProgramClass target) {
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
     this.appView = appView;
-    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
-    this.dexItemFactory = dexItemFactory;
+    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder();
+    this.dexItemFactory = appView.dexItemFactory();
     this.lensBuilder = lensBuilder;
-    this.mergedClasses = mergedClasses;
+    this.verticallyMergedClassesBuilder = verticallyMergedClassesBuilder;
     this.source = source;
     this.target = target;
   }
@@ -133,7 +129,7 @@
                         availableMethodSignatures.test(candidate)
                             && source.lookupVirtualMethod(candidate) == null);
             add(directMethods, resultingConstructor, MethodSignatureEquivalence.get());
-            blockRedirectionOfSuperCalls(resultingConstructor.getReference());
+            blockRedirectionOfSuperCalls(resultingConstructor);
           } else {
             DexEncodedMethod resultingDirectMethod =
                 renameMethod(
@@ -141,11 +137,8 @@
                     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());
+            deferredRenamings.recordMove(directMethod.getDefinition(), resultingDirectMethod);
+            blockRedirectionOfSuperCalls(resultingDirectMethod);
 
             // 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.
@@ -153,12 +146,10 @@
                 && definition.isPrivate()
                 && AccessControl.isMemberAccessible(directMethod, source, target, appView)
                     .isTrue()) {
-              deferredRenamings.mapVirtualMethodToDirectInType(
-                  directMethod.getReference(),
-                  prototypeChanges ->
-                      new MethodLookupResult(
-                          resultingDirectMethod.getReference(), null, DIRECT, prototypeChanges),
-                  target.getType());
+              // TODO(b/315283465): Add a test for correct rewriting of invoke-super to nest members
+              //  and determine if we need to record something here or not.
+              // deferredRenamings.mapVirtualMethodToDirectInType(
+              //    directMethod.getReference(), target.getType());
             }
           }
         });
@@ -170,15 +161,12 @@
           // 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());
+          deferredRenamings.recordSplit(virtualMethod, shadowedBy, null, null);
 
           // The override now corresponds to the method in the parent, so unset its synthetic flag
           // if the method in the parent is not synthetic.
           if (!virtualMethod.isSyntheticMethod() && shadowedBy.isSyntheticMethod()) {
-            shadowedBy.accessFlags.demoteFromSynthetic();
+            shadowedBy.getAccessFlags().demoteFromSynthetic();
           }
           continue;
         }
@@ -204,17 +192,14 @@
           DexEncodedMethod resultingVirtualMethod =
               renameMethod(virtualMethod, availableMethodSignatures, Rename.NEVER);
           resultingVirtualMethod.setLibraryMethodOverride(virtualMethod.isLibraryMethodOverride());
-          deferredRenamings.map(
-              virtualMethod.getReference(), resultingVirtualMethod.getReference());
-          deferredRenamings.recordMove(
-              virtualMethod.getReference(), resultingVirtualMethod.getReference());
+          deferredRenamings.recordMove(virtualMethod, resultingVirtualMethod);
           add(virtualMethods, resultingVirtualMethod, MethodSignatureEquivalence.get());
           continue;
         }
       }
 
       DexEncodedMethod resultingMethod;
-      if (source.accessFlags.isInterface()) {
+      if (source.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
@@ -233,16 +218,12 @@
                 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
+        // This virtual method could be called directly from a sub class via an invoke-super
+        // instruction. 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);
-        }
+        makePublicFinal(resultingMethod);
       }
 
       add(
@@ -252,18 +233,17 @@
 
       // Record that invoke-super instructions in the target class should be redirected to the
       // newly created direct method.
-      redirectSuperCallsInTarget(virtualMethod, resultingMethod);
-      blockRedirectionOfSuperCalls(resultingMethod.getReference());
+      redirectSuperCallsInTarget(virtualMethod);
 
+      DexEncodedMethod bridge = null;
+      DexEncodedMethod override = shadowedBy;
       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());
+        bridge = shadowedBy = buildBridgeMethod(virtualMethod, resultingMethod);
         add(virtualMethods, shadowedBy, MethodSignatureEquivalence.get());
       }
 
@@ -281,9 +261,7 @@
                                   .getMethodInfo(virtualMethod, source)
                                   .joiner())));
 
-      deferredRenamings.map(virtualMethod.getReference(), shadowedBy.getReference());
-      deferredRenamings.recordMove(
-          virtualMethod.getReference(), resultingMethod.getReference(), resultingMethod.isStatic());
+      deferredRenamings.recordSplit(virtualMethod, override, bridge, resultingMethod);
     }
 
     if (abortMerge) {
@@ -365,7 +343,12 @@
     source.getMethodCollection().clearVirtualMethods();
     source.clearInstanceFields();
     source.clearStaticFields();
-    // Step 5: Record merging.
+    // Step 5: Merge attributes.
+    if (source.isNestHost()) {
+      target.clearNestHost();
+      target.setNestMemberAttributes(source.getNestMembersClassAttributes());
+    }
+    // Step 6: Record merging.
     assert !abortMerge;
     assert GenericSignatureCorrectnessHelper.createForVerification(
             appView, GenericSignatureContextBuilder.createForSingleClass(appView, target))
@@ -537,11 +520,9 @@
     return synthesizedBridges;
   }
 
-  private void redirectSuperCallsInTarget(DexEncodedMethod oldTarget, DexEncodedMethod newTarget) {
+  private void redirectSuperCallsInTarget(DexEncodedMethod oldTarget) {
     DexMethod oldTargetReference = oldTarget.getReference();
-    DexMethod newTargetReference = newTarget.getReference();
-    InvokeType newTargetType = newTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT;
-    if (source.accessFlags.isInterface()) {
+    if (source.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()".
       //
@@ -550,10 +531,7 @@
       // 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);
+          oldTargetReference, oldTarget, target.getType());
     } else {
       // If we merge class B into class C, and class C contains an invocation super.m(), then it
       // is insufficient to rewrite "invoke-super B.m()" to "invoke-{direct,virtual} C.m$B()" (the
@@ -564,18 +542,14 @@
       //
       // We handle this by adding a mapping for [target] and all of its supertypes.
       DexProgramClass holder = target;
-      while (holder != null && holder.isProgramClass()) {
+      while (holder != null) {
         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;
+            appView.appInfo().resolveMethodOnClass(holder, signatureInHolder).isSingleResolution();
         if (resolutionSucceeds) {
           deferredRenamings.mapVirtualMethodToDirectInType(
-              signatureInHolder,
-              prototypeChanges ->
-                  new MethodLookupResult(newTargetReference, null, newTargetType, prototypeChanges),
-              target.type);
+              signatureInHolder, oldTarget, target.type);
         } else {
           break;
         }
@@ -585,22 +559,20 @@
         // 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());
+        Set<DexType> mergedTypes = verticallyMergedClassesBuilder.getSourcesFor(holder);
         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.
+          // TODO(b/315283244): Should not rely on lens for this. Instead precompute this before
+          //  merging any classes.
           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);
+                signatureInType, oldTarget, target.type);
           }
         }
         holder =
@@ -611,7 +583,7 @@
     }
   }
 
-  private void blockRedirectionOfSuperCalls(DexMethod method) {
+  private void blockRedirectionOfSuperCalls(DexEncodedMethod 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
@@ -644,8 +616,7 @@
         || invocationTarget.isNonStaticPrivateMethod();
     SynthesizedBridgeCode code =
         new SynthesizedBridgeCode(
-            newMethod,
-            invocationTarget.getReference(),
+            method.getReference(),
             invocationTarget.isStatic()
                 ? STATIC
                 : (invocationTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT),
@@ -722,8 +693,8 @@
     int i = 0;
     for (DexEncodedField field : sourceFields) {
       DexEncodedField resultingField = renameFieldIfNeeded(field, availableFieldSignatures);
-      existingFieldNames.add(resultingField.getReference().name);
-      deferredRenamings.map(field.getReference(), resultingField.getReference());
+      existingFieldNames.add(resultingField.getName());
+      deferredRenamings.recordMove(field, resultingField);
       result[i] = resultingField;
       i++;
     }
@@ -761,8 +732,7 @@
     DexEncodedMethod result =
         method.toTypeSubstitutedMethodAsInlining(newSignature, dexItemFactory);
     result.getMutableOptimizationInfo().markForceInline();
-    deferredRenamings.map(method.getReference(), result.getReference());
-    deferredRenamings.recordMove(method.getReference(), result.getReference());
+    deferredRenamings.recordMove(method, result);
     // Renamed constructors turn into ordinary private functions. They can be private, as
     // they are only references from their direct subclass, which they were merged into.
     result.getAccessFlags().unsetConstructor();
@@ -842,12 +812,13 @@
     accessFlags.setPrivate();
   }
 
-  private static void makePublic(DexEncodedMethod method) {
+  private static void makePublicFinal(DexEncodedMethod method) {
     MethodAccessFlags accessFlags = method.getAccessFlags();
     assert !accessFlags.isAbstract();
     accessFlags.unsetPrivate();
     accessFlags.unsetProtected();
     accessFlags.setPublic();
+    accessFlags.setFinal();
   }
 
   private void makeStatic(DexEncodedMethod method) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
deleted file mode 100644
index ca22d3d..0000000
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
+++ /dev/null
@@ -1,142 +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.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/ConnectedComponentVerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
new file mode 100644
index 0000000..22316f3c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.verticalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+public class ConnectedComponentVerticalClassMerger {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final Set<DexProgramClass> classesToMerge;
+
+  // 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 VerticallyMergedClasses.Builder verticallyMergedClassesBuilder =
+      VerticallyMergedClasses.builder();
+
+  ConnectedComponentVerticalClassMerger(
+      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> classesToMerge) {
+    this.appView = appView;
+    this.classesToMerge = classesToMerge;
+    this.lensBuilder = new VerticalClassMergerGraphLens.Builder();
+  }
+
+  public boolean isEmpty() {
+    return classesToMerge.isEmpty();
+  }
+
+  public VerticalClassMergerResult.Builder run(ImmediateProgramSubtypingInfo immediateSubtypingInfo)
+      throws ExecutionException {
+    List<DexProgramClass> classesToMergeSorted =
+        ListUtils.sort(classesToMerge, Comparator.comparing(DexProgramClass::getType));
+    for (DexProgramClass clazz : classesToMergeSorted) {
+      mergeClassIfPossible(clazz, immediateSubtypingInfo);
+    }
+    return VerticalClassMergerResult.builder(
+        lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
+  }
+
+  private void mergeClassIfPossible(
+      DexProgramClass sourceClass, ImmediateProgramSubtypingInfo immediateSubtypingInfo)
+      throws ExecutionException {
+    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
+    assert subclasses.size() == 1;
+    DexProgramClass targetClass = ListUtils.first(subclasses);
+    if (verticallyMergedClassesBuilder.isMergeSource(targetClass)
+        || verticallyMergedClassesBuilder.isMergeTarget(sourceClass)) {
+      return;
+    }
+    ClassMerger merger =
+        new ClassMerger(
+            appView, lensBuilder, verticallyMergedClassesBuilder, sourceClass, targetClass);
+    if (merger.merge()) {
+      verticallyMergedClassesBuilder.add(sourceClass, targetClass);
+      // Commit the changes to the graph lens.
+      lensBuilder.merge(merger.getRenamings());
+      synthesizedBridges.addAll(merger.getSynthesizedBridges());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
index c71a51d..cfbc759 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
@@ -20,12 +20,14 @@
 public class IllegalAccessDetector extends UseRegistryWithResult<Boolean, ProgramMethod> {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy;
+  private final GraphLens codeLens;
 
   public IllegalAccessDetector(
       AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy,
       ProgramMethod context) {
     super(appViewWithClassHierarchy, context, false);
     this.appViewWithClassHierarchy = appViewWithClassHierarchy;
+    this.codeLens = context.getDefinition().getCode().getCodeLens(appViewWithClassHierarchy);
   }
 
   protected boolean checkFoundPackagePrivateAccess() {
@@ -43,7 +45,8 @@
   }
 
   private boolean checkFieldReference(DexField field) {
-    return checkRewrittenFieldReference(appViewWithClassHierarchy.graphLens().lookupField(field));
+    return checkRewrittenFieldReference(
+        appViewWithClassHierarchy.graphLens().lookupField(field, codeLens));
   }
 
   private boolean checkRewrittenFieldReference(DexField field) {
@@ -102,16 +105,18 @@
   }
 
   private boolean checkTypeReference(DexType type) {
-    return internalCheckTypeReference(type, appViewWithClassHierarchy.graphLens());
+    return internalCheckTypeReference(type, appViewWithClassHierarchy.graphLens(), codeLens);
   }
 
   private boolean checkRewrittenTypeReference(DexType type) {
-    return internalCheckTypeReference(type, GraphLens.getIdentityLens());
+    return internalCheckTypeReference(
+        type, GraphLens.getIdentityLens(), GraphLens.getIdentityLens());
   }
 
-  private boolean internalCheckTypeReference(DexType type, GraphLens graphLens) {
+  private boolean internalCheckTypeReference(
+      DexType type, GraphLens graphLens, GraphLens codeLens) {
     DexType baseType =
-        graphLens.lookupType(type.toBaseType(appViewWithClassHierarchy.dexItemFactory()));
+        graphLens.lookupType(type.toBaseType(appViewWithClassHierarchy.dexItemFactory()), codeLens);
     if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
       DexClass clazz = appViewWithClassHierarchy.definitionFor(baseType);
       if (clazz == null || !clazz.isPublic()) {
@@ -126,7 +131,7 @@
     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);
+      DexType rewrittenType = appViewWithClassHierarchy.graphLens().lookupType(clazz, codeLens);
       DexField initClassField =
           appViewWithClassHierarchy.initClassLens().getInitClassField(rewrittenType);
       checkRewrittenFieldReference(initClassField);
@@ -138,35 +143,35 @@
   @Override
   public void registerInvokeVirtual(DexMethod method) {
     MethodLookupResult lookup =
-        appViewWithClassHierarchy.graphLens().lookupInvokeVirtual(method, getContext());
+        appViewWithClassHierarchy.graphLens().lookupInvokeVirtual(method, getContext(), codeLens);
     checkRewrittenMethodReference(lookup.getReference(), OptionalBool.FALSE);
   }
 
   @Override
   public void registerInvokeDirect(DexMethod method) {
     MethodLookupResult lookup =
-        appViewWithClassHierarchy.graphLens().lookupInvokeDirect(method, getContext());
+        appViewWithClassHierarchy.graphLens().lookupInvokeDirect(method, getContext(), codeLens);
     checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
   }
 
   @Override
   public void registerInvokeStatic(DexMethod method) {
     MethodLookupResult lookup =
-        appViewWithClassHierarchy.graphLens().lookupInvokeStatic(method, getContext());
+        appViewWithClassHierarchy.graphLens().lookupInvokeStatic(method, getContext(), codeLens);
     checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
   }
 
   @Override
   public void registerInvokeInterface(DexMethod method) {
     MethodLookupResult lookup =
-        appViewWithClassHierarchy.graphLens().lookupInvokeInterface(method, getContext());
+        appViewWithClassHierarchy.graphLens().lookupInvokeInterface(method, getContext(), codeLens);
     checkRewrittenMethodReference(lookup.getReference(), OptionalBool.TRUE);
   }
 
   @Override
   public void registerInvokeSuper(DexMethod method) {
     MethodLookupResult lookup =
-        appViewWithClassHierarchy.graphLens().lookupInvokeSuper(method, getContext());
+        appViewWithClassHierarchy.graphLens().lookupInvokeSuper(method, getContext(), codeLens);
     checkRewrittenMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
   }
 
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java b/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java
new file mode 100644
index 0000000..c69002e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/MergeMayLeadToNoSuchMethodErrorUseRegistry.java
@@ -0,0 +1,64 @@
+// 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.DefaultUseRegistryWithResult;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+class MergeMayLeadToNoSuchMethodErrorUseRegistry
+    extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
+
+  private final AppView<AppInfoWithLiveness> appViewWithLiveness;
+  private final GraphLens graphLens;
+  private final GraphLens codeLens;
+  private final DexProgramClass source;
+
+  MergeMayLeadToNoSuchMethodErrorUseRegistry(
+      AppView<AppInfoWithLiveness> appView, ProgramMethod context, DexProgramClass source) {
+    super(appView, context, Boolean.FALSE);
+    assert context.getHolder().getSuperType().isIdenticalTo(source.getType());
+    this.appViewWithLiveness = appView;
+    this.graphLens = appView.graphLens();
+    this.codeLens = context.getDefinition().getCode().getCodeLens(appView);
+    this.source = source;
+  }
+
+  boolean mayLeadToNoSuchMethodError() {
+    return getResult();
+  }
+
+  /**
+   * Sets the result of this registry to true if (1) it finds an invoke-super instruction that
+   * targets a (default) interface method and (2) the invoke-super instruction does not have a
+   * target when the context of the super class is used.
+   */
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    MethodLookupResult lookupResult = graphLens.lookupInvokeSuper(method, getContext(), codeLens);
+    DexMethod rewrittenMethod = lookupResult.getReference();
+    MethodResolutionResult currentResolutionResult =
+        appViewWithLiveness.appInfo().unsafeResolveMethodDueToDexFormat(rewrittenMethod);
+    DexClassAndMethod currentSuperTarget =
+        currentResolutionResult.lookupInvokeSuperTarget(getContext(), appViewWithLiveness);
+    if (currentSuperTarget == null || !currentSuperTarget.getHolder().isInterface()) {
+      return;
+    }
+
+    MethodResolutionResult parentResolutionResult =
+        appViewWithLiveness.appInfo().resolveMethodOnClass(source, method);
+    DexClassAndMethod parentSuperTarget =
+        parentResolutionResult.lookupInvokeSuperTarget(source, appViewWithLiveness);
+    if (parentSuperTarget == null) {
+      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
deleted file mode 100644
index 7042bd5..0000000
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java
+++ /dev/null
@@ -1,140 +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.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
index 4471ff5..edffcd5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/SynthesizedBridgeCode.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/SynthesizedBridgeCode.java
@@ -1,26 +1,27 @@
 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.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.graph.lens.GraphLens;
 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 InvokeType type;
   private final boolean isInterface;
+  private VerticalClassMergerGraphLens codeLens;
 
-  public SynthesizedBridgeCode(
-      DexMethod method, DexMethod invocationTarget, InvokeType type, boolean isInterface) {
+  public SynthesizedBridgeCode(DexMethod method, InvokeType type, boolean isInterface) {
     this.method = method;
-    this.invocationTarget = invocationTarget;
+    this.invocationTarget = null;
     this.type = type;
     this.isInterface = isInterface;
   }
@@ -44,9 +45,15 @@
   // 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);
+  public void updateMethodSignatures(VerticalClassMergerGraphLens lens) {
+    codeLens = lens;
+    invocationTarget = lens.getNextImplementationMethodSignature(method);
+    method = lens.getNextBridgeMethodSignature(method);
+  }
+
+  @Override
+  public GraphLens getCodeLens(AppView<?> appView) {
+    return codeLens;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
index 0ffde6c..cb482b9 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -5,63 +5,39 @@
 
 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.classmerging.SyntheticArgumentClass;
 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.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
+import com.android.tools.r8.profile.art.ArtProfile;
 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.ConsumerUtils;
 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.ThreadUtils;
 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.android.tools.r8.utils.Timing.TimingMerger;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import com.google.common.collect.Streams;
 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.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -79,74 +55,31 @@
   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() {
+  private Set<DexProgramClass> getPinnedClasses() {
+    Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
+
     // 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<>();
+    List<DexReference> pinnedReferences = new ArrayList<>();
     KeepInfoCollection keepInfo = appView.getKeepInfo();
-    keepInfo.forEachPinnedType(pinnedItems::add, options);
-    keepInfo.forEachPinnedMethod(pinnedItems::add, options);
-    keepInfo.forEachPinnedField(pinnedItems::add, options);
-    extractPinnedItems(pinnedItems);
+    keepInfo.forEachPinnedType(pinnedReferences::add, options);
+    keepInfo.forEachPinnedMethod(pinnedReferences::add, options);
+    keepInfo.forEachPinnedField(pinnedReferences::add, options);
+    extractPinnedClasses(pinnedReferences, pinnedClasses);
 
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (Iterables.any(clazz.methods(), method -> method.getAccessFlags().isNative())) {
-        markClassAsPinned(clazz);
+        markClassAsPinned(clazz, pinnedClasses);
       }
     }
 
@@ -155,358 +88,80 @@
     // 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());
+      markTypeAsPinned(signature.getHolderType(), pinnedClasses);
     }
 
     // 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());
+    extractPinnedClasses(appView.appInfo().getFailedMethodResolutionTargets(), pinnedClasses);
+
+    // The ART profiles may contain method rules that do not exist in the app. These method may
+    // refer to classes that will be vertically merged into their unique subtype, but the vertical
+    // class merger lens will not contain any mappings for the missing methods in the ART profiles.
+    // Therefore, trying to perform a lens lookup on these methods will fail.
+    for (ArtProfile artProfile : appView.getArtProfileCollection()) {
+      artProfile.forEachRule(
+          ConsumerUtils.emptyThrowingConsumer(),
+          methodRule -> {
+            DexMethod method = methodRule.getMethod();
+            if (method.getHolderType().isArrayType()) {
+              return;
+            }
+            DexClass holder =
+                appView.appInfo().definitionForWithoutExistenceAssert(method.getHolderType());
+            if (method.lookupOnClass(holder) == null) {
+              extractPinnedClasses(methodRule.getMethod(), pinnedClasses);
+            }
+          });
+    }
+
+    return pinnedClasses;
   }
 
-  private <T extends DexReference> void extractPinnedItems(Iterable<T> items) {
+  private <T extends DexReference> void extractPinnedClasses(
+      Iterable<T> items, Set<DexProgramClass> pinnedClasses) {
     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);
-        }
-      }
+      extractPinnedClasses(item, pinnedClasses);
     }
   }
 
-  private void markTypeAsPinned(DexType type) {
+  private void extractPinnedClasses(DexReference reference, Set<DexProgramClass> pinnedClasses) {
+    markTypeAsPinned(reference.getContextType(), pinnedClasses);
+    reference.accept(
+        ConsumerUtils.emptyConsumer(),
+        field -> {
+          // Pin the type of the field.
+          markTypeAsPinned(field.getType(), pinnedClasses);
+        },
+        method -> {
+          // Pin 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.
+          for (DexType type : method.getReferencedTypes()) {
+            markTypeAsPinned(type, pinnedClasses);
+          }
+        });
+  }
+
+  private void markTypeAsPinned(DexType type, Set<DexProgramClass> pinnedClasses) {
     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.
+    if (!baseType.isClassType()) {
       return;
     }
 
-    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(baseType));
-    if (clazz != null) {
-      markClassAsPinned(clazz);
+    DexProgramClass clazz =
+        asProgramClassOrNull(appView.appInfo().definitionForWithoutExistenceAssert(baseType));
+    if (clazz != null && !appView.getKeepInfo(clazz).isPinned(options)) {
+      // We check for the case where the type is pinned according to its keep info, so we only need
+      // to add it here if it is not the case.
+      markClassAsPinned(clazz, pinnedClasses);
     }
   }
 
-  private void markClassAsPinned(DexProgramClass clazz) {
+  private void markClassAsPinned(DexProgramClass clazz, Set<DexProgramClass> pinnedClasses) {
     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).isVerticalClassMergingAllowed(options)
-        || pinnedClasses.contains(sourceClass)) {
-      return false;
-    }
-
-    assert sourceClass
-        .traverseProgramMembers(
-            member -> {
-              assert !appView.getKeepInfo(member).isPinned(options);
-              return TraversalContinuation.doContinue();
-            })
-        .shouldContinue();
-
-    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (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 {
@@ -527,59 +182,176 @@
   }
 
   private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Setup");
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
 
-    initializePinnedTypes(); // Must be initialized prior to mergeCandidates.
-    initializeMergeCandidates(immediateSubtypingInfo);
+    // Compute the disjoint class hierarchies for parallel processing.
+    List<Set<DexProgramClass>> connectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
 
-    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));
+    // Remove singleton class hierarchies as they are not subject to vertical class merging.
+    connectedComponents.removeIf(connectedComponent -> connectedComponent.size() == 1);
     timing.end();
 
-    VerticallyMergedClasses verticallyMergedClasses =
-        new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
-    appView.setVerticallyMergedClasses(verticallyMergedClasses);
-    if (verticallyMergedClasses.isEmpty()) {
+    // Apply class merging concurrently in disjoint class hierarchies.
+    VerticalClassMergerResult verticalClassMergerResult =
+        mergeClassesInConnectedComponents(
+            connectedComponents, immediateSubtypingInfo, executorService, timing);
+    appView.setVerticallyMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses());
+    if (verticalClassMergerResult.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);
+    VerticalClassMergerGraphLens lens =
+        runFixup(profileCollectionAdditions, verticalClassMergerResult, executorService, timing);
+    updateKeepInfoForMergedClasses(verticalClassMergerResult);
+    assert verifyGraphLens(lens, verticalClassMergerResult);
+    updateArtProfiles(profileCollectionAdditions, lens, verticalClassMergerResult);
+    appView.rewriteWithLens(lens, executorService, timing);
+    updateKeepInfoForSynthesizedBridges(verticalClassMergerResult);
+    appView.notifyOptimizationFinishedForTesting();
+  }
+
+  private VerticalClassMergerResult mergeClassesInConnectedComponents(
+      List<Set<DexProgramClass>> connectedComponents,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
+        getConnectedComponentMergers(
+            connectedComponents, immediateSubtypingInfo, executorService, timing);
+    return applyConnectedComponentMergers(
+        connectedComponentMergers, immediateSubtypingInfo, executorService, timing);
+  }
+
+  private Collection<ConnectedComponentVerticalClassMerger> getConnectedComponentMergers(
+      List<Set<DexProgramClass>> connectedComponents,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    TimingMerger merger = timing.beginMerger("Compute classes to merge", executorService);
+    List<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
+        new ArrayList<>(connectedComponents.size());
+    Set<DexProgramClass> pinnedClasses = getPinnedClasses();
+    Collection<Timing> timings =
+        ThreadUtils.processItemsWithResults(
+            connectedComponents,
+            connectedComponent -> {
+              Timing threadTiming = Timing.create("Compute classes to merge in component", options);
+              ConnectedComponentVerticalClassMerger connectedComponentMerger =
+                  new VerticalClassMergerPolicyExecutor(appView, pinnedClasses)
+                      .run(connectedComponent, immediateSubtypingInfo);
+              if (!connectedComponentMerger.isEmpty()) {
+                synchronized (connectedComponentMergers) {
+                  connectedComponentMergers.add(connectedComponentMerger);
+                }
+              }
+              threadTiming.end();
+              return threadTiming;
+            },
+            appView.options().getThreadingModule(),
+            executorService);
+    merger.add(timings);
+    merger.end();
+    return connectedComponentMergers;
+  }
+
+  private VerticalClassMergerResult applyConnectedComponentMergers(
+      Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    TimingMerger merger = timing.beginMerger("Merge classes", executorService);
+    VerticalClassMergerResult.Builder verticalClassMergerResult =
+        VerticalClassMergerResult.builder(appView);
+    Collection<Timing> timings =
+        ThreadUtils.processItemsWithResults(
+            connectedComponentMergers,
+            connectedComponentMerger -> {
+              Timing threadTiming = Timing.create("Merge classes in component", options);
+              VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
+                  connectedComponentMerger.run(immediateSubtypingInfo);
+              verticalClassMergerResult.merge(verticalClassMergerComponentResult);
+              threadTiming.end();
+              return threadTiming;
+            },
+            appView.options().getThreadingModule(),
+            executorService);
+    merger.add(timings);
+    merger.end();
+    return verticalClassMergerResult.build();
+  }
+
+  private VerticalClassMergerGraphLens runFixup(
+      ProfileCollectionAdditions profileCollectionAdditions,
+      VerticalClassMergerResult verticalClassMergerResult,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    DexProgramClass deterministicContext =
+        appView
+            .definitionFor(
+                ListUtils.first(
+                    ListUtils.sort(
+                        verticalClassMergerResult.getVerticallyMergedClasses().getTargets(),
+                        Comparator.naturalOrder())))
+            .asProgramClass();
+    SyntheticArgumentClass syntheticArgumentClass =
+        new SyntheticArgumentClass.Builder(appView).build(deterministicContext);
+    VerticalClassMergerGraphLens lens =
+        new VerticalClassMergerTreeFixer(
+                appView,
+                profileCollectionAdditions,
+                syntheticArgumentClass,
+                verticalClassMergerResult)
+            .run(executorService, timing);
+    return lens;
+  }
+
+  private void updateArtProfiles(
+      ProfileCollectionAdditions profileCollectionAdditions,
+      VerticalClassMergerGraphLens verticalClassMergerLens,
+      VerticalClassMergerResult verticalClassMergerResult) {
+    // Include bridges in art profiles.
     if (!profileCollectionAdditions.isNop()) {
+      List<SynthesizedBridgeCode> synthesizedBridges =
+          verticalClassMergerResult.getSynthesizedBridges();
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         profileCollectionAdditions.applyIfContextIsInProfile(
-            lens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
+            verticalClassMergerLens.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.
+  private void updateKeepInfoForMergedClasses(VerticalClassMergerResult verticalClassMergerResult) {
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
     keepInfo.mutate(
         mutator -> {
+          VerticallyMergedClasses verticallyMergedClasses =
+              verticalClassMergerResult.getVerticallyMergedClasses();
+          mutator.removeKeepInfoForMergedClasses(
+              PrunedItems.builder()
+                  .setRemovedClasses(verticallyMergedClasses.getSources())
+                  .build());
+        });
+  }
+
+  private void updateKeepInfoForSynthesizedBridges(
+      VerticalClassMergerResult verticalClassMergerResult) {
+    // Copy keep info to newly synthesized methods.
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    keepInfo.mutate(
+        mutator -> {
+          List<SynthesizedBridgeCode> synthesizedBridges =
+              verticalClassMergerResult.getSynthesizedBridges();
           for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
             ProgramMethod bridge =
                 asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getMethod()));
@@ -592,11 +364,10 @@
             assert false;
           }
         });
-
-    appView.notifyOptimizationFinishedForTesting();
   }
 
-  private boolean verifyGraphLens(VerticalClassMergerGraphLens graphLens) {
+  private boolean verifyGraphLens(
+      VerticalClassMergerGraphLens graphLens, VerticalClassMergerResult verticalClassMergerResult) {
     // 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
@@ -625,6 +396,7 @@
     // pinned, because this rewriting does not affect A.method() in any way.
     assert graphLens.assertPinnedNotModified(appView);
 
+    VerticallyMergedClasses mergedClasses = verticalClassMergerResult.getVerticallyMergedClasses();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       for (DexEncodedMethod encodedMethod : clazz.methods()) {
         DexMethod method = encodedMethod.getReference();
@@ -650,198 +422,10 @@
 
         // 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);
+        assert Streams.stream(method.getReferencedBaseTypes(dexItemFactory))
+            .noneMatch(mergedClasses::hasBeenMergedIntoSubtype);
       }
     }
     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/verticalclassmerging/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index e3ad747..ad653bd 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -4,32 +4,39 @@
 
 package com.android.tools.r8.verticalclassmerging;
 
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.classmerging.ClassMergerGraphLens;
 import com.android.tools.r8.graph.AppView;
+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.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
-import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.ir.conversion.ExtraParameter;
+import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 // This graph lens is instantiated during vertical class merging. The graph lens is context
@@ -56,40 +63,43 @@
 // invocation will hit the same implementation as the original super.m() call.
 //
 // For the invocation "invoke-virtual A.m()" in B.m2, this graph lens will return the method B.m.
-public class VerticalClassMergerGraphLens extends NestedGraphLens {
+public class VerticalClassMergerGraphLens extends ClassMergerGraphLens {
 
-  interface GraphLensLookupResultProvider {
+  private final VerticallyMergedClasses mergedClasses;
+  private final Map<DexType, Map<DexMethod, DexMethod>> contextualSuperToImplementationInContexts;
+  private final BidirectionalOneToOneMap<DexMethod, DexMethod> extraNewMethodSignatures;
 
-    MethodLookupResult get(RewrittenPrototypeDescription prototypeChanges);
-  }
-
-  private final AppView<?> appView;
-
-  private VerticallyMergedClasses mergedClasses;
-  private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
-      contextualVirtualToDirectMethodMaps;
-  private Set<DexMethod> mergedMethods;
-  private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges;
-  private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges;
+  private final Set<DexMethod> mergedMethods;
+  private final Set<DexMethod> staticizedMethods;
 
   private VerticalClassMergerGraphLens(
       AppView<?> appView,
       VerticallyMergedClasses mergedClasses,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
-      Map<DexMethod, DexMethod> methodMap,
-      Set<DexMethod> mergedMethods,
-      Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
-          contextualVirtualToDirectMethodMaps,
+      Map<DexType, Map<DexMethod, DexMethod>> contextualSuperToImplementationInContexts,
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> newMethodSignatures,
-      Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
-      Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges) {
-    super(appView, fieldMap, methodMap, mergedClasses.getBidirectionalMap(), newMethodSignatures);
-    this.appView = appView;
+      BidirectionalOneToOneMap<DexMethod, DexMethod> extraNewMethodSignatures,
+      Set<DexMethod> mergedMethods,
+      Set<DexMethod> staticizedMethods) {
+    super(
+        appView,
+        fieldMap,
+        Collections.emptyMap(),
+        mergedClasses.getBidirectionalMap(),
+        newMethodSignatures);
     this.mergedClasses = mergedClasses;
-    this.contextualVirtualToDirectMethodMaps = contextualVirtualToDirectMethodMaps;
+    this.contextualSuperToImplementationInContexts = contextualSuperToImplementationInContexts;
+    this.extraNewMethodSignatures = extraNewMethodSignatures;
     this.mergedMethods = mergedMethods;
-    this.originalMethodSignaturesForBridges = originalMethodSignaturesForBridges;
-    this.prototypeChanges = prototypeChanges;
+    this.staticizedMethods = staticizedMethods;
+  }
+
+  private boolean isMerged(DexMethod method) {
+    return mergedMethods.contains(method);
+  }
+
+  private boolean isStaticized(DexMethod method) {
+    return staticizedMethods.contains(method);
   }
 
   @Override
@@ -98,11 +108,49 @@
   }
 
   @Override
+  public VerticalClassMergerGraphLens asVerticalClassMergerLens() {
+    return this;
+  }
+
+  @Override
   public DexType getPreviousClassType(DexType type) {
     return type;
   }
 
   @Override
+  public DexField getNextFieldSignature(DexField previous) {
+    DexField field = super.getNextFieldSignature(previous);
+    assert field.verifyReferencedBaseTypesMatches(
+        type -> !mergedClasses.isMergeSource(type), dexItemFactory());
+    return field;
+  }
+
+  @Override
+  public DexMethod getNextMethodSignature(DexMethod previous) {
+    if (extraNewMethodSignatures.containsKey(previous)) {
+      return getNextImplementationMethodSignature(previous);
+    }
+    DexMethod method = super.getNextMethodSignature(previous);
+    assert method.verifyReferencedBaseTypesMatches(
+        type -> !mergedClasses.isMergeSource(type), dexItemFactory());
+    return method;
+  }
+
+  public DexMethod getNextBridgeMethodSignature(DexMethod previous) {
+    DexMethod method = newMethodSignatures.getRepresentativeValueOrDefault(previous, previous);
+    assert method.verifyReferencedBaseTypesMatches(
+        type -> !mergedClasses.isMergeSource(type), dexItemFactory());
+    return method;
+  }
+
+  public DexMethod getNextImplementationMethodSignature(DexMethod previous) {
+    DexMethod method = extraNewMethodSignatures.getRepresentativeValueOrDefault(previous, previous);
+    assert method.verifyReferencedBaseTypesMatches(
+        type -> !mergedClasses.isMergeSource(type), dexItemFactory());
+    return method;
+  }
+
+  @Override
   protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
     Collection<DexType> originalTypes = mergedClasses.getSourcesFor(previous);
     Iterable<DexType> currentType = IterableUtils.singleton(previous);
@@ -113,57 +161,111 @@
   }
 
   @Override
+  protected MethodLookupResult internalLookupMethod(
+      DexMethod reference,
+      DexMethod context,
+      InvokeType type,
+      GraphLens codeLens,
+      LookupMethodContinuation continuation) {
+    if (this == codeLens) {
+      MethodLookupResult lookupResult =
+          MethodLookupResult.builder(this, codeLens)
+              .setReboundReference(reference)
+              .setReference(reference)
+              .setType(type)
+              .build();
+      return continuation.lookupMethod(lookupResult);
+    }
+    return super.internalLookupMethod(reference, context, type, codeLens, continuation);
+  }
+
+  @Override
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     assert context != null || verifyIsContextFreeForMethod(previous.getReference(), codeLens);
     assert context == null || previous.getType() != null;
-    if (previous.getType() == InvokeType.SUPER && !mergedMethods.contains(context)) {
-      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
-          contextualVirtualToDirectMethodMaps.get(context.getHolderType());
-      if (virtualToDirectMethodMap != null) {
-        GraphLensLookupResultProvider result =
-            virtualToDirectMethodMap.get(previous.getReference());
-        if (result != null) {
-          // If the super class A of the enclosing class B (i.e., context.holder())
-          // has been merged into B during vertical class merging, and this invoke-super instruction
-          // was resolving to a method in A, then the target method has been changed to a direct
-          // method and moved into B, so that we need to use an invoke-direct instruction instead of
-          // invoke-super (or invoke-static, if the method was originally a default interface
-          // method).
-          return result.get(previous.getPrototypeChanges());
-        }
-      }
-    }
+    assert previous.hasReboundReference();
     MethodLookupResult lookupResult;
-    DexMethod newMethod = methodMap.apply(previous.getReference());
-    if (newMethod == null) {
-      lookupResult = previous;
-    } else {
+    DexMethod implementationReference = getImplementationTargetForInvokeSuper(previous, context);
+    if (implementationReference != null) {
       lookupResult =
-          MethodLookupResult.builder(this)
-              .setReference(newMethod)
+          MethodLookupResult.builder(this, codeLens)
+              .setReboundReference(implementationReference)
+              .setReference(implementationReference)
               .setPrototypeChanges(
-                  internalDescribePrototypeChanges(previous.getPrototypeChanges(), newMethod))
-              .setType(mapInvocationType(newMethod, previous.getReference(), previous.getType()))
+                  internalDescribePrototypeChanges(
+                      previous.getPrototypeChanges(),
+                      previous.getReboundReference(),
+                      implementationReference))
+              .setType(
+                  isStaticized(implementationReference) ? InvokeType.STATIC : InvokeType.VIRTUAL)
+              .build();
+    } else {
+      DexMethod newReboundReference = previous.getRewrittenReboundReference(newMethodSignatures);
+      assert newReboundReference.verifyReferencedBaseTypesMatches(
+          type -> !mergedClasses.isMergeSource(type), dexItemFactory());
+      DexMethod newReference =
+          previous.getRewrittenReferenceFromRewrittenReboundReference(
+              newReboundReference, this::getNextClassType, dexItemFactory());
+      lookupResult =
+          MethodLookupResult.builder(this, codeLens)
+              .setReboundReference(newReboundReference)
+              .setReference(newReference)
+              .setType(mapInvocationType(newReference, previous.getReference(), previous.getType()))
+              .setPrototypeChanges(
+                  internalDescribePrototypeChanges(
+                      previous.getPrototypeChanges(),
+                      previous.getReboundReference(),
+                      newReboundReference))
               .build();
     }
     assert !appView.testing().enableVerticalClassMergerLensAssertion
         || Streams.stream(lookupResult.getReference().getReferencedBaseTypes(dexItemFactory()))
-            .noneMatch(type -> mergedClasses.hasBeenMergedIntoSubtype(type));
+            .noneMatch(mergedClasses::hasBeenMergedIntoSubtype);
     return lookupResult;
   }
 
+  private DexMethod getImplementationTargetForInvokeSuper(
+      MethodLookupResult previous, DexMethod context) {
+    if (previous.getType().isSuper() && !isMerged(context)) {
+      return contextualSuperToImplementationInContexts
+          .getOrDefault(context.getHolderType(), Collections.emptyMap())
+          .get(previous.getReference());
+    }
+    return null;
+  }
+
   @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
-      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
-    return prototypeChanges.combine(
-        this.prototypeChanges.getOrDefault(method, RewrittenPrototypeDescription.none()));
+      RewrittenPrototypeDescription prototypeChanges,
+      DexMethod previousMethod,
+      DexMethod newMethod) {
+    if (isStaticized(newMethod)) {
+      // The receiver has been added as an explicit argument.
+      assert newMethod.getArity() == previousMethod.getArity() + 1;
+      RewrittenPrototypeDescription isConvertedToStaticMethod =
+          RewrittenPrototypeDescription.createForArgumentsInfo(
+              ArgumentInfoCollection.builder()
+                  .setArgumentInfosSize(newMethod.getParameters().size())
+                  .setIsConvertedToStaticMethod()
+                  .build());
+      return prototypeChanges.combine(isConvertedToStaticMethod);
+    }
+    if (newMethod.getArity() > previousMethod.getArity()) {
+      assert dexItemFactory().isConstructor(previousMethod);
+      RewrittenPrototypeDescription collisionResolution =
+          RewrittenPrototypeDescription.createForExtraParameters(
+              ExtraUnusedNullParameter.computeExtraUnusedNullParameters(previousMethod, newMethod));
+      return prototypeChanges.combine(collisionResolution);
+    }
+    assert newMethod.getArity() == previousMethod.getArity();
+    return prototypeChanges;
   }
 
   @Override
   public DexMethod getPreviousMethodSignature(DexMethod method) {
     return super.getPreviousMethodSignature(
-        originalMethodSignaturesForBridges.getOrDefault(method, method));
+        extraNewMethodSignatures.getRepresentativeKeyOrDefault(method, method));
   }
 
   @Override
@@ -174,8 +276,16 @@
 
   @Override
   protected InvokeType mapInvocationType(
-      DexMethod newMethod, DexMethod originalMethod, InvokeType type) {
-    return mapVirtualInterfaceInvocationTypes(appView, newMethod, originalMethod, type);
+      DexMethod newMethod, DexMethod previousMethod, InvokeType type) {
+    if (isStaticized(newMethod)) {
+      return InvokeType.STATIC;
+    }
+    if (type.isInterface()
+        && mergedClasses.hasInterfaceBeenMergedIntoClass(
+            previousMethod.getHolderType(), newMethod.getHolderType())) {
+      return InvokeType.VIRTUAL;
+    }
+    return type;
   }
 
   @Override
@@ -183,7 +293,7 @@
     if (codeLens == this) {
       return true;
     }
-    return contextualVirtualToDirectMethodMaps.isEmpty()
+    return contextualSuperToImplementationInContexts.isEmpty()
         && getPrevious().isContextFreeForMethods(codeLens);
   }
 
@@ -194,237 +304,227 @@
     }
     assert getPrevious().verifyIsContextFreeForMethod(method, codeLens);
     DexMethod previous = getPrevious().lookupMethod(method, null, null, codeLens).getReference();
-    assert contextualVirtualToDirectMethodMaps.values().stream()
+    assert contextualSuperToImplementationInContexts.values().stream()
         .noneMatch(virtualToDirectMethodMap -> virtualToDirectMethodMap.containsKey(previous));
     return true;
   }
 
-  public static class Builder {
+  public static class Builder
+      extends BuilderBase<VerticalClassMergerGraphLens, VerticallyMergedClasses> {
 
-    private final DexItemFactory dexItemFactory;
-
-    protected final MutableBidirectionalOneToOneMap<DexField, DexField> fieldMap =
+    protected final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
         new BidirectionalOneToOneHashMap<>();
-    protected final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
-    private final ImmutableSet.Builder<DexMethod> mergedMethodsBuilder = ImmutableSet.builder();
-    private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
-        contextualVirtualToDirectMethodMaps = new IdentityHashMap<>();
+    protected final Map<DexField, DexField> pendingNewFieldSignatureUpdates =
+        new IdentityHashMap<>();
+
+    private final Map<DexType, Map<DexMethod, DexMethod>>
+        contextualSuperToImplementationInContexts = new IdentityHashMap<>();
 
     private final MutableBidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod>
         newMethodSignatures = BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
-    private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges =
-        new IdentityHashMap<>();
-    private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges =
+    private final Map<DexMethod, DexMethod> pendingNewMethodSignatureUpdates =
         new IdentityHashMap<>();
 
-    private final Map<DexProto, DexProto> cache = new IdentityHashMap<>();
+    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> extraNewMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
+    private final Map<DexMethod, DexMethod> pendingExtraNewMethodSignatureUpdates =
+        new IdentityHashMap<>();
 
-    Builder(DexItemFactory dexItemFactory) {
-      this.dexItemFactory = dexItemFactory;
+    private final Set<DexMethod> mergedMethods = Sets.newIdentityHashSet();
+    private final Map<DexMethod, DexMethod> pendingMergedMethodUpdates = new IdentityHashMap<>();
+
+    private final Set<DexMethod> staticizedMethods = Sets.newIdentityHashSet();
+    private final Map<DexMethod, DexMethod> pendingStaticizedMethodUpdates =
+        new IdentityHashMap<>();
+
+    static Builder createBuilderForFixup(VerticalClassMergerResult verticalClassMergerResult) {
+      return verticalClassMergerResult.getLensBuilder();
     }
 
-    @SuppressWarnings("ReferenceEquality")
-    static Builder createBuilderForFixup(Builder builder, VerticallyMergedClasses mergedClasses) {
-      Builder newBuilder = new Builder(builder.dexItemFactory);
-      builder.fieldMap.forEach(
-          (key, value) ->
-              newBuilder.map(
-                  key, builder.getFieldSignatureAfterClassMerging(value, mergedClasses)));
-      for (Map.Entry<DexMethod, DexMethod> entry : builder.methodMap.entrySet()) {
-        newBuilder.map(
-            entry.getKey(),
-            builder.getMethodSignatureAfterClassMerging(entry.getValue(), mergedClasses));
-      }
-      for (DexMethod method : builder.mergedMethodsBuilder.build()) {
-        newBuilder.markMethodAsMerged(
-            builder.getMethodSignatureAfterClassMerging(method, mergedClasses));
-      }
-      for (Map.Entry<DexType, Map<DexMethod, GraphLensLookupResultProvider>> entry :
-          builder.contextualVirtualToDirectMethodMaps.entrySet()) {
-        DexType context = entry.getKey();
-        assert context == builder.getTypeAfterClassMerging(context, mergedClasses);
-        for (Map.Entry<DexMethod, GraphLensLookupResultProvider> innerEntry :
-            entry.getValue().entrySet()) {
-          DexMethod from = innerEntry.getKey();
-          MethodLookupResult rewriting =
-              innerEntry.getValue().get(RewrittenPrototypeDescription.none());
-          DexMethod to =
-              builder.getMethodSignatureAfterClassMerging(rewriting.getReference(), mergedClasses);
-          newBuilder.mapVirtualMethodToDirectInType(
-              from,
-              prototypeChanges ->
-                  new MethodLookupResult(to, null, rewriting.getType(), prototypeChanges),
-              context);
-        }
-      }
-      builder.newMethodSignatures.forEachManyToOneMapping(
-          (originalMethodSignatures, renamedMethodSignature, representative) -> {
-            DexMethod methodSignatureAfterClassMerging =
-                builder.getMethodSignatureAfterClassMerging(renamedMethodSignature, mergedClasses);
-            newBuilder.newMethodSignatures.put(
-                originalMethodSignatures, methodSignatureAfterClassMerging);
-            if (originalMethodSignatures.size() > 1) {
-              newBuilder.newMethodSignatures.setRepresentative(
-                  methodSignatureAfterClassMerging, representative);
+    @Override
+    public void addExtraParameters(
+        DexMethod from, DexMethod to, List<? extends ExtraParameter> extraParameters) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void commitPendingUpdates() {
+      // Commit new field signatures.
+      newFieldSignatures.putAll(pendingNewFieldSignatureUpdates);
+      pendingNewFieldSignatureUpdates.clear();
+
+      // Commit new method signatures.
+      Map<DexMethod, DexMethod> newMethodSignatureUpdates = new IdentityHashMap<>();
+      Map<DexMethod, DexMethod> newMethodSignatureRepresentativeUpdates = new IdentityHashMap<>();
+      pendingNewMethodSignatureUpdates.forEach(
+          (from, to) -> {
+            Set<DexMethod> originalMethodSignatures = newMethodSignatures.getKeys(from);
+            if (originalMethodSignatures.isEmpty()) {
+              newMethodSignatureUpdates.put(from, to);
+            } else {
+              for (DexMethod originalMethodSignature : originalMethodSignatures) {
+                newMethodSignatureUpdates.put(originalMethodSignature, to);
+              }
+              if (newMethodSignatures.hasExplicitRepresentativeKey(from)) {
+                assert originalMethodSignatures.size() > 1;
+                newMethodSignatureRepresentativeUpdates.put(
+                    to, newMethodSignatures.getRepresentativeKey(from));
+              } else {
+                assert originalMethodSignatures.size() == 1;
+              }
             }
           });
-      for (Map.Entry<DexMethod, DexMethod> entry :
-          builder.originalMethodSignaturesForBridges.entrySet()) {
-        newBuilder.recordCreationOfBridgeMethod(
-            entry.getValue(),
-            builder.getMethodSignatureAfterClassMerging(entry.getKey(), mergedClasses));
-      }
-      builder.prototypeChanges.forEach(
-          (method, prototypeChangesForMethod) ->
-              newBuilder.prototypeChanges.put(
-                  builder.getMethodSignatureAfterClassMerging(method, mergedClasses),
-                  prototypeChangesForMethod));
-      return newBuilder;
+      newMethodSignatures.removeValues(pendingNewMethodSignatureUpdates.keySet());
+      newMethodSignatures.putAll(newMethodSignatureUpdates);
+      newMethodSignatureRepresentativeUpdates.forEach(
+          (value, representative) -> {
+            assert newMethodSignatures.getKeys(value).size() > 1;
+            newMethodSignatures.setRepresentative(value, representative);
+          });
+      pendingNewMethodSignatureUpdates.clear();
+
+      // Commit extra new method signatures.
+      extraNewMethodSignatures.putAll(pendingExtraNewMethodSignatureUpdates);
+      pendingExtraNewMethodSignatureUpdates.clear();
+
+      // Commit merged methods.
+      mergedMethods.removeAll(pendingMergedMethodUpdates.keySet());
+      mergedMethods.addAll(pendingMergedMethodUpdates.values());
+      pendingMergedMethodUpdates.clear();
+
+      // Commit staticized methods.
+      staticizedMethods.removeAll(pendingStaticizedMethodUpdates.keySet());
+      staticizedMethods.addAll(pendingStaticizedMethodUpdates.values());
+      pendingStaticizedMethodUpdates.clear();
     }
 
+    @Override
+    public void fixupField(DexField oldFieldSignature, DexField newFieldSignature) {
+      DexField originalFieldSignature =
+          newFieldSignatures.getKeyOrDefault(oldFieldSignature, oldFieldSignature);
+      pendingNewFieldSignatureUpdates.put(originalFieldSignature, newFieldSignature);
+    }
+
+    @Override
+    public void fixupMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      if (extraNewMethodSignatures.containsValue(oldMethodSignature)) {
+        DexMethod originalMethodSignature = extraNewMethodSignatures.getKey(oldMethodSignature);
+        pendingExtraNewMethodSignatureUpdates.put(originalMethodSignature, newMethodSignature);
+      } else {
+        pendingNewMethodSignatureUpdates.put(oldMethodSignature, newMethodSignature);
+      }
+
+      if (mergedMethods.contains(oldMethodSignature)) {
+        pendingMergedMethodUpdates.put(oldMethodSignature, newMethodSignature);
+      }
+
+      if (staticizedMethods.contains(oldMethodSignature)) {
+        pendingStaticizedMethodUpdates.put(oldMethodSignature, newMethodSignature);
+      }
+    }
+
+    public void fixupContextualVirtualToDirectMethodMaps() {
+      for (Entry<DexType, Map<DexMethod, DexMethod>> entry :
+          contextualSuperToImplementationInContexts.entrySet()) {
+        for (Entry<DexMethod, DexMethod> innerEntry : entry.getValue().entrySet()) {
+          DexMethod virtualMethod = innerEntry.getValue();
+          DexMethod implementationMethod = extraNewMethodSignatures.get(virtualMethod);
+          assert implementationMethod != null;
+          innerEntry.setValue(implementationMethod);
+        }
+      }
+    }
+
+    @Override
+    public Set<DexMethod> getOriginalMethodReferences(DexMethod method) {
+      if (extraNewMethodSignatures.containsValue(method)) {
+        return Set.of(extraNewMethodSignatures.getKey(method));
+      }
+      Set<DexMethod> previousMethodSignatures = newMethodSignatures.getKeys(method);
+      if (!previousMethodSignatures.isEmpty()) {
+        return previousMethodSignatures;
+      }
+      return Set.of(method);
+    }
+
+    @Override
     public VerticalClassMergerGraphLens build(
         AppView<?> appView, VerticallyMergedClasses mergedClasses) {
-      if (mergedClasses.isEmpty()) {
-        return null;
-      }
       // Build new graph lens.
+      assert !mergedClasses.isEmpty();
       return new VerticalClassMergerGraphLens(
           appView,
           mergedClasses,
-          fieldMap,
-          methodMap,
-          mergedMethodsBuilder.build(),
-          contextualVirtualToDirectMethodMaps,
+          newFieldSignatures,
+          contextualSuperToImplementationInContexts,
           newMethodSignatures,
-          originalMethodSignaturesForBridges,
-          prototypeChanges);
+          extraNewMethodSignatures,
+          mergedMethods,
+          staticizedMethods);
     }
 
-    @SuppressWarnings("ReferenceEquality")
-    private DexField getFieldSignatureAfterClassMerging(
-        DexField field, VerticallyMergedClasses mergedClasses) {
-      assert !field.holder.isArrayType();
-
-      DexType holder = field.holder;
-      DexType newHolder = mergedClasses.getTargetForOrDefault(holder, holder);
-
-      DexType type = field.type;
-      DexType newType = getTypeAfterClassMerging(type, mergedClasses);
-
-      if (holder == newHolder && type == newType) {
-        return field;
-      }
-      return dexItemFactory.createField(newHolder, newType, field.name);
-    }
-
-    @SuppressWarnings("ReferenceEquality")
-    private DexMethod getMethodSignatureAfterClassMerging(
-        DexMethod signature, VerticallyMergedClasses mergedClasses) {
-      assert !signature.holder.isArrayType();
-
-      DexType holder = signature.holder;
-      DexType newHolder = mergedClasses.getTargetForOrDefault(holder, holder);
-
-      DexProto proto = signature.proto;
-      DexProto newProto =
-          dexItemFactory.applyClassMappingToProto(
-              proto, type -> getTypeAfterClassMerging(type, mergedClasses), cache);
-
-      if (holder == newHolder && proto == newProto) {
-        return signature;
-      }
-      return dexItemFactory.createMethod(newHolder, newProto, signature.name);
-    }
-
-    @SuppressWarnings("ReferenceEquality")
-    private DexType getTypeAfterClassMerging(DexType type, VerticallyMergedClasses mergedClasses) {
-      if (type.isArrayType()) {
-        DexType baseType = type.toBaseType(dexItemFactory);
-        DexType newBaseType = mergedClasses.getTargetForOrDefault(baseType, baseType);
-        if (newBaseType != baseType) {
-          return type.replaceBaseType(newBaseType, dexItemFactory);
-        }
-        return type;
-      }
-      return mergedClasses.getTargetForOrDefault(type, type);
-    }
-
+    // TODO: should be removed.
     public boolean hasMappingForSignatureInContext(DexProgramClass context, DexMethod signature) {
-      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
-          contextualVirtualToDirectMethodMaps.get(context.type);
-      if (virtualToDirectMethodMap != null) {
-        return virtualToDirectMethodMap.containsKey(signature);
+      return contextualSuperToImplementationInContexts
+          .getOrDefault(context.getType(), Collections.emptyMap())
+          .containsKey(signature);
+    }
+
+    public void markMethodAsMerged(DexEncodedMethod method) {
+      mergedMethods.add(method.getReference());
+    }
+
+    public void recordMove(DexEncodedField from, DexEncodedField to) {
+      newFieldSignatures.put(from.getReference(), to.getReference());
+    }
+
+    public void recordMove(DexEncodedMethod from, DexEncodedMethod to) {
+      newMethodSignatures.put(from.getReference(), to.getReference());
+    }
+
+    public void recordSplit(
+        DexEncodedMethod from,
+        DexEncodedMethod override,
+        DexEncodedMethod bridge,
+        DexEncodedMethod implementation) {
+      if (override != null) {
+        assert bridge == null;
+        newMethodSignatures.put(from.getReference(), override.getReference());
+        newMethodSignatures.put(override.getReference(), override.getReference());
+        newMethodSignatures.setRepresentative(override.getReference(), override.getReference());
+      } else {
+        assert bridge != null;
+        newMethodSignatures.put(from.getReference(), bridge.getReference());
       }
-      return false;
-    }
 
-    public boolean hasOriginalSignatureMappingFor(DexField field) {
-      return fieldMap.containsValue(field);
-    }
+      if (implementation == null) {
+        assert from.isAbstract();
+        return;
+      }
 
-    public boolean hasOriginalSignatureMappingFor(DexMethod method) {
-      return newMethodSignatures.containsValue(method)
-          || originalMethodSignaturesForBridges.containsKey(method);
-    }
+      extraNewMethodSignatures.put(from.getReference(), implementation.getReference());
+      mergedMethods.add(implementation.getReference());
 
-    public void markMethodAsMerged(DexMethod method) {
-      mergedMethodsBuilder.add(method);
-    }
-
-    public void map(DexField from, DexField to) {
-      fieldMap.put(from, to);
-    }
-
-    public Builder map(DexMethod from, DexMethod to) {
-      methodMap.put(from, to);
-      return this;
-    }
-
-    public void recordMerge(DexMethod from, DexMethod to) {
-      newMethodSignatures.put(from, to);
-      newMethodSignatures.put(to, to);
-      newMethodSignatures.setRepresentative(to, to);
-    }
-
-    public void recordMove(DexMethod from, DexMethod to) {
-      recordMove(from, to, false);
-    }
-
-    public void recordMove(DexMethod from, DexMethod to, boolean isStaticized) {
-      newMethodSignatures.put(from, to);
-      if (isStaticized) {
-        RewrittenPrototypeDescription prototypeChangesForMethod =
-            RewrittenPrototypeDescription.create(
-                ImmutableList.of(),
-                null,
-                ArgumentInfoCollection.builder()
-                    .setArgumentInfosSize(to.getParameters().size())
-                    .setIsConvertedToStaticMethod()
-                    .build());
-        prototypeChanges.put(to, prototypeChangesForMethod);
+      if (implementation.isStatic()) {
+        staticizedMethods.add(implementation.getReference());
       }
     }
 
-    public void recordCreationOfBridgeMethod(DexMethod from, DexMethod to) {
-      originalMethodSignaturesForBridges.put(to, from);
+    public void mapVirtualMethodToDirectInType(DexMethod from, DexEncodedMethod to, DexType type) {
+      contextualSuperToImplementationInContexts
+          .computeIfAbsent(type, ignoreKey(IdentityHashMap::new))
+          .put(from, to.getReference());
     }
 
-    public void mapVirtualMethodToDirectInType(
-        DexMethod from, GraphLensLookupResultProvider to, DexType type) {
-      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
-          contextualVirtualToDirectMethodMaps.computeIfAbsent(type, key -> new IdentityHashMap<>());
-      virtualToDirectMethodMap.put(from, to);
-    }
-
-    @SuppressWarnings("ReferenceEquality")
     public void merge(VerticalClassMergerGraphLens.Builder builder) {
-      fieldMap.putAll(builder.fieldMap);
-      methodMap.putAll(builder.methodMap);
-      mergedMethodsBuilder.addAll(builder.mergedMethodsBuilder.build());
+      newFieldSignatures.putAll(builder.newFieldSignatures);
+      mergedMethods.addAll(builder.mergedMethods);
       builder.newMethodSignatures.forEachManyToOneMapping(
           (keys, value, representative) -> {
             boolean isRemapping =
-                Iterables.any(keys, key -> newMethodSignatures.containsValue(key) && key != value);
+                Iterables.any(
+                    keys,
+                    key -> newMethodSignatures.containsValue(key) && key.isNotIdenticalTo(value));
             if (isRemapping) {
               // If I and J are merged into A and both I.m() and J.m() exists, then we may map J.m()
               // to I.m() as a result of merging J into A, and then subsequently merge I.m() to
@@ -453,19 +553,13 @@
               }
             }
           });
-      prototypeChanges.putAll(builder.prototypeChanges);
-      originalMethodSignaturesForBridges.putAll(builder.originalMethodSignaturesForBridges);
-      for (DexType context : builder.contextualVirtualToDirectMethodMaps.keySet()) {
-        Map<DexMethod, GraphLensLookupResultProvider> current =
-            contextualVirtualToDirectMethodMaps.get(context);
-        Map<DexMethod, GraphLensLookupResultProvider> other =
-            builder.contextualVirtualToDirectMethodMaps.get(context);
-        if (current != null) {
-          current.putAll(other);
-        } else {
-          contextualVirtualToDirectMethodMaps.put(context, other);
-        }
-      }
+      staticizedMethods.addAll(builder.staticizedMethods);
+      extraNewMethodSignatures.putAll(builder.extraNewMethodSignatures);
+      builder.contextualSuperToImplementationInContexts.forEach(
+          (key, value) ->
+              contextualSuperToImplementationInContexts
+                  .computeIfAbsent(key, ignoreKey(IdentityHashMap::new))
+                  .putAll(value));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
new file mode 100644
index 0000000..e2bfb17
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -0,0 +1,438 @@
+// 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.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.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+// TODO(b/315252934): Parallelize policy execution over connected program components.
+public class VerticalClassMergerPolicyExecutor {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
+  private final MainDexInfo mainDexInfo;
+  private final Set<DexProgramClass> pinnedClasses;
+
+  VerticalClassMergerPolicyExecutor(
+      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+    this.appView = appView;
+    this.options = appView.options();
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.pinnedClasses = pinnedClasses;
+  }
+
+  ConnectedComponentVerticalClassMerger run(
+      Set<DexProgramClass> connectedComponent,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    Set<DexProgramClass> mergeCandidates = Sets.newIdentityHashSet();
+    for (DexProgramClass sourceClass : connectedComponent) {
+      List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
+      if (subclasses.size() != 1) {
+        continue;
+      }
+      DexProgramClass targetClass = ListUtils.first(subclasses);
+      if (!isMergeCandidate(sourceClass, targetClass)) {
+        continue;
+      }
+      if (!isStillMergeCandidate(sourceClass, targetClass)) {
+        continue;
+      }
+      if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
+          || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
+        continue;
+      }
+      mergeCandidates.add(sourceClass);
+    }
+    return new ConnectedComponentVerticalClassMerger(appView, mergeCandidates);
+  }
+
+  // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
+  // method do not change in response to any class merges.
+  private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    assert targetClass != null;
+    ObjectAllocationInfoCollection allocationInfo =
+        appView.appInfo().getObjectAllocationInfoCollection();
+    if (allocationInfo.isInstantiatedDirectly(sourceClass)
+        || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
+        || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
+        || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
+        || pinnedClasses.contains(sourceClass)) {
+      return false;
+    }
+
+    assert sourceClass
+        .traverseProgramMembers(
+            member -> {
+              assert !appView.getKeepInfo(member).isPinned(options);
+              return TraversalContinuation.doContinue();
+            })
+        .shouldContinue();
+
+    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView)) {
+      return false;
+    }
+    if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView)) {
+      return false;
+    }
+    if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
+        && appView.getKeepInfo(targetClass).isPinned(options)) {
+      return false;
+    }
+    if (sourceClass.isAnnotation()) {
+      return false;
+    }
+    if (!sourceClass.isInterface()
+        && targetClass.isSerializable(appView)
+        && !appView.appInfo().isSerializable(sourceClass.getType())) {
+      // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
+      //   1.10 The Serializable Interface
+      //   ...
+      //   A Serializable class must do the following:
+      //   ...
+      //     * Have access to the no-arg constructor of its first non-serializable superclass
+      return false;
+    }
+
+    // If there is a constructor in the target, make sure that all source constructors can be
+    // inlined.
+    if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
+      TraversalContinuation<?, ?> result =
+          sourceClass.traverseProgramInstanceInitializers(
+              method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
+      if (result.shouldBreak()) {
+        return false;
+      }
+    }
+    if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
+      return false;
+    }
+    // We abort class merging when merging across nests or from a nest to non-nest.
+    // Without nest this checks null == null.
+    if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
+      return false;
+    }
+
+    // If there is an invoke-special to a default interface method and we are not merging into an
+    // interface, then abort, since invoke-special to a virtual class method requires desugaring.
+    if (sourceClass.isInterface() && !targetClass.isInterface()) {
+      TraversalContinuation<?, ?> result =
+          sourceClass.traverseProgramMethods(
+              method -> {
+                boolean foundInvokeSpecialToDefaultLibraryMethod =
+                    method.registerCodeReferencesWithResult(
+                        new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
+                return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
+              });
+      if (result.shouldBreak()) {
+        return false;
+      }
+    }
+
+    // Check with main dex classes to see if we are allowed to merge.
+    if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
+   * in this method may change in response to class merges. Therefore, this method should always be
+   * called before merging {@param sourceClass} into {@param targetClass}.
+   */
+  boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    // For interface types, this is more complicated, see:
+    // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
+    // We basically can't move the clinit, since it is not called when implementing classes have
+    // their clinit called - except when the interface has a default method.
+    if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
+        || targetClass.classInitializationMayHaveSideEffects(
+            appView, type -> type.isIdenticalTo(sourceClass.getType()))
+        || (sourceClass.isInterface()
+            && sourceClass.classInitializationMayHaveSideEffects(appView))) {
+      return false;
+    }
+    boolean sourceCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(sourceClass)
+            || sourceClass.hasStaticSynchronizedMethods();
+    boolean targetCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(targetClass)
+            || targetClass.hasStaticSynchronizedMethods();
+    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
+      return false;
+    }
+    if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
+      return false;
+    }
+    if (methodResolutionMayChange(sourceClass, targetClass)) {
+      return false;
+    }
+    // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+    // to the super class.
+    if (fieldResolutionMayChange(sourceClass, targetClass)) {
+      return false;
+    }
+    // Only merge if api reference level of source class is equal to target class. The check is
+    // somewhat expensive.
+    if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
+      AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
+      ComputedApiLevel sourceApiLevel =
+          getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
+      ComputedApiLevel targetApiLevel =
+          getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
+      if (!sourceApiLevel.equals(targetApiLevel)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
+    if (!appView.options().inlinerOptions().enableInlining) {
+      return true;
+    }
+    Code code = method.getDefinition().getCode();
+    if (code.isCfCode()) {
+      CfCode cfCode = code.asCfCode();
+      ConstraintWithTarget constraint =
+          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
+      if (constraint.isNever()) {
+        return true;
+      }
+      // Constructors can have references beyond the root main dex classes. This can increase the
+      // size of the main dex dependent classes and we should bail out.
+      if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
+        return true;
+      }
+      return false;
+    }
+    if (code.isDefaultInstanceInitializerCode()) {
+      return false;
+    }
+    return true;
+  }
+
+  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+    if (source.getType().isIdenticalTo(target.getSuperType())) {
+      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+      // Target implements an interface that declares a static final field f, this should yield an
+      // IncompatibleClassChangeError.
+      // TODO(christofferqa): In the following we only check if a static field from an interface
+      //  shadows an instance field from [source]. We could actually check if there is an iget/iput
+      //  instruction whose resolution would be affected by the merge. The situation where a static
+      //  field shadows an instance field is probably not widespread in practice, though.
+      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+      for (DexType interfaceType : target.getInterfaces()) {
+        DexClass clazz = appView.definitionFor(interfaceType);
+        for (DexEncodedField staticField : clazz.staticFields()) {
+          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
+        }
+      }
+      for (DexEncodedField instanceField : source.instanceFields()) {
+        if (staticFieldsInInterfacesOfTarget.contains(
+            equivalence.wrap(instanceField.getReference()))) {
+          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+          // interface would now hit an instance field from [source], so that an IncompatibleClass-
+          // ChangeError would no longer be thrown. Abort merge.
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
+    if (source.isSamePackage(target)) {
+      // When merging two classes from the same package, we only need to make sure that [source]
+      // does not get less visible, since that could make a valid access to [source] from another
+      // package illegal after [source] has been merged into [target].
+      assert source.getAccessFlags().isPackagePrivateOrPublic();
+      assert target.getAccessFlags().isPackagePrivateOrPublic();
+      // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
+      return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
+    }
+
+    // Check that all accesses to [source] and its members from inside the current package of
+    // [source] will continue to work. This is guaranteed if [target] is public and all members of
+    // [source] are either private or public.
+    //
+    // (Deliberately not checking all accesses to [source] since that would be expensive.)
+    if (!target.isPublic()) {
+      return true;
+    }
+    for (DexType sourceInterface : source.getInterfaces()) {
+      DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
+      if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
+        return true;
+      }
+    }
+    for (DexEncodedField field : source.fields()) {
+      if (!(field.isPublic() || field.isPrivate())) {
+        return true;
+      }
+    }
+    for (DexEncodedMethod method : source.methods()) {
+      if (!(method.isPublic() || method.isPrivate())) {
+        return true;
+      }
+      // Check if the target is overriding and narrowing the access.
+      if (method.isPublic()) {
+        DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
+        if (targetOverride != null && !targetOverride.isPublic()) {
+          return true;
+        }
+      }
+    }
+    // Check that all accesses from [source] to classes or members from the current package of
+    // [source] will continue to work. This is guaranteed if the methods of [source] do not access
+    // any private or protected classes or members from the current package of [source].
+    TraversalContinuation<?, ?> result =
+        source.traverseProgramMethods(
+            method -> {
+              boolean foundIllegalAccess =
+                  method.registerCodeReferencesWithResult(
+                      new IllegalAccessDetector(appView, method));
+              if (foundIllegalAccess) {
+                return TraversalContinuation.doBreak();
+              }
+              return TraversalContinuation.doContinue();
+            },
+            DexEncodedMethod::hasCode);
+    return result.shouldBreak();
+  }
+
+  // TODO: maybe skip this check if target does not implement any interfaces (directly or
+  // indirectly)?
+  private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
+    // This only returns true when an invoke-super instruction is found that targets a default
+    // interface method.
+    if (!options.canUseDefaultAndStaticInterfaceMethods()) {
+      return false;
+    }
+    // This problem may only arise when merging (non-interface) classes into classes.
+    if (source.isInterface() || target.isInterface()) {
+      return false;
+    }
+    return target
+        .traverseProgramMethods(
+            method -> {
+              MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
+                  new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
+              method.registerCodeReferencesWithResult(registry);
+              return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
+            },
+            DexEncodedMethod::hasCode)
+        .shouldBreak();
+  }
+
+  private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
+    for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
+      DexEncodedMethod directTargetMethod =
+          target.lookupDirectMethod(virtualSourceMethod.getReference());
+      if (directTargetMethod != null) {
+        // A private method shadows a virtual method. This situation is rare, since it is not
+        // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
+        // possible to rename the private method in the subclass, and then move the virtual method
+        // to the subclass without changing its name.)
+        return true;
+      }
+    }
+
+    // When merging an interface into a class, all instructions on the form "invoke-interface
+    // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
+    // transformation could hide IncompatibleClassChangeErrors.
+    if (source.isInterface() && !target.isInterface()) {
+      List<DexEncodedMethod> defaultMethods = new ArrayList<>();
+      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+        if (!virtualMethod.accessFlags.isAbstract()) {
+          defaultMethods.add(virtualMethod);
+        }
+      }
+
+      // For each of the default methods, the subclass [target] could inherit another default method
+      // with the same signature from another interface (i.e., there is a conflict). In such cases,
+      // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
+      // ClassChangeError.
+      //
+      // Example:
+      //   interface I1 { default void m() {} }
+      //   interface I2 { default void m() {} }
+      //   class C implements I1, I2 {
+      //     ... invoke-interface I1.m ... <- IncompatibleClassChangeError
+      //   }
+      for (DexEncodedMethod method : defaultMethods) {
+        // Conservatively find all possible targets for this method.
+        LookupResultSuccess lookupResult =
+            appView
+                .appInfo()
+                .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
+                .lookupVirtualDispatchTargets(target, appView)
+                .asLookupResultSuccess();
+        assert lookupResult != null;
+        if (lookupResult == null) {
+          return true;
+        }
+        if (lookupResult.contains(method)) {
+          Box<Boolean> found = new Box<>(false);
+          lookupResult.forEach(
+              interfaceTarget -> {
+                if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
+                  return;
+                }
+                DexClass enclosingClass = interfaceTarget.getHolder();
+                if (enclosingClass != null && enclosingClass.isInterface()) {
+                  // Found a default method that is different from the one in [source], aborting.
+                  found.set(true);
+                }
+              },
+              lambdaTarget -> {
+                // The merger should already have excluded lambda implemented interfaces.
+                assert false;
+              });
+          if (found.get()) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java
new file mode 100644
index 0000000..390131a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java
@@ -0,0 +1,87 @@
+// 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.shaking.AppInfoWithLiveness;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VerticalClassMergerResult {
+
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final List<SynthesizedBridgeCode> synthesizedBridges;
+  private final VerticallyMergedClasses verticallyMergedClasses;
+
+  public VerticalClassMergerResult(
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      List<SynthesizedBridgeCode> synthesizedBridges,
+      VerticallyMergedClasses verticallyMergedClasses) {
+    this.lensBuilder = lensBuilder;
+    this.synthesizedBridges = synthesizedBridges;
+    this.verticallyMergedClasses = verticallyMergedClasses;
+  }
+
+  public static Builder builder(AppView<AppInfoWithLiveness> appView) {
+    return new Builder(appView);
+  }
+
+  public static Builder builder(
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      List<SynthesizedBridgeCode> synthesizedBridges,
+      VerticallyMergedClasses.Builder verticallyMergedClassesBuilder) {
+    return new Builder(lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
+  }
+
+  VerticalClassMergerGraphLens.Builder getLensBuilder() {
+    return lensBuilder;
+  }
+
+  List<SynthesizedBridgeCode> getSynthesizedBridges() {
+    return synthesizedBridges;
+  }
+
+  VerticallyMergedClasses getVerticallyMergedClasses() {
+    return verticallyMergedClasses;
+  }
+
+  boolean isEmpty() {
+    return verticallyMergedClasses.isEmpty();
+  }
+
+  public static class Builder {
+
+    private final VerticalClassMergerGraphLens.Builder lensBuilder;
+    private final List<SynthesizedBridgeCode> synthesizedBridges;
+    private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
+
+    Builder(AppView<AppInfoWithLiveness> appView) {
+      this(
+          new VerticalClassMergerGraphLens.Builder(),
+          new ArrayList<>(),
+          VerticallyMergedClasses.builder());
+    }
+
+    Builder(
+        VerticalClassMergerGraphLens.Builder lensBuilder,
+        List<SynthesizedBridgeCode> synthesizedBridges,
+        VerticallyMergedClasses.Builder verticallyMergedClassesBuilder) {
+      this.lensBuilder = lensBuilder;
+      this.synthesizedBridges = synthesizedBridges;
+      this.verticallyMergedClassesBuilder = verticallyMergedClassesBuilder;
+    }
+
+    synchronized void merge(VerticalClassMergerResult.Builder other) {
+      lensBuilder.merge(other.lensBuilder);
+      synthesizedBridges.addAll(other.synthesizedBridges);
+      verticallyMergedClassesBuilder.merge(other.verticallyMergedClassesBuilder);
+    }
+
+    VerticalClassMergerResult build() {
+      VerticallyMergedClasses verticallyMergedClasses = verticallyMergedClassesBuilder.build();
+      return new VerticalClassMergerResult(
+          lensBuilder, synthesizedBridges, verticallyMergedClasses);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
index e544c25..6b9955c 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
@@ -3,95 +3,55 @@
 // 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.classmerging.ClassMergerTreeFixer;
+import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 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.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
-class VerticalClassMergerTreeFixer extends TreeFixerBase {
+class VerticalClassMergerTreeFixer
+    extends ClassMergerTreeFixer<
+        VerticalClassMergerGraphLens.Builder,
+        VerticalClassMergerGraphLens,
+        VerticallyMergedClasses> {
 
-  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;
+      ProfileCollectionAdditions profileCollectionAdditions,
+      SyntheticArgumentClass syntheticArgumentClass,
+      VerticalClassMergerResult verticalClassMergerResult) {
+    super(
+        appView,
+        VerticalClassMergerGraphLens.Builder.createBuilderForFixup(verticalClassMergerResult),
+        verticalClassMergerResult.getVerticallyMergedClasses(),
+        profileCollectionAdditions,
+        syntheticArgumentClass);
+    this.synthesizedBridges = verticalClassMergerResult.getSynthesizedBridges();
   }
 
-  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()));
-    }
+  @Override
+  public VerticalClassMergerGraphLens run(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    VerticalClassMergerGraphLens lens = super.run(executorService, timing);
     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());
+      synthesizedBridge.updateMethodSignatures(lens);
     }
     return lens;
   }
 
   @Override
-  public DexType mapClassType(DexType type) {
-    while (mergedClasses.hasBeenMergedIntoSubtype(type)) {
-      type = mergedClasses.getTargetFor(type);
-    }
-    return type;
+  public void postprocess() {
+    lensBuilder.fixupContextualVirtualToDirectMethodMaps();
   }
 
   @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;
+  public boolean isRunningBeforePrimaryOptimizationPass() {
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
index d6f3e64..0003478 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
@@ -5,33 +5,43 @@
 package com.android.tools.r8.verticalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import java.util.Collection;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
 public class VerticallyMergedClasses implements MergedClasses {
 
   private final BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
-  private final BidirectionalManyToOneMap<DexType, DexType> mergedInterfaces;
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedInterfacesToClasses;
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedInterfacesToInterfaces;
 
   public VerticallyMergedClasses(
       BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
-      BidirectionalManyToOneMap<DexType, DexType> mergedInterfaces) {
+      BidirectionalManyToOneMap<DexType, DexType> mergedInterfacesToClasses,
+      BidirectionalManyToOneMap<DexType, DexType> mergedInterfacesToInterfaces) {
     this.mergedClasses = mergedClasses;
-    this.mergedInterfaces = mergedInterfaces;
+    this.mergedInterfacesToClasses = mergedInterfacesToClasses;
+    this.mergedInterfacesToInterfaces = mergedInterfacesToInterfaces;
+  }
+
+  public static Builder builder() {
+    return new Builder();
   }
 
   public static VerticallyMergedClasses empty() {
     EmptyBidirectionalOneToOneMap<DexType, DexType> emptyMap =
         new EmptyBidirectionalOneToOneMap<>();
-    return new VerticallyMergedClasses(emptyMap, emptyMap);
+    return new VerticallyMergedClasses(emptyMap, emptyMap, emptyMap);
   }
 
   @Override
@@ -43,8 +53,17 @@
     return mergedClasses;
   }
 
-  public Map<DexType, DexType> getForwardMap() {
-    return mergedClasses.getForwardMap();
+  @Override
+  public DexType getMergeTargetOrDefault(DexType type, DexType defaultValue) {
+    return mergedClasses.getOrDefault(type, defaultValue);
+  }
+
+  public Set<DexType> getSources() {
+    return mergedClasses.keySet();
+  }
+
+  public Set<DexType> getTargets() {
+    return mergedClasses.values();
   }
 
   public Collection<DexType> getSourcesFor(DexType type) {
@@ -56,16 +75,22 @@
     return mergedClasses.get(type);
   }
 
-  public DexType getTargetForOrDefault(DexType type, DexType defaultValue) {
-    return mergedClasses.getOrDefault(type, defaultValue);
+  @Override
+  public boolean isMergeSource(DexType type) {
+    return hasBeenMergedIntoSubtype(type);
   }
 
   public boolean hasBeenMergedIntoSubtype(DexType type) {
     return mergedClasses.containsKey(type);
   }
 
+  public boolean hasInterfaceBeenMergedIntoClass(DexType interfaceType, DexType classType) {
+    return classType.isIdenticalTo(mergedInterfacesToClasses.get(interfaceType));
+  }
+
   public boolean hasInterfaceBeenMergedIntoSubtype(DexType type) {
-    return mergedInterfaces.containsKey(type);
+    return mergedInterfacesToClasses.containsKey(type)
+        || mergedInterfacesToInterfaces.containsKey(type);
   }
 
   public boolean isEmpty() {
@@ -78,11 +103,6 @@
   }
 
   @Override
-  public boolean hasBeenMergedIntoDifferentType(DexType type) {
-    return hasBeenMergedIntoSubtype(type);
-  }
-
-  @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
     for (DexType source : mergedClasses.keySet()) {
       assert appView.appInfo().wasPruned(source)
@@ -90,4 +110,50 @@
     }
     return true;
   }
+
+  public static class Builder {
+
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
+        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
+
+    private final BidirectionalManyToOneHashMap<DexType, DexType> mergedInterfacesToClasses =
+        BidirectionalManyToOneHashMap.newIdentityHashMap();
+
+    private final BidirectionalManyToOneHashMap<DexType, DexType> mergedInterfacesToInterfaces =
+        BidirectionalManyToOneHashMap.newIdentityHashMap();
+
+    void add(DexProgramClass source, DexProgramClass target) {
+      mergedClasses.put(source.getType(), target.getType());
+      if (source.isInterface()) {
+        if (target.isInterface()) {
+          mergedInterfacesToInterfaces.put(source.getType(), target.getType());
+        } else {
+          mergedInterfacesToClasses.put(source.getType(), target.getType());
+        }
+      }
+    }
+
+    Set<DexType> getSourcesFor(DexProgramClass target) {
+      return mergedClasses.getKeys(target.getType());
+    }
+
+    boolean isMergeSource(DexProgramClass clazz) {
+      return mergedClasses.containsKey(clazz.getType());
+    }
+
+    boolean isMergeTarget(DexProgramClass clazz) {
+      return mergedClasses.containsValue(clazz.getType());
+    }
+
+    void merge(VerticallyMergedClasses.Builder other) {
+      mergedClasses.putAll(other.mergedClasses);
+      mergedInterfacesToClasses.putAll(other.mergedInterfacesToClasses);
+      mergedInterfacesToInterfaces.putAll(other.mergedInterfacesToInterfaces);
+    }
+
+    VerticallyMergedClasses build() {
+      return new VerticallyMergedClasses(
+          mergedClasses, mergedInterfacesToClasses, mergedInterfacesToInterfaces);
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 0bc2093..0dbe829 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -1176,7 +1176,7 @@
   // These tests match on paths relative to the execution directory (normally the repo root).
   // Cached stdout might be from a different directory.
   private static List<String> noArtCommandCaching =
-      ImmutableList.of("068-classloader", "086-null-superTest", "087-gc-after-linkTest");
+      ImmutableList.of("068-classloader", "086-null-super", "087-gc-after-link");
 
   private static final String NO_CLASS_ACCESS_MODIFICATION_RULE =
       "-keep,allowobfuscation,allowoptimization,allowshrinking class *";
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 2183979..0e08d8f 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -75,7 +75,9 @@
 
   private AllowedDiagnosticMessages allowedDiagnosticMessages = AllowedDiagnosticMessages.NONE;
   private boolean allowUnusedProguardConfigurationRules = false;
+  private boolean enableIsolatedSplits = false;
   private boolean enableMissingLibraryApiModeling = true;
+  private boolean enableStartupLayoutOptimization = true;
   private CollectingGraphConsumer graphConsumer = null;
   private final List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
   private final List<String> keepRules = new ArrayList<>();
@@ -151,7 +153,9 @@
     ToolHelper.addSyntheticProguardRulesConsumerForTesting(
         builder, rules -> box.syntheticProguardRules = rules);
     libraryDesugaringTestConfiguration.configure(builder);
+    builder.setEnableExperimentalIsolatedSplits(enableIsolatedSplits);
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
+    builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
     StringBuilder pgConfOutput = wrapProguardConfigConsumer(builder);
     ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
     R8TestCompileResult compileResult =
@@ -854,6 +858,11 @@
     return self();
   }
 
+  public T enableIsolatedSplits(boolean enableIsolatedSplits) {
+    this.enableIsolatedSplits = enableIsolatedSplits;
+    return self();
+  }
+
   public T addArtProfileForRewriting(ArtProfileProvider artProfileProvider) {
     return addArtProfileForRewriting(
         artProfileProvider,
@@ -880,6 +889,11 @@
     return self();
   }
 
+  public T enableStartupLayoutOptimization(boolean enableStartupLayoutOptimization) {
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
+    return self();
+  }
+
   public T setFakeCompilerVersion(SemanticVersion version) {
     getBuilder().setFakeCompilerVersion(version);
     return self();
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 25001ac..8237f9b 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.List;
@@ -136,8 +137,12 @@
   @SafeVarargs
   @Override
   public final <E extends Throwable> R8TestCompileResult inspectMultiDex(
-      ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
-    return inspectMultiDex(writeProguardMap(), consumers);
+      ThrowingConsumer<CodeInspector, E>... consumers) throws E {
+    try {
+      return inspectMultiDex(writeProguardMap(), consumers);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
   }
 
   public final <E extends Throwable> R8TestCompileResult inspectGraph(
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index dfdfa9d..4c2d6a8 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -39,6 +39,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -456,21 +457,25 @@
 
   @SuppressWarnings("unchecked")
   public <E extends Throwable> CR inspectMultiDex(ThrowingConsumer<CodeInspector, E>... consumers)
-      throws IOException, E {
+      throws E {
     return inspectMultiDex(null, consumers);
   }
 
   @SafeVarargs
   public final <E extends Throwable> CR inspectMultiDex(
-      Path mappingFile, ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
-    Path out = state.getNewTempFolder();
-    getApp().writeToDirectory(out, OutputMode.DexIndexed);
-    consumers[0].accept(new CodeInspector(out.resolve("classes.dex"), mappingFile));
-    for (int i = 1; i < consumers.length; i++) {
-      Path dex = out.resolve("classes" + (i + 1) + ".dex");
-      CodeInspector inspector =
-          dex.toFile().exists() ? new CodeInspector(dex, mappingFile) : CodeInspector.empty();
-      consumers[i].accept(inspector);
+      Path mappingFile, ThrowingConsumer<CodeInspector, E>... consumers) throws E {
+    try {
+      Path out = state.getNewTempFolder();
+      getApp().writeToDirectory(out, OutputMode.DexIndexed);
+      consumers[0].accept(new CodeInspector(out.resolve("classes.dex"), mappingFile));
+      for (int i = 1; i < consumers.length; i++) {
+        Path dex = out.resolve("classes" + (i + 1) + ".dex");
+        CodeInspector inspector =
+            dex.toFile().exists() ? new CodeInspector(dex, mappingFile) : CodeInspector.empty();
+        consumers[i].accept(inspector);
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
     }
     return self();
   }
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 892c68f..0515b8d 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -90,6 +90,10 @@
     return isDexRuntime() && getDexRuntimeVersion().isNewerThanOrEqual(DexVm.Version.V8_1_0);
   }
 
+  public boolean canUseNativeMultidex() {
+    return isDexRuntime() && getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+  }
+
   public boolean canUseNestBasedAccesses() {
     assert isCfRuntime() || isDexRuntime();
     return isCfRuntime() && getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11);
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
index e180f05..7b685b6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
@@ -71,6 +71,7 @@
 
   @NoHorizontalClassMerging
   @NeverClassInline
+  @NoVerticalClassMerging
   public static class C {
     @NeverInline
     public void foo(A a) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java
index b1185f0..3eec75f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java
@@ -6,37 +6,42 @@
 
 import com.android.tools.r8.KeepUnusedArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoParameterTypeStrengthening;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.codeinspector.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 ConflictWasDetectedTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public ConflictWasDetectedTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .assertClassesMerged(
+                        ClassWithConflictingMethod.class, OtherClassWithConflictingMethod.class)
+                    .assertNoOtherClassesMerged())
         .addVerticallyMergedClassesInspector(
-            VerticallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> inspector.assertMergedIntoSubtype(ConflictingInterface.class))
         .enableInliningAnnotations()
-        // .enableNoHorizontalClassMergingAnnotations()
+        .enableNoParameterTypeStrengtheningAnnotations()
         .enableUnusedArgumentAnnotations()
         .setMinApi(parameters)
         .compile()
@@ -53,6 +58,8 @@
       escape(impl);
     }
 
+    @NeverInline
+    @NoParameterTypeStrengthening
     private static void callMethodOnIface(ConflictingInterface iface) {
       System.out.println(iface.method());
       System.out.println(ClassWithConflictingMethod.conflict(null));
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 8cd641d..513561a 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,15 +4,11 @@
 
 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.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;
@@ -38,36 +34,31 @@
 
   @Test
   public void test() throws Exception {
-    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();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(IncorrectRewritingOfInvokeSuperTest.class)
+        .addKeepMainRule(TestClass.class)
+        .addOptionsModification(
+            options -> {
+              options.enableUnusedInterfaceRemoval = false;
+              options.testing.enableVerticalClassMergerLensAssertion = verifyLensLookup;
+            })
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Caught NPE");
   }
 
   static class TestClass {
 
     public static void main(String[] args) {
-      new B() {}.m(new SubArgType());
+      B b = new B() {};
+      b.m(new SubArgType());
+      try {
+        b.m(null);
+      } catch (RuntimeException e) {
+        System.out.println("Caught NPE");
+      }
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java
index ea8f326..3555fa6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java
@@ -9,37 +9,30 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.codeinspector.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 MethodCollisionTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public MethodCollisionTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(christofferqa): Currently we do not allow merging A into B because we find a
-        //  collision. However, we are free to change the names of private methods, so we should
-        //  handle them similar to fields (i.e., we should allow merging A into B). This would also
-        //  improve the performance of the collision detector, because it would only have to
-        //  consider non-private methods.
         .addVerticallyMergedClassesInspector(
-            VerticallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> inspector.assertMergedIntoSubtype(A.class, C.class))
         .enableInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters)
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 7db55ae..7254cd5 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
@@ -145,7 +145,6 @@
     Set<String> preservedClassNames =
         ImmutableSet.of(
             "classmerging.ArrayTypeCollisionTest",
-            "classmerging.ArrayTypeCollisionTest$A",
             "classmerging.ArrayTypeCollisionTest$B");
     runTest(
         testForR8(parameters.getBackend())
@@ -177,8 +176,7 @@
   public void testArrayReturnTypeCollision() throws Throwable {
     String main = "classmerging.ArrayReturnTypeCollisionTest";
     Set<String> preservedClassNames =
-        ImmutableSet.of(
-            "classmerging.ArrayReturnTypeCollisionTest", "classmerging.A", "classmerging.B");
+        ImmutableSet.of("classmerging.ArrayReturnTypeCollisionTest", "classmerging.B");
 
     JasminBuilder jasminBuilder = new JasminBuilder();
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
index d5bfade..de57198 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
@@ -34,7 +34,7 @@
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  @Test()
+  @Test
   public void test() throws Exception {
     testForR8Compat(parameters.getBackend())
         .addInnerClasses(getClass())
@@ -63,14 +63,22 @@
               // copied to the virtual bridge.
               MethodSubject fooMovedFromB =
                   classSubject.uniqueMethodThatMatches(
-                      method -> !method.isVirtual() && method.getOriginalName(false).equals("foo"));
+                      method ->
+                          method.isFinal()
+                              && method.isVirtual()
+                              && !method.isSynthetic()
+                              && method.getOriginalName(false).equals("foo"));
               assertThat(fooMovedFromB, isPresentAndRenamed());
               assertEquals(
                   "(Ljava/lang/Object;)Ljava/lang/Object;",
                   fooMovedFromB.getFinalSignatureAttribute());
               MethodSubject fooBridge =
                   classSubject.uniqueMethodThatMatches(
-                      method -> method.isVirtual() && method.getOriginalName(false).equals("foo"));
+                      method ->
+                          method.isFinal()
+                              && method.isVirtual()
+                              && method.isSynthetic()
+                              && method.getOriginalName(false).equals("foo"));
               assertThat(fooBridge, isPresentAndRenamed());
               assertEquals(
                   "(Ljava/lang/Object;)Ljava/lang/Object;", fooBridge.getFinalSignatureAttribute());
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictTest.java
new file mode 100644
index 0000000..b357ea8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictTest.java
@@ -0,0 +1,234 @@
+// 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.lambdas;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.lang.invoke.LambdaConversionException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Type;
+
+// Regression for b/314821730
+@RunWith(Parameterized.class)
+public class LambdaWithPrimitiveReferenceTypeConflictTest extends TestBase {
+
+  private enum Variant {
+    ID,
+    RETURN_BYTE("B"),
+    RETURN_SHORT("S"),
+    RETURN_INT("I"),
+    RETURN_LONG("J"),
+    RETURN_FLOAT("F"),
+    RETURN_DOUBLE("D"),
+    ARGUMENT;
+
+    final String desc;
+
+    Variant() {
+      this(null);
+    }
+
+    Variant(String desc) {
+      this.desc = desc;
+    }
+  }
+
+  static final String EXPECTED = StringUtils.lines("1,2,3");
+
+  private final TestParameters parameters;
+  private final Variant variant;
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimes()
+            .withDefaultDexRuntime()
+            .withApiLevel(AndroidApiLevel.B)
+            .enableApiLevelsForCf()
+            .build(),
+        Variant.values());
+  }
+
+  public LambdaWithPrimitiveReferenceTypeConflictTest(TestParameters parameters, Variant variant) {
+    this.parameters = parameters;
+    this.variant = variant;
+  }
+
+  private boolean expectCompilationError() {
+    return variant == Variant.ARGUMENT || expectJvmRuntimeError();
+  }
+
+  private boolean expectJvmRuntimeError() {
+    // This should be the same as what we allow compilation failure for, but JDK8 doesn't verify
+    // the argument constraints so the program runs.
+    return variant == Variant.RETURN_BYTE
+        || (variant == Variant.ARGUMENT && !parameters.isCfRuntime(CfVm.JDK8));
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addProgramClasses(getProgramClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            expectJvmRuntimeError(),
+            r -> r.assertFailureWithErrorThatThrows(LambdaConversionException.class),
+            r -> r.assertSuccessWithOutput(EXPECTED));
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    try {
+      testForD8(parameters.getBackend())
+          .addProgramClasses(getProgramClasses())
+          .addProgramClassFileData(getTransformedClasses())
+          .setMinApi(parameters)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED);
+      assertFalse(expectCompilationError());
+    } catch (CompilationFailedException e) {
+      if (!expectCompilationError()) {
+        throw e;
+      }
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    try {
+      testForR8(parameters.getBackend())
+          .addProgramClasses(getProgramClasses())
+          .addProgramClassFileData(getTransformedClasses())
+          .addKeepMainRule(TestClass.class)
+          .setMinApi(parameters)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED);
+      assertFalse(expectCompilationError());
+    } catch (CompilationFailedException e) {
+      if (!expectCompilationError()) {
+        throw e;
+      }
+    }
+  }
+
+  List<Class<?>> getProgramClasses() {
+    return ImmutableList.of(F1.class, Seq.class, LSeq.class, TestClass.class);
+  }
+
+  List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(getTransformedExample(variant));
+  }
+
+  private static byte[] getTransformedExample(Variant variant) throws IOException {
+    return transformer(Example.class)
+        .transformInvokeDynamicInsnInMethod(
+            "foo",
+            (invokedName,
+                invokedType,
+                bootstrapMethodHandle,
+                bootstrapMethodArguments,
+                visitor) -> {
+              Type samMethodType = (Type) bootstrapMethodArguments.get(0);
+              Handle implMethod = (Handle) bootstrapMethodArguments.get(1);
+              Type instantiatedMethodType = (Type) bootstrapMethodArguments.get(2);
+              String desc = instantiatedMethodType.getDescriptor();
+              if (variant == Variant.ID) {
+                // No change.
+              } else if (variant == Variant.ARGUMENT) {
+                instantiatedMethodType = Type.getType(desc.replace("(Ljava/lang/Short;)", "(S)"));
+              } else {
+                assertNotNull(variant.desc);
+                instantiatedMethodType =
+                    Type.getType(desc.replace(")Ljava/lang/Short;", ")" + variant.desc));
+              }
+              visitor.visitInvokeDynamicInsn(
+                  invokedName,
+                  invokedType,
+                  bootstrapMethodHandle,
+                  samMethodType,
+                  implMethod,
+                  instantiatedMethodType);
+            })
+        .transform();
+  }
+
+  interface F1<T, R> {
+    R apply(T o);
+  }
+
+  interface Seq<T> {
+    <R> Seq<R> map(F1<T, R> fn);
+
+    String join();
+  }
+
+  static class LSeq<T> implements Seq<T> {
+    private final List<T> items;
+
+    public LSeq(List<T> items) {
+      this.items = items;
+    }
+
+    @Override
+    public <R> Seq<R> map(F1<T, R> fn) {
+      ArrayList<R> mapped = new ArrayList<>(items.size());
+      for (T item : items) {
+        mapped.add(fn.apply(item));
+      }
+      return new LSeq<>(mapped);
+    }
+
+    @Override
+    public String join() {
+      StringBuilder builder = new StringBuilder();
+      for (int i = 0; i < items.size(); i++) {
+        if (i > 0) {
+          builder.append(",");
+        }
+        builder.append(items.get(i));
+      }
+      return builder.toString();
+    }
+  }
+
+  static class Example {
+
+    // Implementation method defined on primitive types.
+    static short fn(short x) {
+      return x;
+    }
+
+    static String foo(Seq<Short> values) {
+      // JavaC generated bootstrap method will use boxed types for arguments and return type.
+      // The method reference is transformed to various primitive types.
+      return values.map(Example::fn).join();
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(Example.foo(new LSeq<>(Arrays.asList((short) 1, (short) 2, (short) 3))));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictWideTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictWideTest.java
new file mode 100644
index 0000000..2cb8af2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaWithPrimitiveReferenceTypeConflictWideTest.java
@@ -0,0 +1,237 @@
+// 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.lambdas;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.lang.invoke.LambdaConversionException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Type;
+
+// Regression for b/314821730 (wide variant).
+@RunWith(Parameterized.class)
+public class LambdaWithPrimitiveReferenceTypeConflictWideTest extends TestBase {
+
+  private enum Variant {
+    ID,
+    RETURN_BYTE("B"),
+    RETURN_SHORT("S"),
+    RETURN_INT("I"),
+    RETURN_LONG("J"),
+    RETURN_FLOAT("F"),
+    RETURN_DOUBLE("D"),
+    ARGUMENT;
+
+    final String desc;
+
+    Variant() {
+      this(null);
+    }
+
+    Variant(String desc) {
+      this.desc = desc;
+    }
+  }
+
+  static final String EXPECTED = StringUtils.lines("1,2,3");
+
+  private final TestParameters parameters;
+  private final Variant variant;
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimes()
+            .withDefaultDexRuntime()
+            .withApiLevel(AndroidApiLevel.B)
+            .enableApiLevelsForCf()
+            .build(),
+        Variant.values());
+  }
+
+  public LambdaWithPrimitiveReferenceTypeConflictWideTest(
+      TestParameters parameters, Variant variant) {
+    this.parameters = parameters;
+    this.variant = variant;
+  }
+
+  private boolean expectCompilationError() {
+    return variant == Variant.ARGUMENT || expectJvmRuntimeError();
+  }
+
+  private boolean expectJvmRuntimeError() {
+    // This should be the same as what we allow compilation failure for, but JDK8 doesn't verify
+    // the argument constraints so the program runs.
+    return variant == Variant.RETURN_BYTE
+        || variant == Variant.RETURN_SHORT
+        || variant == Variant.RETURN_INT
+        || (variant == Variant.ARGUMENT && !parameters.isCfRuntime(CfVm.JDK8));
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addProgramClasses(getProgramClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            expectJvmRuntimeError(),
+            r -> r.assertFailureWithErrorThatThrows(LambdaConversionException.class),
+            r -> r.assertSuccessWithOutput(EXPECTED));
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    try {
+      testForD8(parameters.getBackend())
+          .addProgramClasses(getProgramClasses())
+          .addProgramClassFileData(getTransformedClasses())
+          .setMinApi(parameters)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED);
+      assertFalse(expectCompilationError());
+    } catch (CompilationFailedException e) {
+      if (!expectCompilationError()) {
+        throw e;
+      }
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    try {
+      testForR8(parameters.getBackend())
+          .addProgramClasses(getProgramClasses())
+          .addProgramClassFileData(getTransformedClasses())
+          .addKeepMainRule(TestClass.class)
+          .setMinApi(parameters)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(EXPECTED);
+      assertFalse(expectCompilationError());
+    } catch (CompilationFailedException e) {
+      if (!expectCompilationError()) {
+        throw e;
+      }
+    }
+  }
+
+  List<Class<?>> getProgramClasses() {
+    return ImmutableList.of(F1.class, Seq.class, LSeq.class, TestClass.class);
+  }
+
+  List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(getTransformedExample(variant));
+  }
+
+  private static byte[] getTransformedExample(Variant variant) throws IOException {
+    return transformer(Example.class)
+        .transformInvokeDynamicInsnInMethod(
+            "foo",
+            (invokedName,
+                invokedType,
+                bootstrapMethodHandle,
+                bootstrapMethodArguments,
+                visitor) -> {
+              Type samMethodType = (Type) bootstrapMethodArguments.get(0);
+              Handle implMethod = (Handle) bootstrapMethodArguments.get(1);
+              Type instantiatedMethodType = (Type) bootstrapMethodArguments.get(2);
+              String desc = instantiatedMethodType.getDescriptor();
+              if (variant == Variant.ID) {
+                // No change.
+              } else if (variant == Variant.ARGUMENT) {
+                instantiatedMethodType = Type.getType(desc.replace("(Ljava/lang/Long;)", "(J)"));
+              } else {
+                assertNotNull(variant.desc);
+                instantiatedMethodType =
+                    Type.getType(desc.replace(")Ljava/lang/Long;", ")" + variant.desc));
+              }
+              visitor.visitInvokeDynamicInsn(
+                  invokedName,
+                  invokedType,
+                  bootstrapMethodHandle,
+                  samMethodType,
+                  implMethod,
+                  instantiatedMethodType);
+            })
+        .transform();
+  }
+
+  interface F1<T, R> {
+    R apply(T o);
+  }
+
+  interface Seq<T> {
+    <R> Seq<R> map(F1<T, R> fn);
+
+    String join();
+  }
+
+  static class LSeq<T> implements Seq<T> {
+    private final List<T> items;
+
+    public LSeq(List<T> items) {
+      this.items = items;
+    }
+
+    @Override
+    public <R> Seq<R> map(F1<T, R> fn) {
+      ArrayList<R> mapped = new ArrayList<>(items.size());
+      for (T item : items) {
+        mapped.add(fn.apply(item));
+      }
+      return new LSeq<>(mapped);
+    }
+
+    @Override
+    public String join() {
+      StringBuilder builder = new StringBuilder();
+      for (int i = 0; i < items.size(); i++) {
+        if (i > 0) {
+          builder.append(",");
+        }
+        builder.append(items.get(i));
+      }
+      return builder.toString();
+    }
+  }
+
+  static class Example {
+
+    // Implementation method defined on primitive types.
+    static long fn(long x) {
+      return x;
+    }
+
+    static String foo(Seq<Long> values) {
+      // JavaC generated bootstrap method will use boxed types for arguments and return type.
+      // The method reference is transformed to various primitive types.
+      return values.map(Example::fn).join();
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(Example.foo(new LSeq<>(Arrays.asList((long) 1, (long) 2, (long) 3))));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
index 0a7c200..e4847bd 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.PACKAGE_NAME;
 import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.classesMatching;
 import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertSame;
 import static junit.framework.TestCase.assertTrue;
@@ -151,7 +152,7 @@
   }
 
   public static void assertNestAttributesCorrect(CodeInspector inspector) {
-    assertTrue(inspector.allClasses().size() > 0);
+    assertFalse(inspector.allClasses().isEmpty());
     for (FoundClassSubject classSubject : inspector.allClasses()) {
       DexClass clazz = classSubject.getDexProgramClass();
       if (clazz.isInANest()) {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
index 0f84aa8..f7c850a 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
@@ -5,9 +5,11 @@
 package com.android.tools.r8.enumunboxing;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -40,6 +42,7 @@
         .addKeepRules(EnumKeepRules.STUDIO.getKeepRules())
         .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableInliningAnnotations()
         .allowDiagnosticMessages()
         .setMinApi(parameters)
@@ -64,6 +67,11 @@
     assertEquals(MyEnum.class.getName(), debugInfoMethod.getOriginalSignature().parameters[0]);
     // TODO(b/314076309): The original parameter should be MyEnum.class but is int.
     assertEquals("int", noDebugInfoMethod.getOriginalSignature().parameters[0]);
+
+    ClassSubject indirection = codeInspector.clazz(Indirection.class);
+    MethodSubject abstractMethod = indirection.uniqueMethodWithOriginalName("intermediate");
+    assertTrue(abstractMethod.isAbstract());
+    assertEquals(MyEnum.class.getName(), abstractMethod.getOriginalSignature().parameters[0]);
   }
 
   @NeverClassInline
@@ -72,22 +80,53 @@
     B
   }
 
+  @NeverClassInline
+  abstract static class Indirection {
+
+    @NeverInline
+    public abstract int intermediate(MyEnum e);
+  }
+
+  @NoHorizontalClassMerging
+  @NeverClassInline
+  static class A extends Indirection {
+
+    @Override
+    public int intermediate(MyEnum e) {
+      return Main.noDebugInfoAfterUnboxing(e);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NeverClassInline
+  static class B extends Indirection {
+
+    @Override
+    public int intermediate(MyEnum e) {
+      return Main.debugInfoAfterUnboxing(e);
+    }
+  }
+
   static class Main {
 
     public static void main(String[] args) {
-      System.out.println(noDebugInfoAfterUnboxing(MyEnum.A));
-      System.out.println(noDebugInfoAfterUnboxing(null));
-      System.out.println(debugInfoAfterUnboxing(MyEnum.A));
-      System.out.println(debugInfoAfterUnboxing(null));
+      Indirection indirection1 = new A();
+      Indirection indirection2 = new B();
+      Indirection i = System.nanoTime() > 0 ? indirection1 : indirection2;
+      System.out.println(i.intermediate(MyEnum.A));
+      System.out.println(i.intermediate(null));
+      i = System.nanoTime() < 0 ? indirection1 : indirection2;
+      System.out.println(i.intermediate(MyEnum.A));
+      System.out.println(i.intermediate(null));
     }
 
     @NeverInline
-    private static int noDebugInfoAfterUnboxing(MyEnum e) {
+    static int noDebugInfoAfterUnboxing(MyEnum e) {
       return (e == null ? 1 : 0) + 1;
     }
 
     @NeverInline
-    private static int debugInfoAfterUnboxing(MyEnum e) {
+    static int debugInfoAfterUnboxing(MyEnum e) {
       System.out.println("DebugInfoForThisPrint");
       return (e == null ? 1 : 0) + 1;
     }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
index 4f1d44e..c2446e9 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
@@ -42,7 +42,6 @@
         .addKeepMainRule(Main.class)
         .addKeepRules("-keep class " + SUBTYPE_NAME + " { public void method(); }")
         .addKeepRules(enumKeepRules.getKeepRules())
-        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
         .addEnumUnboxingInspector(
             inspector -> inspector.assertNotUnboxed(EnumWithVirtualOverride.class))
         .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.java
new file mode 100644
index 0000000..61df4af
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.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.enumunboxing.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+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 NoEnumFlagEnumMergingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public NoEnumFlagEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(getEnumSubtypesOrOtherInputs(false))
+        .addProgramClassFileData(getSubEnumProgramData(getEnumSubtypesOrOtherInputs(true)))
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum2Cases.class))
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("336", "74", "96", "44");
+  }
+
+  private List<Path> getEnumSubtypesOrOtherInputs(boolean enumSubtypes) throws IOException {
+    return ToolHelper.getClassFilesForInnerClasses(getClass()).stream()
+        .filter(path -> isMyEnum2CasesSubtype(path) == enumSubtypes)
+        .collect(Collectors.toList());
+  }
+
+  private boolean isMyEnum2CasesSubtype(Path c) {
+    return c.toString().contains("MyEnum2Cases") && !c.toString().endsWith("MyEnum2Cases.class");
+  }
+
+  private List<byte[]> getSubEnumProgramData(List<Path> input) {
+    // Some Kotlin enum subclasses don't have the enum flag set. See b/315186101.
+    return input.stream()
+        .map(
+            path -> {
+              try {
+                String subtype = path.getFileName().toString();
+                String subtypeNoClass =
+                    subtype.substring(0, (subtype.length() - ".class".length()));
+                String descr =
+                    DescriptorUtils.replaceSimpleClassNameInDescriptor(
+                        DescriptorUtils.javaTypeToDescriptor(MyEnum2Cases.class.getTypeName()),
+                        subtypeNoClass);
+                return transformer(path, Reference.classFromDescriptor(descr))
+                    .setAccessFlags(ClassAccessFlags::unsetEnum)
+                    .transform();
+              } catch (IOException e) {
+                throw new RuntimeException(e);
+              }
+            })
+        .collect(Collectors.toList());
+  }
+
+  enum MyEnum2Cases {
+    A(8) {
+      @NeverInline
+      @Override
+      public long operate(long another) {
+        return num * another;
+      }
+    },
+    B(32) {
+      @NeverInline
+      @Override
+      public long operate(long another) {
+        return num + another;
+      }
+    };
+    final long num;
+
+    MyEnum2Cases(long num) {
+      this.num = num;
+    }
+
+    public abstract long operate(long another);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum2Cases.A.operate(42));
+      System.out.println(MyEnum2Cases.B.operate(42));
+      System.out.println(indirect(MyEnum2Cases.A));
+      System.out.println(indirect(MyEnum2Cases.B));
+    }
+
+    @NeverInline
+    public static long indirect(MyEnum2Cases e) {
+      return e.operate(12);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/features/IllegalValuePropagationIntoIsolatedSplitTest.java b/src/test/java/com/android/tools/r8/features/IllegalValuePropagationIntoIsolatedSplitTest.java
new file mode 100644
index 0000000..595c0e5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/features/IllegalValuePropagationIntoIsolatedSplitTest.java
@@ -0,0 +1,118 @@
+// 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.features;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPackagePrivate;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoAccessModification;
+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.Box;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+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 IllegalValuePropagationIntoIsolatedSplitTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableIsolatedSplits;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, isolated splits: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    Box<CodeInspector> baseInspectorBox = new Box<>();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Base.class, NonPublicBaseEnum.class)
+        .addKeepClassRules(Base.class)
+        .addFeatureSplit(Feature.class)
+        .addKeepClassAndMembersRules(Feature.class)
+        .enableIsolatedSplits(enableIsolatedSplits)
+        .enableInliningAnnotations()
+        .enableNoAccessModificationAnnotationsForClasses()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            baseInspectorBox::set,
+            featureInspector -> {
+              CodeInspector baseInspector = baseInspectorBox.get();
+              ClassSubject baseClassSubject = baseInspector.clazz(Base.class);
+              assertThat(baseClassSubject, isPresent());
+
+              MethodSubject getMethodSubject = baseClassSubject.uniqueMethodWithOriginalName("get");
+              assertThat(getMethodSubject, isPresentIf(enableIsolatedSplits));
+
+              ClassSubject enumClassSubject = baseInspector.clazz(NonPublicBaseEnum.class);
+              assertThat(enumClassSubject, isPresent());
+              assertThat(enumClassSubject, isPackagePrivate());
+
+              FieldSubject enumFieldSubject = enumClassSubject.uniqueFieldWithOriginalName("A");
+              assertThat(enumFieldSubject, isPresent());
+
+              ClassSubject featureClassSubject = featureInspector.clazz(Feature.class);
+              assertThat(featureClassSubject, isPresent());
+
+              MethodSubject featureMethodSubject =
+                  featureClassSubject.uniqueMethodWithOriginalName("test");
+              assertThat(featureMethodSubject, isPresent());
+              assertEquals(
+                  enableIsolatedSplits,
+                  featureMethodSubject
+                      .streamInstructions()
+                      .noneMatch(
+                          instruction ->
+                              instruction.isStaticGet()
+                                  && instruction
+                                      .getField()
+                                      .asFieldReference()
+                                      .equals(enumFieldSubject.getFinalReference())));
+              if (enableIsolatedSplits) {
+                assertThat(featureMethodSubject, invokesMethod(getMethodSubject));
+              }
+            });
+  }
+
+  public static class Base {
+
+    @NeverInline
+    public static Enum<?> get() {
+      return NonPublicBaseEnum.A;
+    }
+  }
+
+  @NoAccessModification
+  enum NonPublicBaseEnum {
+    A
+  }
+
+  public static class Feature {
+
+    public static void test() {
+      Enum<?> a = Base.get();
+      System.out.println(a);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/features/InliningLeadsToPackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java b/src/test/java/com/android/tools/r8/features/InliningLeadsToPackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
new file mode 100644
index 0000000..60162ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/features/InliningLeadsToPackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.features;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoAccessModification;
+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.Box;
+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 InliningLeadsToPackagePrivateIsolatedSplitCrossBoundaryReferenceTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableIsolatedSplits;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, isolated splits: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    Box<CodeInspector> baseInspectorBox = new Box<>();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Base.class)
+        .addFeatureSplit(Feature.class)
+        .addKeepClassAndMembersRules(Feature.class)
+        .enableInliningAnnotations()
+        .enableIsolatedSplits(enableIsolatedSplits)
+        .enableNoAccessModificationAnnotationsForMembers()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            baseInspectorBox::set,
+            featureInspector -> {
+              CodeInspector baseInspector = baseInspectorBox.get();
+              ClassSubject baseClassSubject = baseInspector.clazz(Base.class);
+              assertThat(baseClassSubject, isPresent());
+
+              MethodSubject callNonPublicMethodSubject =
+                  baseClassSubject.uniqueMethodWithOriginalName("callNonPublicMethod");
+              assertThat(callNonPublicMethodSubject, isPresentIf(enableIsolatedSplits));
+
+              MethodSubject nonPublicMethodSubject =
+                  baseClassSubject.uniqueMethodWithOriginalName("nonPublicMethod");
+              assertThat(nonPublicMethodSubject, isPresent());
+
+              ClassSubject featureClassSubject = featureInspector.clazz(Feature.class);
+              assertThat(featureClassSubject, isPresent());
+
+              MethodSubject featureMethodSubject =
+                  featureClassSubject.uniqueMethodWithOriginalName("test");
+              assertThat(featureMethodSubject, isPresent());
+              assertThat(
+                  featureMethodSubject,
+                  invokesMethod(
+                      enableIsolatedSplits ? callNonPublicMethodSubject : nonPublicMethodSubject));
+            });
+  }
+
+  public static class Base {
+
+    public static void callNonPublicMethod() {
+      nonPublicMethod();
+    }
+
+    @NoAccessModification
+    @NeverInline
+    static void nonPublicMethod() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  public static class Feature {
+
+    public static void test() {
+      Base.callNonPublicMethod();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java b/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
new file mode 100644
index 0000000..1b24d61
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
@@ -0,0 +1,151 @@
+// 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.features;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilationIf;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder.DiagnosticsConsumer;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.features.diagnostic.IllegalAccessWithIsolatedFeatureSplitsDiagnostic;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+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 PackagePrivateIsolatedSplitCrossBoundaryReferenceTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableIsolatedSplits;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, isolated splits: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void testPublicToPublic() throws Exception {
+    runTest("testPublicToPublic", TestDiagnosticMessages::assertNoMessages);
+  }
+
+  @Test
+  public void testPublicToNonPublic() throws Exception {
+    MethodReference accessedMethod =
+        Reference.methodFromMethod(NonPublicBase.class.getDeclaredMethod("nonPublicMethod"));
+    MethodReference context =
+        Reference.methodFromMethod(Feature.class.getDeclaredMethod("testPublicToNonPublic"));
+    assertFailsCompilationIf(
+        enableIsolatedSplits,
+        () ->
+            runTest(
+                "testPublicToNonPublic",
+                diagnostics ->
+                    diagnostics.assertErrorsMatch(
+                        allOf(
+                            diagnosticType(IllegalAccessWithIsolatedFeatureSplitsDiagnostic.class),
+                            diagnosticMessage(
+                                equalTo(getExpectedDiagnosticMessage(accessedMethod, context)))))));
+  }
+
+  @Test
+  public void testNonPublicToPublic() throws Exception {
+    ClassReference accessedClass = Reference.classFromClass(NonPublicBaseSub.class);
+    MethodReference context =
+        Reference.methodFromMethod(Feature.class.getDeclaredMethod("testNonPublicToPublic"));
+    assertFailsCompilationIf(
+        enableIsolatedSplits,
+        () ->
+            runTest(
+                "testNonPublicToPublic",
+                diagnostics ->
+                    diagnostics.assertErrorsMatch(
+                        allOf(
+                            diagnosticType(IllegalAccessWithIsolatedFeatureSplitsDiagnostic.class),
+                            diagnosticMessage(
+                                equalTo(getExpectedDiagnosticMessage(accessedClass, context)))))));
+  }
+
+  private static String getExpectedDiagnosticMessage(
+      ClassReference accessedClass, MethodReference context) {
+    return "Unexpected illegal access to non-public class in another feature split "
+        + "(accessed: "
+        + accessedClass.getTypeName()
+        + ", context: "
+        + MethodReferenceUtils.toSourceString(context)
+        + ").";
+  }
+
+  private static String getExpectedDiagnosticMessage(
+      MethodReference accessedMethod, MethodReference context) {
+    return "Unexpected illegal access to non-public method in another feature split "
+        + "(accessed: "
+        + MethodReferenceUtils.toSourceString(accessedMethod)
+        + ", context: "
+        + MethodReferenceUtils.toSourceString(context)
+        + ").";
+  }
+
+  private void runTest(String name, DiagnosticsConsumer inspector) throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(NonPublicBase.class, PublicBaseSub.class, NonPublicBaseSub.class)
+        .addKeepClassAndMembersRules(
+            NonPublicBase.class, PublicBaseSub.class, NonPublicBaseSub.class)
+        .addFeatureSplit(Feature.class)
+        .addKeepRules("-keep class " + Feature.class.getTypeName() + " { void " + name + "(); }")
+        .enableIsolatedSplits(enableIsolatedSplits)
+        .setMinApi(parameters)
+        .compileWithExpectedDiagnostics(enableIsolatedSplits ? inspector : this::assertNoMessages);
+  }
+
+  private void assertNoMessages(TestDiagnosticMessages diagnostics) {
+    diagnostics.assertNoMessages();
+  }
+
+  static class NonPublicBase {
+
+    public static void publicMethod() {
+      // Intentionally empty.
+    }
+
+    static void nonPublicMethod() {
+      // Intentionally empty.
+    }
+  }
+
+  public static class PublicBaseSub extends NonPublicBase {}
+
+  static class NonPublicBaseSub extends NonPublicBase {}
+
+  public static class Feature {
+
+    public static void testPublicToPublic() {
+      PublicBaseSub.publicMethod();
+    }
+
+    public static void testPublicToNonPublic() {
+      PublicBaseSub.nonPublicMethod();
+    }
+
+    public static void testNonPublicToPublic() {
+      NonPublicBaseSub.publicMethod();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/features/PreserveIllegalAccessAcrossIsolatedSplitBoundaryTest.java b/src/test/java/com/android/tools/r8/features/PreserveIllegalAccessAcrossIsolatedSplitBoundaryTest.java
new file mode 100644
index 0000000..ecae36b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/features/PreserveIllegalAccessAcrossIsolatedSplitBoundaryTest.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.features;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPackagePrivate;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.Box;
+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 PreserveIllegalAccessAcrossIsolatedSplitBoundaryTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableIsolatedSplits;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, isolated splits: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    Box<CodeInspector> baseInspectorBox = new Box<>();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Base.class)
+        .addKeepClassRules(Base.class)
+        .addFeatureSplit(Feature.class)
+        .addKeepClassAndMembersRules(Feature.class)
+        .applyIf(enableIsolatedSplits, testBuilder -> testBuilder.addDontWarn(Feature.class))
+        .enableIsolatedSplits(enableIsolatedSplits)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            baseInspectorBox::set,
+            featureInspector -> {
+              CodeInspector baseInspector = baseInspectorBox.get();
+              ClassSubject baseClassSubject = baseInspector.clazz(Base.class);
+              assertThat(baseClassSubject, isPresent());
+
+              MethodSubject nonPublicMethodSubject =
+                  baseClassSubject.uniqueMethodWithOriginalName("nonPublicMethod");
+              assertThat(nonPublicMethodSubject, isPresentIf(enableIsolatedSplits));
+              if (enableIsolatedSplits) {
+                assertThat(nonPublicMethodSubject, isPackagePrivate());
+              }
+
+              MethodSubject otherMethodSubject =
+                  baseClassSubject.uniqueMethodWithOriginalName("otherMethod");
+              assertThat(otherMethodSubject, isPresent());
+
+              ClassSubject featureClassSubject = featureInspector.clazz(Feature.class);
+              assertThat(featureClassSubject, isPresent());
+
+              MethodSubject featureMethodSubject =
+                  featureClassSubject.uniqueMethodWithOriginalName("test");
+              assertThat(featureMethodSubject, isPresent());
+              assertThat(
+                  featureMethodSubject,
+                  invokesMethod(
+                      enableIsolatedSplits ? nonPublicMethodSubject : otherMethodSubject));
+            });
+  }
+
+  public static class Base {
+
+    static void nonPublicMethod() {
+      otherMethod();
+    }
+
+    @NeverInline
+    public static void otherMethod() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  public static class Feature {
+
+    public static void test() {
+      Base.nonPublicMethod();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
index f0955fc..e070f46 100644
--- a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
@@ -9,6 +9,7 @@
 import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
 
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -43,13 +44,17 @@
         testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
             .addProgramClasses(I.class, A.class, Main.class)
             .addProgramClassFileData(getClassWithTransformedInvoked())
-            .run(parameters.getRuntime(), Main.class);
+            .run(parameters.getRuntime(), Main.class)
+            .apply(this::inspectRunResult);
+  }
+
+  private void inspectRunResult(SingleTestRunResult<?> runResult) {
     if (parameters.isDexRuntime() && parameters.canUseDefaultAndStaticInterfaceMethods()) {
       // TODO(b/166210854): Runs really should fail, but since DEX does not have interface
       //  method references the VM will just dispatch.
-      result.assertSuccessWithOutput(EXPECTED);
+      runResult.assertSuccessWithOutput(EXPECTED);
     } else {
-      result.assertFailureWithErrorThatThrows(getExpectedException());
+      runResult.assertFailureWithErrorThatThrows(getExpectedException());
     }
   }
 
@@ -74,8 +79,11 @@
         .addKeepMainRule(Main.class)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class)
-        // TODO(b/166210854): Runs but should have failed.
-        .assertSuccessWithOutput(EXPECTED);
+        .applyIf(
+            parameters.isCfRuntime(),
+            // TODO(b/166210854): Runs but should have failed.
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED),
+            this::inspectRunResult);
   }
 
   private byte[] getClassWithTransformedInvoked() throws IOException {
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridgeTest.java b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridgeTest.java
index e89317f..0421acd 100644
--- a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridgeTest.java
@@ -73,7 +73,7 @@
 
   public static class A implements I {}
 
-  public static class B extends A {
+  public static class B extends A { // B extends Object implements I
 
     public void bar() {
       foo(); // Will be rewritten to invoke-special B.foo()
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java
index 15f1e9c..455e037 100644
--- a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java
@@ -6,17 +6,13 @@
 import static com.android.tools.r8.utils.DescriptorUtils.getBinaryNameFromJavaType;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
 
-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.DexVm.Version;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.codeinspector.AssertUtils;
 import java.io.IOException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,30 +59,17 @@
 
   @Test
   public void testR8() throws Exception {
-    Box<R8TestCompileResult> compileResult = new Box<>();
-
-    // TODO(b/313065227): Should succeed.
-    AssertUtils.assertFailsCompilationIf(
-        parameters.isCfRuntime(),
-        () ->
-            testForR8(parameters.getBackend())
-                .addProgramClasses(I.class, Main.class)
-                .addProgramClassFileData(getClassWithTransformedInvoked())
-                .addKeepMainRule(Main.class)
-                .setMinApi(parameters)
-                .compile()
-                .apply(compileResult::set));
-
-    if (!compileResult.isSet()) {
-      assertTrue(parameters.isCfRuntime());
-      return;
-    }
-
-    // TODO(b/313065227): Should succeed.
-    compileResult
-        .get()
+    testForR8(parameters.getBackend())
+        .addProgramClasses(I.class, Main.class)
+        .addProgramClassFileData(getClassWithTransformedInvoked())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class)
-        .assertFailureWithErrorThatThrows(NullPointerException.class);
+        // TODO(b/313065227): Should succeed.
+        .applyIf(
+            parameters.isCfRuntime(),
+            runResult -> runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class),
+            runResult -> runResult.assertFailureWithErrorThatThrows(NullPointerException.class));
   }
 
   private byte[] getClassWithTransformedInvoked() throws IOException {
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 4817266..f983650 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
@@ -109,7 +109,9 @@
     DexType originalType =
         dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptor("inlining.Nullability"));
     nullabilityClass =
-        horizontallyMergedClasses.getMergeTargetOrDefault(originalType).toSourceString();
+        horizontallyMergedClasses
+            .getMergeTargetOrDefault(originalType, originalType)
+            .toSourceString();
   }
 
   private void generateR8Version(Path out, Path mapFile, boolean inlining) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/keepanno/FieldPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/FieldPatternsTest.java
new file mode 100644
index 0000000..4f26412
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/FieldPatternsTest.java
@@ -0,0 +1,114 @@
+// 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 static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldPatternsTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world", "42", A.class.toString());
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public FieldPatternsTest(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)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueFieldWithOriginalName("fieldA"), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueFieldWithOriginalName("fieldB"), isAbsent());
+    assertThat(inspector.clazz(A.class).uniqueFieldWithOriginalName("fieldC"), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueFieldWithOriginalName("fieldD"), isPresent());
+  }
+
+  static class A {
+
+    public String fieldA = "Hello, world";
+    public Integer fieldB = 42; // Not used or kept.
+    public int fieldC = 42;
+    public Object fieldD = A.class;
+
+    @UsesReflection({
+      @KeepTarget(classConstant = A.class),
+      @KeepTarget(
+          classConstant = A.class,
+          fieldTypePattern = @TypePattern(name = "java.lang.String"),
+          constraints = {KeepConstraint.LOOKUP, KeepConstraint.FIELD_GET}),
+      @KeepTarget(
+          classConstant = A.class,
+          fieldTypeConstant = Object.class,
+          constraints = {KeepConstraint.LOOKUP, KeepConstraint.FIELD_GET}),
+      @KeepTarget(
+          classConstant = A.class,
+          fieldTypePattern = @TypePattern(constant = int.class),
+          constraints = {KeepConstraint.LOOKUP, KeepConstraint.FIELD_GET})
+    })
+    public void foo() throws Exception {
+      for (Field field : getClass().getDeclaredFields()) {
+        if (field.getType().equals(String.class)
+            || field.getType().equals(Object.class)
+            || field.getType().equals(int.class)) {
+          System.out.println(field.get(this));
+        }
+      }
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
index c8ea937..210cea0 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -47,7 +48,7 @@
   }
 
   @Test
-  public void testWithRuleExtraction() throws Exception {
+  public void testR8() throws Exception {
     testForR8(parameters.getBackend())
         .enableExperimentalKeepAnnotations()
         .addProgramClasses(getInputClasses())
@@ -58,8 +59,29 @@
         .inspect(this::checkOutput);
   }
 
+  @Test
+  public void testNoRefReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClassNoRef.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testNoRefR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClassNoRef.class)
+        .allowUnusedProguardConfigurationRules()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClassNoRef.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutputNoRef);
+  }
+
   public List<Class<?>> getInputClasses() {
-    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+    return ImmutableList.of(TestClass.class, TestClassNoRef.class, A.class, B.class, C.class);
   }
 
   private void checkOutput(CodeInspector inspector) {
@@ -70,8 +92,17 @@
     assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
   }
 
+  private void checkOutputNoRef(CodeInspector inspector) {
+    // A remains as it has an unconditional keep annotation.
+    assertThat(inspector.clazz(A.class), isPresent());
+    // B should be inlined and eliminated since A.foo is not live and its keep annotation inactive.
+    assertThat(inspector.clazz(B.class), isAbsent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+  }
+
   @UsedByReflection(
-      description = "Ensure that the class A remains as we are assuming the contents of its name.")
+      description = "Ensure that A remains valid for lookup as we compute B's name from it.",
+      constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME})
   static class A {
 
     public void foo() throws Exception {
@@ -86,7 +117,9 @@
         // Only if A.foo is live do we need to keep this.
         preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
         // Both the class and method are reflectively accessed.
-        kind = KeepItemKind.CLASS_AND_MEMBERS)
+        kind = KeepItemKind.CLASS_AND_METHODS,
+        // Both the class and method need to be looked up. Since static, only the method is invoked.
+        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.METHOD_INVOKE})
     public static void bar() {
       System.out.println("Hello, world");
     }
@@ -106,4 +139,11 @@
       new A().foo();
     }
   }
+
+  static class TestClassNoRef {
+
+    public static void main(String[] args) throws Exception {
+      B.bar();
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
index 414146d6..c20cc8a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
@@ -11,6 +11,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.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -86,7 +87,8 @@
         value = {
           @KeepTarget(
               className = "com.android.tools.r8.keepanno.KeepUsesReflectionOnFieldTest$A",
-              fieldType = "java.lang.String")
+              fieldType = "java.lang.String",
+              constraints = {KeepConstraint.LOOKUP, KeepConstraint.FIELD_GET})
         })
     public void foo() throws Exception {
       for (Field field : getClass().getDeclaredFields()) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/MethodPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/MethodPatternsTest.java
new file mode 100644
index 0000000..86f2f9c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/MethodPatternsTest.java
@@ -0,0 +1,156 @@
+// 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 static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MethodPatternsTest extends TestBase {
+
+  static final String EXPECTED =
+      StringUtils.lines("Hello 42", "Hello 12", "int", "long", "class java.lang.Integer");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public MethodPatternsTest(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 testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int", "int"), isPresent());
+    assertThat(
+        inspector.clazz(B.class).method("void", "bar", "java.lang.Object", "java.lang.Object"),
+        isAbsent());
+    assertThat(
+        inspector.clazz(B.class).method("int", "bar", "int", "long", "java.lang.Integer"),
+        isPresent());
+    assertThat(
+        inspector.clazz(B.class).method("int", "bar", "int", "long", "java.lang.Integer", "int"),
+        isAbsent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodReturnTypePattern = @TypePattern(name = "void"),
+          methodParameterTypePatterns = {@TypePattern(constant = int.class)}),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodReturnTypeConstant = void.class,
+          methodParameterTypePatterns = {
+            @TypePattern(constant = int.class),
+            @TypePattern(name = "int")
+          }),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodReturnTypeConstant = int.class,
+          methodParameterTypePatterns = {@TypePattern, @TypePattern, @TypePattern}),
+    })
+    public void foo() throws Exception {
+      // Invoke the first and second method.
+      B.class.getDeclaredMethod("bar", int.class).invoke(null, 42);
+      B.class.getDeclaredMethod("bar", int.class, int.class).invoke(null, 1, 2);
+      // Print args of third method.
+      for (Method method : B.class.getDeclaredMethods()) {
+        if (method.getReturnType().equals(int.class) && method.getParameterCount() == 3) {
+          for (Class<?> type : method.getParameterTypes()) {
+            System.out.println(type);
+          }
+        }
+      }
+    }
+  }
+
+  static class B {
+    public static void bar() {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static void bar(int value) {
+      System.out.println("Hello " + value);
+    }
+
+    public static void bar(int value1, int value2) {
+      System.out.println("Hello " + value1 + value2);
+    }
+
+    public static void bar(Object value1, Object value2) {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static int bar(int value1, long value2, Integer value3) {
+      System.out.println("Hello " + value1 + value2 + value3);
+      return value1 + (int) value2 + value3;
+    }
+
+    public static int bar(int value1, long value2, Integer value3, int value4) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
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 8baa5ce..f0ea424 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
@@ -77,8 +77,7 @@
                     .build())
             .build();
     // Disallow will issue the full inverse of the known options, e.g., 'allowaccessmodification'.
-    List<String> options =
-        ImmutableList.of("shrinking", "obfuscation", "accessmodification", "annotationremoval");
+    List<String> options = ImmutableList.of("shrinking", "obfuscation", "accessmodification");
     String allows = String.join(",allow", options);
     // The "any" item will be split in two rules, one for the targeted types and one for the
     // targeted members.
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
index 1225db2..0cf7639 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -6,11 +6,14 @@
 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.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 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.lang.reflect.Field;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -19,7 +22,8 @@
 @RunWith(Parameterized.class)
 public class UsesReflectionDocumentationTest extends TestBase {
 
-  static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+  static final String EXPECTED =
+      StringUtils.lines("on Base", "on Sub", "intField = 42", "stringField = Hello!");
 
   private final TestParameters parameters;
 
@@ -35,7 +39,8 @@
   @Test
   public void testReference() throws Exception {
     testForRuntime(parameters)
-        .addProgramClasses(getInputClasses())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED);
   }
@@ -44,64 +49,122 @@
   public void testWithRuleExtraction() throws Exception {
     testForR8(parameters.getBackend())
         .enableExperimentalKeepAnnotations()
-        .addProgramClasses(getInputClasses())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
         .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);
+  public List<Class<?>> getExampleClasses() {
+    return ImmutableList.of(Example1.class, Example2.class);
   }
 
-  static class BaseClass {
-    void hiddenMethod() {
-      System.out.println("on Base");
+  static class Example1 {
+
+    static class BaseClass {
+      void hiddenMethod() {
+        System.out.println("on Base");
+      }
+    }
+
+    static class SubClass extends BaseClass {
+      void hiddenMethod() {
+        System.out.println("on Sub");
+      }
+    }
+
+    /* INCLUDE DOC: UsesReflectionOnVirtualMethod
+    For example, if your program is reflectively invoking a method, you
+    should annotate the method that is doing the reflection. The annotation must describe the
+    assumptions the reflective code makes.
+
+    In the following example, the method `callHiddenMethod` 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.
+
+    The assumptions the code makes are that all methods with the name
+    `hiddenMethod` and the empty list of parameters must remain valid for `getDeclaredMethod` if they
+    are objects that are instances of the class `BaseClass` or subclasses thereof.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsesReflectionOnVirtualMethod
+    public class MyHiddenMethodCaller {
+
+      @UsesReflection({
+        @KeepTarget(
+            instanceOfClassConstant = BaseClass.class,
+            methodName = "hiddenMethod",
+            methodParameters = {})
+      })
+      public void callHiddenMethod(BaseClass base) throws Exception {
+        base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      new MyHiddenMethodCaller().callHiddenMethod(new BaseClass());
+      new MyHiddenMethodCaller().callHiddenMethod(new SubClass());
     }
   }
 
-  static class SubClass extends BaseClass {
-    void hiddenMethod() {
-      System.out.println("on Sub");
+  static class Example2 {
+
+    interface PrintableFieldInterface {}
+
+    static class ClassWithFields implements PrintableFieldInterface {
+      final int intField = 42;
+      String stringField = "Hello!";
+    }
+
+    /* INCLUDE DOC: UsesReflectionFieldPrinter
+    For example, if your program is reflectively accessing the fields on a class, you should
+    annotate the method that is doing the reflection.
+
+    In the following example, the `printFieldValues` method takes in an object of
+    type `PrintableFieldInterface` and then looks for all the fields declared on the class
+    of the object.
+
+    The `@KeepTarget` describes these field targets. Since the printing only cares about preserving
+    the fields, the `@KeepTarget#kind` is set to `@KeepItemKind#ONLY_FIELDS`. Also, since printing
+    the field names and values only requires looking up the field, printing its name and getting
+    its value the `@KeepTarget#constraints` are set to just `@KeepConstraint#LOOKUP`,
+    `@KeepConstraint#NAME` and `@KeepConstraint#FIELD_GET`.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsesReflectionFieldPrinter
+    public class MyFieldValuePrinter {
+
+      @UsesReflection({
+        @KeepTarget(
+            instanceOfClassConstant = PrintableFieldInterface.class,
+            kind = KeepItemKind.ONLY_FIELDS,
+            constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+      })
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      new MyFieldValuePrinter().printFieldValues(new ClassWithFields());
     }
   }
 
-  /* INCLUDE DOC: UsesReflectionOnVirtualMethod
-  For example, if your program is reflectively invoking a method, you
-  should annotate the method that is doing the reflection. The annotation must describe the
-  assumptions the reflective code makes.
-
-  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.
-
-  The assumptions the code makes are that all methods with the name
-  `hiddenMethod` and the empty list of parameters must remain valid for `getDeclaredMethod` if they
-  are objects that are instances of the class `BaseClass` or subclasses thereof.
-  INCLUDE END */
-
-  // INCLUDE CODE: UsesReflectionOnVirtualMethod
-  static class MyClass {
-
-    @UsesReflection({
-      @KeepTarget(
-          instanceOfClassConstant = BaseClass.class,
-          methodName = "hiddenMethod",
-          methodParameters = {})
-    })
-    public void foo(BaseClass base) throws Exception {
-      base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
-    }
-  }
-
-  // INCLUDE END
-
   static class TestClass {
 
     public static void main(String[] args) throws Exception {
-      new MyClass().foo(new BaseClass());
-      new MyClass().foo(new SubClass());
+      Example1.run();
+      Example2.run();
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index 6fd05eb..9088752 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -7,11 +7,16 @@
 import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
 
 import com.android.tools.r8.ToolHelper;
+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.KeepConstraint;
 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.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;
@@ -22,6 +27,8 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -69,6 +76,7 @@
     this.generator = generator;
     typeLinkReplacements =
         getTypeLinkReplacements(
+            // Annotations.
             KeepEdge.class,
             KeepBinding.class,
             KeepTarget.class,
@@ -76,7 +84,13 @@
             UsesReflection.class,
             UsedByReflection.class,
             UsedByNative.class,
-            KeepForApi.class);
+            KeepForApi.class,
+            // Enums.
+            KeepConstraint.class,
+            KeepItemKind.class,
+            MemberAccessFlags.class,
+            MethodAccessFlags.class,
+            FieldAccessFlags.class);
     populateCodeAndDocReplacements(
         UsesReflectionDocumentationTest.class, MainMethodsDocumentationTest.class);
   }
@@ -84,7 +98,22 @@
   private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
     ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
     for (Class<?> clazz : classes) {
-      builder.put("`@" + clazz.getSimpleName() + "`", getMdLink(clazz));
+      String prefix = "`@" + clazz.getSimpleName();
+      String suffix = "`";
+      if (clazz.isAnnotation()) {
+        builder.put(prefix + suffix, getMdAnnotationLink(clazz));
+        for (Method method : clazz.getDeclaredMethods()) {
+          builder.put(
+              prefix + "#" + method.getName() + suffix, getMdAnnotationPropertyLink(method));
+        }
+      } else if (clazz.isEnum()) {
+        builder.put(prefix + suffix, getMdEnumLink(clazz));
+        for (Field field : clazz.getDeclaredFields()) {
+          builder.put(prefix + "#" + field.getName() + suffix, getMdEnumFieldLink(field));
+        }
+      } else {
+        throw new RuntimeException("Unexpected type of class for doc links");
+      }
     }
     return builder.build();
   }
@@ -128,9 +157,30 @@
     }
   }
 
-  private String getMdLink(Class<?> clazz) {
-    String url = JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
-    return "[@" + clazz.getSimpleName() + "](" + url + ")";
+  private static String getClassJavaDocUrl(Class<?> clazz) {
+    return JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
+  }
+
+  private String getMdAnnotationLink(Class<?> clazz) {
+    return "[@" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
+  }
+
+  private String getMdAnnotationPropertyLink(Method method) {
+    Class<?> clazz = method.getDeclaringClass();
+    String methodName = method.getName();
+    String url = getClassJavaDocUrl(clazz) + "#" + methodName + "()";
+    return "[@" + clazz.getSimpleName() + "." + methodName + "](" + url + ")";
+  }
+
+  private String getMdEnumLink(Class<?> clazz) {
+    return "[" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
+  }
+
+  private String getMdEnumFieldLink(Field field) {
+    Class<?> clazz = field.getDeclaringClass();
+    String fieldName = field.getName();
+    String url = getClassJavaDocUrl(clazz) + "#" + fieldName;
+    return "[" + clazz.getSimpleName() + "." + fieldName + "](" + url + ")";
   }
 
   private void println() {
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index d139b43..bd3d82f 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -12,6 +12,7 @@
 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.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepEdge;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
@@ -19,9 +20,12 @@
 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.TypePattern;
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import java.io.ByteArrayOutputStream;
@@ -259,7 +263,7 @@
     }
 
     private static String KIND_GROUP = "kind";
-    private static String OPTIONS_GROUP = "options";
+    private static String CONSTRAINTS_GROUP = "constraints";
     private static String CLASS_GROUP = "class";
     private static String CLASS_NAME_GROUP = "class-name";
     private static String INSTANCE_OF_GROUP = "instance-of";
@@ -330,6 +334,38 @@
                   .defaultEmptyArray("KeepTarget"));
     }
 
+    private Group typePatternGroup() {
+      return new Group("type-pattern")
+          .addMember(
+              new GroupMember("name")
+                  .setDocTitle("Exact type name as a string.")
+                  .addParagraph("For example, {@code \"long\"} or {@code \"java.lang.String\"}.")
+                  .defaultEmptyString())
+          .addMember(
+              new GroupMember("constant")
+                  .setDocTitle("Exact type from a class constant.")
+                  .addParagraph("For example, {@code String.class}.")
+                  .defaultObjectClass());
+      // TODO(b/248408342): Add more injections on type pattern variants.
+      // /** Exact type name as a string to match any array with that type as member. */
+      // String arrayOf() default "";
+      //
+      // /** Exact type as a class constant to match any array with that type as member. */
+      // Class<?> arrayOfConstant() default TypePattern.class;
+      //
+      // /** If true, the pattern matches any primitive type. Such as, boolean, int, etc. */
+      // boolean anyPrimitive() default false;
+      //
+      // /** If true, the pattern matches any array type. */
+      // boolean anyArray() default false;
+      //
+      // /** If true, the pattern matches any class type. */
+      // boolean anyClass() default false;
+      //
+      // /** If true, the pattern matches any reference type, namely: arrays or classes. */
+      // boolean anyReference() default false;
+    }
+
     private Group getKindGroup() {
       return new Group(KIND_GROUP).addMember(getKindMember());
     }
@@ -342,41 +378,78 @@
           .setDocReturn("The kind for this pattern.")
           .addParagraph("Possible values are:")
           .addUnorderedList(
-              KeepItemKind.ONLY_CLASS.name(),
-              KeepItemKind.ONLY_MEMBERS.name(),
-              KeepItemKind.CLASS_AND_MEMBERS.name())
+              docLink(KeepItemKind.ONLY_CLASS),
+              docLink(KeepItemKind.ONLY_MEMBERS),
+              docLink(KeepItemKind.ONLY_METHODS),
+              docLink(KeepItemKind.ONLY_FIELDS),
+              docLink(KeepItemKind.CLASS_AND_MEMBERS),
+              docLink(KeepItemKind.CLASS_AND_METHODS),
+              docLink(KeepItemKind.CLASS_AND_FIELDS))
           .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());
+              "If unspecified the default kind for an item depends on its member patterns:")
+          .addUnorderedList(
+              docLink(KeepItemKind.ONLY_CLASS) + " if no member patterns are defined",
+              docLink(KeepItemKind.ONLY_METHODS) + " if method patterns are defined",
+              docLink(KeepItemKind.ONLY_FIELDS) + " if field patterns are defined",
+              docLink(KeepItemKind.ONLY_MEMBERS) + " otherwise.");
     }
 
-    private Group getKeepOptionsGroup() {
-      return new Group(OPTIONS_GROUP)
+    private Group getKeepConstraintsGroup() {
+      return new Group(CONSTRAINTS_GROUP)
+          .addMember(constraints())
           .addMember(
               new GroupMember("allow")
-                  .setDocTitle("Define the " + OPTIONS_GROUP + " that are allowed to be modified.")
-                  .addParagraph("The specified options do not need to be preserved for the target.")
-                  .setDocReturn("Options allowed to be modified for the target.")
+                  .setDeprecated("Use " + docLink(constraints()) + " instead.")
+                  .setDocTitle(
+                      "Define the " + CONSTRAINTS_GROUP + " that are allowed to be modified.")
+                  .addParagraph(
+                      "The specified option constraints do not need to be preserved for the"
+                          + " target.")
+                  .setDocReturn("Option constraints allowed to be modified for the target.")
                   .defaultEmptyArray("KeepOption"))
           .addMember(
               new GroupMember("disallow")
+                  .setDeprecated("Use " + docLink(constraints()) + " instead.")
                   .setDocTitle(
-                      "Define the " + OPTIONS_GROUP + " that are not allowed to be modified.")
-                  .addParagraph("The specified options *must* be preserved for the target.")
-                  .setDocReturn("Options not allowed to be modified for the target.")
+                      "Define the " + CONSTRAINTS_GROUP + " that are not allowed to be modified.")
+                  .addParagraph(
+                      "The specified option constraints *must* be preserved for the target.")
+                  .setDocReturn("Option constraints not allowed to be modified for the target.")
                   .defaultEmptyArray("KeepOption"))
           .addDocFooterParagraph(
               "If nothing is specified for "
-                  + OPTIONS_GROUP
-                  + " the default is "
-                  + quote("allow none")
-                  + " / "
-                  + quote("disallow all")
+                  + CONSTRAINTS_GROUP
+                  + " the default is the default for "
+                  + docLink(constraints())
                   + ".");
     }
 
+    private static String docLinkList(Enum<?>... values) {
+      return StringUtils.join(", ", values, v -> docLink(v), BraceType.TUBORG);
+    }
+
+    private static GroupMember constraints() {
+      return new GroupMember("constraints")
+          .setDocTitle("Define the usage constraints of the target.")
+          .addParagraph("The specified constraints must remain valid for the target.")
+          .addParagraph("The default constraints depend on the type of the target.")
+          .addUnorderedList(
+              "For classes, the default is "
+                  + docLinkList(
+                      KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.CLASS_INSTANTIATE),
+              "For methods, the default is "
+                  + docLinkList(
+                      KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.METHOD_INVOKE),
+              "For fields, the default is "
+                  + docLinkList(
+                      KeepConstraint.LOOKUP,
+                      KeepConstraint.NAME,
+                      KeepConstraint.FIELD_GET,
+                      KeepConstraint.FIELD_SET))
+          .setDocReturn("Usage constraints for the target.")
+          .defaultEmptyArray(KeepConstraint.class);
+    }
+
     private GroupMember bindingName() {
       return new GroupMember("bindingName")
           .setDocTitle(
@@ -585,7 +658,22 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any return type"))
                   .setDocReturn("The qualified type name of the method return type.")
-                  .defaultEmptyString());
+                  .defaultEmptyString())
+          .addMember(
+              new GroupMember("methodReturnTypeConstant")
+                  .setDocTitle("Define the method return-type pattern by a class constant.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any return type"))
+                  .setDocReturn("A class constant denoting the type of the method return type.")
+                  .defaultObjectClass())
+          .addMember(
+              new GroupMember("methodReturnTypePattern")
+                  .setDocTitle("Define the method return-type pattern by a type pattern.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any return type"))
+                  .setDocReturn("The pattern of the method return type.")
+                  .defaultType("TypePattern")
+                  .defaultValue("@TypePattern(name = \"\")"));
     }
 
     private Group createMethodParametersGroup() {
@@ -598,7 +686,16 @@
                   .addParagraph(getMethodDefaultDoc("any parameters"))
                   .setDocReturn("The list of qualified type names of the method parameters.")
                   .defaultType("String[]")
-                  .defaultValue("{\"<default>\"}"));
+                  .defaultValue("{\"\"}"))
+          .addMember(
+              new GroupMember("methodParameterTypePatterns")
+                  .setDocTitle(
+                      "Define the method parameters pattern by a list of patterns on types.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any parameters"))
+                  .setDocReturn("The list of type patterns for the method parameters.")
+                  .defaultType("TypePattern[]")
+                  .defaultValue("{@TypePattern(name = \"\")}"));
     }
 
     private Group createFieldAccessGroup() {
@@ -630,8 +727,23 @@
                   .setDocTitle("Define the field-type pattern by a fully qualified type.")
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any type"))
-                  .setDocReturn("The qualified type name of the field type.")
-                  .defaultEmptyString());
+                  .setDocReturn("The qualified type name for the field type.")
+                  .defaultEmptyString())
+          .addMember(
+              new GroupMember("fieldTypeConstant")
+                  .setDocTitle("Define the field-type pattern by a class constant.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any type"))
+                  .setDocReturn("The class constant for the field type.")
+                  .defaultObjectClass())
+          .addMember(
+              new GroupMember("fieldTypePattern")
+                  .setDocTitle("Define the field-type pattern by a pattern on types.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any type"))
+                  .setDocReturn("The type pattern for the field type.")
+                  .defaultType("TypePattern")
+                  .defaultValue("@TypePattern(name = \"\")"));
     }
 
     private void generateClassAndMemberPropertiesWithClassAndMemberBinding() {
@@ -691,6 +803,24 @@
       createFieldTypeGroup().generate(this);
     }
 
+    private void generateTypePattern() {
+      printCopyRight(2023);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A pattern structure for matching types.")
+          .addParagraph("If no properties are set, the default pattern matches any type.")
+          .addParagraph("All properties on this annotation are mutually exclusive.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface TypePattern {");
+      println();
+      withIndent(() -> typePatternGroup().generate(this));
+      println();
+      println("}");
+    }
+
     private void generateKeepBinding() {
       printCopyRight(2022);
       printPackage("annotations");
@@ -740,7 +870,7 @@
           () -> {
             getKindGroup().generate(this);
             println();
-            getKeepOptionsGroup().generate(this);
+            getKeepConstraintsGroup().generate(this);
             println();
             generateClassAndMemberPropertiesWithClassAndMemberBinding();
           });
@@ -942,41 +1072,47 @@
             GroupMember kindProperty = getKindMember();
             kindProperty
                 .clearDocLines()
+                .addParagraph("If unspecified the default kind depends on the annotated item.")
+                .addParagraph("When annotating a class the default kind is:")
+                .addUnorderedList(
+                    docLink(KeepItemKind.ONLY_CLASS) + " if no member patterns are defined;",
+                    docLink(KeepItemKind.CLASS_AND_METHODS) + " if method patterns are defined;",
+                    docLink(KeepItemKind.CLASS_AND_FIELDS) + " if field patterns are defined;",
+                    docLink(KeepItemKind.CLASS_AND_MEMBERS) + "otherwise.")
                 .addParagraph(
-                    "When annotating a class without member patterns, the default kind is "
+                    "When annotating a method the default kind is: "
+                        + docLink(KeepItemKind.ONLY_METHODS))
+                .addParagraph(
+                    "When annotating a field the default kind is: "
+                        + docLink(KeepItemKind.ONLY_FIELDS))
+                .addParagraph(
+                    "It is not possible to use "
                         + 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.")
+                        + " if annotating a member.")
                 .generate(this);
             println();
+            constraints().generate(this);
+            println();
             generateMemberPropertiesNoBinding();
           });
       println();
       println("}");
     }
 
-    private String annoSimpleName(Class<?> clazz) {
+    private static String annoSimpleName(Class<?> clazz) {
       return "@" + simpleName(clazz);
     }
 
-    private String docLink(Class<?> clazz) {
+    private static String docLink(Class<?> clazz) {
       return "{@link " + simpleName(clazz) + "}";
     }
 
-    private String docLink(GroupMember member) {
+    private static String docLink(GroupMember member) {
       return "{@link #" + member.name + "}";
     }
 
-    private String docLink(KeepItemKind kind) {
-      return "{@link KeepItemKind#" + kind.name() + "}";
+    private static String docLink(Enum<?> kind) {
+      return "{@link " + simpleName(kind.getClass()) + "#" + kind.name() + "}";
     }
 
     private void generateConstants() {
@@ -1008,10 +1144,13 @@
             generateConditionConstants();
             generateTargetConstants();
             generateKindConstants();
+            generateConstraintConstants();
             generateOptionConstants();
             generateMemberAccessConstants();
             generateMethodAccessConstants();
             generateFieldAccessConstants();
+
+            generateTypePatternConstants();
           });
       println("}");
     }
@@ -1163,7 +1302,7 @@
           () -> {
             generateAnnotationConstants(KeepTarget.class);
             getKindGroup().generateConstants(this);
-            getKeepOptionsGroup().generateConstants(this);
+            getKeepConstraintsGroup().generateConstants(this);
           });
       println("}");
       println();
@@ -1189,6 +1328,20 @@
       println();
     }
 
+    private void generateConstraintConstants() {
+      println("public static final class Constraints {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(KeepConstraint.class);
+            for (KeepConstraint value : KeepConstraint.values()) {
+              println(
+                  "public static final String " + value.name() + " = " + quote(value.name()) + ";");
+            }
+          });
+      println("}");
+      println();
+    }
+
     private void generateOptionConstants() {
       println("public static final class Option {");
       withIndent(
@@ -1267,6 +1420,17 @@
       println();
     }
 
+    private void generateTypePatternConstants() {
+      println("public static final class TypePattern {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(TypePattern.class);
+            typePatternGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private static void writeFile(Path file, Consumer<Generator> fn) throws IOException {
       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
       PrintStream printStream = new PrintStream(byteStream);
@@ -1288,6 +1452,7 @@
       writeFile(astPkg.resolve("AnnotationConstants.java"), Generator::generateConstants);
 
       Path annoPkg = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno/annotations");
+      writeFile(annoPkg.resolve("TypePattern.java"), Generator::generateTypePattern);
       writeFile(annoPkg.resolve("KeepBinding.java"), Generator::generateKeepBinding);
       writeFile(annoPkg.resolve("KeepTarget.java"), Generator::generateKeepTarget);
       writeFile(annoPkg.resolve("KeepCondition.java"), Generator::generateKeepCondition);
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
index 9d261da..2fd2c50 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -10,6 +10,8 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KeepUnusedReturnValue;
+import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -17,8 +19,6 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -26,7 +26,6 @@
 
 @RunWith(Parameterized.class)
 public class VerticalClassMergingRetraceTest extends RetraceTestBase {
-  private Set<StackTraceLine> haveSeenLines = new HashSet<>();
 
   @Parameters(name = "{0}, mode: {1}, compat: {2}")
   public static Collection<Object[]> data() {
@@ -43,7 +42,10 @@
 
   @Override
   public void configure(R8TestBuilder builder) {
-    builder.enableInliningAnnotations();
+    builder
+        .enableInliningAnnotations()
+        .enableKeepUnusedReturnValueAnnotations()
+        .enableNeverClassInliningAnnotations();
   }
 
   @Override
@@ -126,7 +128,6 @@
     // since the synthetic bridge belongs to ResourceWrapper.foo.
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
-    haveSeenLines.clear();
     runTest(
         ImmutableList.of(),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -142,11 +143,14 @@
   // Will be merged down, and represented as:
   //     java.lang.String ...ResourceWrapper.foo() -> a
   @NeverInline
+  // TODO(b/313404813): Remove @KeepUnusedReturnValue as a workaround for a retrace failure.
+  @KeepUnusedReturnValue
   String foo() {
     throw null;
   }
 }
 
+@NeverClassInline
 class TintResources extends ResourceWrapper {}
 
 class MainApp {
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/CannotUnboxNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/CannotUnboxNumberUnboxingTest.java
new file mode 100644
index 0000000..a143236
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/CannotUnboxNumberUnboxingTest.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.numberunboxing;
+
+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.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CannotUnboxNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public CannotUnboxNumberUnboxingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNumberUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertUnboxing)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("null", "2", "2", "1", "null", "2", "1");
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    ClassSubject mainClass = codeInspector.clazz(Main.class);
+    assertThat(mainClass, isPresent());
+
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName("print");
+    assertThat(methodSubject, isPresent());
+    assertEquals("java.lang.Integer", methodSubject.getOriginalSignature().parameters[0]);
+    assertEquals(
+        "java.lang.Integer", methodSubject.getFinalSignature().asMethodSignature().parameters[0]);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      cannotUnboxPrint();
+      depsNonUnboxable();
+    }
+
+    @NeverInline
+    private static void depsNonUnboxable() {
+      try {
+        forward(null);
+      } catch (NullPointerException npe) {
+        System.out.println("null");
+      }
+      forward(1);
+      forward(0);
+    }
+
+    @NeverInline
+    private static void forward(Integer i) {
+      // Here print2 will get i as a deps which is non-unboxable.
+      print2(i);
+    }
+
+    @NeverInline
+    private static void print2(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+
+    @NeverInline
+    private static void cannotUnboxPrint() {
+      try {
+        print(System.currentTimeMillis() > 0 ? null : -1);
+      } catch (NullPointerException npe) {
+        System.out.println("null");
+      }
+      print(System.currentTimeMillis() > 0 ? 1 : 0);
+      print(1);
+      print(0);
+    }
+
+    @NeverInline
+    private static void print(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsNumberUnboxingTest.java
similarity index 65%
rename from src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
rename to src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsNumberUnboxingTest.java
index ceaf904..97769af 100644
--- a/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsNumberUnboxingTest.java
@@ -6,7 +6,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -16,14 +16,13 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 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 {
+public class StaticMethodsNumberUnboxingTest extends TestBase {
 
   private final TestParameters parameters;
 
@@ -32,7 +31,7 @@
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public SimpleNumberUnboxingTest(TestParameters parameters) {
+  public StaticMethodsNumberUnboxingTest(TestParameters parameters) {
     this.parameters = parameters;
   }
 
@@ -43,35 +42,9 @@
         .addKeepMainRule(Main.class)
         .enableInliningAnnotations()
         .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
-        .addOptionsModification(opt -> opt.testing.printNumberUnboxed = true)
         .setMinApi(parameters)
-        .allowDiagnosticWarningMessages()
         .compile()
         .inspect(this::assertUnboxing)
-        .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");
   }
@@ -79,15 +52,19 @@
   private void assertFirstParameterUnboxed(ClassSubject mainClass, String methodName) {
     MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
     assertThat(methodSubject, isPresent());
-    assertEquals("java.lang.Integer", methodSubject.getOriginalSignature().parameters[0]);
-    assertEquals("int", methodSubject.getFinalSignature().asMethodSignature().parameters[0]);
+    assertTrue(methodSubject.getProgramMethod().getParameter(0).isIntType());
+  }
+
+  private void assertFirstParameterBoxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(0).isReferenceType());
   }
 
   private void assertReturnUnboxed(ClassSubject mainClass, String methodName) {
     MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
     assertThat(methodSubject, isPresent());
-    assertEquals("java.lang.Integer", methodSubject.getOriginalSignature().type);
-    assertEquals("int", methodSubject.getFinalSignature().asMethodSignature().type);
+    assertTrue(methodSubject.getProgramMethod().getReturnType().isIntType());
   }
 
   private void assertUnboxing(CodeInspector codeInspector) {
@@ -101,6 +78,8 @@
 
     assertReturnUnboxed(mainClass, "get");
     assertReturnUnboxed(mainClass, "forwardGet");
+
+    assertFirstParameterBoxed(mainClass, "directPrintNotUnbox");
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsWideUnboxingNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsWideUnboxingNumberUnboxingTest.java
new file mode 100644
index 0000000..7047133
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/StaticMethodsWideUnboxingNumberUnboxingTest.java
@@ -0,0 +1,146 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.numberunboxing;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Objects;
+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 StaticMethodsWideUnboxingNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StaticMethodsWideUnboxingNumberUnboxingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNumberUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertUnboxing)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("32", "33", "42", "43", "52", "53", "2");
+  }
+
+  private void assertSecondParameterUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(1).isLongType());
+  }
+
+  private void assertSecondParameterBoxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(1).isReferenceType());
+  }
+
+  private void assertReturnUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getReturnType().isLongType());
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    ClassSubject mainClass = codeInspector.clazz(Main.class);
+    assertThat(mainClass, isPresent());
+
+    assertSecondParameterUnboxed(mainClass, "print");
+    assertSecondParameterUnboxed(mainClass, "forwardToPrint2");
+    assertSecondParameterUnboxed(mainClass, "directPrintUnbox");
+    assertSecondParameterUnboxed(mainClass, "forwardToPrint");
+
+    assertReturnUnboxed(mainClass, "get");
+    assertReturnUnboxed(mainClass, "forwardGet");
+
+    assertSecondParameterBoxed(mainClass, "directPrintNotUnbox");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      long shift = System.currentTimeMillis() > 0 ? 1L : 0L;
+
+      // The number unboxer should immediately find this method is worth unboxing.
+      directPrintUnbox(shift, 31L);
+      directPrintUnbox(shift, 32L);
+
+      // The number unboxer should find the chain of calls is worth unboxing.
+      forwardToPrint(shift, 41L);
+      forwardToPrint(shift, 42L);
+
+      // The number unboxer should find this method is *not* worth unboxing.
+      Long decode1 = Long.decode("51");
+      Objects.requireNonNull(decode1);
+      directPrintNotUnbox(shift, decode1);
+      Long decode2 = Long.decode("52");
+      Objects.requireNonNull(decode2);
+      directPrintNotUnbox(shift, decode2);
+
+      // The number unboxer should unbox the return values.
+      System.out.println(forwardGet() + 1);
+    }
+
+    @NeverInline
+    private static Long get() {
+      return System.currentTimeMillis() > 0 ? 1L : -1L;
+    }
+
+    @NeverInline
+    private static Long forwardGet() {
+      return get();
+    }
+
+    @NeverInline
+    private static void forwardToPrint(long shift, Long boxed) {
+      forwardToPrint2(shift, boxed);
+    }
+
+    @NeverInline
+    private static void forwardToPrint2(long shift, Long boxed) {
+      print(shift, boxed);
+    }
+
+    @NeverInline
+    private static void print(long shift, Long boxed) {
+      System.out.println(boxed + shift);
+    }
+
+    @NeverInline
+    private static void directPrintUnbox(long shift, Long boxed) {
+      System.out.println(boxed + shift);
+    }
+
+    @NeverInline
+    private static void directPrintNotUnbox(long shift, Long boxed) {
+      Long newBox = Long.valueOf(boxed + shift);
+      System.out.println(newBox);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsNumberUnboxingTest.java
new file mode 100644
index 0000000..6b7e0ec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/numberunboxing/VirtualMethodsNumberUnboxingTest.java
@@ -0,0 +1,146 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.numberunboxing;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Objects;
+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 VirtualMethodsNumberUnboxingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public VirtualMethodsNumberUnboxingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNumberUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> opt.testing.enableNumberUnboxer = true)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertUnboxing)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("32", "33", "42", "43", "51", "52", "2");
+  }
+
+  private void assertFirstParameterUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(0).isIntType());
+  }
+
+  private void assertFirstParameterBoxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getParameter(0).isReferenceType());
+  }
+
+  private void assertReturnUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getProgramMethod().getReturnType().isIntType());
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    ClassSubject mainClass = codeInspector.clazz(Main.class);
+    assertThat(mainClass, isPresent());
+
+    assertFirstParameterUnboxed(mainClass, "print");
+    assertFirstParameterUnboxed(mainClass, "forwardToPrint2");
+    assertFirstParameterUnboxed(mainClass, "directPrintUnbox");
+    assertFirstParameterUnboxed(mainClass, "forwardToPrint");
+
+    assertReturnUnboxed(mainClass, "get");
+    assertReturnUnboxed(mainClass, "forwardGet");
+
+    assertFirstParameterBoxed(mainClass, "directPrintNotUnbox");
+  }
+
+  static class Main {
+
+    private static final Main MAIN = new Main();
+
+    public static void main(String[] args) {
+
+      // The number unboxer should immediately find this method is worth unboxing.
+      MAIN.directPrintUnbox(31);
+      MAIN.directPrintUnbox(32);
+
+      // The number unboxer should find the chain of calls is worth unboxing.
+      MAIN.forwardToPrint(41);
+      MAIN.forwardToPrint(42);
+
+      // The number unboxer should find this method is *not* worth unboxing.
+      Integer decode1 = Integer.decode("51");
+      Objects.requireNonNull(decode1);
+      MAIN.directPrintNotUnbox(decode1);
+      Integer decode2 = Integer.decode("52");
+      Objects.requireNonNull(decode2);
+      MAIN.directPrintNotUnbox(decode2);
+
+      // The number unboxer should unbox the return values.
+      System.out.println(MAIN.forwardGet() + 1);
+    }
+
+    @NeverInline
+    public Integer get() {
+      return System.currentTimeMillis() > 0 ? 1 : -1;
+    }
+
+    @NeverInline
+    public Integer forwardGet() {
+      return get();
+    }
+
+    @NeverInline
+    public void forwardToPrint(Integer boxed) {
+      forwardToPrint2(boxed);
+    }
+
+    @NeverInline
+    public void forwardToPrint2(Integer boxed) {
+      print(boxed);
+    }
+
+    @NeverInline
+    public void print(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+
+    @NeverInline
+    public void directPrintUnbox(Integer boxed) {
+      System.out.println(boxed + 1);
+    }
+
+    @NeverInline
+    public void directPrintNotUnbox(Integer boxed) {
+      System.out.println(boxed);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
index b04e01b..56ea0b9 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
@@ -7,6 +7,7 @@
 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.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -16,7 +17,6 @@
 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.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +46,7 @@
             options -> options.callSiteOptimizationOptions().disableOptimization())
         .addVerticallyMergedClassesInspector(
             inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .enableNeverClassInliningAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspectResidualArtProfile(this::inspect)
@@ -64,11 +65,13 @@
     assertThat(bClassSubject, isPresent());
 
     MethodSubject movedMethodSubject =
-        bClassSubject.uniqueMethodThatMatches(FoundMethodSubject::isPrivate);
+        bClassSubject.uniqueMethodThatMatches(
+            method -> method.isBridge() && method.isSynthetic() && method.isVirtual());
     assertThat(movedMethodSubject, isPresent());
 
     MethodSubject syntheticBridgeMethodSubject =
-        bClassSubject.uniqueMethodThatMatches(FoundMethodSubject::isVirtual);
+        bClassSubject.uniqueMethodThatMatches(
+            method -> !method.isBridge() && !method.isSynthetic() && method.isVirtual());
     assertThat(syntheticBridgeMethodSubject, isPresent());
 
     profileInspector
@@ -90,5 +93,6 @@
     }
   }
 
+  @NeverClassInline
   static class B extends A {}
 }
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 9d3f2c5..0fe9536 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -4,8 +4,7 @@
 package com.android.tools.r8.shaking;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -142,28 +141,22 @@
             .inspector();
 
     ClassSubject superInterface1 = inspector.clazz(B112452064SuperInterface1.class);
-    if (enableUnusedInterfaceRemoval && enableVerticalClassMerging) {
-      assertThat(superInterface1, isAbsent());
-    } else {
-      assertThat(superInterface1, isPresentAndRenamed());
-    }
+    assertThat(
+        superInterface1, isAbsentIf(enableUnusedInterfaceRemoval && enableVerticalClassMerging));
+
     MethodSubject foo = superInterface1.uniqueMethodWithOriginalName("foo");
-    assertThat(foo, not(isPresent()));
+    assertThat(foo, isAbsent());
+
     ClassSubject superInterface2 = inspector.clazz(B112452064SuperInterface2.class);
-    if (enableVerticalClassMerging) {
-      assertThat(superInterface2, not(isPresent()));
-    } else {
-      assertThat(superInterface2, isPresentAndRenamed());
-    }
+    assertThat(
+        superInterface2, isAbsentIf(enableUnusedInterfaceRemoval && enableVerticalClassMerging));
+
     MethodSubject bar = superInterface2.uniqueMethodWithOriginalName("bar");
-    assertThat(bar, not(isPresent()));
+    assertThat(bar, isAbsent());
+
     ClassSubject subInterface = inspector.clazz(B112452064SubInterface.class);
-    if (enableUnusedInterfaceRemoval) {
-      assertThat(subInterface, not(isPresent()));
-    } else {
-      assertThat(subInterface, isPresent());
-      assertThat(subInterface, isPresentAndRenamed());
-    }
+    assertThat(
+        subInterface, isAbsentIf(enableUnusedInterfaceRemoval || enableVerticalClassMerging));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java b/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java
index 1cb68de..88d5f71 100644
--- a/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java
+++ b/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java
@@ -63,12 +63,12 @@
         .inspect(
             inspector -> {
               // Assert that foo is not inlined.
-              ClassSubject A = inspector.clazz(A.class);
-              assertThat(A, isPresent());
-              assertThat(A.uniqueMethodWithOriginalName("foo"), isPresent());
+              ClassSubject B = inspector.clazz(B.class);
+              assertThat(B, isPresent());
+              assertThat(B.uniqueMethodWithOriginalName("foo"), isPresent());
             })
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("A::foo", "A::foo");
+        .assertSuccessWithOutputLines("B::foo", "B::foo");
   }
 
   static class Main {
@@ -81,12 +81,8 @@
 
   public static class A {
 
-    private static void foo() {
-      System.out.println("A::foo");
-    }
-
     private static void bar() {
-      foo();
+      B.foo();
     }
 
     public static void callBarInStartup() {
@@ -97,4 +93,11 @@
       bar();
     }
   }
+
+  public static class B {
+
+    static void foo() {
+      System.out.println("B::foo");
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java b/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java
new file mode 100644
index 0000000..4ca9640
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java
@@ -0,0 +1,169 @@
+// 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.startup.optimization;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+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 static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.errors.StartupClassesNonStartupFractionDiagnostic;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.profile.ExternalStartupClass;
+import com.android.tools.r8.startup.profile.ExternalStartupItem;
+import com.android.tools.r8.startup.profile.ExternalStartupMethod;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+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 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;
+
+/**
+ * Tests the option to provide a startup profile for guiding optimizations without impacting the DEX
+ * layout.
+ */
+@RunWith(Parameterized.class)
+public class NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableStartupProfile;
+
+  @Parameter(1)
+  public boolean enableStartupLayoutOptimization;
+
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameters(name = "{2}, profile: {0}, layout: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    // Startup profile requires native multi dex.
+    assumeTrue(!enableStartupProfile || parameters.canUseNativeMultidex());
+    // Startup layout optimization is meaningless without startup profile.
+    assumeTrue(enableStartupProfile || !enableStartupLayoutOptimization);
+    List<ExternalStartupItem> startupProfile =
+        ImmutableList.of(
+            ExternalStartupClass.builder()
+                .setClassReference(Reference.classFromClass(Main.class))
+                .build(),
+            ExternalStartupMethod.builder()
+                .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+                .build());
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowDiagnosticMessages()
+        .applyIf(
+            enableStartupProfile,
+            testBuilder ->
+                testBuilder
+                    .apply(StartupTestingUtils.addStartupProfile(startupProfile))
+                    .enableStartupLayoutOptimization(enableStartupLayoutOptimization))
+        .setMinApi(parameters)
+        .compileWithExpectedDiagnostics(this::inspectDiagnostics)
+        .applyIf(
+            !enableStartupProfile || !enableStartupLayoutOptimization,
+            compileResult ->
+                compileResult.inspectMultiDex(
+                    primaryDexInspector ->
+                        inspectPrimaryDex(primaryDexInspector, compileResult.inspector())),
+            compileResult ->
+                compileResult.inspectMultiDex(
+                    primaryDexInspector ->
+                        inspectPrimaryDex(primaryDexInspector, compileResult.inspector()),
+                    this::inspectSecondaryDex))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Click!");
+  }
+
+  private void inspectDiagnostics(TestDiagnosticMessages diagnostics) {
+    if (enableStartupProfile && enableStartupLayoutOptimization) {
+      diagnostics.assertInfosMatch(
+          diagnosticType(StartupClassesNonStartupFractionDiagnostic.class));
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void inspectPrimaryDex(CodeInspector primaryDexInspector, CodeInspector appInspector) {
+    if (!enableStartupProfile) {
+      // Everything should be inlined into main().
+      assertEquals(1, primaryDexInspector.allClasses().size());
+      assertEquals(1, primaryDexInspector.clazz(Main.class).allMethods().size());
+      return;
+    }
+
+    // Main.onClick() should be inlined into Main.main(), but OnClickHandler.handle() should
+    // remain.
+    ClassSubject onClickHandlerClassSubject = appInspector.clazz(OnClickHandler.class);
+    assertThat(onClickHandlerClassSubject, isPresent());
+
+    MethodSubject handleMethodSubject =
+        onClickHandlerClassSubject.uniqueMethodWithOriginalName("handle");
+    assertThat(handleMethodSubject, isPresent());
+
+    ClassSubject mainClassSubject = primaryDexInspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+    assertEquals(1, mainClassSubject.allMethods().size());
+    assertThat(mainClassSubject.mainMethod(), invokesMethod(handleMethodSubject));
+
+    // OnClickHandler should not be in the primary DEX when startup layout is enabled.
+    assertThat(
+        primaryDexInspector.clazz(OnClickHandler.class),
+        isAbsentIf(enableStartupLayoutOptimization));
+  }
+
+  private void inspectSecondaryDex(CodeInspector secondaryDexInspector) {
+    assertTrue(enableStartupProfile);
+    assertTrue(enableStartupLayoutOptimization);
+    assertEquals(1, secondaryDexInspector.allClasses().size());
+
+    ClassSubject onClickHandlerClassSubject = secondaryDexInspector.clazz(OnClickHandler.class);
+    assertThat(onClickHandlerClassSubject, isPresent());
+
+    MethodSubject handleMethodSubject =
+        onClickHandlerClassSubject.uniqueMethodWithOriginalName("handle");
+    assertThat(handleMethodSubject, isPresent());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      onClick();
+    }
+
+    static void onClick() {
+      OnClickHandler.handle();
+    }
+  }
+
+  static class OnClickHandler {
+
+    static void handle() {
+      System.out.println("Click!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/startup/optimization/StartupBoundaryOptimizationsWithAlwaysInlineTest.java b/src/test/java/com/android/tools/r8/startup/optimization/StartupBoundaryOptimizationsWithAlwaysInlineTest.java
new file mode 100644
index 0000000..f20675e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/startup/optimization/StartupBoundaryOptimizationsWithAlwaysInlineTest.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.startup.optimization;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.AlwaysInline;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.profile.ExternalStartupClass;
+import com.android.tools.r8.startup.profile.ExternalStartupItem;
+import com.android.tools.r8.startup.profile.ExternalStartupMethod;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StartupBoundaryOptimizationsWithAlwaysInlineTest extends TestBase {
+
+  @Parameter(0)
+  public boolean alwaysInline;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, always inline: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    List<ExternalStartupItem> startupProfile =
+        ImmutableList.of(
+            ExternalStartupClass.builder()
+                .setClassReference(Reference.classFromClass(Main.class))
+                .build(),
+            ExternalStartupMethod.builder()
+                .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+                .build());
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .apply(StartupTestingUtils.addStartupProfile(startupProfile))
+        .applyIf(
+            alwaysInline,
+            R8TestBuilder::enableAlwaysInliningAnnotations,
+            TestShrinkerBuilder::addAlwaysInliningAnnotations)
+        .enableStartupLayoutOptimization(false)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Click!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    if (alwaysInline) {
+      assertEquals(1, inspector.allClasses().size());
+      assertEquals(1, inspector.clazz(Main.class).allMethods().size());
+    } else {
+      assertEquals(2, inspector.allClasses().size());
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      onClick();
+    }
+
+    static void onClick() {
+      OnClickHandler.handle();
+    }
+  }
+
+  static class OnClickHandler {
+
+    @AlwaysInline
+    static void handle() {
+      System.out.println("Click!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
index 8805931..f51ab4b 100644
--- a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -192,6 +193,11 @@
     }
   }
 
+  public static <B extends R8TestBuilder<B>> ThrowableConsumer<B> addStartupProfile(
+      Collection<ExternalStartupItem> startupItems) {
+    return testBuilder -> addStartupProfile(testBuilder, startupItems);
+  }
+
   private static byte[] getTransformedAndroidUtilLog() throws IOException {
     return transformer(Log.class).setClassDescriptor("Landroid/util/Log;").transform();
   }
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 7f74f87..7d6378e 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
@@ -69,7 +69,7 @@
   }
 
   public DexType getTarget(DexType clazz) {
-    return horizontallyMergedClasses.getMergeTargetOrDefault(clazz);
+    return horizontallyMergedClasses.getMergeTargetOrDefault(clazz, clazz);
   }
 
   public Set<DexType> getTargets() {
@@ -99,8 +99,7 @@
 
   public HorizontallyMergedClassesInspector assertMergedInto(
       ClassReference from, ClassReference target) {
-    assertEquals(
-        horizontallyMergedClasses.getMergeTargetOrDefault(toDexType(from)), toDexType(target));
+    assertEquals(getTarget(toDexType(from)), toDexType(target));
     seen.add(toDexType(from).asClassReference());
     seen.add(toDexType(target).asClassReference());
     return this;
@@ -128,7 +127,7 @@
   public HorizontallyMergedClassesInspector assertTypesMerged(Collection<DexType> types) {
     List<DexType> unmerged = new ArrayList<>();
     for (DexType type : types) {
-      if (!horizontallyMergedClasses.hasBeenMergedOrIsMergeTarget(type)) {
+      if (!horizontallyMergedClasses.isMergeSourceOrTarget(type)) {
         unmerged.add(type);
       }
     }
@@ -190,7 +189,7 @@
   public HorizontallyMergedClassesInspector assertTypesNotMerged(Collection<DexType> types) {
     for (DexType type : types) {
       assertTrue(type.isClassType());
-      assertFalse(horizontallyMergedClasses.hasBeenMergedOrIsMergeTarget(type));
+      assertFalse(horizontallyMergedClasses.isMergeSourceOrTarget(type));
     }
     seen.addAll(types.stream().map(DexType::asClassReference).collect(Collectors.toList()));
     return this;
@@ -232,11 +231,8 @@
     }
     if (uniqueTarget == null) {
       for (DexType type : types) {
-        if (horizontallyMergedClasses.hasBeenMergedIntoDifferentType(type)) {
-          fail(
-              "Expected merge target "
-                  + horizontallyMergedClasses.getMergeTargetOrDefault(type).getTypeName()
-                  + " to be in merge group");
+        if (horizontallyMergedClasses.isMergeSource(type)) {
+          fail("Expected merge target " + getTarget(type).getTypeName() + " to be in merge group");
         }
       }
       fail("Expected to find a merge target, but none found");
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index d5fa8bf..04202fd 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -380,11 +380,11 @@
     };
   }
 
-  public static <T extends MemberSubject> Matcher<T> isPrivate() {
+  public static <T extends ClassOrMemberSubject> Matcher<T> isPrivate() {
     return hasVisibility(Visibility.PRIVATE);
   }
 
-  public static <T extends MemberSubject> Matcher<T> isPackagePrivate() {
+  public static <T extends ClassOrMemberSubject> Matcher<T> isPackagePrivate() {
     return hasVisibility(Visibility.PACKAGE_PRIVATE);
   }
 
diff --git a/tools/r8_release.py b/tools/r8_release.py
index ce83d32..4e22b56 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -17,7 +17,7 @@
 
 import utils
 
-R8_DEV_BRANCH = '8.3'
+R8_DEV_BRANCH = '8.4'
 R8_VERSION_FILE = os.path.join('src', 'main', 'java', 'com', 'android', 'tools',
                                'r8', 'Version.java')
 THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')
@@ -225,6 +225,13 @@
 
 def prepare_aosp(args):
     assert args.version
+
+    if (not args.legacy_release):
+        print("Please use the new release process, see go/r8-release-prebuilts. "
+            + "If for some reason the legacy release process is needed "
+            + "pass --legacy-release")
+        sys.exit(1)
+
     assert os.path.exists(args.aosp), "Could not find AOSP path %s" % args.aosp
 
     def release_aosp(options):
@@ -310,12 +317,10 @@
     assert args.version
     assert os.path.exists(args.studio), ("Could not find STUDIO path %s" %
                                          args.studio)
-    if (not args.studio.endswith('-dev')
-        and not args.studio.endswith('-dev/')
-        and not args.studio_legacy_release):
+    if (not args.legacy_release):
         print("Please use the new release process, see go/r8-release-prebuilts. "
             + "If for some reason the legacy release process is needed "
-            + "pass --studio-legacy-release")
+            + "pass --legacy-release")
         sys.exit(1)
 
     def release_studio(options):
@@ -812,8 +817,10 @@
 
 
 def prepare_branch(args):
+    if (len(args.new_dev_branch) < 1 or len(args.new_dev_branch) > 2):
+        print("One or two arguments required for --new-dev-branch")
+        sys.exit(1)
     branch_version = args.new_dev_branch[0]
-    commithash = args.new_dev_branch[1]
 
     current_semver = utils.check_basic_semver_version(
         R8_DEV_BRANCH, ", current release branch version should be x.y", 2)
@@ -829,12 +836,43 @@
         with utils.TempDir() as temp:
             subprocess.check_call(['git', 'clone', utils.REPO_SOURCE, temp])
             with utils.ChangedWorkingDirectory(temp):
-                subprocess.check_call(
-                    ['git', 'branch', branch_version, commithash])
+                if len(options.new_dev_branch) == 1:
+                    # Calculate the usual branch hash.
+                    subprocess.check_call(['git',  'fetch', 'origin', R8_DEV_BRANCH])
+                    hashes = subprocess.check_output(
+                      ['git',
+                      'show',
+                      '-s',
+                      '--pretty=%P',
+                      'origin/%s~1' % R8_DEV_BRANCH]).decode('utf-8').strip()
+                    if (len(hashes.split()) != 2):
+                        print('Expected two parent hashes for commit origin/%s~1'
+                            % R8_DEV_BRANCH)
+                        sys.exit(0)
+                    commithash = hashes.split()[1]
+                    print()
+                    print('Calculated branch hash: %s' % commithash)
+                    print('Please double check that this is the correct branch hash. It'
+                        ' is obtained as the second parent of origin/%s~1.' % R8_DEV_BRANCH)
+                    print('If not rerun the script passing an explicit hash to branch from.')
+                else:
+                    commithash = options.new_dev_branch[1]
+                    print()
+                    print('Using explicit branch hash %s' % commithash)
+                print()
+                print('Use the Gerrit admin UI at'
+                     ' https://r8-review.googlesource.com/admin/repos/r8,branches'
+                     ' to create the %s branch from %s.' % (branch_version, commithash))
+                answer = input("Branch created in Gerrit UI? [y/N]:")
+                if answer != 'y':
+                    print('Aborting preparing branch for %s' % branch_version)
+                    sys.exit(1)
 
+                # Fetch and checkout the new branch created through the Gerrit UI.
+                subprocess.check_call(['git',  'fetch', 'origin', branch_version])
                 subprocess.check_call(['git', 'checkout', branch_version])
 
-                # Rewrite the version, commit and validate.
+                # Rewrite the version on the branch, commit and validate.
                 old_version = 'main'
                 full_version = branch_version + '.0-dev'
                 version_prefix = 'public static final String LABEL = "'
@@ -851,19 +889,22 @@
                 validate_version_change_diff(version_diff_output, old_version,
                                              full_version)
 
+                if options.dry_run:
+                    input(
+                        'DryRun: check %s for content of version %s [enter to continue]:'
+                        % (temp, branch_version))
+
                 # Double check that we want to create a new release branch.
                 if not options.dry_run:
-                    answer = input('Create new branch for %s [y/N]:' %
+                    answer = input('Continue with branch for %s [y/N]:' %
                                    branch_version)
                     if answer != 'y':
-                        print('Aborting new branch for %s' % branch_version)
+                        print('Aborting preparing branch for %s' % branch_version)
                         sys.exit(1)
 
                 maybe_check_call(
                     options,
-                    ['git', 'push', 'origin',
-                     'HEAD:%s' % branch_version])
-                maybe_tag(options, full_version)
+                    ['git', 'cl', 'upload', '--bypass-hooks'])
 
                 print(
                     'Updating tools/r8_release.py to make new dev releases on %s'
@@ -900,11 +941,17 @@
                 validate_branch_change_diff(branch_diff_output, R8_DEV_BRANCH,
                                             branch_version)
 
+                if options.dry_run:
+                    input(
+                        'DryRun: check %s for content of version main [enter to continue]:'
+                        % temp)
+
                 maybe_check_call(options,
                                  ['git', 'cl', 'upload', '-f', '-m', message])
 
                 print('')
-                print('Make sure to send out the branch change CL for review.')
+                print('Make sure to send out the two branch change CL for review'
+                      ' (on %s and main).' % branch_version)
                 print('')
 
     return make_branch
@@ -933,9 +980,11 @@
         help='Update studio mirror of com.android.tools:desugar_jdk_libs')
     group.add_argument(
         '--new-dev-branch',
-        nargs=2,
-        metavar=('<version>', '<main hash>'),
-        help='Create a new branch starting a version line (e.g. 2.0)')
+        nargs='+',
+        metavar=('<version>', '<branch hash>'),
+        help=('Prepare new branch for a version line (e.g. 8.0)'
+             ' Suggested branch hash is calculated '
+             ' if not explicitly specified'))
     result.add_argument('--dev-pre-cherry-pick',
                         metavar=('<main hash(s)>'),
                         default=[],
@@ -961,10 +1010,10 @@
         metavar=('<path>'),
         help='Release for studio by setting the path to a studio '
         'checkout')
-    result.add_argument('--studio-legacy-release',
+    result.add_argument('--legacy-release',
                         default=False,
                         action='store_true',
-                        help='Allow Studio release using the legacy process')
+                        help='Allow Studio/AOSP release using the legacy process')
     result.add_argument('--aosp',
                         metavar=('<path>'),
                         help='Release for aosp by setting the path to the '
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index 8684f89..3aaa40d 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -739,7 +739,7 @@
                 if should_build(options):
                     gradle.RunGradle([
                         utils.GRADLE_TASK_R8LIB
-                        if tool.startswith('r8lib') else UTILS.GRADLE_TASK_R8
+                        if tool.startswith('r8lib') else utils.GRADLE_TASK_R8
                     ])
                 t0 = time.time()
                 exit_code = toolhelper.run(