Merge commit 'bca9efd8de9ca3af84deef1855671a67ee1afb43' into dev-release

Change-Id: Icebe5a3a03186699f55cdbb6c40e8f4ee1776702
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 3f1152e..b98f4ab 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
@@ -306,11 +306,27 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the property `methodNamePattern` also defining method-name.
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the property `methodName` also defining method-name.
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
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 74b2add..bab5ca7 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
@@ -254,6 +254,8 @@
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining member-access.
+   *
    * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
@@ -266,6 +268,8 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining method-access.
+   *
    * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
@@ -278,11 +282,37 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the following other properties defining method-name:
+   *
+   * <ul>
+   *   <li>methodNamePattern
+   *   <li>memberFromBinding
+   * </ul>
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the following other properties defining method-name:
+   *
+   * <ul>
+   *   <li>methodName
+   *   <li>memberFromBinding
+   * </ul>
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
@@ -295,6 +325,7 @@
    * <ul>
    *   <li>methodReturnTypeConstant
    *   <li>methodReturnTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The qualified type name of the method return type.
@@ -314,6 +345,7 @@
    * <ul>
    *   <li>methodReturnType
    *   <li>methodReturnTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return A class constant denoting the type of the method return type.
@@ -333,6 +365,7 @@
    * <ul>
    *   <li>methodReturnType
    *   <li>methodReturnTypeConstant
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The pattern of the method return type.
@@ -347,7 +380,12 @@
    * <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.
+   * <p>Mutually exclusive with the following other properties defining parameters:
+   *
+   * <ul>
+   *   <li>methodParameterTypePatterns
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The list of qualified type names of the method parameters.
    */
@@ -361,7 +399,12 @@
    * <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.
+   * <p>Mutually exclusive with the following other properties defining parameters:
+   *
+   * <ul>
+   *   <li>methodParameters
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The list of type patterns for the method parameters.
    */
@@ -375,6 +418,8 @@
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-access.
+   *
    * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
@@ -387,6 +432,8 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
@@ -403,6 +450,7 @@
    * <ul>
    *   <li>fieldTypeConstant
    *   <li>fieldTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The qualified type name for the field type.
@@ -421,6 +469,7 @@
    * <ul>
    *   <li>fieldType
    *   <li>fieldTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The class constant for the field type.
@@ -439,6 +488,7 @@
    * <ul>
    *   <li>fieldType
    *   <li>fieldTypeConstant
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The type pattern for the field type.
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 5ece578..9d097f0 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
@@ -85,11 +85,27 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the property `methodNamePattern` also defining method-name.
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the property `methodName` also defining method-name.
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
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 d60525d..d601a6a 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
@@ -351,6 +351,8 @@
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining member-access.
+   *
    * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
@@ -363,6 +365,8 @@
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining method-access.
+   *
    * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
@@ -375,11 +379,37 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the following other properties defining method-name:
+   *
+   * <ul>
+   *   <li>methodNamePattern
+   *   <li>memberFromBinding
+   * </ul>
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the following other properties defining method-name:
+   *
+   * <ul>
+   *   <li>methodName
+   *   <li>memberFromBinding
+   * </ul>
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
@@ -392,6 +422,7 @@
    * <ul>
    *   <li>methodReturnTypeConstant
    *   <li>methodReturnTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The qualified type name of the method return type.
@@ -411,6 +442,7 @@
    * <ul>
    *   <li>methodReturnType
    *   <li>methodReturnTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return A class constant denoting the type of the method return type.
@@ -430,6 +462,7 @@
    * <ul>
    *   <li>methodReturnType
    *   <li>methodReturnTypeConstant
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The pattern of the method return type.
@@ -444,7 +477,12 @@
    * <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.
+   * <p>Mutually exclusive with the following other properties defining parameters:
+   *
+   * <ul>
+   *   <li>methodParameterTypePatterns
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The list of qualified type names of the method parameters.
    */
@@ -458,7 +496,12 @@
    * <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.
+   * <p>Mutually exclusive with the following other properties defining parameters:
+   *
+   * <ul>
+   *   <li>methodParameters
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The list of type patterns for the method parameters.
    */
@@ -472,6 +515,8 @@
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-access.
+   *
    * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
@@ -484,6 +529,8 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
@@ -500,6 +547,7 @@
    * <ul>
    *   <li>fieldTypeConstant
    *   <li>fieldTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The qualified type name for the field type.
@@ -518,6 +566,7 @@
    * <ul>
    *   <li>fieldType
    *   <li>fieldTypePattern
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The class constant for the field type.
@@ -536,6 +585,7 @@
    * <ul>
    *   <li>fieldType
    *   <li>fieldTypeConstant
+   *   <li>memberFromBinding
    * </ul>
    *
    * @return The type pattern for the field type.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/StringPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/StringPattern.java
new file mode 100644
index 0000000..f2aec77
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/StringPattern.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// 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 strings.
+ *
+ * <p>If no properties are set, the default pattern matches any string.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface StringPattern {
+
+  /**
+   * Exact string content.
+   *
+   * <p>For example, {@code "foo"} or {@code "java.lang.String"}.
+   *
+   * <p>Mutually exclusive with the following other properties defining string-exact-pattern:
+   *
+   * <ul>
+   *   <li>startsWith
+   *   <li>endsWith
+   * </ul>
+   */
+  String exact() default "";
+
+  /**
+   * Matches strings beginning with the given prefix.
+   *
+   * <p>For example, {@code "get"} to match strings such as {@code "getMyValue"}.
+   *
+   * <p>Mutually exclusive with the property `exact` also defining string-prefix-pattern.
+   */
+  String startsWith() default "";
+
+  /**
+   * Matches strings ending with the given suffix.
+   *
+   * <p>For example, {@code "Setter"} to match strings such as {@code "myValueSetter"}.
+   *
+   * <p>Mutually exclusive with the property `exact` also defining string-suffix-pattern.
+   */
+  String endsWith() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
index 149e51c..229d0f1 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
@@ -128,11 +128,27 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the property `methodNamePattern` also defining method-name.
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the property `methodName` also defining method-name.
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
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 24073af..7b8fa8e 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
@@ -128,11 +128,27 @@
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
    *
+   * <p>Mutually exclusive with the property `methodNamePattern` also defining method-name.
+   *
    * @return The exact method name of the method.
    */
   String methodName() default "";
 
   /**
+   * Define the method-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all field properties.
+   *
+   * <p>If none, and other properties define this item as a method, the default matches any method
+   * name.
+   *
+   * <p>Mutually exclusive with the property `methodName` also defining method-name.
+   *
+   * @return The string pattern of the method name.
+   */
+  StringPattern methodNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the method return-type pattern by a fully qualified type or 'void'.
    *
    * <p>Mutually exclusive with all field properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
new file mode 100644
index 0000000..3c6cd08
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class ArrayPropertyParser<T, P> extends PropertyParserBase<List<T>, P> {
+
+  private final Function<ParsingContext, PropertyParser<T, P>> elementParser;
+  private List<T> values;
+
+  public ArrayPropertyParser(
+      ParsingContext parsingContext, Function<ParsingContext, PropertyParser<T, P>> elementParser) {
+    super(parsingContext);
+    this.elementParser = elementParser;
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyArray(P property, String name, Consumer<List<T>> setValue) {
+    // The property name and type is forwarded to the element parser.
+    values = new ArrayList<>();
+    ParsingContext parsingContext = getParsingContext();
+    return new AnnotationVisitorBase(parsingContext) {
+
+      private PropertyParser<T, P> getParser() {
+        PropertyParser<T, P> parser = elementParser.apply(parsingContext);
+        getMapping().forEach(parser::setProperty);
+        return parser;
+      }
+
+      @Override
+      public void visitEnd() {
+        setValue.accept(values);
+      }
+
+      @Override
+      public void visit(String unusedName, Object value) {
+        if (!getParser().tryParse(name, value, values::add)) {
+          super.visit(name, value);
+        }
+      }
+
+      @Override
+      public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
+        AnnotationVisitor visitor = getParser().tryParseAnnotation(name, descriptor, values::add);
+        if (visitor != null) {
+          return visitor;
+        }
+        return super.visitAnnotation(name, descriptor);
+      }
+
+      @Override
+      public void visitEnum(String unusedName, String descriptor, String value) {
+        if (!getParser().tryParseEnum(name, descriptor, value, values::add)) {
+          super.visitEnum(name, descriptor, value);
+        }
+      }
+
+      @Override
+      public AnnotationVisitor visitArray(String unusedName) {
+        AnnotationVisitor visitor = getParser().tryParseArray(name, values::add);
+        if (visitor != null) {
+          return visitor;
+        }
+        return super.visitArray(name);
+      }
+    };
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
index d6c41ac..9137b3e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
@@ -7,30 +7,69 @@
 import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
 import com.android.tools.r8.keepanno.asm.ClassSimpleNameParser.ClassSimpleNameProperty;
 import com.android.tools.r8.keepanno.asm.PackageNameParser.PackageNameProperty;
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext;
 import com.google.common.collect.ImmutableList;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
 public class ClassNameParser
-    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty, ClassNameParser> {
+    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty> {
 
   public ClassNameParser(ParsingContext parsingContext) {
     super(parsingContext);
   }
 
   public enum ClassNameProperty {
-    PATTERN
+    PATTERN,
+    NAME,
+    CONSTANT,
   }
 
   @Override
-  public ClassNameParser self() {
-    return this;
+  boolean tryProperty(
+      ClassNameProperty property,
+      String name,
+      Object value,
+      Consumer<KeepQualifiedClassNamePattern> setValue) {
+    switch (property) {
+      case NAME:
+        return new TypeParser(getParsingContext())
+            .tryProperty(
+                TypeProperty.TYPE_NAME,
+                name,
+                value,
+                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
+      case CONSTANT:
+        return new TypeParser(getParsingContext())
+            .tryProperty(
+                TypeProperty.TYPE_CONSTANT,
+                name,
+                value,
+                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
+      default:
+        return false;
+    }
+  }
+
+  KeepQualifiedClassNamePattern typeToClassType(
+      KeepTypePattern typePattern, PropertyParsingContext parsingContext) {
+    return typePattern.match(
+        KeepQualifiedClassNamePattern::any,
+        primitiveTypePattern -> {
+          throw parsingContext.error("Invalid use of primitive type where class type was expected");
+        },
+        arrayTypePattern -> {
+          throw parsingContext.error("Invalid use of array type where class type was expected");
+        },
+        classNamePattern -> classNamePattern);
   }
 
   @Override
@@ -43,13 +82,11 @@
       case PATTERN:
         {
           AnnotationParsingContext parsingContext =
-              new AnnotationParsingContext(getParsingContext(), descriptor);
-          PackageNameParser packageParser =
-              new PackageNameParser(parsingContext)
-                  .setProperty(PackageNameProperty.NAME, ClassNamePattern.packageName);
-          ClassSimpleNameParser nameParser =
-              new ClassSimpleNameParser(parsingContext)
-                  .setProperty(ClassSimpleNameProperty.NAME, ClassNamePattern.simpleName);
+              getParsingContext().property(name).annotation(descriptor);
+          PackageNameParser packageParser = new PackageNameParser(parsingContext);
+          ClassSimpleNameParser nameParser = new ClassSimpleNameParser(parsingContext);
+          packageParser.setProperty(ClassNamePattern.packageName, PackageNameProperty.NAME);
+          nameParser.setProperty(ClassNamePattern.simpleName, ClassSimpleNameProperty.NAME);
           return new ParserVisitor(
               parsingContext,
               descriptor,
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
index d349898..ed46972 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
@@ -10,8 +10,7 @@
 import java.util.function.Consumer;
 
 public class ClassSimpleNameParser
-    extends PropertyParserBase<
-        KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty, ClassSimpleNameParser> {
+    extends PropertyParserBase<KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty> {
 
   public ClassSimpleNameParser(ParsingContext parsingContext) {
     super(parsingContext);
@@ -22,11 +21,6 @@
   }
 
   @Override
-  public ClassSimpleNameParser self() {
-    return this;
-  }
-
-  @Override
   public boolean tryProperty(
       ClassSimpleNameProperty property,
       String name,
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java
new file mode 100644
index 0000000..bbc07f3
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.objectweb.asm.AnnotationVisitor;
+
+/**
+ * Abstract base parser to help converting a parser of one type to another via delegation.
+ *
+ * @param <T1> Type of input parser.
+ * @param <T2> Type of output parser (e.g., the parser subclassing this base).
+ * @param <P> Properties (same for both parsers).
+ */
+public abstract class ConvertingPropertyParser<T1, T2, P> implements PropertyParser<T2, P> {
+
+  private final PropertyParser<T1, P> parser;
+  private final Function<T1, T2> converter;
+
+  public ConvertingPropertyParser(PropertyParser<T1, P> parser, Function<T1, T2> converter) {
+    this.parser = parser;
+    this.converter = converter;
+  }
+
+  private Consumer<T1> wrap(Consumer<T2> fn) {
+    return v1 -> fn.accept(converter.apply(v1));
+  }
+
+  @Override
+  public void setProperty(String name, P property) {
+    parser.setProperty(name, property);
+  }
+
+  @Override
+  public boolean isDeclared() {
+    return parser.isDeclared();
+  }
+
+  @Override
+  public T2 getValue() {
+    return converter.apply(parser.getValue());
+  }
+
+  @Override
+  public T2 tryParse(String name, Object value) {
+    T1 t1 = parser.tryParse(name, value);
+    return t1 == null ? null : converter.apply(t1);
+  }
+
+  @Override
+  public boolean tryParseEnum(String name, String descriptor, String value, Consumer<T2> setValue) {
+    return parser.tryParseEnum(name, descriptor, value, wrap(setValue));
+  }
+
+  @Override
+  public AnnotationVisitor tryParseArray(String name, Consumer<T2> setValue) {
+    return parser.tryParseArray(name, wrap(setValue));
+  }
+
+  @Override
+  public AnnotationVisitor tryParseAnnotation(
+      String name, String descriptor, Consumer<T2> setValue) {
+    return parser.tryParseAnnotation(name, descriptor, wrap(setValue));
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java
new file mode 100644
index 0000000..176b1cd
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+
+public class FieldTypeParser
+    extends ConvertingPropertyParser<KeepTypePattern, KeepFieldTypePattern, TypeProperty> {
+
+  public FieldTypeParser(ParsingContext parsingContext) {
+    super(new TypeParser(parsingContext), KeepFieldTypePattern::fromType);
+  }
+}
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 00729cb..7f03de2 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
@@ -3,10 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.asm;
 
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.asm.StringPatternParser.StringProperty;
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Constraints;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
@@ -18,9 +20,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.KeepAnnotationParserException;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
 import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
@@ -51,18 +51,17 @@
 import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
-import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepStringPattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
-import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.ClassParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.FieldParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.GroupParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.MethodParsingContext;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -260,7 +259,6 @@
       if (descriptor.equals(AnnotationConstants.CheckRemoved.DESCRIPTOR)) {
         return new CheckRemovedClassVisitor(
             annotationParsingContext(descriptor),
-            descriptor,
             parent::accept,
             this::setContext,
             className,
@@ -269,7 +267,6 @@
       if (descriptor.equals(AnnotationConstants.CheckOptimizedOut.DESCRIPTOR)) {
         return new CheckRemovedClassVisitor(
             annotationParsingContext(descriptor),
-            descriptor,
             parent::accept,
             this::setContext,
             className,
@@ -381,7 +378,6 @@
       if (descriptor.equals(AnnotationConstants.CheckRemoved.DESCRIPTOR)) {
         return new CheckRemovedMemberVisitor(
             annotationParsingContext(descriptor),
-            descriptor,
             parent::accept,
             this::setContext,
             createMethodItemContext(),
@@ -390,7 +386,6 @@
       if (descriptor.equals(AnnotationConstants.CheckOptimizedOut.DESCRIPTOR)) {
         return new CheckRemovedMemberVisitor(
             annotationParsingContext(descriptor),
-            descriptor,
             parent::accept,
             this::setContext,
             createMethodItemContext(),
@@ -863,7 +858,6 @@
   private static class UsedByReflectionMemberVisitor extends AnnotationVisitorBase {
 
     private final AnnotationParsingContext parsingContext;
-    private final String annotationDescriptor;
     private final Parent<KeepEdge> parent;
     private final KeepItemPattern context;
     private final KeepEdge.Builder builder = KeepEdge.builder();
@@ -881,7 +875,6 @@
         KeepItemPattern context) {
       super(parsingContext);
       this.parsingContext = parsingContext;
-      this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.context = context;
       addContext.accept(metaInfoBuilder);
@@ -1110,7 +1103,6 @@
   private static class CheckRemovedClassVisitor extends AnnotationVisitorBase {
 
     private final AnnotationParsingContext parsingContext;
-    private final String annotationDescriptor;
     private final Parent<KeepCheck> parent;
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
     private final String className;
@@ -1118,14 +1110,12 @@
 
     public CheckRemovedClassVisitor(
         AnnotationParsingContext parsingContext,
-        String annotationDescriptor,
         Parent<KeepCheck> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         String className,
         KeepCheckKind kind) {
       super(parsingContext);
       this.parsingContext = parsingContext;
-      this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.className = className;
       this.kind = kind;
@@ -1143,7 +1133,6 @@
 
     @Override
     public void visitEnd() {
-      CheckRemovedClassVisitor superVisitor = this;
       KeepItemVisitorBase itemVisitor =
           new KeepItemVisitorBase(parsingContext) {
             @Override
@@ -1165,7 +1154,6 @@
   /** Parsing of @CheckRemoved and @CheckOptimizedOut on a class context. */
   private static class CheckRemovedMemberVisitor extends AnnotationVisitorBase {
 
-    private final String annotationDescriptor;
     private final Parent<KeepDeclaration> parent;
     private final KeepItemPattern context;
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
@@ -1173,13 +1161,11 @@
 
     CheckRemovedMemberVisitor(
         AnnotationParsingContext parsingContext,
-        String annotationDescriptor,
         Parent<KeepDeclaration> parent,
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepItemPattern context,
         KeepCheckKind kind) {
       super(parsingContext);
-      this.annotationDescriptor = annotationDescriptor;
       this.parent = parent;
       this.context = context;
       this.kind = kind;
@@ -1208,7 +1194,6 @@
   }
 
   abstract static class Declaration<T> {
-    abstract String kind();
 
     boolean isDefault() {
       for (Declaration<?> declaration : declarations()) {
@@ -1219,18 +1204,27 @@
       return true;
     }
 
-    abstract T getValue();
-
     List<Declaration<?>> declarations() {
       return Collections.emptyList();
     }
 
+    List<PropertyParser<?, ?>> parsers() {
+      return Collections.emptyList();
+    }
+
+    private void ignore(Object arg) {}
+
     boolean tryParse(String name, Object value) {
       for (Declaration<?> declaration : declarations()) {
         if (declaration.tryParse(name, value)) {
           return true;
         }
       }
+      for (PropertyParser<?, ?> parser : parsers()) {
+        if (parser.tryParse(name, value, this::ignore)) {
+          return true;
+        }
+      }
       return false;
     }
 
@@ -1241,6 +1235,12 @@
           return visitor;
         }
       }
+      for (PropertyParser<?, ?> parser : parsers()) {
+        AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
+        if (visitor != null) {
+          return visitor;
+        }
+      }
       return null;
     }
 
@@ -1251,6 +1251,12 @@
           return visitor;
         }
       }
+      for (PropertyParser<?, ?> parser : parsers()) {
+        AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
+        if (visitor != null) {
+          return visitor;
+        }
+      }
       return null;
     }
   }
@@ -1261,7 +1267,7 @@
     private T declarationValue = null;
     private AnnotationVisitor declarationVisitor = null;
 
-    private SingleDeclaration(ParsingContext parsingContext) {
+    private SingleDeclaration(GroupParsingContext parsingContext) {
       this.parsingContext = parsingContext;
     }
 
@@ -1291,19 +1297,11 @@
     }
 
     private void error(String name) {
-      throw new KeepAnnotationParserException(
-          parsingContext,
-          "Multiple declarations defining "
-              + kind()
-              + ": '"
-              + declarationName
-              + "' and '"
-              + name
-              + "'");
+      throw parsingContext.error(
+          "Multiple properties: '" + declarationName + "' and '" + name + "'");
     }
 
-    @Override
-    public final T getValue() {
+    final T getValue() {
       return declarationValue == null ? getDefaultValue() : declarationValue;
     }
 
@@ -1350,54 +1348,10 @@
     }
   }
 
-  private static class ClassNameDeclaration
-      extends SingleDeclaration<KeepQualifiedClassNamePattern> {
-
-    private ClassNameDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-    }
-
-    @Override
-    String kind() {
-      return "class-name";
-    }
-
-    @Override
-    KeepQualifiedClassNamePattern getDefaultValue() {
-      return KeepQualifiedClassNamePattern.any();
-    }
-
-    @Override
-    KeepQualifiedClassNamePattern parse(String name, Object value) {
-      if (name.equals(Item.classConstant) && value instanceof Type) {
-        return KeepQualifiedClassNamePattern.exact(((Type) value).getClassName());
-      }
-      if (name.equals(Item.className) && value instanceof String) {
-        return KeepQualifiedClassNamePattern.exact(((String) value));
-      }
-      return null;
-    }
-
-    @Override
-    AnnotationVisitor parseAnnotation(
-        String name, String descriptor, Consumer<KeepQualifiedClassNamePattern> setValue) {
-      if (name.equals(Item.classNamePattern) && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
-        return new ClassNamePatternVisitor(
-            new AnnotationParsingContext(getParsingContext(), descriptor), setValue);
-      }
-      return super.parseAnnotation(name, descriptor, setValue);
-    }
-  }
-
   private static class InstanceOfDeclaration extends SingleDeclaration<KeepInstanceOfPattern> {
 
     private InstanceOfDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-    }
-
-    @Override
-    String kind() {
-      return "instance-of";
+      super(parsingContext.group(Item.instanceOfGroup));
     }
 
     @Override
@@ -1451,17 +1405,23 @@
     private final Supplier<UserBindingsHelper> getBindingsHelper;
 
     private KeepClassItemReference boundClassItemReference = null;
-    private final ClassNameDeclaration classNameDeclaration;
+    private final ClassNameParser classNameParser;
     private final InstanceOfDeclaration instanceOfDeclaration;
     private final List<Declaration<?>> declarations;
+    private final List<PropertyParser<?, ?>> parsers;
 
     public ClassDeclaration(
         ParsingContext parsingContext, Supplier<UserBindingsHelper> getBindingsHelper) {
-      this.parsingContext = parsingContext;
+      this.parsingContext = parsingContext.group(Item.classGroup);
       this.getBindingsHelper = getBindingsHelper;
-      classNameDeclaration = new ClassNameDeclaration(parsingContext);
+      classNameParser = new ClassNameParser(parsingContext.group(Item.classNameGroup));
+      classNameParser.setProperty(Item.className, ClassNameProperty.NAME);
+      classNameParser.setProperty(Item.classConstant, ClassNameProperty.CONSTANT);
+      classNameParser.setProperty(Item.classNamePattern, ClassNameProperty.PATTERN);
+      parsers = ImmutableList.of(classNameParser);
+
       instanceOfDeclaration = new InstanceOfDeclaration(parsingContext);
-      declarations = ImmutableList.of(classNameDeclaration, instanceOfDeclaration);
+      declarations = ImmutableList.of(instanceOfDeclaration);
     }
 
     @Override
@@ -1469,12 +1429,17 @@
       return declarations;
     }
 
+    @Override
+    List<PropertyParser<?, ?>> parsers() {
+      return parsers;
+    }
+
     private boolean isBindingReferenceDefined() {
       return boundClassItemReference != null;
     }
 
     private boolean classPatternsAreDefined() {
-      return !classNameDeclaration.isDefault() || !instanceOfDeclaration.isDefault();
+      return !classNameParser.isDefault() || !instanceOfDeclaration.isDefault();
     }
 
     private void checkAllowedDefinitions() {
@@ -1485,24 +1450,19 @@
     }
 
     @Override
-    String kind() {
-      return "class";
-    }
-
-    @Override
     boolean isDefault() {
       return !isBindingReferenceDefined() && super.isDefault();
     }
 
-    @Override
-    KeepClassItemReference getValue() {
+    private KeepClassItemReference getValue() {
       checkAllowedDefinitions();
       if (isBindingReferenceDefined()) {
         return boundClassItemReference;
       }
       if (classPatternsAreDefined()) {
         return KeepClassItemPattern.builder()
-            .setClassNamePattern(classNameDeclaration.getValue())
+            .setClassNamePattern(
+                classNameParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
             .setInstanceOfPattern(instanceOfDeclaration.getValue())
             .build()
             .toClassItemReference();
@@ -1530,139 +1490,39 @@
     }
   }
 
-  private static class MethodReturnTypeDeclaration
-      extends SingleDeclaration<KeepMethodReturnTypePattern> {
-
-    private final TypeParser typeParser;
-
-    private MethodReturnTypeDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-      typeParser =
-          new TypeParser(parsingContext)
-              .setKind("return type")
-              .enableTypePattern(Item.methodReturnTypePattern)
-              .enableTypeName(Item.methodReturnType)
-              .enableTypeConstant(Item.methodReturnTypeConstant);
-    }
-
-    @Override
-    String kind() {
-      return "return type";
-    }
-
-    @Override
-    KeepMethodReturnTypePattern getDefaultValue() {
-      return KeepMethodReturnTypePattern.any();
-    }
-
-    KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
-      if (typePattern == null) {
-        return null;
-      }
-      // Special-case method return types to allow void.
-      String descriptor = typePattern.getDescriptor();
-      if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
-        return KeepMethodReturnTypePattern.voidType();
-      }
-      return KeepMethodReturnTypePattern.fromType(typePattern);
-    }
-
-    @Override
-    public KeepMethodReturnTypePattern parse(String name, Object value) {
-      return fromType(typeParser.tryParse(name, value));
-    }
-
-    @Override
-    public AnnotationVisitor parseAnnotation(
-        String name, String descriptor, Consumer<KeepMethodReturnTypePattern> setValue) {
-      return typeParser.tryParseAnnotation(name, descriptor, t -> setValue.accept(fromType(t)));
-    }
-  }
-
-  private static class MethodParametersDeclaration
-      extends SingleDeclaration<KeepMethodParametersPattern> {
-
-    private final ParsingContext parsingContext;
-    private KeepMethodParametersPattern pattern = null;
-
-    public MethodParametersDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-      this.parsingContext = parsingContext;
-    }
-
-    private void setPattern(
-        KeepMethodParametersPattern pattern, Consumer<KeepMethodParametersPattern> setValue) {
-      assert setValue != null;
-      if (this.pattern != null) {
-        throw parsingContext.error("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(
-            getParsingContext(),
-            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(
-            getParsingContext(),
-            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 ParsingContext parsingContext;
     private KeepMethodAccessPattern.Builder accessBuilder = null;
     private KeepMethodPattern.Builder builder = null;
-    private final MethodReturnTypeDeclaration returnTypeDeclaration;
-    private final MethodParametersDeclaration parametersDeclaration;
+    private final StringPatternParser nameParser;
+    private final MethodReturnTypeParser returnTypeParser;
+    private final MethodParametersParser parametersParser;
 
-    private final List<Declaration<?>> declarations;
+    private final List<PropertyParser<?, ?>> parsers;
 
     private MethodDeclaration(ParsingContext parsingContext) {
       this.parsingContext = parsingContext;
-      returnTypeDeclaration = new MethodReturnTypeDeclaration(parsingContext);
-      parametersDeclaration = new MethodParametersDeclaration(parsingContext);
-      declarations = ImmutableList.of(returnTypeDeclaration, parametersDeclaration);
+
+      nameParser = new StringPatternParser(parsingContext.group(Item.methodNameGroup));
+      nameParser.setProperty(Item.methodName, StringProperty.EXACT);
+      nameParser.setProperty(Item.methodNamePattern, StringProperty.PATTERN);
+
+      returnTypeParser = new MethodReturnTypeParser(parsingContext.group(Item.returnTypeGroup));
+      returnTypeParser.setProperty(Item.methodReturnType, TypeProperty.TYPE_NAME);
+      returnTypeParser.setProperty(Item.methodReturnTypeConstant, TypeProperty.TYPE_CONSTANT);
+      returnTypeParser.setProperty(Item.methodReturnTypePattern, TypeProperty.TYPE_PATTERN);
+
+      parametersParser = new MethodParametersParser(parsingContext.group(Item.parametersGroup));
+      parametersParser.setProperty(Item.methodParameters, TypeProperty.TYPE_NAME);
+      parametersParser.setProperty(Item.methodParameterTypePatterns, TypeProperty.TYPE_PATTERN);
+
+      parsers = ImmutableList.of(nameParser, returnTypeParser, parametersParser);
     }
 
     @Override
-    List<Declaration<?>> declarations() {
-      return declarations;
+    List<PropertyParser<?, ?>> parsers() {
+      return parsers;
     }
 
     private KeepMethodPattern.Builder getBuilder() {
@@ -1673,39 +1533,28 @@
     }
 
     @Override
-    String kind() {
-      return "method";
-    }
-
-    @Override
     boolean isDefault() {
       return accessBuilder == null && builder == null && super.isDefault();
     }
 
-    @Override
-    KeepMethodPattern getValue() {
+    private KeepMethodPattern getValue() {
       if (accessBuilder != null) {
         getBuilder().setAccessPattern(accessBuilder.build());
       }
-      if (!returnTypeDeclaration.isDefault()) {
-        getBuilder().setReturnTypePattern(returnTypeDeclaration.getValue());
+      if (!nameParser.isDefault()) {
+        KeepStringPattern namePattern = nameParser.getValue();
+        getBuilder().setNamePattern(KeepMethodNamePattern.fromStringPattern(namePattern));
       }
-      if (!parametersDeclaration.isDefault()) {
-        getBuilder().setParametersPattern(parametersDeclaration.getValue());
+      if (!returnTypeParser.isDefault()) {
+        getBuilder().setReturnTypePattern(returnTypeParser.getValue());
+      }
+      if (!parametersParser.isDefault()) {
+        getBuilder().setParametersPattern(parametersParser.getValue());
       }
       return builder != null ? builder.build() : null;
     }
 
     @Override
-    boolean tryParse(String name, Object value) {
-      if (name.equals(Item.methodName) && value instanceof String) {
-        getBuilder().setNamePattern(KeepMethodNamePattern.exact((String) value));
-        return true;
-      }
-      return super.tryParse(name, value);
-    }
-
-    @Override
     AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.methodAccess)) {
         accessBuilder = KeepMethodAccessPattern.builder();
@@ -1715,64 +1564,27 @@
     }
   }
 
-  private static class FieldTypeDeclaration extends SingleDeclaration<KeepFieldTypePattern> {
-
-    private final TypeParser typeParser;
-
-    private FieldTypeDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-      this.typeParser =
-          new TypeParser(parsingContext)
-              .setKind("field type")
-              .enableTypePattern(Item.fieldTypePattern)
-              .enableTypeName(Item.fieldType)
-              .enableTypeConstant(Item.fieldTypeConstant);
-    }
-
-    @Override
-    String kind() {
-      return "field type";
-    }
-
-    @Override
-    KeepFieldTypePattern getDefaultValue() {
-      return KeepFieldTypePattern.any();
-    }
-
-    @Override
-    public KeepFieldTypePattern parse(String name, Object value) {
-      KeepTypePattern typePattern = typeParser.tryParse(name, value);
-      if (typePattern != null) {
-        return KeepFieldTypePattern.fromType(typePattern);
-      }
-      return null;
-    }
-
-    @Override
-    public AnnotationVisitor parseAnnotation(
-        String name, String descriptor, Consumer<KeepFieldTypePattern> setValue) {
-      return typeParser.tryParseAnnotation(
-          name, descriptor, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
-    }
-  }
-
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
 
     private final ParsingContext parsingContext;
-    private final FieldTypeDeclaration typeDeclaration;
+    private final FieldTypeParser typeParser;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
-    private final List<Declaration<?>> declarations;
+    private final List<PropertyParser<?, ?>> parsers;
 
     public FieldDeclaration(ParsingContext parsingContext) {
       this.parsingContext = parsingContext;
-      typeDeclaration = new FieldTypeDeclaration(parsingContext);
-      declarations = Collections.singletonList(typeDeclaration);
+      typeParser = new FieldTypeParser(parsingContext.group(Item.fieldTypeGroup));
+      typeParser.setProperty(Item.fieldTypePattern, TypeProperty.TYPE_PATTERN);
+      typeParser.setProperty(Item.fieldType, TypeProperty.TYPE_NAME);
+      typeParser.setProperty(Item.fieldTypeConstant, TypeProperty.TYPE_CONSTANT);
+
+      parsers = Collections.singletonList(typeParser);
     }
 
     @Override
-    List<Declaration<?>> declarations() {
-      return declarations;
+    List<PropertyParser<?, ?>> parsers() {
+      return parsers;
     }
 
     private KeepFieldPattern.Builder getBuilder() {
@@ -1783,22 +1595,16 @@
     }
 
     @Override
-    String kind() {
-      return "field";
-    }
-
-    @Override
     boolean isDefault() {
       return accessBuilder == null && builder == null;
     }
 
-    @Override
-    KeepFieldPattern getValue() {
+    private KeepFieldPattern getValue() {
       if (accessBuilder != null) {
         getBuilder().setAccessPattern(accessBuilder.build());
       }
-      if (!typeDeclaration.isDefault()) {
-        getBuilder().setTypePattern(typeDeclaration.getValue());
+      if (!typeParser.isDefault()) {
+        getBuilder().setTypePattern(typeParser.getValue());
       }
       return builder != null ? builder.build() : null;
     }
@@ -1831,7 +1637,7 @@
     private final List<Declaration<?>> declarations;
 
     MemberDeclaration(ParsingContext parsingContext) {
-      this.parsingContext = parsingContext;
+      this.parsingContext = parsingContext.group(Item.memberGroup);
       methodDeclaration = new MethodDeclaration(parsingContext);
       fieldDeclaration = new FieldDeclaration(parsingContext);
       declarations = ImmutableList.of(methodDeclaration, fieldDeclaration);
@@ -1843,17 +1649,11 @@
     }
 
     @Override
-    String kind() {
-      return "member";
-    }
-
-    @Override
     public boolean isDefault() {
       return accessBuilder == null && methodDeclaration.isDefault() && fieldDeclaration.isDefault();
     }
 
-    @Override
-    public KeepMemberPattern getValue() {
+    private KeepMemberPattern getValue() {
       KeepMethodPattern method = methodDeclaration.getValue();
       KeepFieldPattern field = fieldDeclaration.getValue();
       if (accessBuilder != null) {
@@ -2175,237 +1975,10 @@
     }
   }
 
-  private static class StringArrayVisitor extends AnnotationVisitorBase {
-    private final Consumer<List<String>> fn;
-    private final List<String> strings = new ArrayList<>();
-
-    public StringArrayVisitor(ParsingContext parsingContext, Consumer<List<String>> fn) {
-      super(parsingContext);
-      this.fn = fn;
-    }
-
-    @Override
-    public void visit(String name, Object value) {
-      if (value instanceof String) {
-        strings.add((String) value);
-      } else {
-        super.visit(name, value);
-      }
-    }
-
-    @Override
-    public void visitEnd() {
-      super.visitEnd();
-      fn.accept(strings);
-    }
-  }
-
-  private static class ClassSimpleNameDeclaration
-      extends SingleDeclaration<KeepUnqualfiedClassNamePattern> {
-
-    private ClassSimpleNameDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-    }
-
-    @Override
-    String kind() {
-      return "class-simple-name";
-    }
-
-    @Override
-    KeepUnqualfiedClassNamePattern getDefaultValue() {
-      return KeepUnqualfiedClassNamePattern.any();
-    }
-
-    @Override
-    KeepUnqualfiedClassNamePattern parse(String name, Object value) {
-      if (name.equals(ClassNamePattern.simpleName) && value instanceof String) {
-        return KeepUnqualfiedClassNamePattern.builder().exact((String) value).build();
-      }
-      return null;
-    }
-  }
-
-  private static class PackageDeclaration extends SingleDeclaration<KeepPackagePattern> {
-
-    private PackageDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-    }
-
-    @Override
-    String kind() {
-      return "package";
-    }
-
-    @Override
-    KeepPackagePattern getDefaultValue() {
-      return KeepPackagePattern.any();
-    }
-
-    @Override
-    KeepPackagePattern parse(String name, Object value) {
-      if (name.equals(ClassNamePattern.packageName) && value instanceof String) {
-        return KeepPackagePattern.builder().exact((String) value).build();
-      }
-      return null;
-    }
-  }
-
-  private static class ClassNamePatternDeclaration
-      extends Declaration<KeepQualifiedClassNamePattern> {
-
-    private final ClassSimpleNameDeclaration nameDeclaration;
-    private final PackageDeclaration packageDeclaration;
-    private final List<Declaration<?>> declarations;
-
-    public ClassNamePatternDeclaration(ParsingContext parsingContext) {
-      nameDeclaration = new ClassSimpleNameDeclaration(parsingContext);
-      packageDeclaration = new PackageDeclaration(parsingContext);
-      declarations = ImmutableList.of(nameDeclaration, packageDeclaration);
-    }
-
-    @Override
-    String kind() {
-      return "class-name";
-    }
-
-    @Override
-    KeepQualifiedClassNamePattern getValue() {
-      if (!packageDeclaration.isDefault() || !nameDeclaration.isDefault()) {
-        return KeepQualifiedClassNamePattern.builder()
-            .setPackagePattern(packageDeclaration.getValue())
-            .setNamePattern(nameDeclaration.getValue())
-            .build();
-      }
-      return null;
-    }
-
-    @Override
-    List<Declaration<?>> declarations() {
-      return declarations;
-    }
-  }
-
-  private static class ClassNamePatternVisitor extends AnnotationVisitorBase {
-
-    private final ClassNamePatternDeclaration declaration;
-    private final Consumer<KeepQualifiedClassNamePattern> setValue;
-
-    public ClassNamePatternVisitor(
-        AnnotationParsingContext parsingContext, Consumer<KeepQualifiedClassNamePattern> setValue) {
-      super(parsingContext);
-      this.setValue = setValue;
-      declaration = new ClassNamePatternDeclaration(parsingContext);
-    }
-
-    @Override
-    public void visit(String name, Object value) {
-      if (!declaration.tryParse(name, value)) {
-        super.visit(name, value);
-      }
-    }
-
-    @Override
-    public void visitEnd() {
-      if (!declaration.isDefault()) {
-        setValue.accept(declaration.getValue());
-      }
-      super.visitEnd();
-    }
-  }
-
-  private static class TypePatternVisitor extends AnnotationVisitorBase {
-    private final ParsingContext parsingContext;
-    private final Consumer<KeepTypePattern> consumer;
-    private KeepTypePattern result = null;
-
-    private TypePatternVisitor(ParsingContext parsingContext, Consumer<KeepTypePattern> consumer) {
-      super(parsingContext);
-      this.parsingContext = parsingContext;
-      this.consumer = consumer;
-    }
-
-    private void setResult(KeepTypePattern result) {
-      if (this.result != null) {
-        throw parsingContext.error("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 AnnotationVisitor visitAnnotation(String name, String descriptor) {
-      if (TypePattern.classNamePattern.equals(name)
-          && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
-        return new ClassNamePatternVisitor(
-            new AnnotationParsingContext(parsingContext, descriptor),
-            p -> {
-              if (p.isExact()) {
-                setResult(KeepTypePattern.fromDescriptor(p.getExactDescriptor()));
-              } else {
-                // TODO(b/248408342): Extend the AST type patterns.
-                throw new Unimplemented("Non-exact class patterns are not implemented yet");
-              }
-            });
-      }
-      return super.visitAnnotation(name, descriptor);
-    }
-
-    @Override
-    public void visitEnd() {
-      consumer.accept(result != null ? result : KeepTypePattern.any());
-    }
-  }
-
-  private static class TypePatternsArrayVisitor extends AnnotationVisitorBase {
-    private final ParsingContext parsingContext;
-    private final Consumer<List<KeepTypePattern>> fn;
-    private final List<KeepTypePattern> patterns = new ArrayList<>();
-
-    public TypePatternsArrayVisitor(
-        ParsingContext parsingContext, Consumer<List<KeepTypePattern>> fn) {
-      super(parsingContext);
-      this.parsingContext = parsingContext;
-      this.fn = fn;
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
-      if (TypePattern.DESCRIPTOR.equals(descriptor)) {
-        return new TypePatternVisitor(parsingContext, patterns::add);
-      }
-      return null;
-    }
-
-    @Override
-    public void visitEnd() {
-      super.visitEnd();
-      fn.accept(patterns);
-    }
-  }
-
   private static class OptionsDeclaration extends SingleDeclaration<KeepOptions> {
 
     public OptionsDeclaration(ParsingContext parsingContext) {
-      super(parsingContext);
-    }
-
-    @Override
-    String kind() {
-      return "options";
+      super(parsingContext.group(Target.constraintsGroup));
     }
 
     @Override
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
index 18e5c02..21645d5 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
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.asm;
 
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext;
+import java.util.function.Function;
 
 /**
  * Utilities for mapping the syntax used in annotations to the keep-edge AST.
@@ -57,14 +57,20 @@
     throw new IllegalStateException("Unexpected descriptor: " + descriptor);
   }
 
-  public static KeepTypePattern typePatternFromString(String string) {
+  public static KeepTypePattern typePatternFromString(
+      String string, PropertyParsingContext property) {
     if (string.equals("<any>")) {
       return KeepTypePattern.any();
     }
-    return KeepTypePattern.fromDescriptor(getDescriptorFromJavaType(string));
+    return KeepTypePattern.fromDescriptor(internalDescriptorFromJavaType(string, property::error));
   }
 
   public static String getDescriptorFromJavaType(String type) {
+    return internalDescriptorFromJavaType(type, IllegalStateException::new);
+  }
+
+  private static String internalDescriptorFromJavaType(
+      String type, Function<String, RuntimeException> onError) {
     switch (type) {
       case "boolean":
         return "Z";
@@ -86,9 +92,12 @@
         {
           StringBuilder builder = new StringBuilder(type.length());
           int i = type.length() - 1;
+          if (i < 0) {
+            throw onError.apply("Invalid empty type");
+          }
           while (type.charAt(i) == ']') {
             if (type.charAt(--i) != '[') {
-              throw new KeepEdgeException("Invalid type: " + type);
+              throw onError.apply("Invalid type: '" + type + "'");
             }
             builder.append('[');
             --i;
@@ -103,19 +112,4 @@
         }
     }
   }
-
-  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/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index 568e6fe..c714960 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern.KeepMethodNameExactPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
@@ -203,9 +202,9 @@
   }
 
   private void writeMethod(KeepMethodPattern method, AnnotationVisitor targetVisitor) {
-    KeepMethodNameExactPattern exactMethodName = method.getNamePattern().asExact();
+    String exactMethodName = method.getNamePattern().asExactString();
     if (exactMethodName != null) {
-      targetVisitor.visit(Item.methodName, exactMethodName.getName());
+      targetVisitor.visit(Item.methodName, exactMethodName);
     } else {
       throw new Unimplemented();
     }
@@ -213,9 +212,8 @@
       throw new Unimplemented();
     }
     if (!method.getReturnTypePattern().isAny()) {
-      if (exactMethodName != null
-          && (exactMethodName.getName().equals("<init>")
-              || exactMethodName.getName().equals("<clinit>"))
+      if ((method.getNamePattern().isInstanceInitializer()
+              || method.getNamePattern().isClassInitializer())
           && method.getReturnTypePattern().isVoid()) {
         // constructors have implicit void return.
       } else {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java
new file mode 100644
index 0000000..f1cba56
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.List;
+
+public class MethodParametersParser
+    extends ConvertingPropertyParser<
+        List<KeepTypePattern>, KeepMethodParametersPattern, TypeProperty> {
+
+  public MethodParametersParser(ParsingContext parsingContext) {
+    super(
+        new ArrayPropertyParser<>(parsingContext, TypeParser::new),
+        MethodParametersParser::convert);
+  }
+
+  private static KeepMethodParametersPattern convert(List<KeepTypePattern> params) {
+    KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+    for (KeepTypePattern param : params) {
+      builder.addParameterTypePattern(param);
+    }
+    return builder.build();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
new file mode 100644
index 0000000..ccf5183
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
+
+/**
+ * Parser for parsing method return types.
+ *
+ * <p>This parser wraps a type parser and adds support for parsing the string name {@code "void"} or
+ * the class constant {@code void.class} as the void method return type.
+ */
+public class MethodReturnTypeParser
+    extends PropertyParserBase<KeepMethodReturnTypePattern, TypeProperty> {
+
+  private final TypeParser typeParser;
+
+  public MethodReturnTypeParser(ParsingContext parsingContext) {
+    super(parsingContext);
+    typeParser = new TypeParser(parsingContext);
+  }
+
+  static Consumer<KeepTypePattern> wrap(Consumer<KeepMethodReturnTypePattern> fn) {
+    return t -> fn.accept(KeepMethodReturnTypePattern.fromType(t));
+  }
+
+  @Override
+  public KeepMethodReturnTypePattern getValue() {
+    return super.getValue();
+  }
+
+  @Override
+  boolean tryProperty(
+      TypeProperty property,
+      String name,
+      Object value,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    if (property == TypeProperty.TYPE_NAME && "void".equals(value)) {
+      setValue.accept(KeepMethodReturnTypePattern.voidType());
+      return true;
+    }
+    if (property == TypeProperty.TYPE_CONSTANT && Type.getType("V").equals(value)) {
+      setValue.accept(KeepMethodReturnTypePattern.voidType());
+      return true;
+    }
+    return typeParser.tryProperty(property, name, value, wrap(setValue));
+  }
+
+  @Override
+  public boolean tryPropertyEnum(
+      TypeProperty property,
+      String name,
+      String descriptor,
+      String value,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyEnum(property, name, descriptor, value, wrap(setValue));
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyArray(
+      TypeProperty property, String name, Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyArray(property, name, wrap(setValue));
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      TypeProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyAnnotation(property, name, descriptor, wrap(setValue));
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
index dadb8de..9c9295d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
@@ -9,8 +9,7 @@
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import java.util.function.Consumer;
 
-public class PackageNameParser
-    extends PropertyParserBase<KeepPackagePattern, PackageNameProperty, PackageNameParser> {
+public class PackageNameParser extends PropertyParserBase<KeepPackagePattern, PackageNameProperty> {
 
   public PackageNameParser(ParsingContext parsingContext) {
     super(parsingContext);
@@ -21,11 +20,6 @@
   }
 
   @Override
-  public PackageNameParser self() {
-    return this;
-  }
-
-  @Override
   public boolean tryProperty(
       PackageNameProperty property,
       String name,
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
index 77d6087..5f1f69b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
@@ -12,13 +12,13 @@
 /** Convert parser(s) into an annotation visitor. */
 public class ParserVisitor extends AnnotationVisitorBase {
 
-  private final List<PropertyParser<?, ?, ?>> parsers;
+  private final List<PropertyParser<?, ?>> parsers;
   private final Runnable onVisitEnd;
 
   public ParserVisitor(
       AnnotationParsingContext parsingContext,
       String annotationDescriptor,
-      List<PropertyParser<?, ?, ?>> parsers,
+      List<PropertyParser<?, ?>> parsers,
       Runnable onVisitEnd) {
     super(parsingContext);
     this.parsers = parsers;
@@ -29,7 +29,7 @@
   public ParserVisitor(
       AnnotationParsingContext parsingContext,
       String annotationDescriptor,
-      PropertyParser<?, ?, ?> declaration,
+      PropertyParser<?, ?> declaration,
       Runnable onVisitEnd) {
     this(parsingContext, annotationDescriptor, Collections.singletonList(declaration), onVisitEnd);
   }
@@ -38,7 +38,7 @@
 
   @Override
   public void visit(String name, Object value) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       if (parser.tryParse(name, value, this::ignore)) {
         return;
       }
@@ -48,7 +48,7 @@
 
   @Override
   public AnnotationVisitor visitArray(String name) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
       if (visitor != null) {
         return visitor;
@@ -59,7 +59,7 @@
 
   @Override
   public void visitEnum(String name, String descriptor, String value) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       if (parser.tryParseEnum(name, descriptor, value, this::ignore)) {
         return;
       }
@@ -69,7 +69,7 @@
 
   @Override
   public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
       if (visitor != null) {
         return visitor;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
index 67b181b..0fd8a75 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
@@ -7,13 +7,9 @@
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
-public interface PropertyParser<T, P, S> {
+public interface PropertyParser<T, P> {
 
-  S self();
-
-  String kind();
-
-  S setProperty(P property, String name);
+  void setProperty(String name, P property);
 
   boolean isDeclared();
 
@@ -23,7 +19,16 @@
 
   T getValue();
 
-  boolean tryParse(String name, Object value, Consumer<T> setValue);
+  T tryParse(String name, Object value);
+
+  default boolean tryParse(String name, Object value, Consumer<T> setValue) {
+    T result = tryParse(name, value);
+    if (result != null) {
+      setValue.accept(result);
+      return true;
+    }
+    return false;
+  }
 
   boolean tryParseEnum(String name, String descriptor, String value, Consumer<T> setValue);
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
index 108f086..1ab1fea 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.keepanno.asm;
 
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import java.util.HashMap;
 import java.util.Map;
@@ -12,11 +11,10 @@
 import org.objectweb.asm.AnnotationVisitor;
 
 /** Special case of a property parser allowing only a single value callback. */
-public abstract class PropertyParserBase<T, P, S> implements PropertyParser<T, P, S> {
+public abstract class PropertyParserBase<T, P> implements PropertyParser<T, P> {
 
   private final ParsingContext parsingContext;
 
-  private String kind;
   private final Map<String, P> mapping = new HashMap<>();
   private String resultPropertyName = null;
   private T resultValue = null;
@@ -29,6 +27,10 @@
     return parsingContext;
   }
 
+  Map<String, P> getMapping() {
+    return mapping;
+  }
+
   boolean tryProperty(P property, String name, Object value, Consumer<T> setValue) {
     return false;
   }
@@ -62,14 +64,8 @@
   }
 
   private void error(String name) {
-    throw new KeepEdgeException(
-        "Multiple properties defining "
-            + kind()
-            + ": '"
-            + resultPropertyName
-            + "' and '"
-            + name
-            + "'");
+    throw parsingContext.error(
+        "Multiple properties: '" + resultPropertyName + "' and '" + name + "'");
   }
 
   public final boolean isDeclared() {
@@ -87,38 +83,26 @@
     return isDeclared() ? resultValue : defaultValue;
   }
 
-  /** Helper for parsing directly. Returns non-null if the property-name triggered parsing. */
-  public final T tryParse(String name, Object value) {
-    boolean triggered = tryParse(name, value, unused -> {});
-    assert triggered == (resultValue != null);
-    return resultValue;
-  }
-
-  public String kind() {
-    return kind != null ? kind : "";
-  }
-
-  public S setKind(String kind) {
-    this.kind = kind;
-    return self();
-  }
-
   /** Add property parsing for the given property-name. */
-  public S setProperty(P property, String name) {
+  public void setProperty(String name, P property) {
     P old = mapping.put(name, property);
     if (old != null) {
       throw new IllegalArgumentException("Unexpected attempt to redefine property " + name);
     }
-    return self();
   }
 
   @Override
-  public final boolean tryParse(String name, Object value, Consumer<T> setValue) {
+  public final T tryParse(String name, Object value) {
     P prop = mapping.get(name);
     if (prop != null) {
-      return tryProperty(prop, name, value, wrap(name, setValue));
+      try {
+        tryProperty(prop, name, value, wrap(name, unused -> {}));
+      } catch (RuntimeException e) {
+        throw parsingContext.rethrow(e);
+      }
+      return resultValue;
     }
-    return false;
+    return null;
   }
 
   @Override
@@ -126,7 +110,11 @@
       String name, String descriptor, String value, Consumer<T> setValue) {
     P prop = mapping.get(name);
     if (prop != null) {
-      return tryPropertyEnum(prop, name, descriptor, value, wrap(name, setValue));
+      try {
+        return tryPropertyEnum(prop, name, descriptor, value, wrap(name, setValue));
+      } catch (RuntimeException e) {
+        throw parsingContext.rethrow(e);
+      }
     }
     return false;
   }
@@ -135,7 +123,11 @@
   public final AnnotationVisitor tryParseArray(String name, Consumer<T> setValue) {
     P prop = mapping.get(name);
     if (prop != null) {
-      return tryPropertyArray(prop, name, wrap(name, setValue));
+      try {
+        return tryPropertyArray(prop, name, wrap(name, setValue));
+      } catch (RuntimeException e) {
+        throw parsingContext.rethrow(e);
+      }
     }
     return null;
   }
@@ -145,7 +137,11 @@
       String name, String descriptor, Consumer<T> setValue) {
     P prop = mapping.get(name);
     if (prop != null) {
-      return tryPropertyAnnotation(prop, name, descriptor, wrap(name, setValue));
+      try {
+        return tryPropertyAnnotation(prop, name, descriptor, wrap(name, setValue));
+      } catch (RuntimeException e) {
+        throw parsingContext.rethrow(e);
+      }
     }
     return null;
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java
new file mode 100644
index 0000000..a7b8a3b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.StringPatternParser.StringProperty;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.StringPattern;
+import com.android.tools.r8.keepanno.ast.KeepStringPattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.google.common.collect.ImmutableList;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class StringPatternParser extends PropertyParserBase<KeepStringPattern, StringProperty> {
+
+  public StringPatternParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  public enum StringProperty {
+    EXACT,
+    PATTERN
+  }
+
+  @Override
+  boolean tryProperty(
+      StringProperty property, String name, Object value, Consumer<KeepStringPattern> setValue) {
+    switch (property) {
+      case EXACT:
+        {
+          setValue.accept(KeepStringPattern.exact((String) value));
+          return true;
+        }
+      default:
+        return false;
+    }
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      StringProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepStringPattern> setValue) {
+    switch (property) {
+      case PATTERN:
+        {
+          AnnotationParsingContext parsingContext =
+              getParsingContext().property(name).annotation(descriptor);
+          StringPatternParser exactParser = new StringPatternParser(parsingContext);
+          StringParser prefixParser = new StringParser(parsingContext);
+          StringParser suffixParser = new StringParser(parsingContext);
+          exactParser.setProperty(StringPattern.exact, StringProperty.EXACT);
+          prefixParser.setProperty(StringPattern.startsWith, StringParser.Property.STRING);
+          suffixParser.setProperty(StringPattern.endsWith, StringParser.Property.STRING);
+          return new ParserVisitor(
+              parsingContext,
+              descriptor,
+              ImmutableList.of(exactParser, prefixParser, suffixParser),
+              () -> {
+                if (exactParser.isDeclared()) {
+                  if (prefixParser.isDeclared()) {
+                    throw parsingContext.error("Cannot specify both the exact string and a prefix");
+                  }
+                  if (suffixParser.isDeclared()) {
+                    throw parsingContext.error("Cannot specify both the exact string and a suffix");
+                  }
+                  setValue.accept(exactParser.getValue());
+                } else {
+                  setValue.accept(
+                      KeepStringPattern.builder()
+                          .setPrefix(prefixParser.getValueOrDefault(null))
+                          .setSuffix(suffixParser.getValueOrDefault(null))
+                          .build());
+                }
+              });
+        }
+      default:
+        return null;
+    }
+  }
+
+  private static class StringParser extends PropertyParserBase<String, StringParser.Property> {
+
+    enum Property {
+      STRING
+    }
+
+    protected StringParser(ParsingContext parsingContext) {
+      super(parsingContext);
+    }
+
+    @Override
+    boolean tryProperty(
+        StringParser.Property property, String name, Object value, Consumer<String> setValue) {
+      assert Property.STRING.equals(property);
+      if (value instanceof String) {
+        setValue.accept((String) value);
+      }
+      return false;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
index 6b11f70..a40f450 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
@@ -10,51 +10,31 @@
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Type;
 
-public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty, TypeParser> {
+public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty> {
 
   public TypeParser(ParsingContext parsingContext) {
     super(parsingContext);
   }
 
   public enum TypeProperty {
-    SELF_PATTERN,
+    TYPE_PATTERN,
     TYPE_NAME,
     TYPE_CONSTANT,
     CLASS_NAME_PATTERN
   }
 
-  public TypeParser enableTypePattern(String propertyName) {
-    return setProperty(TypeProperty.SELF_PATTERN, propertyName);
-  }
-
-  public TypeParser enableTypeName(String propertyName) {
-    return setProperty(TypeProperty.TYPE_NAME, propertyName);
-  }
-
-  public TypeParser enableTypeConstant(String propertyName) {
-    return setProperty(TypeProperty.TYPE_CONSTANT, propertyName);
-  }
-
-  public TypeParser enableTypeClassNamePattern(String propertyName) {
-    return setProperty(TypeProperty.CLASS_NAME_PATTERN, propertyName);
-  }
-
-  @Override
-  public TypeParser self() {
-    return this;
-  }
-
   @Override
   public boolean tryProperty(
       TypeProperty property, String name, Object value, Consumer<KeepTypePattern> setValue) {
     switch (property) {
       case TYPE_NAME:
-        setValue.accept(KeepEdgeReaderUtils.typePatternFromString((String) value));
+        setValue.accept(
+            KeepEdgeReaderUtils.typePatternFromString(
+                (String) value, getParsingContext().property(name)));
         return true;
       case TYPE_CONSTANT:
         setValue.accept(KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
@@ -68,39 +48,28 @@
   public AnnotationVisitor tryPropertyAnnotation(
       TypeProperty property, String name, String descriptor, Consumer<KeepTypePattern> setValue) {
     switch (property) {
-      case SELF_PATTERN:
+      case TYPE_PATTERN:
         {
-          AnnotationParsingContext parsingContext =
-              new AnnotationParsingContext(getParsingContext(), descriptor);
-          TypeParser typeParser =
-              new TypeParser(parsingContext)
-                  .setKind(kind())
-                  .enableTypeName(TypePattern.name)
-                  .enableTypeConstant(TypePattern.constant)
-                  .enableTypeClassNamePattern(TypePattern.classNamePattern);
+          AnnotationParsingContext context =
+              getParsingContext().property(name).annotation(descriptor);
+          TypeParser typeParser = new TypeParser(context);
+          typeParser.setProperty(TypePattern.name, TypeProperty.TYPE_NAME);
+          typeParser.setProperty(TypePattern.constant, TypeProperty.TYPE_CONSTANT);
+          typeParser.setProperty(TypePattern.classNamePattern, TypeProperty.CLASS_NAME_PATTERN);
           return new ParserVisitor(
-              parsingContext,
+              context,
               descriptor,
               typeParser,
               () -> setValue.accept(typeParser.getValueOrDefault(KeepTypePattern.any())));
         }
       case CLASS_NAME_PATTERN:
         {
-          return new ClassNameParser(getParsingContext())
-              .setKind(kind())
-              .tryPropertyAnnotation(
-                  ClassNameProperty.PATTERN,
-                  name,
-                  descriptor,
-                  classNamePattern -> {
-                    if (classNamePattern.isExact()) {
-                      setValue.accept(
-                          KeepTypePattern.fromDescriptor(classNamePattern.getExactDescriptor()));
-                    } else {
-                      // TODO(b/248408342): Extend the AST type patterns.
-                      throw new Unimplemented("Non-exact class patterns are not implemented yet");
-                    }
-                  });
+          ClassNameParser parser = new ClassNameParser(getParsingContext());
+          return parser.tryPropertyAnnotation(
+              ClassNameProperty.PATTERN,
+              name,
+              descriptor,
+              value -> setValue.accept(KeepTypePattern.fromClass(value)));
         }
       default:
         return null;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 13a228b..c489701 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
@@ -65,11 +65,15 @@
 
   /** Item properties common to binding items, conditions and targets. */
   public static final class Item {
+    public static final String classGroup = "class";
     public static final String classFromBinding = "classFromBinding";
+    public static final String memberGroup = "member";
     public static final String memberFromBinding = "memberFromBinding";
+    public static final String classNameGroup = "class-name";
     public static final String className = "className";
     public static final String classConstant = "classConstant";
     public static final String classNamePattern = "classNamePattern";
+    public static final String instanceOfGroup = "instance-of";
     public static final String instanceOfClassName = "instanceOfClassName";
     public static final String instanceOfClassNameExclusive = "instanceOfClassNameExclusive";
     public static final String instanceOfClassConstant = "instanceOfClassConstant";
@@ -79,14 +83,19 @@
     public static final String extendsClassConstant = "extendsClassConstant";
     public static final String memberAccess = "memberAccess";
     public static final String methodAccess = "methodAccess";
+    public static final String methodNameGroup = "method-name";
     public static final String methodName = "methodName";
+    public static final String methodNamePattern = "methodNamePattern";
+    public static final String returnTypeGroup = "return-type";
     public static final String methodReturnType = "methodReturnType";
     public static final String methodReturnTypeConstant = "methodReturnTypeConstant";
     public static final String methodReturnTypePattern = "methodReturnTypePattern";
+    public static final String parametersGroup = "parameters";
     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 fieldTypeGroup = "field-type";
     public static final String fieldType = "fieldType";
     public static final String fieldTypeConstant = "fieldTypeConstant";
     public static final String fieldTypePattern = "fieldTypePattern";
@@ -107,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 constraintsGroup = "constraints";
     public static final String constraints = "constraints";
     public static final String allow = "allow";
     public static final String disallow = "disallow";
@@ -182,9 +192,19 @@
     public static final String TRANSIENT = "TRANSIENT";
   }
 
+  public static final class StringPattern {
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/StringPattern;";
+    public static final String stringExactPatternGroup = "string-exact-pattern";
+    public static final String exact = "exact";
+    public static final String startsWith = "startsWith";
+    public static final String endsWith = "endsWith";
+  }
+
   public static final class TypePattern {
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/TypePattern;";
+    public static final String typePatternGroup = "type-pattern";
     public static final String name = "name";
     public static final String constant = "constant";
     public static final String classNamePattern = "classNamePattern";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java
index d98d410..7a08a6a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationParserException.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.keepanno.ast;
 
-public class KeepAnnotationParserException extends KeepEdgeException {
+public class KeepAnnotationParserException extends RuntimeException {
 
   private final ParsingContext context;
 
@@ -13,6 +13,11 @@
     this.context = context;
   }
 
+  public KeepAnnotationParserException(ParsingContext context, RuntimeException cause) {
+    super(cause);
+    this.context = context;
+  }
+
   @Override
   public String getMessage() {
     return super.getMessage() + getContextAsString();
@@ -22,7 +27,11 @@
     StringBuilder builder = new StringBuilder();
     ParsingContext current = context;
     while (current != null) {
-      builder.append("\n  in ").append(current.getContextFrameAsString());
+      builder
+          .append("\n  at ")
+          .append(current.getContextType())
+          .append(": ")
+          .append(current.getContextFrameAsString());
       current = current.getParentContext();
     }
     return builder.toString();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
new file mode 100644
index 0000000..96bdd8a
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Objects;
+
+public class KeepArrayTypePattern {
+
+  private static final KeepArrayTypePattern ANY =
+      new KeepArrayTypePattern(KeepTypePattern.any(), 1);
+
+  public static KeepArrayTypePattern getAny() {
+    return ANY;
+  }
+
+  private final KeepTypePattern baseType;
+  private final int dimensions;
+
+  public KeepArrayTypePattern(KeepTypePattern baseType, int dimensions) {
+    assert baseType != null;
+    assert dimensions > 0;
+    this.baseType = baseType;
+    this.dimensions = dimensions;
+  }
+
+  public boolean isAny() {
+    return ANY.equals(this);
+  }
+
+  public KeepTypePattern getBaseType() {
+    return baseType;
+  }
+
+  public int getDimensions() {
+    return dimensions;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof KeepArrayTypePattern)) {
+      return false;
+    }
+    KeepArrayTypePattern that = (KeepArrayTypePattern) o;
+    return dimensions == that.dimensions && Objects.equals(baseType, that.baseType);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(baseType, dimensions);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index 34dcab5..04485d7 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -44,7 +44,15 @@
  *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN instance-of INSTANCE_OF_PATTERN
  *   MEMBER_ITEM_PATTERN ::= CLASS_ITEM_REFERENCE { MEMBER_PATTERN }
  *
- *   TYPE_PATTERN ::= any | exact type-descriptor
+ *   TYPE_PATTERN
+ *     ::= any
+ *       | PRIMITIVE_TYPE_PATTERN
+ *       | ARRAY_TYPE_PATTERN
+ *       | QUALIFIED_CLASS_NAME_PATTERN
+ *
+ *   PRIMITIVE_TYPE_PATTERN ::= any | boolean | byte | char | short | int | long | float | double
+ *   ARRAY_TYPE_PATTERN ::= any | TYPE_PATTERN dimensions(N > 0)
+ *
  *   PACKAGE_PATTERN ::= any | exact package-name
  *
  *   QUALIFIED_CLASS_NAME_PATTERN
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
index 18a399a..6843e61 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
@@ -5,99 +5,115 @@
 
 
 public abstract class KeepMethodNamePattern {
+  private static final String INIT_STRING = "<init>";
+  private static final String CLINIT_STRING = "<clinit>";
 
   public static KeepMethodNamePattern any() {
-    return Any.getInstance();
+    return SomePattern.ANY;
   }
 
   public static KeepMethodNamePattern initializer() {
-    return new KeepMethodNameExactPattern("<init>");
+    return SomePattern.INSTANCE_INIT;
   }
 
   public static KeepMethodNamePattern exact(String methodName) {
-    return new KeepMethodNameExactPattern(methodName);
+    return fromStringPattern(KeepStringPattern.exact(methodName));
+  }
+
+  public static KeepMethodNamePattern fromStringPattern(KeepStringPattern pattern) {
+    if (pattern.isAny()) {
+      return SomePattern.ANY;
+    }
+    if (pattern.isExact()) {
+      String exact = pattern.asExactString();
+      if (INIT_STRING.equals(exact)) {
+        return SomePattern.INSTANCE_INIT;
+      }
+      if (CLINIT_STRING.equals(exact)) {
+        return SomePattern.CLASS_INIT;
+      }
+    }
+    return new SomePattern(pattern);
   }
 
   private KeepMethodNamePattern() {}
 
-  public boolean isAny() {
-    return false;
-  }
+  public abstract boolean isAny();
 
-  public final boolean isExact() {
-    return asExact() != null;
-  }
+  public abstract boolean isInstanceInitializer();
 
-  public KeepMethodNameExactPattern asExact() {
-    return null;
-  }
+  public abstract boolean isClassInitializer();
 
-  private static class Any extends KeepMethodNamePattern {
-    private static final Any INSTANCE = new Any();
+  public abstract boolean isExact();
 
-    public static Any getInstance() {
-      return INSTANCE;
+  public abstract String asExactString();
+
+  public abstract KeepStringPattern asStringPattern();
+
+  private static class SomePattern extends KeepMethodNamePattern {
+    private static final SomePattern ANY = new SomePattern(KeepStringPattern.any());
+    private static final KeepMethodNamePattern INSTANCE_INIT =
+        new SomePattern(KeepStringPattern.exact("<init>"));
+    private static final KeepMethodNamePattern CLASS_INIT =
+        new SomePattern(KeepStringPattern.exact("<clinit>"));
+
+    private final KeepStringPattern pattern;
+
+    public SomePattern(KeepStringPattern pattern) {
+      assert pattern != null;
+      this.pattern = pattern;
     }
 
     @Override
-    public boolean isAny() {
-      return true;
+    public KeepStringPattern asStringPattern() {
+      return pattern;
     }
 
     @Override
-    public boolean equals(Object obj) {
-      return this == obj;
-    }
-
-    @Override
-    public int hashCode() {
-      return System.identityHashCode(this);
-    }
-
-    @Override
-    public String toString() {
-      return "*";
-    }
-  }
-
-  public static class KeepMethodNameExactPattern extends KeepMethodNamePattern {
-    private final String name;
-
-    public KeepMethodNameExactPattern(String name) {
-      assert name != null;
-      this.name = name;
-    }
-
-    @Override
-    public KeepMethodNameExactPattern asExact() {
-      return this;
-    }
-
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object o) {
       if (this == o) {
         return true;
       }
-      if (o == null || getClass() != o.getClass()) {
+      if (!(o instanceof SomePattern)) {
         return false;
       }
-      KeepMethodNameExactPattern that = (KeepMethodNameExactPattern) o;
-      return name.equals(that.name);
+      SomePattern that = (SomePattern) o;
+      return pattern.equals(that.pattern);
     }
 
     @Override
     public int hashCode() {
-      return name.hashCode();
+      return pattern.hashCode();
     }
 
     @Override
     public String toString() {
-      return name;
+      return pattern.toString();
+    }
+
+    @Override
+    public boolean isAny() {
+      return ANY == this;
+    }
+
+    @Override
+    public boolean isClassInitializer() {
+      return CLASS_INIT == this;
+    }
+
+    @Override
+    public boolean isInstanceInitializer() {
+      return INSTANCE_INIT == this;
+    }
+
+    @Override
+    public boolean isExact() {
+      return pattern.isExact();
+    }
+
+    @Override
+    public String asExactString() {
+      return pattern.asExactString();
     }
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index a7a4977..98ebbb9 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
-import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern.KeepMethodNameExactPattern;
 import java.util.Objects;
 
 public final class KeepMethodPattern extends KeepMemberPattern {
@@ -55,9 +54,7 @@
 
     public KeepMethodPattern build() {
       KeepMethodReturnTypePattern returnTypePattern = this.returnTypePattern;
-      KeepMethodNameExactPattern exactName = namePattern.asExact();
-      if (exactName != null
-          && (exactName.getName().equals("<init>") || exactName.getName().equals("<clinit>"))) {
+      if (namePattern.isInstanceInitializer() || namePattern.isClassInitializer()) {
         if (!this.returnTypePattern.isAny() && !this.returnTypePattern.isVoid()) {
           throw new KeepEdgeException("Method constructor pattern must match 'void' type.");
         }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java
new file mode 100644
index 0000000..f79093d
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class KeepPrimitiveTypePattern {
+
+  private static final KeepPrimitiveTypePattern ANY = new KeepPrimitiveTypePattern('*');
+  private static final KeepPrimitiveTypePattern BOOLEAN = new KeepPrimitiveTypePattern('Z');
+  private static final KeepPrimitiveTypePattern BYTE = new KeepPrimitiveTypePattern('B');
+  private static final KeepPrimitiveTypePattern CHAR = new KeepPrimitiveTypePattern('C');
+  private static final KeepPrimitiveTypePattern SHORT = new KeepPrimitiveTypePattern('S');
+  private static final KeepPrimitiveTypePattern INT = new KeepPrimitiveTypePattern('I');
+  private static final KeepPrimitiveTypePattern LONG = new KeepPrimitiveTypePattern('J');
+  private static final KeepPrimitiveTypePattern FLOAT = new KeepPrimitiveTypePattern('F');
+  private static final KeepPrimitiveTypePattern DOUBLE = new KeepPrimitiveTypePattern('D');
+
+  private static final Map<String, KeepPrimitiveTypePattern> PRIMITIVES =
+      populate(BOOLEAN, BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE);
+
+  private static ImmutableMap<String, KeepPrimitiveTypePattern> populate(
+      KeepPrimitiveTypePattern... types) {
+    ImmutableMap.Builder<String, KeepPrimitiveTypePattern> builder = ImmutableMap.builder();
+    for (KeepPrimitiveTypePattern type : types) {
+      builder.put(type.getDescriptor(), type);
+    }
+    return builder.build();
+  }
+
+  public static KeepPrimitiveTypePattern getAny() {
+    return ANY;
+  }
+
+  public static KeepPrimitiveTypePattern getBoolean() {
+    return BOOLEAN;
+  }
+
+  public static KeepPrimitiveTypePattern getByte() {
+    return BYTE;
+  }
+
+  public static KeepPrimitiveTypePattern getChar() {
+    return CHAR;
+  }
+
+  public static KeepPrimitiveTypePattern getShort() {
+    return SHORT;
+  }
+
+  public static KeepPrimitiveTypePattern getInt() {
+    return INT;
+  }
+
+  public static KeepPrimitiveTypePattern getLong() {
+    return LONG;
+  }
+
+  public static KeepPrimitiveTypePattern getFloat() {
+    return FLOAT;
+  }
+
+  public static KeepPrimitiveTypePattern getDouble() {
+    return DOUBLE;
+  }
+
+  private final char descriptor;
+
+  public KeepPrimitiveTypePattern(char descriptor) {
+    this.descriptor = descriptor;
+  }
+
+  public boolean isAny() {
+    return this == ANY;
+  }
+
+  public char getDescriptorChar() {
+    if (isAny()) {
+      throw new KeepEdgeException("No descriptor exists for 'any' primitive");
+    }
+    return descriptor;
+  }
+
+  public String getDescriptor() {
+    return Character.toString(getDescriptorChar());
+  }
+
+  public static void forEachPrimitive(Consumer<KeepPrimitiveTypePattern> fn) {
+    PRIMITIVES.values().forEach(fn);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
index ee7f3d9..574336d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
@@ -18,6 +18,13 @@
         .build();
   }
 
+  public static KeepQualifiedClassNamePattern exactFromDescriptor(String classDescriptor) {
+    if (!classDescriptor.startsWith("L") && classDescriptor.endsWith(";")) {
+      throw new KeepEdgeException("Invalid class descriptor: " + classDescriptor);
+    }
+    return exact(classDescriptor.substring(1, classDescriptor.length() - 1).replace('/', '.'));
+  }
+
   public static KeepQualifiedClassNamePattern exact(String qualifiedClassName) {
     int pkgSeparator = qualifiedClassName.lastIndexOf('.');
     if (pkgSeparator == 0) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepStringPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepStringPattern.java
new file mode 100644
index 0000000..8d36ea9
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepStringPattern.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+public class KeepStringPattern {
+
+  private static final KeepStringPattern ANY = new KeepStringPattern(null, null, null);
+
+  public static KeepStringPattern any() {
+    return ANY;
+  }
+
+  public static KeepStringPattern exact(String exact) {
+    return KeepStringPattern.builder().setExact(exact).build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String exact = null;
+    private String prefix = null;
+    private String suffix = null;
+
+    private Builder() {}
+
+    public Builder setExact(String exact) {
+      this.exact = exact;
+      return this;
+    }
+
+    public Builder setPrefix(String prefix) {
+      this.prefix = prefix;
+      return this;
+    }
+
+    public Builder setSuffix(String suffix) {
+      this.suffix = suffix;
+      return this;
+    }
+
+    public KeepStringPattern build() {
+      if (exact != null) {
+        return new KeepStringPattern(exact, null, null);
+      }
+      if (prefix == null && suffix == null) {
+        return ANY;
+      }
+      return new KeepStringPattern(exact, prefix, suffix);
+    }
+  }
+
+  private final String exact;
+  private final String prefix;
+  private final String suffix;
+
+  private KeepStringPattern(String exact, String prefix, String suffix) {
+    this.exact = exact;
+    this.prefix = prefix;
+    this.suffix = suffix;
+  }
+
+  public boolean isAny() {
+    return ANY == this;
+  }
+
+  public boolean isExact() {
+    return exact != null;
+  }
+
+  public String asExactString() {
+    return exact;
+  }
+
+  public boolean hasPrefix() {
+    return prefix != null;
+  }
+
+  public boolean hasSuffix() {
+    return suffix != null;
+  }
+
+  public String getPrefixString() {
+    return prefix;
+  }
+
+  public String getSuffixString() {
+    return suffix;
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
index dd56be4..b7c2b0d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
@@ -3,16 +3,60 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
 public abstract class KeepTypePattern {
 
   public static KeepTypePattern any() {
     return Any.getInstance();
   }
 
-  public static KeepTypePattern fromDescriptor(String typeDescriptor) {
-    return new Some(typeDescriptor);
+  public static KeepTypePattern fromPrimitive(KeepPrimitiveTypePattern type) {
+    return type.isAny() ? PrimitiveType.ANY : PrimitiveType.PRIMITIVES.get(type.getDescriptor());
   }
 
+  public static KeepTypePattern fromArray(KeepArrayTypePattern type) {
+    return new ArrayType(type);
+  }
+
+  public static KeepTypePattern fromClass(KeepQualifiedClassNamePattern type) {
+    return new ClassType(type);
+  }
+
+  public static KeepTypePattern fromDescriptor(String typeDescriptor) {
+    char c = typeDescriptor.charAt(0);
+    if (c == 'L') {
+      int end = typeDescriptor.length() - 1;
+      if (typeDescriptor.charAt(end) != ';') {
+        throw new KeepEdgeException("Invalid type descriptor: " + typeDescriptor);
+      }
+      return fromClass(KeepQualifiedClassNamePattern.exactFromDescriptor(typeDescriptor));
+    }
+    if (c == '[') {
+      int dim = 1;
+      while (typeDescriptor.charAt(dim) == '[') {
+        dim++;
+      }
+      KeepTypePattern baseType = fromDescriptor(typeDescriptor.substring(dim));
+      return fromArray(new KeepArrayTypePattern(baseType, dim));
+    }
+    PrimitiveType primitiveType = PrimitiveType.PRIMITIVES.get(typeDescriptor);
+    if (primitiveType != null) {
+      return primitiveType;
+    }
+    throw new KeepEdgeException("Invalid type descriptor: " + typeDescriptor);
+  }
+
+  public abstract <T> T match(
+      Supplier<T> onAny,
+      Function<KeepPrimitiveTypePattern, T> onPrimitive,
+      Function<KeepArrayTypePattern, T> onArray,
+      Function<KeepQualifiedClassNamePattern, T> onClass);
+
   public boolean isAny() {
     return false;
   }
@@ -21,44 +65,6 @@
     return null;
   }
 
-  private static class Some extends KeepTypePattern {
-
-    private final String descriptor;
-
-    private Some(String descriptor) {
-      assert descriptor != null;
-      this.descriptor = descriptor;
-    }
-
-    @Override
-    public String getDescriptor() {
-      return descriptor;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      Some some = (Some) o;
-      return descriptor.equals(some.descriptor);
-    }
-
-    @Override
-    public int hashCode() {
-      return descriptor.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return descriptor;
-    }
-  }
-
   private static class Any extends KeepTypePattern {
 
     private static final Any INSTANCE = new Any();
@@ -68,6 +74,15 @@
     }
 
     @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onAny.get();
+    }
+
+    @Override
     public boolean isAny() {
       return true;
     }
@@ -87,4 +102,117 @@
       return "<any>";
     }
   }
+
+  private static class PrimitiveType extends KeepTypePattern {
+
+    private static final PrimitiveType ANY = new PrimitiveType(KeepPrimitiveTypePattern.getAny());
+    private static final Map<String, PrimitiveType> PRIMITIVES = populate();
+
+    private static Map<String, PrimitiveType> populate() {
+      ImmutableMap.Builder<String, PrimitiveType> builder = ImmutableMap.builder();
+      KeepPrimitiveTypePattern.forEachPrimitive(
+          primitive -> {
+            builder.put(primitive.getDescriptor(), new PrimitiveType(primitive));
+          });
+      return builder.build();
+    }
+
+    private final KeepPrimitiveTypePattern type;
+
+    private PrimitiveType(KeepPrimitiveTypePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return getDescriptor();
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onPrimitive.apply(type);
+    }
+  }
+
+  private static class ClassType extends KeepTypePattern {
+    private final KeepQualifiedClassNamePattern type;
+
+    public ClassType(KeepQualifiedClassNamePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onClass.apply(type);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ClassType)) {
+        return false;
+      }
+      ClassType classType = (ClassType) o;
+      return Objects.equals(type, classType.type);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(type);
+    }
+  }
+
+  private static class ArrayType extends KeepTypePattern {
+    private final KeepArrayTypePattern type;
+
+    public ArrayType(KeepArrayTypePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onArray.apply(type);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ArrayType)) {
+        return false;
+      }
+      ArrayType arrayType = (ArrayType) o;
+      return Objects.equals(type, arrayType.type);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(type);
+    }
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
index d7ce9a4..0d83a25 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
@@ -14,14 +14,50 @@
     throw new KeepAnnotationParserException(this, message);
   }
 
+  public KeepAnnotationParserException rethrow(RuntimeException e) {
+    if (e instanceof KeepAnnotationParserException) {
+      throw e;
+    }
+    throw new KeepAnnotationParserException(this, e);
+  }
+
   public abstract String getHolderName();
 
   public ParsingContext getParentContext() {
     return null;
   }
 
+  public abstract String getContextType();
+
   public abstract String getContextFrameAsString();
 
+  public boolean isSynthetic() {
+    return false;
+  }
+
+  private ParsingContext nonSyntheticParent() {
+    // We don't want to maintain nested property groups as they are "synthetic" and only the
+    // inner-most group is useful when diagnosing an error.
+    if (isSynthetic()) {
+      ParsingContext parent = getParentContext();
+      assert !parent.isSynthetic();
+      return parent;
+    }
+    return this;
+  }
+
+  public GroupParsingContext group(String propertyGroupDescription) {
+    return new GroupParsingContext(this, propertyGroupDescription);
+  }
+
+  public AnnotationParsingContext annotation(String annotationDescriptor) {
+    return new AnnotationParsingContext(this, annotationDescriptor);
+  }
+
+  public PropertyParsingContext property(String propertyName) {
+    return new PropertyParsingContext(this, propertyName);
+  }
+
   public static class ClassParsingContext extends ParsingContext {
     private final String className;
 
@@ -35,6 +71,11 @@
     }
 
     @Override
+    public String getContextType() {
+      return "class";
+    }
+
+    @Override
     public String getContextFrameAsString() {
       return className;
     }
@@ -71,11 +112,15 @@
     }
 
     @Override
+    public String getContextType() {
+      return "method";
+    }
+
+    @Override
     public String getContextFrameAsString() {
       Type methodType = Type.getMethodType(methodDescriptor);
       StringBuilder builder = new StringBuilder();
       builder
-          .append("method ")
           .append(getJavaTypeFromDescriptor(methodType.getReturnType().getDescriptor()))
           .append(' ')
           .append(methodName)
@@ -106,8 +151,13 @@
     }
 
     @Override
+    public String getContextType() {
+      return "field";
+    }
+
+    @Override
     public String getContextFrameAsString() {
-      return "field " + getJavaTypeFromDescriptor(fieldDescriptor) + " " + fieldName;
+      return getJavaTypeFromDescriptor(fieldDescriptor) + " " + fieldName;
     }
   }
 
@@ -116,7 +166,7 @@
     private final String annotationDescriptor;
 
     public AnnotationParsingContext(ParsingContext parentContext, String annotationDescriptor) {
-      this.parentContext = parentContext;
+      this.parentContext = parentContext.nonSyntheticParent();
       this.annotationDescriptor = annotationDescriptor;
     }
 
@@ -140,8 +190,88 @@
     }
 
     @Override
+    public String getContextType() {
+      return "annotation";
+    }
+
+    @Override
     public String getContextFrameAsString() {
       return "@" + getSimpleAnnotationName();
     }
   }
+
+  public static class GroupParsingContext extends ParsingContext {
+    private final ParsingContext parentContext;
+    private final String propertyGroupDescription;
+
+    public GroupParsingContext(ParsingContext parentContext, String propertyGroupDescription) {
+      this.parentContext = parentContext.nonSyntheticParent();
+      this.propertyGroupDescription = propertyGroupDescription;
+    }
+
+    public String getPropertyGroupDescription() {
+      return propertyGroupDescription;
+    }
+
+    @Override
+    public boolean isSynthetic() {
+      // The property "groups" are not actual source info and should only be used in top-level
+      // reporting.
+      return true;
+    }
+
+    @Override
+    public String getHolderName() {
+      return parentContext.getHolderName();
+    }
+
+    @Override
+    public ParsingContext getParentContext() {
+      return parentContext;
+    }
+
+    @Override
+    public String getContextType() {
+      return "property-group";
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return getPropertyGroupDescription();
+    }
+  }
+
+  public static class PropertyParsingContext extends ParsingContext {
+    private final ParsingContext parentContext;
+    private final String propertyName;
+
+    public PropertyParsingContext(ParsingContext parentContext, String propertyName) {
+      this.parentContext = parentContext.nonSyntheticParent();
+      this.propertyName = propertyName;
+    }
+
+    public String getPropertyName() {
+      return propertyName;
+    }
+
+    @Override
+    public String getHolderName() {
+      return parentContext.getHolderName();
+    }
+
+    @Override
+    public ParsingContext getParentContext() {
+      return parentContext;
+    }
+
+    @Override
+    public String getContextType() {
+      return "property";
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return getPropertyName();
+    }
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
index 94694a2..518eeba 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
@@ -25,6 +25,7 @@
   public RulePrinter append(String str) {
     assert !str.contains("*");
     assert !str.contains("(...)");
+    assert !str.contains("%");
     return appendWithoutBackReferenceAssert(str);
   }
 
@@ -45,6 +46,10 @@
     return appendWithoutBackReferenceAssert("***");
   }
 
+  public RulePrinter appendPercent() {
+    return appendWithoutBackReferenceAssert("%");
+  }
+
   public RulePrinter appendAnyParameters() {
     return appendWithoutBackReferenceAssert("(...)");
   }
@@ -91,6 +96,11 @@
     }
 
     @Override
+    public RulePrinter appendPercent() {
+      return addBackRef("%");
+    }
+
+    @Override
     public RulePrinter appendAnyParameters() {
       // TODO(b/265892343): R8 does not yet support back reference to `...`.
       return appendWithoutBackReferenceAssert("(...)");
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 5d5c357..f16d9dd 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.keepanno.keeprules;
 
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
+import com.android.tools.r8.keepanno.ast.KeepArrayTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
@@ -21,7 +22,9 @@
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepPrimitiveTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepStringPattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.ModifierPattern;
@@ -158,6 +161,20 @@
     return builder.append(")");
   }
 
+  private static RulePrinter printStringPattern(RulePrinter printer, KeepStringPattern pattern) {
+    if (pattern.isExact()) {
+      return printer.append(pattern.asExactString());
+    }
+    if (pattern.hasPrefix()) {
+      printer.append(pattern.getPrefixString());
+    }
+    printer.appendStar();
+    if (pattern.hasSuffix()) {
+      printer.append(pattern.getSuffixString());
+    }
+    return printer;
+  }
+
   private static RulePrinter printFieldName(RulePrinter builder, KeepFieldNamePattern namePattern) {
     return namePattern.isAny()
         ? builder.appendStar()
@@ -166,9 +183,7 @@
 
   private static RulePrinter printMethodName(
       RulePrinter builder, KeepMethodNamePattern namePattern) {
-    return namePattern.isAny()
-        ? builder.appendStar()
-        : builder.append(namePattern.asExact().getName());
+    return printStringPattern(builder, namePattern.asStringPattern());
   }
 
   private static RulePrinter printReturnType(
@@ -179,11 +194,34 @@
     return printType(builder, returnTypePattern.asType());
   }
 
-  private static RulePrinter printType(RulePrinter builder, KeepTypePattern typePattern) {
-    if (typePattern.isAny()) {
-      return builder.appendTripleStar();
+  private static RulePrinter printType(RulePrinter printer, KeepTypePattern typePattern) {
+    return typePattern.match(
+        printer::appendTripleStar,
+        primitivePattern -> printPrimitiveType(printer, primitivePattern),
+        arrayTypePattern -> printArrayType(printer, arrayTypePattern),
+        classTypePattern -> printClassName(classTypePattern, printer));
+  }
+
+  private static RulePrinter printPrimitiveType(
+      RulePrinter printer, KeepPrimitiveTypePattern primitiveTypePattern) {
+    if (primitiveTypePattern.isAny()) {
+      // Matching any primitive type uses the wildcard syntax `%`
+      return printer.appendPercent();
     }
-    return builder.append(descriptorToJavaType(typePattern.getDescriptor()));
+    return printer.append(descriptorToJavaType(primitiveTypePattern.getDescriptor()));
+  }
+
+  private static RulePrinter printArrayType(
+      RulePrinter printer, KeepArrayTypePattern arrayTypePattern) {
+    // The "any" array is simply dimension one of any type. Just assert that to be true as the
+    // general case will emit the correct syntax: ***[]
+    assert !arrayTypePattern.isAny()
+        || (arrayTypePattern.getDimensions() == 1 && arrayTypePattern.getBaseType().isAny());
+    printType(printer, arrayTypePattern.getBaseType());
+    for (int i = 0; i < arrayTypePattern.getDimensions(); i++) {
+      printer.append("[]");
+    }
+    return printer;
   }
 
   public static RulePrinter printMemberAccess(
@@ -254,7 +292,7 @@
   public static RulePrinter printClassName(
       KeepQualifiedClassNamePattern classNamePattern, RulePrinter printer) {
     if (classNamePattern.isAny()) {
-      return printer.appendStar();
+      return printer.appendDoubleStar();
     }
     printPackagePrefix(classNamePattern.getPackagePattern(), printer);
     return printSimpleClassName(classNamePattern.getNamePattern(), printer);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 2250d81..33b687b 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -755,7 +755,6 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
-    assert internal.getVerticalClassMergerOptions().isDisabled();
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 726013e..339e80b 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -195,7 +195,6 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
-    assert internal.getVerticalClassMergerOptions().isDisabled();
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 12a84ab..ad074ed 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -499,7 +499,8 @@
 
       assert ArtProfileCompletenessChecker.verify(appView);
 
-      VerticalClassMerger.runIfNecessary(appViewWithLiveness, executorService, timing);
+      VerticalClassMerger.createForInitialClassMerging(appViewWithLiveness)
+          .runIfNecessary(executorService, timing);
       HorizontalClassMerger.createForInitialClassMerging(appViewWithLiveness)
           .runIfNecessary(
               executorService,
@@ -737,6 +738,11 @@
       GenericSignatureContextBuilder genericContextBuilderBeforeFinalMerging =
           GenericSignatureContextBuilder.create(appView);
 
+      if (appView.hasLiveness()) {
+        VerticalClassMerger.createForFinalClassMerging(appView.withLiveness())
+            .runIfNecessary(executorService, timing);
+      }
+
       // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
       // are always merged.
       HorizontalClassMerger.createForFinalClassMerging(appView)
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index d2647bb..405f214 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -54,6 +54,7 @@
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.SystemPropertyUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
@@ -132,7 +133,10 @@
     private GraphConsumer mainDexKeptGraphConsumer = null;
     private InputDependencyGraphConsumer inputDependencyGraphConsumer = null;
     private final FeatureSplitConfiguration.Builder featureSplitConfigurationBuilder =
-        FeatureSplitConfiguration.builder();
+        FeatureSplitConfiguration.builder()
+            .setEnableIsolatedSplits(
+                SystemPropertyUtils.parseSystemPropertyOrDefault(
+                    "com.android.tools.r8.isolatedSplits", false));
     private String synthesizedClassPrefix = "";
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations =
@@ -1331,6 +1335,9 @@
         .setMainDexKeepRules(mainDexKeepRules)
         .setDesugaredLibraryConfiguration(desugaredLibrarySpecification)
         .setEnableMissingLibraryApiModeling(enableMissingLibraryApiModeling)
+        .applyIf(
+            featureSplitConfiguration != null,
+            b -> b.setIsolatedSplits(featureSplitConfiguration.isIsolatedSplitsEnabled()))
         .build();
   }
 
diff --git a/src/main/java/com/android/tools/r8/classmerging/ClassMergerMode.java b/src/main/java/com/android/tools/r8/classmerging/ClassMergerMode.java
new file mode 100644
index 0000000..bb9a37b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/classmerging/ClassMergerMode.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging;
+
+public enum ClassMergerMode {
+  INITIAL,
+  FINAL;
+
+  public boolean isInitial() {
+    return this == INITIAL;
+  }
+
+  public boolean isFinal() {
+    return this == FINAL;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
index 8bf6828..0417118 100644
--- a/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/classmerging/ClassMergerTreeFixer.java
@@ -184,8 +184,9 @@
     if (newMethodSignature == null) {
       newMethodSignature = fixupMethodReference(originalMethodReference).getSignature();
 
-      // If the signature is already reserved by another interface, find a fresh one.
-      if (reservedInterfaceSignatures.containsValue(newMethodSignature)) {
+      // If the signature is kept or already reserved by another interface, find a fresh one.
+      if (keptSignatures.contains(newMethodSignature)
+          || reservedInterfaceSignatures.containsValue(newMethodSignature)) {
         DexString name =
             dexItemFactory.createGloballyFreshMemberString(
                 originalMethodReference.getName().toSourceString());
diff --git a/src/main/java/com/android/tools/r8/dump/DumpOptions.java b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
index ac46112..c52f655 100644
--- a/src/main/java/com/android/tools/r8/dump/DumpOptions.java
+++ b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
@@ -26,6 +26,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
+import java.util.function.Consumer;
 
 @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
 public class DumpOptions {
@@ -45,6 +46,7 @@
   private static final String DESUGAR_STATE_KEY = "desugar-state";
   private static final String INTERMEDIATE_KEY = "intermediate";
   private static final String INCLUDE_DATA_RESOURCES_KEY = "include-data-resources";
+  private static final String ISOLATED_SPLITS_KEY = "isolated-splits";
   private static final String TREE_SHAKING_KEY = "tree-shaking";
   private static final String MINIFICATION_KEY = "minification";
   private static final String FORCE_PROGUARD_COMPATIBILITY_KEY = "force-proguard-compatibility";
@@ -63,6 +65,7 @@
   private final DesugarState desugarState;
   private final Optional<Boolean> intermediate;
   private final Optional<Boolean> includeDataResources;
+  private final Optional<Boolean> isolatedSplits;
   private final Optional<Boolean> treeShaking;
   private final Optional<Boolean> minification;
   private final Optional<Boolean> forceProguardCompatibility;
@@ -96,6 +99,7 @@
       DesugarState desugarState,
       Optional<Boolean> intermediate,
       Optional<Boolean> includeDataResources,
+      Optional<Boolean> isolatedSplits,
       Optional<Boolean> treeShaking,
       Optional<Boolean> minification,
       Optional<Boolean> forceProguardCompatibility,
@@ -120,6 +124,7 @@
     this.desugarState = desugarState;
     this.intermediate = intermediate;
     this.includeDataResources = includeDataResources;
+    this.isolatedSplits = isolatedSplits;
     this.treeShaking = treeShaking;
     this.minification = minification;
     this.forceProguardCompatibility = forceProguardCompatibility;
@@ -167,6 +172,7 @@
       }
       addOptionalDumpEntry(buildProperties, INTERMEDIATE_KEY, intermediate);
       addOptionalDumpEntry(buildProperties, INCLUDE_DATA_RESOURCES_KEY, includeDataResources);
+      addOptionalDumpEntry(buildProperties, ISOLATED_SPLITS_KEY, isolatedSplits);
       addOptionalDumpEntry(buildProperties, TREE_SHAKING_KEY, treeShaking);
       addOptionalDumpEntry(
           buildProperties, FORCE_PROGUARD_COMPATIBILITY_KEY, forceProguardCompatibility);
@@ -239,6 +245,9 @@
       case INCLUDE_DATA_RESOURCES_KEY:
         builder.setIncludeDataResources(Optional.of(Boolean.parseBoolean(value)));
         return;
+      case ISOLATED_SPLITS_KEY:
+        builder.setIsolatedSplits(Boolean.parseBoolean(value));
+        return;
       case TREE_SHAKING_KEY:
         builder.setTreeShaking(Boolean.parseBoolean(value));
         return;
@@ -355,6 +364,7 @@
     private DesugarState desugarState;
     private Optional<Boolean> intermediate = Optional.empty();
     private Optional<Boolean> includeDataResources = Optional.empty();
+    private Optional<Boolean> isolatedSplits = Optional.empty();
     private Optional<Boolean> treeShaking = Optional.empty();
     private Optional<Boolean> minification = Optional.empty();
     private Optional<Boolean> forceProguardCompatibility = Optional.empty();
@@ -379,6 +389,13 @@
 
     public Builder() {}
 
+    public Builder applyIf(boolean condition, Consumer<Builder> thenConsumer) {
+      if (condition) {
+        thenConsumer.accept(this);
+      }
+      return this;
+    }
+
     public Builder setBackend(Backend backend) {
       this.backend = backend;
       return this;
@@ -435,6 +452,11 @@
       return this;
     }
 
+    public Builder setIsolatedSplits(boolean isolatedSplits) {
+      this.isolatedSplits = Optional.of(isolatedSplits);
+      return this;
+    }
+
     public Builder setForceProguardCompatibility(boolean forceProguardCompatibility) {
       this.forceProguardCompatibility = Optional.of(forceProguardCompatibility);
       return this;
@@ -530,6 +552,7 @@
           desugarState,
           intermediate,
           includeDataResources,
+          isolatedSplits,
           treeShaking,
           minification,
           forceProguardCompatibility,
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 4c5c33b..9ac1bd5 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
@@ -42,10 +42,21 @@
     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;
+    // If isolated splits are enabled then the resolved item must be either (1) public or (2)
+    // a protected member and the access is in a subclass of the resolved member's holder.
+    if (appView.options().getFeatureSplitConfiguration().isIsolatedSplitsEnabled()) {
+      if (definition.isClass()) {
+        if (!definition.getAccessFlags().isPublic()) {
+          return false;
+        }
+      } else if (definition.getAccessFlags().isPackagePrivateOrProtected()) {
+        if (definition.getAccessFlags().isPackagePrivate()
+            || !appView
+                .appInfo()
+                .isSubtype(context.getContextClass(), definition.getContextClass())) {
+          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
index 1043723..376acee 100644
--- a/src/main/java/com/android/tools/r8/features/IsolatedFeatureSplitsChecker.java
+++ b/src/main/java/com/android/tools/r8/features/IsolatedFeatureSplitsChecker.java
@@ -80,6 +80,10 @@
         || features.isInSameFeature(accessedItem, context, appView)) {
       return;
     }
+    if (accessedItem.getAccessFlags().isProtected()
+        && appView.appInfo().isSubtype(context.getContextClass(), accessedItem.getContextClass())) {
+      return;
+    }
     DontWarnConfiguration dontWarnConfiguration = appView.getDontWarnConfiguration();
     if (dontWarnConfiguration.matches(accessedItem) || dontWarnConfiguration.matches(context)) {
       return;
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 3ed6de7..2cceb1a 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessControl.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessControl.java
@@ -78,20 +78,19 @@
       }
       return classAccessibility;
     }
+    if (!member.getHolderType().isSamePackage(context.getContextType())) {
+      if (memberFlags.isPackagePrivate()
+          || !appInfo.isSubtype(context.getContextType(), member.getHolderType())) {
+        return OptionalBool.FALSE;
+      }
+    }
     if (appView.hasClassHierarchy()
         && context.isProgramDefinition()
         && !FeatureSplitBoundaryOptimizationUtils.isSafeForAccess(
             member, context.asProgramDefinition(), appView.withClassHierarchy())) {
       return OptionalBool.UNKNOWN;
     }
-    if (member.getHolderType().isSamePackage(context.getContextType())) {
-      return classAccessibility;
-    }
-    if (memberFlags.isProtected()
-        && appInfo.isSubtype(context.getContextType(), member.getHolderType())) {
-      return classAccessibility;
-    }
-    return OptionalBool.FALSE;
+    return classAccessibility;
   }
 
   private static boolean isNestMate(DexClass clazz, DexClass context) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index dc2094f..05c6fa8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.DesugarGraphConsumer;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.origin.GlobalSyntheticOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -295,8 +295,7 @@
         : FieldResolutionResult.unknown();
   }
 
-  public void notifyHorizontalClassMergerFinished(
-      HorizontalClassMerger.Mode horizontalClassMergerMode) {
+  public void notifyHorizontalClassMergerFinished(ClassMergerMode horizontalClassMergerMode) {
     // Intentionally empty.
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index a9ebda5..691680c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.contexts.CompilationContext;
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.errors.dontwarn.DontWarnConfiguration;
@@ -19,7 +20,6 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.InitClassLens;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintFactory;
 import com.android.tools.r8.ir.analysis.proto.EnumLiteProtoShrinker;
@@ -843,7 +843,7 @@
   }
 
   public void setHorizontallyMergedClasses(
-      HorizontallyMergedClasses horizontallyMergedClasses, HorizontalClassMerger.Mode mode) {
+      HorizontallyMergedClasses horizontallyMergedClasses, ClassMergerMode mode) {
     assert !hasHorizontallyMergedClasses() || mode.isFinal();
     this.horizontallyMergedClasses = horizontallyMergedClasses().extend(horizontallyMergedClasses);
     testing()
@@ -863,10 +863,16 @@
     return verticallyMergedClasses;
   }
 
-  public void setVerticallyMergedClasses(VerticallyMergedClasses verticallyMergedClasses) {
-    assert this.verticallyMergedClasses == null;
-    this.verticallyMergedClasses = verticallyMergedClasses;
-    testing().verticallyMergedClassesConsumer.accept(dexItemFactory(), verticallyMergedClasses);
+  public void setVerticallyMergedClasses(
+      VerticallyMergedClasses verticallyMergedClasses, ClassMergerMode mode) {
+    if (mode.isInitial()) {
+      assert this.verticallyMergedClasses == null;
+      this.verticallyMergedClasses = verticallyMergedClasses;
+      testing().verticallyMergedClassesConsumer.accept(dexItemFactory(), verticallyMergedClasses);
+    } else {
+      assert this.verticallyMergedClasses != null;
+      assert verticallyMergedClasses.isEmpty();
+    }
   }
 
   public OpenClosedInterfacesCollection getOpenClosedInterfacesCollection() {
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 2097c3a..cb4af41 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.ClassMergerMode;
 import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -25,7 +26,6 @@
 import com.android.tools.r8.graph.ProgramMember;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.code.ClassInitializerMerger;
 import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
 import com.android.tools.r8.ir.analysis.value.NumberFromIntervalValue;
@@ -59,7 +59,7 @@
   private static final OptimizationFeedback feedback = OptimizationFeedbackSimple.getInstance();
 
   private final AppView<?> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
   private final HorizontalMergeGroup group;
   private final DexItemFactory dexItemFactory;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
@@ -79,7 +79,7 @@
   private ClassMerger(
       AppView<?> appView,
       IRCodeProvider codeProvider,
-      Mode mode,
+      ClassMergerMode mode,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       HorizontalMergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers) {
@@ -375,11 +375,14 @@
   public static class Builder {
     private final AppView<?> appView;
     private final IRCodeProvider codeProvider;
-    private final Mode mode;
+    private final ClassMergerMode mode;
     private final HorizontalMergeGroup group;
 
     public Builder(
-        AppView<?> appView, IRCodeProvider codeProvider, HorizontalMergeGroup group, Mode mode) {
+        AppView<?> appView,
+        IRCodeProvider codeProvider,
+        HorizontalMergeGroup group,
+        ClassMergerMode mode) {
       this.appView = appView;
       this.codeProvider = codeProvider;
       this.group = group;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index d7c99ba..03d62ea 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.ClassMergerMode;
 import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.graph.AppInfo;
@@ -40,24 +41,11 @@
 
 public class HorizontalClassMerger {
 
-  public enum Mode {
-    INITIAL,
-    FINAL;
-
-    public boolean isInitial() {
-      return this == INITIAL;
-    }
-
-    public boolean isFinal() {
-      return this == FINAL;
-    }
-  }
-
   private final AppView<?> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
   private final HorizontalClassMergerOptions options;
 
-  private HorizontalClassMerger(AppView<?> appView, Mode mode) {
+  private HorizontalClassMerger(AppView<?> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.mode = mode;
     this.options = appView.options().horizontalClassMergerOptions();
@@ -65,17 +53,17 @@
 
   public static HorizontalClassMerger createForInitialClassMerging(
       AppView<AppInfoWithLiveness> appView) {
-    return new HorizontalClassMerger(appView, Mode.INITIAL);
+    return new HorizontalClassMerger(appView, ClassMergerMode.INITIAL);
   }
 
   public static HorizontalClassMerger createForFinalClassMerging(
       AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return new HorizontalClassMerger(appView, Mode.FINAL);
+    return new HorizontalClassMerger(appView, ClassMergerMode.FINAL);
   }
 
   public static HorizontalClassMerger createForD8ClassMerging(AppView<?> appView) {
     assert appView.options().horizontalClassMergerOptions().isRestrictedToSynthetics();
-    return new HorizontalClassMerger(appView, Mode.FINAL);
+    return new HorizontalClassMerger(appView, ClassMergerMode.FINAL);
   }
 
   public void runIfNecessary(ExecutorService executorService, Timing timing)
@@ -113,7 +101,7 @@
   }
 
   private MutableMethodConversionOptions getConversionOptions() {
-    return mode == Mode.INITIAL
+    return mode == ClassMergerMode.INITIAL
         ? MethodConversionOptions.forPreLirPhase(appView)
         : MethodConversionOptions.forPostLirPhase(appView);
   }
@@ -423,7 +411,7 @@
   private HorizontalClassMergerGraphLens createLens(
       HorizontallyMergedClasses mergedClasses,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode,
+      ClassMergerMode mode,
       ProfileCollectionAdditions profileCollectionAdditions,
       SyntheticArgumentClass syntheticArgumentClass,
       ExecutorService executorService,
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java
index fb27321..5bc07a2 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerTreeFixer.java
@@ -4,10 +4,10 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 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;
@@ -24,13 +24,13 @@
         HorizontalClassMergerGraphLens,
         HorizontallyMergedClasses> {
 
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
   public HorizontalClassMergerTreeFixer(
       AppView<?> appView,
       HorizontallyMergedClasses mergedClasses,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode,
+      ClassMergerMode mode,
       ProfileCollectionAdditions profileCollectionAdditions,
       SyntheticArgumentClass syntheticArgumentClass) {
     super(appView, lensBuilder, mergedClasses, profileCollectionAdditions, syntheticArgumentClass);
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 b522877..2355e0b 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.ClassMergerMode;
 import com.android.tools.r8.classmerging.SyntheticArgumentClass;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -19,7 +20,6 @@
 import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.code.ConstructorEntryPointSynthesizedCode;
 import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
 import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
@@ -49,7 +49,7 @@
   private final List<ProgramMethod> instanceInitializers;
   private final InstanceInitializerDescription instanceInitializerDescription;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
   InstanceInitializerMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
@@ -57,7 +57,7 @@
       HorizontalMergeGroup group,
       List<ProgramMethod> instanceInitializers,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode) {
+      ClassMergerMode mode) {
     this(appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode, null);
   }
 
@@ -67,7 +67,7 @@
       HorizontalMergeGroup group,
       List<ProgramMethod> instanceInitializers,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode,
+      ClassMergerMode mode,
       InstanceInitializerDescription instanceInitializerDescription) {
     this.appView = appView;
     this.classIdentifiers = classIdentifiers;
@@ -175,13 +175,13 @@
     private int estimatedDexCodeSize;
     private final List<List<ProgramMethod>> instanceInitializerGroups = new ArrayList<>();
     private final HorizontalClassMergerGraphLens.Builder lensBuilder;
-    private final Mode mode;
+    private final ClassMergerMode mode;
 
     public Builder(
         AppView<? extends AppInfoWithClassHierarchy> appView,
         Reference2IntMap<DexType> classIdentifiers,
         HorizontalClassMergerGraphLens.Builder lensBuilder,
-        Mode mode) {
+        ClassMergerMode mode) {
       this.appView = appView;
       this.classIdentifiers = classIdentifiers;
       this.lensBuilder = lensBuilder;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
index 62d7522..dcb9ee0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
@@ -6,12 +6,12 @@
 
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerMerger.Builder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -44,7 +44,7 @@
       IRCodeProvider codeProvider,
       HorizontalMergeGroup group,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Mode mode) {
+      ClassMergerMode mode) {
     if (!appView.hasClassHierarchy()) {
       assert appView.options().horizontalClassMergerOptions().isRestrictedToSynthetics();
       assert verifyNoInstanceInitializers(group);
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 fa18dbb..a9fa5b6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -4,12 +4,12 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckSyntheticClasses;
@@ -76,7 +76,7 @@
   public static List<Policy> getPolicies(
       AppView<?> appView,
       IRCodeProvider codeProvider,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     if (appView.hasClassHierarchy()) {
       return getPoliciesForR8(
@@ -86,7 +86,7 @@
     }
   }
 
-  private static List<Policy> getPoliciesForD8(AppView<AppInfo> appView, Mode mode) {
+  private static List<Policy> getPoliciesForD8(AppView<AppInfo> appView, ClassMergerMode mode) {
     assert mode.isFinal();
     List<Policy> policies =
         ImmutableList.<Policy>builder()
@@ -101,7 +101,7 @@
   private static List<Policy> getPoliciesForR8(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       IRCodeProvider codeProvider,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     List<Policy> policies =
         ImmutableList.<Policy>builder()
@@ -115,7 +115,7 @@
 
   private static List<SingleClassPolicy> getSingleClassPolicies(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     ImmutableList.Builder<SingleClassPolicy> builder = ImmutableList.builder();
 
@@ -141,7 +141,7 @@
   }
 
   private static List<SingleClassPolicy> getSingleClassPoliciesForD8(
-      AppView<AppInfo> appView, Mode mode) {
+      AppView<AppInfo> appView, ClassMergerMode mode) {
     ImmutableList.Builder<SingleClassPolicy> builder =
         ImmutableList.<SingleClassPolicy>builder()
             .add(new CheckSyntheticClasses(appView))
@@ -162,7 +162,7 @@
 
   private static void addSingleClassPoliciesForMergingNonSyntheticClasses(
       AppView<AppInfoWithLiveness> appView,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo,
       ImmutableList.Builder<SingleClassPolicy> builder) {
     builder.add(
@@ -183,7 +183,7 @@
 
   private static boolean verifySingleClassPoliciesIrrelevantForMergingSynthetics(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      Mode mode,
+      ClassMergerMode mode,
       ImmutableList.Builder<SingleClassPolicy> builder) {
     List<SingleClassPolicy> policies =
         ImmutableList.of(
@@ -203,7 +203,9 @@
   }
 
   private static boolean verifySingleClassPoliciesIrrelevantForMergingSyntheticsInD8(
-      AppView<AppInfo> appView, Mode mode, ImmutableList.Builder<SingleClassPolicy> builder) {
+      AppView<AppInfo> appView,
+      ClassMergerMode mode,
+      ImmutableList.Builder<SingleClassPolicy> builder) {
     List<SingleClassPolicy> policies =
         ImmutableList.of(
             new NoResourceClasses(),
@@ -222,7 +224,7 @@
   private static List<Policy> getMultiClassPolicies(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       IRCodeProvider codeProvider,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     ImmutableList.Builder<Policy> builder = ImmutableList.builder();
 
@@ -262,7 +264,7 @@
   }
 
   private static List<? extends Policy> getMultiClassPoliciesForD8(
-      AppView<AppInfo> appView, Mode mode) {
+      AppView<AppInfo> appView, ClassMergerMode mode) {
     ImmutableList.Builder<MultiClassPolicy> builder = ImmutableList.builder();
     builder.add(
         new CheckAbstractClasses(appView),
@@ -281,7 +283,7 @@
 
   private static void addRequiredMultiClassPolicies(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      Mode mode,
+      ClassMergerMode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo,
       ImmutableList.Builder<Policy> builder) {
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
@@ -313,7 +315,7 @@
 
   private static void addMultiClassPoliciesForInterfaceMerging(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      Mode mode,
+      ClassMergerMode mode,
       ImmutableList.Builder<Policy> builder) {
     builder.add(
         new NoDefaultInterfaceMethodMerging(appView, mode),
@@ -323,7 +325,9 @@
   }
 
   private static boolean verifyMultiClassPoliciesIrrelevantForMergingSyntheticsInD8(
-      AppView<AppInfo> appView, Mode mode, ImmutableList.Builder<MultiClassPolicy> builder) {
+      AppView<AppInfo> appView,
+      ClassMergerMode mode,
+      ImmutableList.Builder<MultiClassPolicy> builder) {
     List<MultiClassPolicy> policies =
         ImmutableList.of(new SyntheticItemsPolicy(appView, mode), new SameParentClass());
     policies.stream().map(VerifyMultiClassPolicyAlwaysSatisfied::new).forEach(builder::add);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
index d07b48e..2b7d783 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
@@ -4,13 +4,13 @@
 
 package com.android.tools.r8.horizontalclassmerging.code;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -33,7 +33,7 @@
 
   private final AppView<?> appView;
   private final IRCodeProvider codeProvider;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
   private final List<ProgramMethod> classInitializers;
 
@@ -43,7 +43,7 @@
   private SyntheticInitializerConverter(
       AppView<?> appView,
       IRCodeProvider codeProvider,
-      Mode mode,
+      ClassMergerMode mode,
       List<ProgramMethod> classInitializers,
       Set<DexProgramClass> instanceInitializers) {
     this.appView = appView;
@@ -53,7 +53,8 @@
     this.instanceInitializers = instanceInitializers;
   }
 
-  public static Builder builder(AppView<?> appView, IRCodeProvider codeProvider, Mode mode) {
+  public static Builder builder(
+      AppView<?> appView, IRCodeProvider codeProvider, ClassMergerMode mode) {
     return new Builder(appView, codeProvider, mode);
   }
 
@@ -135,12 +136,12 @@
 
     private final AppView<?> appView;
     private final IRCodeProvider codeProvider;
-    private final Mode mode;
+    private final ClassMergerMode mode;
 
     private final List<ProgramMethod> classInitializers = new ArrayList<>();
     private final Set<DexProgramClass> instanceInitializers = Sets.newIdentityHashSet();
 
-    private Builder(AppView<?> appView, IRCodeProvider codeProvider, Mode mode) {
+    private Builder(AppView<?> appView, IRCodeProvider codeProvider, ClassMergerMode mode) {
       this.appView = appView;
       this.codeProvider = codeProvider;
       this.mode = mode;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
index e0dc0fe..e5839b6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
@@ -14,7 +14,8 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public AllInstantiatedOrUninstantiated(AppView<AppInfoWithLiveness> appView, Mode mode) {
+  public AllInstantiatedOrUninstantiated(
+      AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     // This policy is only used to prevent that horizontal class merging regresses the
     // uninstantiated type optimization. Since there won't be any IR processing after the final
     // round of horizontal class merging, there is no need to use the policy.
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
index 16e17f0..1fdb066 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.ListUtils;
@@ -26,9 +26,9 @@
 public class FinalizeMergeGroup extends MultiClassPolicy {
 
   private final AppView<?> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
-  public FinalizeMergeGroup(AppView<?> appView, Mode mode) {
+  public FinalizeMergeGroup(AppView<?> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.mode = mode;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
index f4b929d..f65b9e8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -11,7 +12,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.utils.ArrayUtils;
@@ -44,7 +44,7 @@
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
 
-  public NoConstructorCollisions(AppView<?> appView, Mode mode) {
+  public NoConstructorCollisions(AppView<?> appView, ClassMergerMode mode) {
     assert mode.isFinal();
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
index 8d1205d..c1cf238 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
@@ -4,10 +4,10 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 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.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.ir.analysis.proto.EnumLiteProtoShrinker;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -18,7 +18,7 @@
 
   private final Set<DexType> deadEnumLiteMaps;
 
-  public NoDeadEnumLiteMaps(AppView<AppInfoWithLiveness> appView, Mode mode) {
+  public NoDeadEnumLiteMaps(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     // This policy is only relevant for the initial round of class merging, since the dead enum lite
     // maps have been removed from the application when the final round of class merging runs.
     assert mode.isInitial();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
index f330807..4784ba5 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -8,6 +8,7 @@
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.emptySet;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodCollisions.InterfaceInfo;
@@ -73,10 +73,10 @@
     extends MultiClassPolicyWithPreprocessing<Map<DexType, InterfaceInfo>> {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
   public NoDefaultInterfaceMethodCollisions(
-      AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.mode = mode;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
index 318a2af..b4d76dc 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
@@ -4,12 +4,12 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.WorkList;
@@ -35,7 +35,7 @@
   private final AppView<?> appView;
   private final DexType MULTIPLE_SENTINEL;
 
-  public NoDefaultInterfaceMethodMerging(AppView<?> appView, Mode mode) {
+  public NoDefaultInterfaceMethodMerging(AppView<?> appView, ClassMergerMode mode) {
     this.appView = appView;
     // Use the java.lang.Object type to indicate more than one interface type, as that type
     // itself is not an interface type.
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
index d330434..d8424e8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -18,12 +18,12 @@
   private final RuntimeTypeCheckInfo runtimeTypeCheckInfo;
   private final SyntheticItems syntheticItems;
 
-  public NoDirectRuntimeTypeChecks(AppView<?> appView, Mode mode) {
+  public NoDirectRuntimeTypeChecks(AppView<?> appView, ClassMergerMode mode) {
     this(appView, mode, null);
   }
 
   public NoDirectRuntimeTypeChecks(
-      AppView<?> appView, Mode mode, RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+      AppView<?> appView, ClassMergerMode mode, RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     assert runtimeTypeCheckInfo != null || mode.isFinal();
     this.options = appView.options();
     this.runtimeTypeCheckInfo = runtimeTypeCheckInfo;
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 872bf09..5126df8 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
@@ -4,12 +4,12 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -19,7 +19,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public NoIllegalInlining(AppView<AppInfoWithLiveness> appView, Mode mode) {
+  public NoIllegalInlining(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     // This policy is only relevant for the first round of horizontal class merging, since the final
     // round of horizontal class merging may not require any inlining.
     assert mode.isInitial();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
index b792e84..1edca95 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.ClassInstanceFieldsMerger;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis;
@@ -64,7 +64,7 @@
   public NoInstanceInitializerMerging(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       IRCodeProvider codeProvider,
-      Mode mode) {
+      ClassMergerMode mode) {
     assert mode.isFinal();
     this.appView = appView;
     this.codeProvider = codeProvider;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
index feabe1c..8af3dd9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
@@ -4,18 +4,18 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 
 public class NoInterfaces extends SingleClassPolicy {
 
-  private final Mode mode;
+  private final ClassMergerMode mode;
   private final HorizontalClassMergerOptions options;
 
-  public NoInterfaces(AppView<?> appView, Mode mode) {
+  public NoInterfaces(AppView<?> appView, ClassMergerMode mode) {
     this.mode = mode;
     this.options = appView.options().horizontalClassMergerOptions();
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
index 3259fe7..1aa69c7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
@@ -4,16 +4,16 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public class NoVerticallyMergedClasses extends SingleClassPolicy {
   private final AppView<AppInfoWithLiveness> appView;
 
-  public NoVerticallyMergedClasses(AppView<AppInfoWithLiveness> appView, Mode mode) {
+  public NoVerticallyMergedClasses(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     // This policy is only relevant for the initial round, since all vertically merged classes have
     // been removed from the application in the final round of horizontal class merging.
     assert mode.isInitial();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
index 21283e1..22d8773 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -13,7 +14,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.IterableUtils;
@@ -34,7 +34,8 @@
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
 
-  public NoVirtualMethodMerging(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+  public NoVirtualMethodMerging(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ClassMergerMode mode) {
     assert mode.isFinal();
     this.appView = appView;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
index db74f25..3714024 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
@@ -6,13 +6,13 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 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.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.SubtypingInfo;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.utils.SetUtils;
@@ -59,13 +59,13 @@
     extends MultiClassPolicyWithPreprocessing<SubtypingInfo> {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
   // The interface merge groups that this policy has committed to so far.
   private final Map<DexProgramClass, HorizontalMergeGroup> committed = new IdentityHashMap<>();
 
   public OnlyDirectlyConnectedOrUnrelatedInterfaces(
-      AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.mode = mode;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
index b891bcd..72d2468 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
@@ -4,12 +4,12 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.MethodAccessFlags;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -92,7 +92,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public PreserveMethodCharacteristics(AppView<AppInfoWithLiveness> appView, Mode mode) {
+  public PreserveMethodCharacteristics(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     // This policy checks that method merging does invalidate various properties. Thus there is no
     // reason to run this policy if method merging is not allowed.
     assert mode.isInitial();
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 bfc4cd1..a7e3ccc 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
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -14,7 +15,6 @@
 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;
 import com.android.tools.r8.horizontalclassmerging.HorizontalMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.TraversalContinuation;
@@ -28,9 +28,10 @@
 public class RespectPackageBoundaries extends MultiClassPolicy {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
-  public RespectPackageBoundaries(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+  public RespectPackageBoundaries(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.mode = mode;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
index da0951e..3e82768 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -11,7 +12,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessFlags;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
 import com.google.common.collect.HashMultiset;
@@ -21,9 +21,10 @@
 public class SameInstanceFields extends MultiClassSameReferencePolicy<Multiset<InstanceFieldInfo>> {
 
   private final DexItemFactory dexItemFactory;
-  private final Mode mode;
+  private final ClassMergerMode mode;
 
-  public SameInstanceFields(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+  public SameInstanceFields(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ClassMergerMode mode) {
     this.dexItemFactory = appView.dexItemFactory();
     this.mode = mode;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
index 95764f9..dd3b701 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy.ClassKind;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -18,10 +18,10 @@
     NOT_SYNTHETIC
   }
 
-  private final Mode mode;
+  private final ClassMergerMode mode;
   private final SyntheticItems syntheticItems;
 
-  public SyntheticItemsPolicy(AppView<?> appView, Mode mode) {
+  public SyntheticItemsPolicy(AppView<?> appView, ClassMergerMode mode) {
     this.mode = mode;
     this.syntheticItems = appView.getSyntheticItems();
   }
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 1ed5f9a..734004b 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
@@ -62,7 +62,7 @@
 import java.util.Optional;
 import java.util.Set;
 
-public final class DefaultInliningOracle implements InliningOracle, InliningStrategy {
+public final class DefaultInliningOracle implements InliningOracle {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final InternalOptions options;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index 9cb641e..5dc2341 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -20,7 +20,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Map;
 
-final class ForcedInliningOracle implements InliningOracle, InliningStrategy {
+final class ForcedInliningOracle implements InliningOracle {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final Map<? extends InvokeMethod, Inliner.InliningInfo> invokesToInline;
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 9f25637..612b305 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
@@ -51,7 +51,6 @@
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.SimpleDominatingEffectAnalysis.SimpleEffectAnalysisResult;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.ir.optimize.inliner.DefaultInliningReasonStrategy;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.InliningReasonStrategy;
@@ -905,19 +904,13 @@
       ProgramMethod method,
       IRCode code,
       Map<? extends InvokeMethod, InliningInfo> invokesToInline,
+      OptimizationFeedback feedback,
       InliningIRProvider inliningIRProvider,
       MethodProcessor methodProcessor,
       Timing timing) {
     ForcedInliningOracle oracle = new ForcedInliningOracle(appView, invokesToInline);
     performInliningImpl(
-        oracle,
-        oracle,
-        method,
-        code,
-        OptimizationFeedbackIgnore.getInstance(),
-        inliningIRProvider,
-        methodProcessor,
-        timing);
+        oracle, method, code, feedback, inliningIRProvider, methodProcessor, timing);
   }
 
   public void performInlining(
@@ -953,7 +946,7 @@
         new InliningIRProvider(appView, method, code, lensCodeRewriter, methodProcessor);
     assert inliningIRProvider.verifyIRCacheIsEmpty();
     performInliningImpl(
-        oracle, oracle, method, code, feedback, inliningIRProvider, methodProcessor, timing);
+        oracle, method, code, feedback, inliningIRProvider, methodProcessor, timing);
   }
 
   public InliningReasonStrategy createDefaultInliningReasonStrategy(
@@ -990,7 +983,6 @@
   }
 
   private void performInliningImpl(
-      InliningStrategy strategy,
       InliningOracle oracle,
       ProgramMethod context,
       IRCode code,
@@ -1074,13 +1066,13 @@
             continue;
           }
 
-          if (!strategy.stillHasBudget(action, whyAreYouNotInliningReporter)) {
+          if (!singleTargetOracle.stillHasBudget(action, whyAreYouNotInliningReporter)) {
             assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
 
           IRCode inlinee = action.buildInliningIR(appView, invoke, context, inliningIRProvider);
-          if (strategy.willExceedBudget(
+          if (singleTargetOracle.willExceedBudget(
               action, code, inlinee, invoke, block, whyAreYouNotInliningReporter)) {
             assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
@@ -1094,7 +1086,11 @@
           // Inline the inlinee code in place of the invoke instruction
           // Back up before the invoke instruction.
           iterator.previous();
-          strategy.markInlined(inlinee);
+
+          // Intentionally not using singleTargetOracle here, so that we decrease the inlining
+          // instruction allowance on the default inlining oracle when force inlining methods.
+          oracle.markInlined(inlinee);
+
           iterator.inlineInvoke(
               appView, code, inlinee, blockIterator, blocksToRemove, action.getDowncastClass());
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index b7f06c8..69d5ecf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -4,28 +4,36 @@
 
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.AccessControl;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 /**
  * The InliningOracle contains information needed for when inlining other methods into @method.
  */
 public interface InliningOracle {
 
-  boolean isForcedInliningOracle();
+  AppView<AppInfoWithLiveness> appView();
 
-  // TODO(b/142116551): This should be equivalent to invoke.lookupSingleTarget(appView, context)!
-  ProgramMethod lookupSingleTarget(InvokeMethod invoke, ProgramMethod context);
-
-  boolean passesInliningConstraints(
-      SingleResolutionResult<?> resolutionResult,
-      ProgramMethod candidate,
+  boolean canInlineInstanceInitializer(
+      IRCode code,
+      InvokeDirect invoke,
+      ProgramMethod singleTarget,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
   InlineResult computeInlining(
@@ -37,4 +45,72 @@
       ClassInitializationAnalysis classInitializationAnalysis,
       InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
+
+  default DexProgramClass getDowncastTypeIfNeeded(InvokeMethod invoke, ProgramMethod target) {
+    if (invoke.isInvokeMethodWithReceiver()) {
+      // If the invoke has a receiver but the actual type of the receiver is different from the
+      // computed target holder, inlining requires a downcast of the receiver. In case we don't know
+      // the exact type of the receiver we use the static type of the receiver.
+      Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
+      if (!receiver.getType().isClassType()) {
+        return target.getHolder();
+      }
+
+      ClassTypeElement receiverType =
+          getReceiverTypeOrDefault(invoke, receiver.getType().asClassType());
+      ClassTypeElement targetType = target.getHolderType().toTypeElement(appView()).asClassType();
+      if (!receiverType.lessThanOrEqualUpToNullability(targetType, appView())) {
+        return target.getHolder();
+      }
+    }
+    return null;
+  }
+
+  ClassTypeElement getReceiverTypeOrDefault(InvokeMethod invoke, ClassTypeElement defaultValue);
+
+  boolean isForcedInliningOracle();
+
+  // TODO(b/142116551): This should be equivalent to invoke.lookupSingleTarget(appView, context)!
+  ProgramMethod lookupSingleTarget(InvokeMethod invoke, ProgramMethod context);
+
+  /** Inform the strategy that the inlinee has been inlined. */
+  void markInlined(IRCode inlinee);
+
+  boolean passesInliningConstraints(
+      SingleResolutionResult<?> resolutionResult,
+      ProgramMethod candidate,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
+
+  default boolean setDowncastTypeIfNeeded(
+      AppView<AppInfoWithLiveness> appView,
+      InlineAction.Builder actionBuilder,
+      InvokeMethod invoke,
+      ProgramMethod singleTarget,
+      ProgramMethod context) {
+    DexProgramClass downcastClass = getDowncastTypeIfNeeded(invoke, singleTarget);
+    if (downcastClass != null) {
+      if (AccessControl.isClassAccessible(downcastClass, context, appView).isPossiblyFalse()) {
+        return false;
+      }
+      actionBuilder.setDowncastClass(downcastClass);
+    }
+    return true;
+  }
+
+  /** Return true if there is still budget for inlining into this method. */
+  boolean stillHasBudget(
+      InlineAction action, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
+
+  /**
+   * Check if the inlinee will exceed the the budget for inlining size into current method.
+   *
+   * <p>Return true if the strategy will *not* allow inlining.
+   */
+  boolean willExceedBudget(
+      InlineAction action,
+      IRCode code,
+      IRCode inlinee,
+      InvokeMethod invoke,
+      BasicBlock block,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
deleted file mode 100644
index a7bb682..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ /dev/null
@@ -1,90 +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.ir.optimize;
-
-import com.android.tools.r8.graph.AccessControl;
-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.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
-import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-
-interface InliningStrategy {
-
-  AppView<AppInfoWithLiveness> appView();
-
-  boolean canInlineInstanceInitializer(
-      IRCode code,
-      InvokeDirect invoke,
-      ProgramMethod singleTarget,
-      InliningIRProvider inliningIRProvider,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
-
-  /** Return true if there is still budget for inlining into this method. */
-  boolean stillHasBudget(
-      InlineAction action, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
-
-  /**
-   * Check if the inlinee will exceed the the budget for inlining size into current method.
-   *
-   * <p>Return true if the strategy will *not* allow inlining.
-   */
-  boolean willExceedBudget(
-      InlineAction action,
-      IRCode code,
-      IRCode inlinee,
-      InvokeMethod invoke,
-      BasicBlock block,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
-
-  /** Inform the strategy that the inlinee has been inlined. */
-  void markInlined(IRCode inlinee);
-
-  default boolean setDowncastTypeIfNeeded(
-      AppView<AppInfoWithLiveness> appView,
-      InlineAction.Builder actionBuilder,
-      InvokeMethod invoke,
-      ProgramMethod singleTarget,
-      ProgramMethod context) {
-    DexProgramClass downcastClass = getDowncastTypeIfNeeded(invoke, singleTarget);
-    if (downcastClass != null) {
-      if (AccessControl.isClassAccessible(downcastClass, context, appView).isPossiblyFalse()) {
-        return false;
-      }
-      actionBuilder.setDowncastClass(downcastClass);
-    }
-    return true;
-  }
-
-  default DexProgramClass getDowncastTypeIfNeeded(InvokeMethod invoke, ProgramMethod target) {
-    if (invoke.isInvokeMethodWithReceiver()) {
-      // If the invoke has a receiver but the actual type of the receiver is different from the
-      // computed target holder, inlining requires a downcast of the receiver. In case we don't know
-      // the exact type of the receiver we use the static type of the receiver.
-      Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
-      if (!receiver.getType().isClassType()) {
-        return target.getHolder();
-      }
-
-      ClassTypeElement receiverType =
-          getReceiverTypeOrDefault(invoke, receiver.getType().asClassType());
-      ClassTypeElement targetType = target.getHolderType().toTypeElement(appView()).asClassType();
-      if (!receiverType.lessThanOrEqualUpToNullability(targetType, appView())) {
-        return target.getHolder();
-      }
-    }
-    return null;
-  }
-
-  ClassTypeElement getReceiverTypeOrDefault(InvokeMethod invoke, ClassTypeElement defaultValue);
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 08ff606..b192f8a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -209,7 +209,8 @@
         // Inline the class instance.
         AffectedValues affectedValues = new AffectedValues();
         try {
-          anyInlinedMethods |= processor.processInlining(code, affectedValues, inliningIRProvider);
+          anyInlinedMethods |=
+              processor.processInlining(code, affectedValues, feedback, inliningIRProvider);
         } catch (IllegalClassInlinerStateException e) {
           // We introduced a user that we cannot handle in the class inliner as a result of force
           // inlining. Abort gracefully from class inlining without removing the instance.
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 cb5bbbe..256abbb 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
@@ -63,6 +63,7 @@
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
 import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
@@ -361,13 +362,17 @@
   //
   // Returns `true` if at least one method was inlined.
   boolean processInlining(
-      IRCode code, AffectedValues affectedValues, InliningIRProvider inliningIRProvider)
+      IRCode code,
+      AffectedValues affectedValues,
+      OptimizationFeedback feedback,
+      InliningIRProvider inliningIRProvider)
       throws IllegalClassInlinerStateException {
     // Verify that `eligibleInstance` is not aliased.
     assert eligibleInstance == eligibleInstance.getAliasedValue();
 
-    boolean anyInlinedMethods = forceInlineDirectMethodInvocations(code, inliningIRProvider);
-    anyInlinedMethods |= forceInlineIndirectMethodInvocations(code, inliningIRProvider);
+    boolean anyInlinedMethods =
+        forceInlineDirectMethodInvocations(code, feedback, inliningIRProvider);
+    anyInlinedMethods |= forceInlineIndirectMethodInvocations(code, feedback, inliningIRProvider);
 
     rebindIndirectEligibleInstanceUsersFromPhis();
     removeMiscUsages(code, affectedValues);
@@ -379,13 +384,20 @@
 
   @SuppressWarnings("ReferenceEquality")
   private boolean forceInlineDirectMethodInvocations(
-      IRCode code, InliningIRProvider inliningIRProvider) throws IllegalClassInlinerStateException {
+      IRCode code, OptimizationFeedback feedback, InliningIRProvider inliningIRProvider)
+      throws IllegalClassInlinerStateException {
     if (directMethodCalls.isEmpty()) {
       return false;
     }
 
     inliner.performForcedInlining(
-        method, code, directMethodCalls, inliningIRProvider, methodProcessor, Timing.empty());
+        method,
+        code,
+        directMethodCalls,
+        feedback,
+        inliningIRProvider,
+        methodProcessor,
+        Timing.empty());
 
     // In case we are class inlining an object allocation that does not inherit directly from
     // java.lang.Object, we need keep force inlining the constructor until we reach
@@ -432,7 +444,13 @@
         }
         if (!directMethodCalls.isEmpty()) {
           inliner.performForcedInlining(
-              method, code, directMethodCalls, inliningIRProvider, methodProcessor, Timing.empty());
+              method,
+              code,
+              directMethodCalls,
+              feedback,
+              inliningIRProvider,
+              methodProcessor,
+              Timing.empty());
         }
       } while (!directMethodCalls.isEmpty());
     }
@@ -442,7 +460,8 @@
 
   @SuppressWarnings("ReferenceEquality")
   private boolean forceInlineIndirectMethodInvocations(
-      IRCode code, InliningIRProvider inliningIRProvider) throws IllegalClassInlinerStateException {
+      IRCode code, OptimizationFeedback feedback, InliningIRProvider inliningIRProvider)
+      throws IllegalClassInlinerStateException {
     if (indirectMethodCallsOnInstance.isEmpty()) {
       return false;
     }
@@ -513,7 +532,13 @@
 
     if (!methodCallsOnInstance.isEmpty()) {
       inliner.performForcedInlining(
-          method, code, methodCallsOnInstance, inliningIRProvider, methodProcessor, Timing.empty());
+          method,
+          code,
+          methodCallsOnInstance,
+          feedback,
+          inliningIRProvider,
+          methodProcessor,
+          Timing.empty());
     } else {
       // TODO(b/315284776): Diagnose if this should be removed or reenabled.
       /*assert indirectMethodCallsOnInstance.stream()
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
deleted file mode 100644
index c06c056..0000000
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.synthetic;
-
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.UseRegistry;
-import java.util.function.Consumer;
-
-public abstract class SynthesizedCode extends AbstractSynthesizedCode {
-
-  private final SourceCodeProvider sourceCodeProvider;
-
-  public SynthesizedCode(SourceCodeProvider sourceCodeProvider) {
-    this.sourceCodeProvider = sourceCodeProvider;
-  }
-
-  @Override
-  public SourceCodeProvider getSourceCodeProvider() {
-    return sourceCodeProvider;
-  }
-
-  @Override
-  public abstract Consumer<UseRegistry> getRegistryCallback(DexClassAndMethod method);
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
index 422b122..dd7eac12 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -8,17 +8,16 @@
 
 import com.android.tools.r8.graph.AppView;
 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.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
 import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Sets;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -45,13 +44,13 @@
     }
 
     VirtualRootMethod(ProgramMethod root, VirtualRootMethod parent) {
+      assert root != null;
       this.parent = parent;
       this.root = root;
     }
 
-    @SuppressWarnings("ReferenceEquality")
     void addOverride(ProgramMethod override) {
-      assert override != root;
+      assert override.getDefinition() != root.getDefinition();
       assert override.getMethodSignature().equals(root.getMethodSignature());
       overrides.add(override);
       if (hasParent()) {
@@ -103,7 +102,7 @@
     }
   }
 
-  private final Map<DexProgramClass, Map<DexMethodSignature, VirtualRootMethod>>
+  private final Map<DexProgramClass, DexMethodSignatureMap<VirtualRootMethod>>
       virtualRootMethodsPerClass = new IdentityHashMap<>();
 
   private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
@@ -138,17 +137,18 @@
 
   @Override
   public void visit(DexProgramClass clazz) {
-    Map<DexMethodSignature, VirtualRootMethod> state = computeVirtualRootMethodsState(clazz);
+    DexMethodSignatureMap<VirtualRootMethod> state = computeVirtualRootMethodsState(clazz);
     virtualRootMethodsPerClass.put(clazz, state);
   }
 
-  private Map<DexMethodSignature, VirtualRootMethod> computeVirtualRootMethodsState(
+  private DexMethodSignatureMap<VirtualRootMethod> computeVirtualRootMethodsState(
       DexProgramClass clazz) {
-    Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForClass = new HashMap<>();
+    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
+        DexMethodSignatureMap.create();
     immediateSubtypingInfo.forEachImmediateProgramSuperClass(
         clazz,
         superclass -> {
-          Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForSuperclass =
+          DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForSuperclass =
               virtualRootMethodsPerClass.get(superclass);
           virtualRootMethodsForSuperclass.forEach(
               (signature, info) ->
@@ -157,11 +157,10 @@
         });
     clazz.forEachProgramVirtualMethod(
         method -> {
-          DexMethodSignature signature = method.getMethodSignature();
-          if (virtualRootMethodsForClass.containsKey(signature)) {
-            virtualRootMethodsForClass.get(signature).getParent().addOverride(method);
+          if (virtualRootMethodsForClass.containsKey(method)) {
+            virtualRootMethodsForClass.get(method).getParent().addOverride(method);
           } else {
-            virtualRootMethodsForClass.put(signature, new VirtualRootMethod(method));
+            virtualRootMethodsForClass.put(method, new VirtualRootMethod(method));
           }
         });
     return virtualRootMethodsForClass;
@@ -170,7 +169,7 @@
   @Override
   public void prune(DexProgramClass clazz) {
     // Record the overrides for each virtual method that is rooted at this class.
-    Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForClass =
+    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
         virtualRootMethodsPerClass.remove(clazz);
     clazz.forEachProgramVirtualMethod(
         rootCandidate -> {
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 6fb0c2f..93ac4f4 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.utils.collections.ThrowingSet.isThrowingSet;
 
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -50,7 +51,6 @@
 import com.android.tools.r8.graph.UnknownDispatchTargetLookupResult;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
@@ -447,8 +447,7 @@
   }
 
   @Override
-  public void notifyHorizontalClassMergerFinished(
-      HorizontalClassMerger.Mode horizontalClassMergerMode) {
+  public void notifyHorizontalClassMergerFinished(ClassMergerMode horizontalClassMergerMode) {
     if (horizontalClassMergerMode.isInitial()) {
       getMethodAccessInfoCollection().destroy();
     }
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 8222389..616b98a 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.SyntheticInfoConsumer;
 import com.android.tools.r8.SyntheticInfoConsumerData;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.contexts.CompilationContext.UniqueContext;
 import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
@@ -36,7 +37,6 @@
 import com.android.tools.r8.graph.ProgramOrClasspathDefinition;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
-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.references.ClassReference;
@@ -403,7 +403,7 @@
     return committed.containsType(type) || pending.definitions.containsKey(type);
   }
 
-  public boolean isEligibleForClassMerging(DexProgramClass clazz, HorizontalClassMerger.Mode mode) {
+  public boolean isEligibleForClassMerging(DexProgramClass clazz, ClassMergerMode mode) {
     assert isSyntheticClass(clazz);
     return mode.isFinal() || isSyntheticLambda(clazz);
   }
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 6fe8388..26a4087 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.Version;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.debuginfo.DebugRepresentation;
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
@@ -66,7 +67,6 @@
 import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
@@ -878,12 +878,12 @@
     WholeProgramOptimizations wholeProgramOptimizations = WholeProgramOptimizations.ON;
     if (mode.isInitialTreeShaking()) {
       return horizontalClassMergerOptions.isEnabled(
-              HorizontalClassMerger.Mode.INITIAL, wholeProgramOptimizations)
+              ClassMergerMode.INITIAL, wholeProgramOptimizations)
           && !horizontalClassMergerOptions.isRestrictedToSynthetics();
     }
     if (mode.isFinalTreeShaking()) {
       return horizontalClassMergerOptions.isEnabled(
-              HorizontalClassMerger.Mode.FINAL, wholeProgramOptimizations)
+              ClassMergerMode.FINAL, wholeProgramOptimizations)
           && !horizontalClassMergerOptions.isRestrictedToSynthetics();
     }
     assert false;
@@ -1873,7 +1873,7 @@
     }
 
     public boolean isEnabled(
-        HorizontalClassMerger.Mode mode, WholeProgramOptimizations wholeProgramOptimizations) {
+        ClassMergerMode mode, WholeProgramOptimizations wholeProgramOptimizations) {
       if (!enable || debug || intermediate) {
         return false;
       }
@@ -1901,7 +1901,7 @@
       return enableSyntheticMerging;
     }
 
-    public boolean isInterfaceMergingEnabled(HorizontalClassMerger.Mode mode) {
+    public boolean isInterfaceMergingEnabled(ClassMergerMode mode) {
       if (!enableInterfaceMerging) {
         return false;
       }
@@ -2359,7 +2359,7 @@
     public Function<AppView<AppInfoWithLiveness>, RepackagingConfiguration>
         repackagingConfigurationFactory = DefaultRepackagingConfiguration::new;
 
-    public TriConsumer<DexItemFactory, HorizontallyMergedClasses, HorizontalClassMerger.Mode>
+    public TriConsumer<DexItemFactory, HorizontallyMergedClasses, ClassMergerMode>
         horizontallyMergedClassesConsumer = ConsumerUtils.emptyTriConsumer();
     public Function<List<Policy>, List<Policy>> horizontalClassMergingPolicyRewriter =
         Function.identity();
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 8aa569a..65881a8 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
@@ -52,7 +52,11 @@
   }
 
   public T put(DexEncodedMethod method, T value) {
-    return put(method.getReference(), value);
+    return put(method.getSignature(), value);
+  }
+
+  public T put(DexClassAndMethod method, T value) {
+    return put(method.getMethodSignature(), value);
   }
 
   @Override
@@ -163,6 +167,10 @@
     return containsKey(method.getSignature());
   }
 
+  public boolean containsKey(DexClassAndMethod method) {
+    return containsKey(method.getMethodSignature());
+  }
+
   @Override
   public boolean containsKey(Object o) {
     return backing.containsKey(o);
@@ -182,6 +190,10 @@
     return get(method.getSignature());
   }
 
+  public T get(DexClassAndMethod method) {
+    return get(method.getMethodSignature());
+  }
+
   public boolean containsKey(DexMethodSignature signature) {
     return backing.containsKey(signature);
   }
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 9b0626b..f94208d 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -4,35 +4,28 @@
 package com.android.tools.r8.verticalclassmerging;
 
 import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 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.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 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.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.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.Timing.TimingMerger;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -54,130 +47,41 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory dexItemFactory;
+  private final ClassMergerMode mode;
   private final InternalOptions options;
 
-  public VerticalClassMerger(AppView<AppInfoWithLiveness> appView) {
+  public VerticalClassMerger(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
+    this.mode = mode;
     this.options = appView.options();
   }
 
-  // Returns a set of types that must not be merged into other types.
-  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> pinnedReferences = new ArrayList<>();
-    KeepInfoCollection keepInfo = appView.getKeepInfo();
-    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, pinnedClasses);
-      }
-    }
-
-    // It is valid to have an invoke-direct instruction in a default interface method that targets
-    // another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
-    // SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
-    // Therefore, we disallow merging such interfaces into their subtypes.
-    for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
-      markTypeAsPinned(signature.getHolderType(), 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.
-    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;
+  public static VerticalClassMerger createForInitialClassMerging(
+      AppView<AppInfoWithLiveness> appView) {
+    return new VerticalClassMerger(appView, ClassMergerMode.INITIAL);
   }
 
-  private <T extends DexReference> void extractPinnedClasses(
-      Iterable<T> items, Set<DexProgramClass> pinnedClasses) {
-    for (DexReference item : items) {
-      extractPinnedClasses(item, pinnedClasses);
-    }
+  public static VerticalClassMerger createForFinalClassMerging(
+      AppView<AppInfoWithLiveness> appView) {
+    return new VerticalClassMerger(appView, ClassMergerMode.FINAL);
   }
 
-  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()) {
-      return;
-    }
-
-    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, Set<DexProgramClass> pinnedClasses) {
-    pinnedClasses.add(clazz);
-  }
-
-  public static void runIfNecessary(
-      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+  public void runIfNecessary(ExecutorService executorService, Timing timing)
       throws ExecutionException {
     timing.begin("VerticalClassMerger");
-    if (shouldRun(appView)) {
-      new VerticalClassMerger(appView).run(executorService, timing);
+    if (shouldRun()) {
+      run(executorService, timing);
     } else {
-      appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
+      appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty(), mode);
     }
     assert appView.hasVerticallyMergedClasses();
     assert ArtProfileCompletenessChecker.verify(appView);
     timing.end();
   }
 
-  private static boolean shouldRun(AppView<AppInfoWithLiveness> appView) {
-    return appView.options().getVerticalClassMergerOptions().isEnabled()
+  private boolean shouldRun() {
+    return options.getVerticalClassMergerOptions().isEnabled(mode)
         && !appView.hasCfByteCodePassThroughMethods();
   }
 
@@ -200,7 +104,8 @@
     VerticalClassMergerResult verticalClassMergerResult =
         mergeClassesInConnectedComponents(
             connectedComponents, immediateSubtypingInfo, executorService, timing);
-    appView.setVerticallyMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses());
+    appView.setVerticallyMergedClasses(
+        verticalClassMergerResult.getVerticallyMergedClasses(), mode);
     if (verticalClassMergerResult.isEmpty()) {
       return;
     }
@@ -237,15 +142,13 @@
     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, immediateSubtypingInfo, pinnedClasses)
+                  new VerticalClassMergerPolicyExecutor(appView, immediateSubtypingInfo)
                       .run(connectedComponent, executorService, threadTiming);
               if (!connectedComponentMerger.isEmpty()) {
                 synchronized (connectedComponentMergers) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
index 5205d49..b7f435c 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.utils.InternalOptions;
 
 public class VerticalClassMergerOptions {
@@ -19,12 +20,8 @@
     setEnabled(false);
   }
 
-  public boolean isDisabled() {
-    return !isEnabled();
-  }
-
-  public boolean isEnabled() {
-    return enabled && options.isOptimizing() && options.isShrinking();
+  public boolean isEnabled(ClassMergerMode mode) {
+    return enabled && options.isOptimizing() && options.isShrinking() && mode.isInitial();
   }
 
   public void setEnabled(boolean enabled) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
index 1874199..6b6f314 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -49,15 +49,11 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
-  private final Set<DexProgramClass> pinnedClasses;
 
   VerticalClassMergerPolicyExecutor(
-      AppView<AppInfoWithLiveness> appView,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
-      Set<DexProgramClass> pinnedClasses) {
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
     this.appView = appView;
     this.immediateSubtypingInfo = immediateSubtypingInfo;
-    this.pinnedClasses = pinnedClasses;
   }
 
   ConnectedComponentVerticalClassMerger run(
@@ -69,7 +65,7 @@
         List.of(
             new NoDirectlyInstantiatedClassesPolicy(appView),
             new NoInterfacesWithUnknownSubtypesPolicy(appView),
-            new NoKeptClassesPolicy(appView, pinnedClasses),
+            new NoKeptClassesPolicy(appView),
             new SameFeatureSplitPolicy(appView),
             new SameStartupPartitionPolicy(appView),
             new NoServiceInterfacesPolicy(appView),
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java
index 959d28e..c9b0cb5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoKeptClassesPolicy.java
@@ -3,28 +3,40 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging.policies;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+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.profile.art.ArtProfile;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
-public class NoKeptClassesPolicy extends VerticalClassMergerPolicy {
+public class NoKeptClassesPolicy
+    extends VerticalClassMergerPolicyWithPreprocessing<Set<DexProgramClass>> {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final Set<DexProgramClass> keptClasses;
   private final InternalOptions options;
 
-  public NoKeptClassesPolicy(
-      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> keptClasses) {
+  public NoKeptClassesPolicy(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
-    this.keptClasses = keptClasses;
     this.options = appView.options();
   }
 
   @Override
-  public boolean canMerge(VerticalMergeGroup group) {
+  public boolean canMerge(VerticalMergeGroup group, Set<DexProgramClass> keptClasses) {
     DexProgramClass sourceClass = group.getSource();
     return appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
         && !keptClasses.contains(sourceClass);
@@ -34,4 +46,109 @@
   public String getName() {
     return "NoKeptClassesPolicy";
   }
+
+  @Override
+  public Set<DexProgramClass> preprocess(Collection<VerticalMergeGroup> groups) {
+    return getPinnedClasses();
+  }
+
+  // Returns a set of types that must not be merged into other types.
+  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> pinnedReferences = new ArrayList<>();
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    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, pinnedClasses);
+      }
+    }
+
+    // It is valid to have an invoke-direct instruction in a default interface method that targets
+    // another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
+    // SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
+    // Therefore, we disallow merging such interfaces into their subtypes.
+    for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
+      markTypeAsPinned(signature.getHolderType(), 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.
+    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 extractPinnedClasses(
+      Iterable<T> items, Set<DexProgramClass> pinnedClasses) {
+    for (DexReference item : items) {
+      extractPinnedClasses(item, pinnedClasses);
+    }
+  }
+
+  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(appView.dexItemFactory());
+    if (!baseType.isClassType()) {
+      return;
+    }
+
+    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, Set<DexProgramClass> pinnedClasses) {
+    pinnedClasses.add(clazz);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 5349d73..054fb04 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -10,8 +10,8 @@
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.debug.DebugTestConfig;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.testing.AndroidBuildVersion;
@@ -199,12 +199,12 @@
 
   public T addHorizontallyMergedClassesInspector(
       ThrowableConsumer<HorizontallyMergedClassesInspector> inspector) {
-    return addHorizontallyMergedClassesInspector(inspector, HorizontalClassMerger.Mode::isFinal);
+    return addHorizontallyMergedClassesInspector(inspector, ClassMergerMode::isFinal);
   }
 
   public T addHorizontallyMergedClassesInspector(
       ThrowableConsumer<HorizontallyMergedClassesInspector> inspector,
-      Predicate<HorizontalClassMerger.Mode> predicate) {
+      Predicate<ClassMergerMode> predicate) {
     return addOptionsModification(
         options ->
             options.testing.horizontallyMergedClassesConsumer =
diff --git a/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java b/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
index 1b24d61..10afe1b 100644
--- a/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
+++ b/src/test/java/com/android/tools/r8/features/PackagePrivateIsolatedSplitCrossBoundaryReferenceTest.java
@@ -42,45 +42,43 @@
   }
 
   @Test
+  public void testPublicToProtected() throws Exception {
+    MethodReference accessedMethod =
+        Reference.methodFromMethod(NonPublicBase.class.getDeclaredMethod("protectedMethod"));
+    MethodReference main =
+        Reference.methodFromMethod(Feature.class.getDeclaredMethod("testPublicToProtected"));
+    runTestWithExpectedFailure(main, getExpectedDiagnosticMessage(accessedMethod, main));
+  }
+
+  @Test
+  public void testPublicToProtectedFromSubclass() throws Exception {
+    MethodReference main =
+        Reference.methodFromMethod(FeatureSub.class.getDeclaredMethod("testPublicToProtected"));
+    runTest(main, TestDiagnosticMessages::assertNoMessages);
+  }
+
+  @Test
   public void testPublicToPublic() throws Exception {
-    runTest("testPublicToPublic", TestDiagnosticMessages::assertNoMessages);
+    MethodReference main =
+        Reference.methodFromMethod(Feature.class.getDeclaredMethod("testPublicToPublic"));
+    runTest(main, TestDiagnosticMessages::assertNoMessages);
   }
 
   @Test
   public void testPublicToNonPublic() throws Exception {
     MethodReference accessedMethod =
         Reference.methodFromMethod(NonPublicBase.class.getDeclaredMethod("nonPublicMethod"));
-    MethodReference context =
+    MethodReference main =
         Reference.methodFromMethod(Feature.class.getDeclaredMethod("testPublicToNonPublic"));
-    assertFailsCompilationIf(
-        enableIsolatedSplits,
-        () ->
-            runTest(
-                "testPublicToNonPublic",
-                diagnostics ->
-                    diagnostics.assertErrorsMatch(
-                        allOf(
-                            diagnosticType(IllegalAccessWithIsolatedFeatureSplitsDiagnostic.class),
-                            diagnosticMessage(
-                                equalTo(getExpectedDiagnosticMessage(accessedMethod, context)))))));
+    runTestWithExpectedFailure(main, getExpectedDiagnosticMessage(accessedMethod, main));
   }
 
   @Test
   public void testNonPublicToPublic() throws Exception {
     ClassReference accessedClass = Reference.classFromClass(NonPublicBaseSub.class);
-    MethodReference context =
+    MethodReference main =
         Reference.methodFromMethod(Feature.class.getDeclaredMethod("testNonPublicToPublic"));
-    assertFailsCompilationIf(
-        enableIsolatedSplits,
-        () ->
-            runTest(
-                "testNonPublicToPublic",
-                diagnostics ->
-                    diagnostics.assertErrorsMatch(
-                        allOf(
-                            diagnosticType(IllegalAccessWithIsolatedFeatureSplitsDiagnostic.class),
-                            diagnosticMessage(
-                                equalTo(getExpectedDiagnosticMessage(accessedClass, context)))))));
+    runTestWithExpectedFailure(main, getExpectedDiagnosticMessage(accessedClass, main));
   }
 
   private static String getExpectedDiagnosticMessage(
@@ -103,13 +101,32 @@
         + ").";
   }
 
-  private void runTest(String name, DiagnosticsConsumer inspector) throws Exception {
+  private void runTestWithExpectedFailure(MethodReference main, String expectedDiagnosticMessage)
+      throws Exception {
+    assertFailsCompilationIf(
+        enableIsolatedSplits,
+        () ->
+            runTest(
+                main,
+                diagnostics ->
+                    diagnostics.assertErrorsMatch(
+                        allOf(
+                            diagnosticType(IllegalAccessWithIsolatedFeatureSplitsDiagnostic.class),
+                            diagnosticMessage(equalTo(expectedDiagnosticMessage))))));
+  }
+
+  private void runTest(MethodReference main, 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 + "(); }")
+        .addFeatureSplit(Feature.class, FeatureSub.class)
+        .addKeepRules(
+            "-keep class "
+                + main.getHolderClass().getTypeName()
+                + " { void "
+                + main.getMethodName()
+                + "(); }")
         .enableIsolatedSplits(enableIsolatedSplits)
         .setMinApi(parameters)
         .compileWithExpectedDiagnostics(enableIsolatedSplits ? inspector : this::assertNoMessages);
@@ -121,6 +138,10 @@
 
   static class NonPublicBase {
 
+    protected static void protectedMethod() {
+      // Intentionally empty.
+    }
+
     public static void publicMethod() {
       // Intentionally empty.
     }
@@ -136,6 +157,10 @@
 
   public static class Feature {
 
+    public static void testPublicToProtected() {
+      PublicBaseSub.protectedMethod();
+    }
+
     public static void testPublicToPublic() {
       PublicBaseSub.publicMethod();
     }
@@ -148,4 +173,11 @@
       NonPublicBaseSub.publicMethod();
     }
   }
+
+  public static class FeatureSub extends NonPublicBase {
+
+    public static void testPublicToProtected() {
+      PublicBaseSub.protectedMethod();
+    }
+  }
 }
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 f983650..a92c140 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
@@ -15,12 +15,12 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -105,7 +105,7 @@
   private void fixInliningNullabilityClass(
       DexItemFactory dexItemFactory,
       HorizontallyMergedClasses horizontallyMergedClasses,
-      HorizontalClassMerger.Mode mode) {
+      ClassMergerMode mode) {
     DexType originalType =
         dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptor("inlining.Nullability"));
     nullabilityClass =
diff --git a/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java
new file mode 100644
index 0000000..a7cf54f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java
@@ -0,0 +1,136 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.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.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ArrayPatternsTest extends TestBase {
+
+  static final String EXPECTED =
+      StringUtils.lines("int[] [1, 2, 3]", "int[][] [[42]]", "Integer[][][] [[[333]]]");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ArrayPatternsTest(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[][]"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int[][][]"), isAbsent());
+    assertThat(
+        inspector.clazz(B.class).method("void", "bar", "java.lang.Integer[][][]"), isPresent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameterTypePatterns = {@TypePattern(constant = int[].class)}),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameters = {"int[][]"}),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameterTypePatterns = {@TypePattern(name = "java.lang.Integer[][][]")}),
+    })
+    public void foo() throws Exception {
+      // Invoke the first and second method.
+      B.class.getDeclaredMethod("bar", int[].class).invoke(null, (Object) new int[] {1, 2, 3});
+      B.class
+          .getDeclaredMethod("bar", int[][].class)
+          .invoke(null, (Object) new int[][] {new int[] {42}});
+      B.class
+          .getDeclaredMethod("bar", Integer[][][].class)
+          .invoke(null, (Object) new Integer[][][] {new Integer[][] {new Integer[] {333}}});
+    }
+  }
+
+  static class B {
+    public static void bar() {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static void bar(int[] value) {
+      System.out.println("int[] " + Arrays.toString(value));
+    }
+
+    public static void bar(int[][] value) {
+      System.out.println("int[][] " + Arrays.deepToString(value));
+    }
+
+    public static void bar(int[][][] value) {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static void bar(Integer[][][] value) {
+      System.out.println("Integer[][][] " + Arrays.deepToString(value));
+    }
+  }
+
+  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/KeepInvalidForApiTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidForApiTest.java
index 88d7afa..af2087b 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidForApiTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidForApiTest.java
@@ -15,8 +15,8 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationParserException;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -51,7 +51,7 @@
   private void assertThrowsWith(ThrowingRunnable fn, Matcher<String> matcher) {
     try {
       fn.run();
-    } catch (KeepEdgeException e) {
+    } catch (KeepAnnotationParserException e) {
       assertThat(e.getMessage(), matcher);
       return;
     } catch (Throwable e) {
@@ -66,8 +66,9 @@
         () -> extractRuleForClass(RefineMemberAccess.class),
         allOf(
             containsString("Unexpected array"),
-            containsString("@KeepForApi"),
-            containsString("memberAccess")));
+            containsString("memberAccess"),
+            containsString("at annotation: @KeepForApi"),
+            containsString("at method: void main")));
   }
 
   static class RefineMemberAccess {
@@ -84,8 +85,9 @@
         () -> extractRuleForClass(RefineMethodName.class),
         allOf(
             containsString("Unexpected value"),
-            containsString("@KeepForApi"),
-            containsString("methodName")));
+            containsString("methodName"),
+            containsString("at annotation: @KeepForApi"),
+            containsString("at method: void main")));
   }
 
   static class RefineMethodName {
@@ -102,8 +104,9 @@
         () -> extractRuleForClass(RefineFieldName.class),
         allOf(
             containsString("Unexpected value"),
-            containsString("@KeepForApi"),
-            containsString("fieldName")));
+            containsString("fieldName"),
+            containsString("at annotation: @KeepForApi"),
+            containsString("at method: void main")));
   }
 
   static class RefineFieldName {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
index eb5791a..b3baa2e 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
@@ -14,10 +14,11 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.keepanno.annotations.KeepOption;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.StringPattern;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationParserException;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -52,7 +53,7 @@
   private void assertThrowsWith(ThrowingRunnable fn, Matcher<String> matcher) {
     try {
       fn.run();
-    } catch (KeepEdgeException e) {
+    } catch (KeepAnnotationParserException e) {
       assertThat(e.getMessage(), matcher);
       return;
     } catch (Throwable e) {
@@ -66,9 +67,10 @@
     assertThrowsWith(
         () -> extractRuleForClass(MultipleClassDeclarations.class),
         allOf(
-            containsString("Multiple declarations"),
+            containsString("Multiple properties"),
             containsString("className"),
-            containsString("classConstant")));
+            containsString("classConstant"),
+            containsString("at property-group: class-name")));
   }
 
   static class MultipleClassDeclarations {
@@ -83,13 +85,15 @@
   public void testInvalidClassDeclWithBinding() {
     assertThrowsWith(
         () -> extractRuleForClass(BindingAndClassDeclarations.class),
-        allOf(containsString("class binding"), containsString("class patterns")));
+        allOf(
+            containsString("class binding"),
+            containsString("class patterns"),
+            containsString("at property-group: class")));
   }
 
   static class BindingAndClassDeclarations {
 
-    // Both properties are using the "default" value of an empty string, but should still fail.
-    @UsesReflection({@KeepTarget(classFromBinding = "", className = "")})
+    @UsesReflection({@KeepTarget(classFromBinding = "BINDING", className = "CLASS")})
     public static void main(String[] args) {
       System.out.println("Hello, world");
     }
@@ -100,9 +104,12 @@
     assertThrowsWith(
         () -> extractRuleForClass(MultipleExtendsDeclarations.class),
         allOf(
-            containsString("Multiple declarations"),
+            containsString("Multiple properties"),
             containsString("extendsClassName"),
-            containsString("extendsClassConstant")));
+            containsString("extendsClassConstant"),
+            containsString("at property-group: instance-of"),
+            containsString("at annotation: @UsesReflection"),
+            containsString("at method: void main")));
   }
 
   static class MultipleExtendsDeclarations {
@@ -120,7 +127,10 @@
   public void testInvalidMemberDecl() {
     assertThrowsWith(
         () -> extractRuleForClass(MultipleMemberDeclarations.class),
-        allOf(containsString("field"), containsString("method")));
+        allOf(
+            containsString("field"),
+            containsString("method"),
+            containsString("at property-group: member")));
   }
 
   static class MultipleMemberDeclarations {
@@ -135,7 +145,11 @@
   public void testInvalidOptionsDecl() {
     assertThrowsWith(
         () -> extractRuleForClass(MultipleOptionDeclarations.class),
-        allOf(containsString("options"), containsString("allow"), containsString("disallow")));
+        allOf(
+            containsString("Multiple properties"),
+            containsString("allow"),
+            containsString("disallow"),
+            containsString("at property-group: constraints")));
   }
 
   static class MultipleOptionDeclarations {
@@ -150,6 +164,28 @@
     }
   }
 
+  @Test
+  public void testStringPattern() {
+    assertThrowsWith(
+        () -> extractRuleForClass(StringPatternWithExactAndPrefix.class),
+        allOf(
+            containsString("Cannot specify both"),
+            containsString("exact"),
+            containsString("prefix"),
+            containsString("at property: methodNamePattern")));
+  }
+
+  static class StringPatternWithExactAndPrefix {
+
+    @UsesReflection(
+        @KeepTarget(
+            classConstant = A.class,
+            methodNamePattern = @StringPattern(exact = "foo", startsWith = "f")))
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+
   static class A {
     // just a target.
   }
diff --git a/src/test/java/com/android/tools/r8/keepanno/MethodNameStringPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/MethodNameStringPatternsTest.java
new file mode 100644
index 0000000..2444ccf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/MethodNameStringPatternsTest.java
@@ -0,0 +1,140 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.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.StringPattern;
+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 MethodNameStringPatternsTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("1");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public MethodNameStringPatternsTest(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("int", "getMyI"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "setMyI", "int"), isPresent());
+    assertThat(inspector.clazz(B.class).method("java.lang.String", "getMyS"), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "setMyS", "java.lang.String"), isAbsent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          classConstant = B.class,
+          methodNamePattern = @StringPattern(startsWith = "get", endsWith = "I")),
+      @KeepTarget(
+          classConstant = B.class,
+          methodNamePattern = @StringPattern(startsWith = "set", endsWith = "I")),
+    })
+    public void foo() throws Exception {
+      int counter = 1;
+      for (Method method : B.class.getDeclaredMethods()) {
+        String name = method.getName();
+        if (name.startsWith("set")) {
+          if (name.endsWith("I")) {
+            method.invoke(null, counter++);
+          }
+        }
+      }
+      for (Method method : B.class.getDeclaredMethods()) {
+        String name = method.getName();
+        if (name.startsWith("get")) {
+          if (name.endsWith("I")) {
+            System.out.println(method.invoke(null));
+          }
+        }
+      }
+    }
+  }
+
+  static class B {
+    private static int i;
+    private static String s;
+
+    public static int getMyI() {
+      return i;
+    }
+
+    public static String getMyS() {
+      return s;
+    }
+
+    public static void setMyI(int i) {
+      B.i = i;
+    }
+
+    public static void setMyS(String s) {
+      B.s = s;
+    }
+
+    public static void bar() {
+      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 f0ea424..8a83cf9 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
@@ -53,7 +53,7 @@
             .build();
     assertEquals(
         StringUtils.unixLines(
-            "-keep class * { void finalize(); }", "-keepclassmembers class * { *; }"),
+            "-keep class ** { void finalize(); }", "-keepclassmembers class ** { *; }"),
         extract(edge));
   }
 
@@ -83,8 +83,8 @@
     // targeted members.
     assertEquals(
         StringUtils.unixLines(
-            "-keep,allow" + allows + " class * { void finalize(); }",
-            "-keepclassmembers,allow" + allows + " class * { *; }"),
+            "-keep,allow" + allows + " class ** { void finalize(); }",
+            "-keepclassmembers,allow" + allows + " class ** { *; }"),
         extract(edge));
   }
 
@@ -110,8 +110,8 @@
     // Allow is just the ordered list of options.
     assertEquals(
         StringUtils.unixLines(
-            "-keep,allowshrinking,allowobfuscation class * { void finalize(); }",
-            "-keepclassmembers,allowshrinking,allowobfuscation class * { *; }"),
+            "-keep,allowshrinking,allowobfuscation class ** { void finalize(); }",
+            "-keepclassmembers,allowshrinking,allowobfuscation class ** { *; }"),
         extract(edge));
   }
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
index 3468b4c..699ddd6 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
@@ -30,7 +30,19 @@
   static final Class<?> A2 = com.android.tools.r8.keepanno.classpatterns.pkg2.A.class;
   static final Class<?> B2 = com.android.tools.r8.keepanno.classpatterns.pkg2.B.class;
 
-  static final String EXPECTED_ALL = StringUtils.lines("pkg1.A", "pkg1.B", "pkg2.A", "pkg2.B");
+  static final String EXPECTED_ALL =
+      StringUtils.lines(
+          "pkg1.A",
+          "pkg1.A: pkg1.A",
+          "pkg1.B",
+          "pkg1.B: pkg1.B",
+          "pkg2.A",
+          "pkg2.A: pkg2.A",
+          "pkg2.B",
+          "pkg2.B: pkg2.B");
+
+  static final String EXPECTED_ALL_NON_VOID =
+      StringUtils.lines("pkg1.A", "pkg1.B", "pkg2.A", "pkg2.B");
   static final String EXPECTED_PKG = StringUtils.lines("pkg1.A", "pkg1.B");
   static final String EXPECTED_NAME = StringUtils.lines("pkg1.B", "pkg2.B");
   static final String EXPECTED_SINGLE = StringUtils.lines("pkg2.A");
@@ -71,6 +83,11 @@
   }
 
   @Test
+  public void testAllNoVoidR8() throws Exception {
+    runTestR8(TestAllNoVoid.class, EXPECTED_ALL_NON_VOID);
+  }
+
+  @Test
   public void testPkgR8() throws Exception {
     runTestR8(TestPkg.class, EXPECTED_PKG);
   }
@@ -85,6 +102,11 @@
     runTestR8(TestSingle.class, EXPECTED_SINGLE);
   }
 
+  @Test
+  public void testSingleNonExactR8() throws Exception {
+    runTestR8(TestSingleWithNonExactReturnTypeClassPattern.class, EXPECTED_SINGLE);
+  }
+
   public List<Class<?>> getBaseInputClasses() {
     return ImmutableList.of(Util.class, A1, B1, A2, B2);
   }
@@ -97,6 +119,7 @@
           try {
             Class<?> clazz = Class.forName(type);
             System.out.println(clazz.getDeclaredMethod("foo").invoke(null));
+            clazz.getDeclaredMethod("foo", String.class).invoke(null, pkg + "." + name);
           } catch (ClassNotFoundException ignored) {
           } catch (IllegalAccessException ignored) {
           } catch (InvocationTargetException ignored) {
@@ -115,7 +138,8 @@
           // The empty class pattern is equivalent to "any class".
           classNamePattern = @ClassNamePattern(),
           methodName = "foo",
-          methodReturnTypeConstant = String.class)
+          // The empty type pattern used in a return-type context will match 'void'.
+          methodReturnTypePattern = @TypePattern())
     })
     public void foo() throws Exception {
       Util.lookupClassesAndInvokeMethods();
@@ -127,6 +151,25 @@
     }
   }
 
+  static class TestAllNoVoid {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          methodName = "foo",
+          // Matching any class does not include 'void'.
+          methodReturnTypePattern = @TypePattern(classNamePattern = @ClassNamePattern()))
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestAllNoVoid().foo();
+    }
+  }
+
   static class TestPkg {
 
     @UsesReflection({
@@ -189,4 +232,26 @@
       new TestSingle().foo();
     }
   }
+
+  static class TestSingleWithNonExactReturnTypeClassPattern {
+
+    @UsesReflection(
+        @KeepTarget(
+            kind = KeepItemKind.CLASS_AND_METHODS,
+            classNamePattern =
+                @ClassNamePattern(
+                    simpleName = "A",
+                    packageName = "com.android.tools.r8.keepanno.classpatterns.pkg2"),
+            methodName = "foo",
+            methodReturnTypePattern =
+                @TypePattern(classNamePattern = @ClassNamePattern(simpleName = "String"))))
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestSingleWithNonExactReturnTypeClassPattern().foo();
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
index eab0e5f..377601c 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg1.A";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg1.A: " + arg);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
index edeb6b8..b37f208 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg1.B";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg1.B: " + arg);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
index ca0667e..1670103 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg2.A";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg2.A: " + arg);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
index c98e746..ab18f98 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg2.B";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg2.B: " + arg);
+  }
 }
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 312e8bb..0ab89fa 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
@@ -21,6 +21,7 @@
 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.StringPattern;
 import com.android.tools.r8.keepanno.annotations.TypePattern;
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
@@ -53,6 +54,8 @@
     Generator.run();
   }
 
+  private static final String DEFAULT_INVALID_STRING_PATTERN =
+      "@" + simpleName(StringPattern.class) + "(exact = \"\")";
   private static final String DEFAULT_INVALID_TYPE_PATTERN =
       "@" + simpleName(TypePattern.class) + "(name = \"\")";
   private static final String DEFAULT_INVALID_CLASS_NAME_PATTERN =
@@ -151,10 +154,17 @@
     final List<String> footers = new ArrayList<>();
     final LinkedHashMap<String, Group> mutuallyExclusiveGroups = new LinkedHashMap<>();
 
+    boolean mutuallyExclusiveWithOtherGroups = false;
+
     private Group(String name) {
       this.name = name;
     }
 
+    Group allowMutuallyExclusiveWithOtherGroups() {
+      mutuallyExclusiveWithOtherGroups = true;
+      return this;
+    }
+
     Group addMember(GroupMember member) {
       members.add(member);
       return this;
@@ -199,6 +209,18 @@
     }
 
     void generateConstants(Generator generator) {
+      if (mutuallyExclusiveWithOtherGroups || members.size() > 1) {
+        StringBuilder camelCaseName = new StringBuilder();
+        for (int i = 0; i < name.length(); i++) {
+          char c = name.charAt(i);
+          if (c == '-') {
+            c = Character.toUpperCase(name.charAt(++i));
+          }
+          camelCaseName.append(c);
+        }
+        generator.println(
+            "public static final String " + camelCaseName + "Group = " + quote(name) + ";");
+      }
       for (GroupMember member : members) {
         member.generateConstants(generator);
       }
@@ -206,6 +228,7 @@
 
     public void addMutuallyExclusiveGroups(Group... groups) {
       for (Group group : groups) {
+        assert mutuallyExclusiveWithOtherGroups || group.mutuallyExclusiveWithOtherGroups;
         mutuallyExclusiveGroups.computeIfAbsent(
             group.name,
             k -> {
@@ -341,6 +364,38 @@
                   .defaultArrayEmpty(KeepTarget.class));
     }
 
+    private Group stringPatternExactGroup() {
+      return new Group("string-exact-pattern")
+          .allowMutuallyExclusiveWithOtherGroups()
+          .addMember(
+              new GroupMember("exact")
+                  .setDocTitle("Exact string content.")
+                  .addParagraph("For example, {@code \"foo\"} or {@code \"java.lang.String\"}.")
+                  .defaultEmptyString());
+    }
+
+    private Group stringPatternPrefixGroup() {
+      return new Group("string-prefix-pattern")
+          .addMember(
+              new GroupMember("startsWith")
+                  .setDocTitle("Matches strings beginning with the given prefix.")
+                  .addParagraph(
+                      "For example, {@code \"get\"} to match strings such as {@code"
+                          + " \"getMyValue\"}.")
+                  .defaultEmptyString());
+    }
+
+    private Group stringPatternSuffixGroup() {
+      return new Group("string-suffix-pattern")
+          .addMember(
+              new GroupMember("endsWith")
+                  .setDocTitle("Matches strings ending with the given suffix.")
+                  .addParagraph(
+                      "For example, {@code \"Setter\"} to match strings such as {@code"
+                          + " \"myValueSetter\"}.")
+                  .defaultEmptyString());
+    }
+
     private Group typePatternGroup() {
       return new Group("type-pattern")
           .addMember(
@@ -502,6 +557,7 @@
 
     private Group createClassBindingGroup() {
       return new Group(CLASS_GROUP)
+          .allowMutuallyExclusiveWithOtherGroups()
           .addMember(classFromBinding())
           .addDocFooterParagraph("If none are specified the default is to match any class.");
     }
@@ -627,6 +683,7 @@
 
     private Group createMemberBindingGroup() {
       return new Group("member")
+          .allowMutuallyExclusiveWithOtherGroups()
           .addMember(
               new GroupMember("memberFromBinding")
                   .setDocTitle("Define the member pattern in full by a reference to a binding.")
@@ -689,7 +746,14 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any method name"))
                   .setDocReturn("The exact method name of the method.")
-                  .defaultEmptyString());
+                  .defaultEmptyString())
+          .addMember(
+              new GroupMember("methodNamePattern")
+                  .setDocTitle("Define the method-name pattern by a string pattern.")
+                  .addParagraph(getMutuallyExclusiveForMethodProperties())
+                  .addParagraph(getMethodDefaultDoc("any method name"))
+                  .setDocReturn("The string pattern of the method name.")
+                  .defaultValue(StringPattern.class, DEFAULT_INVALID_STRING_PATTERN));
     }
 
     private Group createMethodReturnTypeGroup() {
@@ -810,36 +874,77 @@
       }
 
       // Member binding properties.
+      Group memberBindingGroup = null;
       if (includeMemberBinding) {
-        createMemberBindingGroup().generate(this);
+        memberBindingGroup = createMemberBindingGroup();
+        memberBindingGroup.generate(this);
         println();
       }
 
       // The remaining member properties.
-      generateMemberPropertiesNoBinding();
+      internalGenerateMemberPropertiesNoBinding(memberBindingGroup);
+    }
+
+    private Group maybeLink(Group group, Group maybeExclusiveGroup) {
+      if (maybeExclusiveGroup != null) {
+        maybeExclusiveGroup.addMutuallyExclusiveGroups(group);
+      }
+      return group;
     }
 
     private void generateMemberPropertiesNoBinding() {
+      internalGenerateMemberPropertiesNoBinding(null);
+    }
+
+    private void internalGenerateMemberPropertiesNoBinding(Group memberBindingGroup) {
       // General member properties.
-      createMemberAccessGroup().generate(this);
+      maybeLink(createMemberAccessGroup(), memberBindingGroup).generate(this);
       println();
 
       // Method properties.
-      createMethodAccessGroup().generate(this);
+      maybeLink(createMethodAccessGroup(), memberBindingGroup).generate(this);
       println();
-      createMethodNameGroup().generate(this);
+      maybeLink(createMethodNameGroup(), memberBindingGroup).generate(this);
       println();
-      createMethodReturnTypeGroup().generate(this);
+      maybeLink(createMethodReturnTypeGroup(), memberBindingGroup).generate(this);
       println();
-      createMethodParametersGroup().generate(this);
+      maybeLink(createMethodParametersGroup(), memberBindingGroup).generate(this);
       println();
 
       // Field properties.
-      createFieldAccessGroup().generate(this);
+      maybeLink(createFieldAccessGroup(), memberBindingGroup).generate(this);
       println();
-      createFieldNameGroup().generate(this);
+      maybeLink(createFieldNameGroup(), memberBindingGroup).generate(this);
       println();
-      createFieldTypeGroup().generate(this);
+      maybeLink(createFieldTypeGroup(), memberBindingGroup).generate(this);
+    }
+
+    private void generateStringPattern() {
+      printCopyRight(2024);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A pattern structure for matching strings.")
+          .addParagraph("If no properties are set, the default pattern matches any string.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + simpleName(StringPattern.class) + " {");
+      println();
+      withIndent(
+          () -> {
+            Group exactGroup = stringPatternExactGroup();
+            Group prefixGroup = stringPatternPrefixGroup();
+            Group suffixGroup = stringPatternSuffixGroup();
+            exactGroup.addMutuallyExclusiveGroups(prefixGroup, suffixGroup);
+            exactGroup.generate(this);
+            println();
+            prefixGroup.generate(this);
+            println();
+            suffixGroup.generate(this);
+          });
+      println();
+      println("}");
     }
 
     private void generateTypePattern() {
@@ -1213,6 +1318,7 @@
             generateMethodAccessConstants();
             generateFieldAccessConstants();
 
+            generateStringPatternConstants();
             generateTypePatternConstants();
             generateClassNamePatternConstants();
           });
@@ -1482,6 +1588,19 @@
       println();
     }
 
+    private void generateStringPatternConstants() {
+      println("public static final class StringPattern {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(StringPattern.class);
+            stringPatternExactGroup().generateConstants(this);
+            stringPatternPrefixGroup().generateConstants(this);
+            stringPatternSuffixGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private void generateTypePatternConstants() {
       println("public static final class TypePattern {");
       withIndent(
@@ -1530,6 +1649,7 @@
       writeFile(source(astPkg, AnnotationConstants.class), Generator::generateConstants);
 
       Path annoPkg = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno/annotations");
+      writeFile(source(annoPkg, StringPattern.class), Generator::generateStringPattern);
       writeFile(source(annoPkg, TypePattern.class), Generator::generateTypePattern);
       writeFile(source(annoPkg, ClassNamePattern.class), Generator::generateClassNamePattern);
       writeFile(source(annoPkg, KeepBinding.class), Generator::generateKeepBinding);
diff --git a/tools/compiledump.py b/tools/compiledump.py
index cc12d2a..5c926b3 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -102,14 +102,13 @@
         'Enable additional test assertions when running the compiler (default disabled)',
         default=False,
         action='store_true')
-    parser.add_argument(
-        '--java-opts',
-        '--java-opts',
-        '-J',
-        metavar='<JVM argument(s)>',
-        default=[],
-        action='append',
-        help='Additional options to pass to JVM invocation')
+    parser.add_argument('--java-opts',
+                        '--java-opts',
+                        '-J',
+                        metavar='<JVM argument(s)>',
+                        default=[],
+                        action='append',
+                        help='Additional options to pass to JVM invocation')
     parser.add_argument('--classfile',
                         help='Run with classfile output',
                         default=False,
@@ -158,15 +157,14 @@
     parser.add_argument(
         '--ignore-features',
         help="Don't split into features when features are present."
-            ' Instead include feature code in main app output.'
-            ' This is always the case when compiler is d8.',
+        ' Instead include feature code in main app output.'
+        ' This is always the case when compiler is d8.',
         default=False,
         action='store_true')
-    parser.add_argument(
-        '--no-build',
-        help="Don't build when using --version main",
-        default=False,
-        action='store_true')
+    parser.add_argument('--no-build',
+                        help="Don't build when using --version main",
+                        default=False,
+                        action='store_true')
     return parser
 
 
@@ -319,6 +317,14 @@
     return compiler
 
 
+def determine_isolated_splits(build_properties, feature_jars):
+    if feature_jars and 'isolated-splits' in build_properties:
+        isolated_splits = build_properties.get('isolated-splits')
+        assert isolated_splits == 'true' or isolated_splits == 'false'
+        return isolated_splits == 'true'
+    return None
+
+
 def determine_trace_references_commands(build_properties, output):
     trace_ref_consumer = build_properties.get('trace_references_consumer')
     if trace_ref_consumer == 'com.android.tools.r8.tracereferences.TraceReferencesCheckConsumer':
@@ -432,9 +438,8 @@
     nolib = args.nolib
     if version == 'main':
         if not args.no_build:
-          gradle.RunGradle(
-            [utils.GRADLE_TASK_R8] if nolib else [utils.GRADLE_TASK_R8LIB]
-          )
+            gradle.RunGradle(
+                [utils.GRADLE_TASK_R8] if nolib else [utils.GRADLE_TASK_R8LIB])
         return utils.R8_JAR if nolib else utils.R8LIB_JAR
     if version == 'source':
         return '%s:%s' % (utils.BUILD_JAVA_MAIN_DIR, utils.ALL_DEPS_JAR)
@@ -576,6 +581,9 @@
             cmd.append('-ea')
         if args.enable_test_assertions:
             cmd.append('-Dcom.android.tools.r8.enableTestAssertions=1')
+        feature_jars = dump.feature_jars()
+        if determine_isolated_splits(build_properties, feature_jars):
+            cmd.append('-Dcom.android.tools.r8.isolatedSplits=1')
         if args.print_times:
             cmd.append('-Dcom.android.tools.r8.printtimes=1')
         if args.r8_flags:
@@ -609,7 +617,7 @@
             cmd.extend(['--output', out])
         else:
             cmd.extend(['--source', program_jar])
-        for feature_jar in dump.feature_jars():
+        for feature_jar in feature_jars:
             if not args.ignore_features and compiler != 'd8':
                 cmd.extend([
                     '--feature-jar', feature_jar,