Merge commit '3699bf31d196048f6d6e0725536bb3bfc31d6c39' into dev-release

Change-Id: If73f698f1a72b0ce6e76571568da79956cdfaf7d
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index c924bdcf..253a756 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -26,6 +26,9 @@
   - [Accessing annotations](#using-reflection-annotations)
 - [Annotating code used by reflection (or via JNI)](#used-by-reflection)
 - [Annotating APIs](#apis)
+- [Constraints](#constraints)
+  - [Defaults](#constraints-defaults)
+  - [Generic signatures](#constraints-signatures)
 - [Migrating rules to annotations](#migrating-rules)
 - [My use case is not covered!](#other-uses)
 - [Troubleshooting](#troubleshooting)
@@ -104,12 +107,11 @@
 ```
 public class MyHiddenMethodCaller {
 
-  @UsesReflection({
-    @KeepTarget(
-        instanceOfClassConstant = BaseClass.class,
-        methodName = "hiddenMethod",
-        methodParameters = {})
-  })
+  @UsesReflection(
+      @KeepTarget(
+          instanceOfClassConstant = BaseClass.class,
+          methodName = "hiddenMethod",
+          methodParameters = {}))
   public void callHiddenMethod(BaseClass base) throws Exception {
     base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
   }
@@ -128,21 +130,16 @@
 of the object.
 
 The [@KeepTarget](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html) describes these field targets. Since the printing only cares about preserving
-the fields, the [@KeepTarget.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#kind()) is set to [KeepItemKind.ONLY_FIELDS](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepItemKind.html#ONLY_FIELDS). Also, since printing
-the field names and values only requires looking up the field, printing its name and getting
-its value the [@KeepTarget.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constraints()) are set to just [KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP),
-[KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) and [KeepConstraint.FIELD_GET](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#FIELD_GET).
+the fields, the [@KeepTarget.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#kind()) is set to [KeepItemKind.ONLY_FIELDS](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepItemKind.html#ONLY_FIELDS).
 
 
 ```
-public class MyFieldValuePrinter {
+static class MyFieldValuePrinter {
 
-  @UsesReflection({
-    @KeepTarget(
-        instanceOfClassConstant = PrintableFieldInterface.class,
-        kind = KeepItemKind.ONLY_FIELDS,
-        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-  })
+  @UsesReflection(
+      @KeepTarget(
+          instanceOfClassConstant = PrintableFieldInterface.class,
+          kind = KeepItemKind.ONLY_FIELDS))
   public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
     for (Field field : objectWithFields.getClass().getDeclaredFields()) {
       System.out.println(field.getName() + " = " + field.get(objectWithFields));
@@ -241,9 +238,6 @@
 field-printing utility of the library. Since the library is reflectively accessing each field
 we annotate them with the [@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) annotation.
 
-We could additionally add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property as we did previously.
-We elide it here for brevity.
-
 
 ```
 public class MyClassWithFields implements PrintableFieldInterface {
@@ -263,15 +257,10 @@
 used reflectively. In particular, the "field printer" example we are considering here does not
 make reflective assumptions about the holder class, so we should not constrain it.
 
-To be more precise let's add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property now. This specifies
-that the fields are looked up, their names are used/assumed and their values are read.
-
 
 ```
-@UsedByReflection(
-    kind = KeepItemKind.ONLY_FIELDS,
-    constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-public class MyClassWithFields implements PrintableFieldInterface {
+@UsedByReflection(kind = KeepItemKind.ONLY_FIELDS) public class MyClassWithFields
+    implements PrintableFieldInterface {
   final int intField = 42;
   String stringField = "Hello!";
 }
@@ -296,9 +285,8 @@
           classConstant = FieldValuePrinterLibrary.class,
           methodName = "printFieldValues")
     },
-    kind = KeepItemKind.ONLY_FIELDS,
-    constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-public class MyClassWithFields implements PrintableFieldInterface {
+    kind = KeepItemKind.ONLY_FIELDS) public class MyClassWithFields
+    implements PrintableFieldInterface {
   final int intField = 42;
   String stringField = "Hello!";
 }
@@ -308,9 +296,8 @@
 
 ## Annotating APIs<a name="apis"></a>
 
-If your code is being shrunk before release as a library, or if you have an API
-surface that is used via dynamic loading at runtime, then you need to keep the
-API surface. For that you should use the [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation.
+If your code is being shrunk before release as a library, then you need to keep
+the API surface. For that you should use the [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation.
 
 When annotating a class the default for [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) is to keep the class as well as all of its
 public and protected members:
@@ -372,6 +359,92 @@
 
 
 
+## Constraints<a name="constraints"></a>
+
+When an item is kept (e.g., items matched by [@KeepTarget](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html) or annotated by
+[@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) or [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html)) you can additionally specify constraints
+about what properties of that item must be kept. Typical constraints are to keep
+the items *name* or its ability to be reflectively *looked up*. You may also be
+interested in keeping the generic signature of an item or annotations associated
+with it.
+
+### Defaults<a name="constraints-defaults"></a>
+
+By default the constraints are to retain the item's name, its ability to be
+looked-up as well as its normal usage. Its normal usage is:
+
+- to be instantiated, for class items;
+- to be invoked, for method items; and
+- to be get and/or set, for field items.
+
+Let us revisit the example reflectively accessing the fields on a class.
+
+Notice that printing the field names and values only requires looking up the field, printing
+its name and getting its value. It does not require setting a new value on the field.
+We can thus use a more restrictive set of constraints
+by setting the [@KeepTarget.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constraints()) property to just [KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP),
+[KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) and [KeepConstraint.FIELD_GET](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#FIELD_GET).
+
+
+```
+static class MyFieldValuePrinter {
+
+  @UsesReflection(
+      @KeepTarget(
+          instanceOfClassConstant = PrintableFieldInterface.class,
+          kind = KeepItemKind.ONLY_FIELDS,
+          constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET}))
+  public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+    for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+      System.out.println(field.getName() + " = " + field.get(objectWithFields));
+    }
+  }
+}
+```
+
+
+
+### Generic signatures<a name="constraints-signatures"></a>
+
+The generic signature information of an item is not kept by default, and
+requires adding constraints to the targeted items.
+
+Imagine we had code that is making use of the template parameters for implementations of a
+generic interface. The code below assumes direct implementations of the `WrappedValue` interface
+and simply prints the type parameter used.
+
+Since we are reflecting on the class structure of implementations of `WrappedValue` we need to
+keep it and any instance of it.
+
+We must also preserve the generic signatures of these classes. We add the
+[KeepConstraint.GENERIC_SIGNATURE](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#GENERIC_SIGNATURE) constraint by using the [@KeepTarget.constraintAdditions](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constraintAdditions())
+property. This ensures that the default constraints are still in place in addition to the
+constraint on generic signatures.
+
+
+```
+public class GenericSignaturePrinter {
+
+  interface WrappedValue<T> {
+    T getValue();
+  }
+
+  @UsesReflection(
+      @KeepTarget(
+          instanceOfClassConstant = WrappedValue.class,
+          constraintAdditions = KeepConstraint.GENERIC_SIGNATURE))
+  public static void printSignature(WrappedValue<?> obj) {
+    Class<? extends WrappedValue> clazz = obj.getClass();
+    for (Type iface : clazz.getGenericInterfaces()) {
+      String typeName = iface.getTypeName();
+      String param = typeName.substring(typeName.lastIndexOf('<') + 1, typeName.lastIndexOf('>'));
+      System.out.println(clazz.getName() + " uses type " + param);
+    }
+  }
+```
+
+
+
 ## Migrating rules to annotations<a name="migrating-rules"></a>
 
 There is no automatic migration of keep rules. Keep annotations often invert the
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 54d910c..da018b3 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -121,9 +121,8 @@
 
 ## [Annotating APIs](apis)
 
-If your code is being shrunk before release as a library, or if you have an API
-surface that is used via dynamic loading at runtime, then you need to keep the
-API surface. For that you should use the `@KeepForApi` annotation.
+If your code is being shrunk before release as a library, then you need to keep
+the API surface. For that you should use the `@KeepForApi` annotation.
 
 [[[INCLUDE DOC:ApiClass]]]
 
@@ -138,6 +137,39 @@
 [[[INCLUDE CODE:ApiMember]]]
 
 
+## [Constraints](constraints)
+
+When an item is kept (e.g., items matched by `@KeepTarget` or annotated by
+`@UsedByReflection` or `@KeepForApi`) you can additionally specify constraints
+about what properties of that item must be kept. Typical constraints are to keep
+the items *name* or its ability to be reflectively *looked up*. You may also be
+interested in keeping the generic signature of an item or annotations associated
+with it.
+
+### [Defaults](constraints-defaults)
+
+By default the constraints are to retain the item's name, its ability to be
+looked-up as well as its normal usage. Its normal usage is:
+
+- to be instantiated, for class items;
+- to be invoked, for method items; and
+- to be get and/or set, for field items.
+
+[[[INCLUDE DOC:UsesReflectionFieldPrinterWithConstraints]]]
+
+[[[INCLUDE CODE:UsesReflectionFieldPrinterWithConstraints]]]
+
+
+### [Generic signatures](constraints-signatures)
+
+The generic signature information of an item is not kept by default, and
+requires adding constraints to the targeted items.
+
+[[[INCLUDE DOC:GenericSignaturePrinter]]]
+
+[[[INCLUDE CODE:GenericSignaturePrinter]]]
+
+
 ## [Migrating rules to annotations](migrating-rules)
 
 There is no automatic migration of keep rules. Keep annotations often invert the
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 4bf6253..24be0b2 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
@@ -65,7 +65,7 @@
    *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined;
    *   <li>{@link KeepItemKind#CLASS_AND_METHODS} if method patterns are defined;
    *   <li>{@link KeepItemKind#CLASS_AND_FIELDS} if field patterns are defined;
-   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}otherwise.
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS} otherwise.
    * </ul>
    *
    * <p>When annotating a method the default kind is: {@link KeepItemKind#ONLY_METHODS}
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 4a0c692..6318fd1 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
@@ -65,7 +65,7 @@
    *   <li>{@link KeepItemKind#ONLY_CLASS} if no member patterns are defined;
    *   <li>{@link KeepItemKind#CLASS_AND_METHODS} if method patterns are defined;
    *   <li>{@link KeepItemKind#CLASS_AND_FIELDS} if field patterns are defined;
-   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS}otherwise.
+   *   <li>{@link KeepItemKind#CLASS_AND_MEMBERS} otherwise.
    * </ul>
    *
    * <p>When annotating a method the default kind is: {@link KeepItemKind#ONLY_METHODS}
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 e0ef6b5..81d3598 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
@@ -9,6 +9,7 @@
 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.KeepClassPattern;
 import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
@@ -46,20 +47,22 @@
                 TypeProperty.TYPE_NAME,
                 name,
                 value,
-                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
+                type ->
+                    setValue.accept(typeToClassNameType(type, getParsingContext().property(name))));
       case CONSTANT:
         return new TypeParser(getParsingContext())
             .tryProperty(
                 TypeProperty.TYPE_CONSTANT,
                 name,
                 value,
-                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
+                type ->
+                    setValue.accept(typeToClassNameType(type, getParsingContext().property(name))));
       default:
         return false;
     }
   }
 
-  KeepQualifiedClassNamePattern typeToClassType(
+  KeepQualifiedClassNamePattern typeToClassNameType(
       KeepTypePattern typePattern, PropertyParsingContext parsingContext) {
     return typePattern.apply(
         KeepQualifiedClassNamePattern::any,
@@ -69,11 +72,7 @@
         arrayTypePattern -> {
           throw parsingContext.error("Invalid use of array type where class type was expected");
         },
-        classNamePattern -> classNamePattern,
-        instanceOfPattern -> {
-          throw parsingContext.error(
-              "Invalid use of instance of type where class type was expected");
-        });
+        KeepClassPattern::getClassNamePattern);
   }
 
   @Override
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 f81610e..85962be 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
@@ -8,6 +8,7 @@
 import com.android.tools.r8.keepanno.asm.InstanceOfParser.InstanceOfProperties;
 import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
+import com.android.tools.r8.keepanno.ast.KeepClassPattern;
 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;
@@ -71,7 +72,8 @@
               ClassNameProperty.PATTERN,
               name,
               descriptor,
-              value -> setValue.accept(KeepTypePattern.fromClass(value)));
+              value ->
+                  setValue.accept(KeepTypePattern.fromClass(KeepClassPattern.fromName(value))));
         }
       case INSTANCE_OF_PATTERN:
         {
@@ -80,7 +82,10 @@
               InstanceOfProperties.PATTERN,
               name,
               descriptor,
-              value -> setValue.accept(KeepTypePattern.fromInstanceOf(value)));
+              value ->
+                  setValue.accept(
+                      KeepTypePattern.fromClass(
+                          KeepClassPattern.builder().setInstanceOfPattern(value).build())));
         }
       default:
         return null;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java
index 13d4df3..830417d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java
@@ -46,7 +46,7 @@
 
   public AnnotationPattern.Builder buildProto() {
     AnnotationPattern.Builder builder = AnnotationPattern.newBuilder();
-    builder.setName(namePattern.buildProto());
+    namePattern.buildProtoIfNotAny(builder::setName);
     if (retentionPolicies == RUNTIME_RETENTION_MASK) {
       builder.setRetention(AnnotationRetention.RETENTION_RUNTIME);
     } else if (retentionPolicies == CLASS_RETENTION_MASK) {
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
index 0c86c6c..23e2c2e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
@@ -52,10 +52,7 @@
             array -> {
               throw new KeepEdgeException("Unexpected nested array");
             },
-            KeepQualifiedClassNamePattern::getExactDescriptor,
-            instanceOf -> {
-              throw new KeepEdgeException("No descriptor exists for instanceOf array");
-            });
+            clazz -> clazz.getClassNamePattern().getExactDescriptor());
   }
 
   @Override
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCheck.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCheck.java
index c7d6d84..1a39181 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCheck.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCheck.java
@@ -118,8 +118,8 @@
     return bindings;
   }
 
-  public KeepItemPattern getItemPattern() {
-    return bindings.get(itemReference).getItem();
+  public KeepBindingReference getItemReference() {
+    return itemReference;
   }
 
   @Override
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
index 4ccfad3..d833da0 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
@@ -25,49 +25,48 @@
 
   public ClassItemPattern.Builder buildClassProto() {
     ClassItemPattern.Builder builder = ClassItemPattern.newBuilder();
+    classPattern.buildProtoIfNotAny(builder::setClassPattern);
     KeepSpecUtils.buildAnnotatedByProto(annotatedByPattern, builder::setAnnotatedBy);
-    instanceOfPattern.buildProto(builder::setInstanceOf);
-    return builder.setClassName(classNamePattern.buildProto());
+    return builder;
   }
 
   public static class Builder {
 
-    private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
-    private KeepInstanceOfPattern instanceOfPattern = KeepInstanceOfPattern.any();
+    private KeepClassPattern.Builder classPattern = KeepClassPattern.builder();
     private OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern =
         OptionalPattern.absent();
 
     private Builder() {}
 
     public Builder applyProto(ClassItemPattern protoItem) {
-      assert classNamePattern.isAny();
-      if (protoItem.hasClassName()) {
-        setClassNamePattern(KeepQualifiedClassNamePattern.fromProto(protoItem.getClassName()));
+      assert classPattern.build().isAny();
+      if (protoItem.hasClassPattern()) {
+        setClassPattern(KeepClassPattern.fromProto(protoItem.getClassPattern()));
       }
       assert annotatedByPattern.isAbsent();
       if (protoItem.hasAnnotatedBy()) {
         setAnnotatedByPattern(KeepSpecUtils.annotatedByFromProto(protoItem.getAnnotatedBy()));
       }
-      assert instanceOfPattern.isAny();
-      if (protoItem.hasInstanceOf()) {
-        setInstanceOfPattern(KeepInstanceOfPattern.fromProto(protoItem.getInstanceOf()));
-      }
       return this;
     }
 
     public Builder copyFrom(KeepClassItemPattern pattern) {
-      return setClassNamePattern(pattern.getClassNamePattern())
-          .setInstanceOfPattern(pattern.getInstanceOfPattern())
+      return setClassPattern(pattern.getClassPattern())
           .setAnnotatedByPattern(pattern.getAnnotatedByPattern());
     }
 
+    public Builder setClassPattern(KeepClassPattern classPattern) {
+      this.classPattern = KeepClassPattern.builder().copyFrom(classPattern);
+      return this;
+    }
+
     public Builder setClassNamePattern(KeepQualifiedClassNamePattern classNamePattern) {
-      this.classNamePattern = classNamePattern;
+      classPattern.setClassNamePattern(classNamePattern);
       return this;
     }
 
     public Builder setInstanceOfPattern(KeepInstanceOfPattern instanceOfPattern) {
-      this.instanceOfPattern = instanceOfPattern;
+      classPattern.setInstanceOfPattern(instanceOfPattern);
       return this;
     }
 
@@ -79,23 +78,19 @@
     }
 
     public KeepClassItemPattern build() {
-      return new KeepClassItemPattern(classNamePattern, instanceOfPattern, annotatedByPattern);
+      return new KeepClassItemPattern(classPattern.build(), annotatedByPattern);
     }
   }
 
-  private final KeepQualifiedClassNamePattern classNamePattern;
-  private final KeepInstanceOfPattern instanceOfPattern;
+  private final KeepClassPattern classPattern;
   private final OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern;
 
   public KeepClassItemPattern(
-      KeepQualifiedClassNamePattern classNamePattern,
-      KeepInstanceOfPattern instanceOfPattern,
+      KeepClassPattern classPattern,
       OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern) {
-    assert classNamePattern != null;
-    assert instanceOfPattern != null;
+    assert classPattern != null;
     assert annotatedByPattern != null;
-    this.classNamePattern = classNamePattern;
-    this.instanceOfPattern = instanceOfPattern;
+    this.classPattern = classPattern;
     this.annotatedByPattern = annotatedByPattern;
   }
 
@@ -109,12 +104,16 @@
     return Collections.emptyList();
   }
 
+  public KeepClassPattern getClassPattern() {
+    return classPattern;
+  }
+
   public KeepQualifiedClassNamePattern getClassNamePattern() {
-    return classNamePattern;
+    return classPattern.getClassNamePattern();
   }
 
   public KeepInstanceOfPattern getInstanceOfPattern() {
-    return instanceOfPattern;
+    return classPattern.getInstanceOfPattern();
   }
 
   public OptionalPattern<KeepQualifiedClassNamePattern> getAnnotatedByPattern() {
@@ -122,7 +121,7 @@
   }
 
   public boolean isAny() {
-    return classNamePattern.isAny() && instanceOfPattern.isAny() && annotatedByPattern.isAbsent();
+    return classPattern.isAny() && annotatedByPattern.isAbsent();
   }
 
   @Override
@@ -134,25 +133,22 @@
       return false;
     }
     KeepClassItemPattern that = (KeepClassItemPattern) obj;
-    return classNamePattern.equals(that.classNamePattern)
-        && instanceOfPattern.equals(that.instanceOfPattern)
+    return classPattern.equals(that.classPattern)
         && annotatedByPattern.equals(that.annotatedByPattern);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(classNamePattern, instanceOfPattern, annotatedByPattern);
+    return Objects.hash(classPattern, annotatedByPattern);
   }
 
   @Override
   public String toString() {
     return "KeepClassItemPattern"
         + "{ class="
-        + classNamePattern
+        + classPattern
         + ", annotated-by="
         + annotatedByPattern
-        + ", instance-of="
-        + instanceOfPattern
         + '}';
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassPattern.java
new file mode 100644
index 0000000..dee7f6b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassPattern.java
@@ -0,0 +1,127 @@
+// 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.android.tools.r8.keepanno.proto.KeepSpecProtos.ClassPattern;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+public class KeepClassPattern {
+
+  private static final KeepClassPattern ANY =
+      new KeepClassPattern(KeepQualifiedClassNamePattern.any(), KeepInstanceOfPattern.any());
+
+  public static KeepClassPattern any() {
+    return ANY;
+  }
+
+  public static KeepClassPattern fromName(KeepQualifiedClassNamePattern namePattern) {
+    return builder().setClassNamePattern(namePattern).build();
+  }
+
+  public static KeepClassPattern fromProto(ClassPattern proto) {
+    return builder().applyProto(proto).build();
+  }
+
+  public static KeepClassPattern exactFromDescriptor(String typeDescriptor) {
+    return fromName(KeepQualifiedClassNamePattern.exactFromDescriptor(typeDescriptor));
+  }
+
+  private final KeepQualifiedClassNamePattern classNamePattern;
+  private final KeepInstanceOfPattern instanceOfPattern;
+
+  public KeepClassPattern(
+      KeepQualifiedClassNamePattern classNamePattern, KeepInstanceOfPattern instanceOfPattern) {
+    this.classNamePattern = classNamePattern;
+    this.instanceOfPattern = instanceOfPattern;
+  }
+
+  public boolean isAny() {
+    return classNamePattern.isAny() && instanceOfPattern.isAny();
+  }
+
+  public KeepQualifiedClassNamePattern getClassNamePattern() {
+    return classNamePattern;
+  }
+
+  public KeepInstanceOfPattern getInstanceOfPattern() {
+    return instanceOfPattern;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public void buildProtoIfNotAny(Consumer<ClassPattern.Builder> setter) {
+    if (!isAny()) {
+      setter.accept(buildProto());
+    }
+  }
+
+  public ClassPattern.Builder buildProto() {
+    ClassPattern.Builder builder = ClassPattern.newBuilder();
+    classNamePattern.buildProtoIfNotAny(builder::setClassName);
+    instanceOfPattern.buildProtoIfNotAny(builder::setInstanceOf);
+    return builder;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof KeepClassPattern)) {
+      return false;
+    }
+    KeepClassPattern other = (KeepClassPattern) obj;
+    return classNamePattern.equals(other.classNamePattern)
+        && instanceOfPattern.equals(other.instanceOfPattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(classNamePattern, instanceOfPattern);
+  }
+
+  public static class Builder {
+
+    private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
+    private KeepInstanceOfPattern instanceOfPattern = KeepInstanceOfPattern.any();
+
+    public Builder applyProto(ClassPattern protoItem) {
+      assert classNamePattern.isAny();
+      if (protoItem.hasClassName()) {
+        setClassNamePattern(KeepQualifiedClassNamePattern.fromProto(protoItem.getClassName()));
+      }
+      assert instanceOfPattern.isAny();
+      if (protoItem.hasInstanceOf()) {
+        setInstanceOfPattern(KeepInstanceOfPattern.fromProto(protoItem.getInstanceOf()));
+      }
+      return this;
+    }
+
+    public Builder copyFrom(KeepClassPattern pattern) {
+      return setClassNamePattern(pattern.getClassNamePattern())
+          .setInstanceOfPattern(pattern.getInstanceOfPattern());
+    }
+
+    public Builder setClassNamePattern(KeepQualifiedClassNamePattern classNamePattern) {
+      this.classNamePattern = classNamePattern;
+      return this;
+    }
+
+    public Builder setInstanceOfPattern(KeepInstanceOfPattern instanceOfPattern) {
+      this.instanceOfPattern = instanceOfPattern;
+      return this;
+    }
+
+    public KeepClassPattern build() {
+      if (classNamePattern.isAny() && instanceOfPattern.isAny()) {
+        return any();
+      }
+      return new KeepClassPattern(classNamePattern, instanceOfPattern);
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java
index b9bf204..2db295e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java
@@ -76,8 +76,8 @@
     }
     MemberAccessField.Builder builder = MemberAccessField.newBuilder();
     buildGeneralProto(builder::setGeneralAccess);
-    volatilePattern.buildProto(builder::setVolatilePattern);
-    transientPattern.buildProto(builder::setTransientPattern);
+    volatilePattern.buildProtoIfNotAny(builder::setVolatilePattern);
+    transientPattern.buildProtoIfNotAny(builder::setTransientPattern);
     callback.accept(builder);
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
index 114ada8..72da0d0 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
@@ -17,14 +17,12 @@
     return builder().applyProto(proto).build();
   }
 
-  public void buildProto(Consumer<InstanceOfPattern.Builder> setter) {
-    if (isAny()) {
-      return;
+  public void buildProtoIfNotAny(Consumer<InstanceOfPattern.Builder> setter) {
+    if (!isAny()) {
+      InstanceOfPattern.Builder builder = InstanceOfPattern.newBuilder();
+      getClassNamePattern().buildProtoIfNotAny(builder::setClassName);
+      setter.accept(builder.setInclusive(isInclusive()));
     }
-    setter.accept(
-        InstanceOfPattern.newBuilder()
-            .setInclusive(isInclusive())
-            .setClassName(getClassNamePattern().buildProto()));
   }
 
   public static class Builder {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberAccessPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberAccessPattern.java
index 487ddc8..0446b39 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberAccessPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberAccessPattern.java
@@ -103,9 +103,9 @@
       }
       builder.setAccessVisibility(visibilityBuilder.build());
     }
-    staticPattern.buildProto(builder::setStaticPattern);
-    finalPattern.buildProto(builder::setFinalPattern);
-    syntheticPattern.buildProto(builder::setSyntheticPattern);
+    staticPattern.buildProtoIfNotAny(builder::setStaticPattern);
+    finalPattern.buildProtoIfNotAny(builder::setFinalPattern);
+    syntheticPattern.buildProtoIfNotAny(builder::setSyntheticPattern);
     callback.accept(builder);
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java
index 5b3a1bd..2c14d18 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java
@@ -65,11 +65,11 @@
     }
     MemberAccessMethod.Builder builder = MemberAccessMethod.newBuilder();
     buildGeneralProto(builder::setGeneralAccess);
-    synchronizedPattern.buildProto(builder::setSynchronizedPattern);
-    bridgePattern.buildProto(builder::setBridgePattern);
-    nativePattern.buildProto(builder::setNativePattern);
-    abstractPattern.buildProto(builder::setAbstractPattern);
-    strictFpPattern.buildProto(builder::setStrictFpPattern);
+    synchronizedPattern.buildProtoIfNotAny(builder::setSynchronizedPattern);
+    bridgePattern.buildProtoIfNotAny(builder::setBridgePattern);
+    nativePattern.buildProtoIfNotAny(builder::setNativePattern);
+    abstractPattern.buildProtoIfNotAny(builder::setAbstractPattern);
+    strictFpPattern.buildProtoIfNotAny(builder::setStrictFpPattern);
     callback.accept(builder);
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java
new file mode 100644
index 0000000..d60c8d0
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import com.android.tools.r8.keepanno.proto.KeepSpecProtos.PackageComponentPattern;
+import java.util.Objects;
+
+/**
+ * Pattern over package components.
+ *
+ * <p>The patterns allow matching a single component with a string pattern, or the special case of
+ * matching any number of arbitrary components (including none).
+ *
+ * <p>It is not possible to match across component separators except by the "zero or more" pattern.
+ */
+public class KeepPackageComponentPattern {
+
+  private static final KeepPackageComponentPattern ZERO_OR_MORE =
+      new KeepPackageComponentPattern(null);
+
+  private static final KeepPackageComponentPattern ANY_SINGLE = single(KeepStringPattern.any());
+
+  public static KeepPackageComponentPattern zeroOrMore() {
+    return ZERO_OR_MORE;
+  }
+
+  public static KeepPackageComponentPattern single(KeepStringPattern singlePattern) {
+    assert singlePattern != null;
+    return singlePattern.isAny() ? ANY_SINGLE : new KeepPackageComponentPattern(singlePattern);
+  }
+
+  public static KeepPackageComponentPattern exact(String exact) {
+    return single(KeepStringPattern.exact(exact));
+  }
+
+  public static KeepPackageComponentPattern fromProto(PackageComponentPattern proto) {
+    if (proto.hasSingleComponent()) {
+      return single(KeepStringPattern.fromProto(proto.getSingleComponent()));
+    }
+    return zeroOrMore();
+  }
+
+  public PackageComponentPattern.Builder buildProto() {
+    PackageComponentPattern.Builder builder = PackageComponentPattern.newBuilder();
+    // The zero-or-more pattern is a component without a single-pattern.
+    if (isSingle()) {
+      builder.setSingleComponent(singlePattern.buildProto());
+    }
+    return builder;
+  }
+
+  private final KeepStringPattern singlePattern;
+
+  private KeepPackageComponentPattern(KeepStringPattern singlePattern) {
+    this.singlePattern = singlePattern;
+  }
+
+  public boolean isZeroOrMore() {
+    return !isSingle();
+  }
+
+  public boolean isSingle() {
+    return singlePattern != null;
+  }
+
+  public boolean isExact() {
+    return isSingle() && singlePattern.isExact();
+  }
+
+  public KeepStringPattern getSinglePattern() {
+    assert isSingle();
+    return singlePattern;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof KeepPackageComponentPattern)) {
+      return false;
+    }
+    KeepPackageComponentPattern other = (KeepPackageComponentPattern) obj;
+    return Objects.equals(singlePattern, other.singlePattern);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
index 3483ff7..e1d24e1 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
@@ -3,223 +3,210 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
-import com.android.tools.r8.keepanno.proto.KeepSpecProtos;
+import com.android.tools.r8.keepanno.proto.KeepSpecProtos.PackageComponentPattern;
 import com.android.tools.r8.keepanno.proto.KeepSpecProtos.PackagePattern;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.Objects;
 
-public abstract class KeepPackagePattern {
+/**
+ * Pattern over the Java package structure.
+ *
+ * <p>The patterns are represented as a list of package component patterns. A package component is
+ * the string content between two package separators (separators are `.` as well as the package
+ * start and end).
+ */
+public class KeepPackagePattern {
+
+  private static final KeepPackagePattern ANY =
+      new KeepPackagePattern(false, ImmutableList.of(KeepPackageComponentPattern.zeroOrMore()));
+
+  private static final KeepPackagePattern TOP =
+      new KeepPackagePattern(true, ImmutableList.of(KeepPackageComponentPattern.exact("")));
+
+  public static KeepPackagePattern any() {
+    return ANY;
+  }
+
+  public static KeepPackagePattern top() {
+    return TOP;
+  }
 
   public static Builder builder() {
     return new Builder();
   }
 
-  public static KeepPackagePattern any() {
-    return Any.getInstance();
-  }
-
-  public static KeepPackagePattern top() {
-    return Top.getInstance();
-  }
-
   public static KeepPackagePattern exact(String fullPackage) {
-    return KeepPackagePattern.builder().exact(fullPackage).build();
+    if (fullPackage.isEmpty()) {
+      return top();
+    }
+    int length = fullPackage.length();
+    Builder builder = KeepPackagePattern.builder();
+    int componentStart = 0;
+    for (int i = 0; i < length; i++) {
+      if (fullPackage.charAt(i) == '.') {
+        if (componentStart == i) {
+          throw new KeepEdgeException("Invalid package string: " + fullPackage + "'");
+        }
+        String substring = fullPackage.substring(componentStart, i);
+        builder.append(KeepPackageComponentPattern.exact(substring));
+        componentStart = i + 1;
+      }
+    }
+    if (componentStart == length) {
+      throw new KeepEdgeException("Invalid package string: '" + fullPackage + "'");
+    }
+    String remaining = fullPackage.substring(componentStart, length);
+    builder.append(KeepPackageComponentPattern.exact(remaining));
+    return builder.build();
   }
 
   public static KeepPackagePattern fromProto(PackagePattern proto) {
     return builder().applyProto(proto).build();
   }
 
-  public PackagePattern.Builder buildProto() {
+  public final PackagePattern.Builder buildProto() {
     PackagePattern.Builder builder = PackagePattern.newBuilder();
     if (isAny()) {
-      // An unset oneof implies "any package" (including multiple package parts).
+      // Any package is serialized out as the empty proto.
       return builder;
     }
-    if (isTop()) {
-      // The top/unspecified package is encoded as the empty package name.
-      return builder.setName(KeepSpecProtos.StringPattern.newBuilder().setExact(""));
+    for (KeepPackageComponentPattern componentPattern : componentPatterns) {
+      builder.addComponents(componentPattern.buildProto());
     }
-    // TODO(b/343389186): Rewrite the package patterns to use the tree structure.
-    return builder.setExactPackageHack(getExactPackageAsString());
+    return builder;
   }
 
   public static class Builder {
 
-    private KeepPackagePattern pattern = KeepPackagePattern.any();
+    private Deque<KeepPackageComponentPattern> componentPatterns = new ArrayDeque<>();
 
-    public Builder applyProto(PackagePattern pkg) {
-      if (pkg.hasExactPackageHack()) {
-        exact(pkg.getExactPackageHack());
-        return this;
+    public Builder applyProto(PackagePattern proto) {
+      for (PackageComponentPattern componentProto : proto.getComponentsList()) {
+        append(KeepPackageComponentPattern.fromProto(componentProto));
       }
-      if (pkg.hasName()) {
-        KeepStringPattern stringPattern = KeepStringPattern.fromProto(pkg.getName());
-        if (stringPattern.isExact() && stringPattern.asExactString().isEmpty()) {
-          return top();
-        }
-        throw new Unimplemented();
-      }
-      if (pkg.hasNode()) {
-        throw new Unimplemented();
-      }
-      // The unset oneof implies any package.
-      assert pattern.isAny();
       return this;
     }
 
     public Builder any() {
-      pattern = Any.getInstance();
+      componentPatterns.clear();
       return this;
     }
 
     public Builder top() {
-      pattern = Top.getInstance();
+      componentPatterns.clear();
+      componentPatterns.add(KeepPackageComponentPattern.single(KeepStringPattern.exact("")));
       return this;
     }
 
-    public Builder exact(String fullPackage) {
-      pattern = fullPackage.isEmpty() ? KeepPackagePattern.top() : new Exact(fullPackage);
+    public Builder append(KeepPackageComponentPattern componentPattern) {
+      componentPatterns.addLast(componentPattern);
       return this;
     }
 
     public KeepPackagePattern build() {
-      if (pattern == null) {
-        throw new KeepEdgeException("Invalid package pattern: null");
+      if (componentPatterns.isEmpty()) {
+        return KeepPackagePattern.any();
       }
-      return pattern;
-    }
-  }
-
-  private static final class Any extends KeepPackagePattern {
-
-    private static final Any INSTANCE = new Any();
-
-    public static Any getInstance() {
-      return INSTANCE;
-    }
-
-    private Any() {}
-
-    @Override
-    public boolean isAny() {
-      return true;
-    }
-
-    @Override
-    public boolean isTop() {
-      return false;
-    }
-
-    @Override
-    public boolean isExact() {
-      return false;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      return obj == this;
-    }
-
-    @Override
-    public int hashCode() {
-      return System.identityHashCode(this);
-    }
-
-    @Override
-    public String toString() {
-      return "*";
-    }
-  }
-
-  private static final class Top extends Exact {
-
-    private static final Top INSTANCE = new Top();
-
-    public static Top getInstance() {
-      return INSTANCE;
-    }
-
-    private Top() {
-      super("");
-    }
-
-    @Override
-    public boolean isAny() {
-      return false;
-    }
-
-    @Override
-    public boolean isTop() {
-      return true;
-    }
-
-    @Override
-    public String toString() {
-      return "";
-    }
-  }
-
-  private static class Exact extends KeepPackagePattern {
-
-    private final String fullPackage;
-
-    private Exact(String fullPackage) {
-      assert fullPackage != null;
-      this.fullPackage = fullPackage;
-      // TODO: Verify valid package identifiers.
-    }
-
-    @Override
-    public boolean isAny() {
-      return false;
-    }
-
-    @Override
-    public boolean isTop() {
-      return fullPackage.equals("");
-    }
-
-    @Override
-    public boolean isExact() {
-      return true;
-    }
-
-    @Override
-    public String getExactPackageAsString() {
-      return fullPackage;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
+      boolean isExact = true;
+      boolean previousIsZeroOrMore = false;
+      ImmutableList.Builder<KeepPackageComponentPattern> builder = ImmutableList.builder();
+      for (KeepPackageComponentPattern component : componentPatterns) {
+        if (component.isZeroOrMore()) {
+          if (!previousIsZeroOrMore) {
+            builder.add(component);
+          }
+          isExact = false;
+          previousIsZeroOrMore = true;
+        } else {
+          builder.add(component);
+          isExact &= component.getSinglePattern().isExact();
+        }
       }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
+      ImmutableList<KeepPackageComponentPattern> finalComponents = builder.build();
+      if (finalComponents.size() == 1) {
+        KeepPackageComponentPattern single = finalComponents.get(0);
+        if (single.isZeroOrMore()) {
+          return KeepPackagePattern.any();
+        }
+        if (isExact && single.getSinglePattern().asExactString().isEmpty()) {
+          return KeepPackagePattern.top();
+        }
       }
-      Exact that = (Exact) o;
-      return fullPackage.equals(that.fullPackage);
-    }
-
-    @Override
-    public int hashCode() {
-      return fullPackage.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return fullPackage;
+      return new KeepPackagePattern(isExact, finalComponents);
     }
   }
 
-  public abstract boolean isAny();
+  // Cached value to avoid traversing the components to determine exact package strings.
+  private final boolean isExact;
+  private final List<KeepPackageComponentPattern> componentPatterns;
 
-  public abstract boolean isTop();
+  private KeepPackagePattern(
+      boolean isExact, ImmutableList<KeepPackageComponentPattern> componentPatterns) {
+    assert !componentPatterns.isEmpty();
+    assert !isExact || componentPatterns.stream().allMatch(KeepPackageComponentPattern::isExact);
+    this.isExact = isExact;
+    this.componentPatterns = componentPatterns;
+  }
 
-  public abstract boolean isExact();
+  public List<KeepPackageComponentPattern> getComponents() {
+    return componentPatterns;
+  }
+
+  public boolean isAny() {
+    return componentPatterns.size() == 1 && componentPatterns.get(0).isZeroOrMore();
+  }
+
+  public boolean isTop() {
+    if (componentPatterns.size() != 1) {
+      return false;
+    }
+    KeepPackageComponentPattern component = componentPatterns.get(0);
+    if (component.isZeroOrMore()) {
+      return false;
+    }
+    KeepStringPattern singlePattern = component.getSinglePattern();
+    return singlePattern.isExact() && singlePattern.asExactString().isEmpty();
+  }
+
+  public boolean isExact() {
+    return isExact;
+  }
 
   public String getExactPackageAsString() {
-    throw new IllegalStateException();
+    if (!isExact) {
+      throw new KeepEdgeException("Invalid attempt to get exact from inexact package pattern");
+    }
+    if (isTop()) {
+      return "";
+    }
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < componentPatterns.size(); i++) {
+      KeepPackageComponentPattern componentPattern = componentPatterns.get(i);
+      if (i > 0) {
+        builder.append('.');
+      }
+      builder.append(componentPattern.getSinglePattern().asExactString());
+    }
+    return builder.toString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof KeepPackagePattern)) {
+      return false;
+    }
+    KeepPackagePattern other = (KeepPackagePattern) obj;
+    return isExact == other.isExact && componentPatterns.equals(other.componentPatterns);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(isExact, componentPatterns);
   }
 }
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 94445bb..fc1a44a 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
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.keepanno.proto.KeepSpecProtos.ClassNamePattern;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 public final class KeepQualifiedClassNamePattern {
 
@@ -49,11 +50,12 @@
     return KeepQualifiedClassNamePattern.builder().applyProto(clazz).build();
   }
 
-  public ClassNamePattern.Builder buildProto() {
-    ClassNamePattern.Builder builder = ClassNamePattern.newBuilder();
-    return builder
-        .setPackage(packagePattern.buildProto())
-        .setUnqualifiedName(namePattern.buildProto());
+  public void buildProtoIfNotAny(Consumer<ClassNamePattern.Builder> setter) {
+    if (!isAny()) {
+      ClassNamePattern.Builder builder = ClassNamePattern.newBuilder();
+      builder.setPackage(packagePattern.buildProto()).setUnqualifiedName(namePattern.buildProto());
+      setter.accept(builder);
+    }
   }
 
   public static class Builder {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepSpecUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepSpecUtils.java
index 5a76764..51da9a9 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepSpecUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepSpecUtils.java
@@ -49,7 +49,11 @@
       Consumer<AnnotatedByPattern.Builder> callback) {
     // If the annotated-by pattern is absent then no restrictions are present, and we don't set it.
     if (pattern.isPresent()) {
-      callback.accept(AnnotatedByPattern.newBuilder().setName(pattern.get().buildProto()));
+      // We must set a pattern here even if the annotation is "any" as that implies that the item
+      // must be annotated by some annotation.
+      AnnotatedByPattern.Builder builder = AnnotatedByPattern.newBuilder();
+      pattern.get().buildProtoIfNotAny(builder::setName);
+      callback.accept(builder);
     }
   }
 
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 15cfef7..a911ca6 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
@@ -25,14 +25,10 @@
     return new ArrayType(type);
   }
 
-  public static KeepTypePattern fromClass(KeepQualifiedClassNamePattern type) {
+  public static KeepTypePattern fromClass(KeepClassPattern type) {
     return new ClassType(type);
   }
 
-  public static KeepTypePattern fromInstanceOf(KeepInstanceOfPattern pattern) {
-    return new KeepInstanceOf(pattern);
-  }
-
   public static KeepTypePattern fromDescriptor(String typeDescriptor) {
     char c = typeDescriptor.charAt(0);
     if (c == 'L') {
@@ -40,7 +36,7 @@
       if (typeDescriptor.charAt(end) != ';') {
         throw new KeepEdgeException("Invalid type descriptor: " + typeDescriptor);
       }
-      return fromClass(KeepQualifiedClassNamePattern.exactFromDescriptor(typeDescriptor));
+      return fromClass(KeepClassPattern.exactFromDescriptor(typeDescriptor));
     }
     if (c == '[') {
       int dim = 1;
@@ -61,21 +57,18 @@
       Supplier<T> onAny,
       Function<KeepPrimitiveTypePattern, T> onPrimitive,
       Function<KeepArrayTypePattern, T> onArray,
-      Function<KeepQualifiedClassNamePattern, T> onClass,
-      Function<KeepInstanceOfPattern, T> onInstanceOf);
+      Function<KeepClassPattern, T> onClass);
 
   public final void match(
       Runnable onAny,
       Consumer<KeepPrimitiveTypePattern> onPrimitive,
       Consumer<KeepArrayTypePattern> onArray,
-      Consumer<KeepQualifiedClassNamePattern> onClass,
-      Consumer<KeepInstanceOfPattern> onInstanceOf) {
+      Consumer<KeepClassPattern> onClass) {
     apply(
         AstUtils.toVoidSupplier(onAny),
         AstUtils.toVoidFunction(onPrimitive),
         AstUtils.toVoidFunction(onArray),
-        AstUtils.toVoidFunction(onClass),
-        AstUtils.toVoidFunction(onInstanceOf));
+        AstUtils.toVoidFunction(onClass));
   }
 
   public boolean isAny() {
@@ -95,8 +88,7 @@
         Supplier<T> onAny,
         Function<KeepPrimitiveTypePattern, T> onPrimitive,
         Function<KeepArrayTypePattern, T> onArray,
-        Function<KeepQualifiedClassNamePattern, T> onClass,
-        Function<KeepInstanceOfPattern, T> onInstanceOf) {
+        Function<KeepClassPattern, T> onClass) {
       return onAny.get();
     }
 
@@ -161,16 +153,15 @@
         Supplier<T> onAny,
         Function<KeepPrimitiveTypePattern, T> onPrimitive,
         Function<KeepArrayTypePattern, T> onArray,
-        Function<KeepQualifiedClassNamePattern, T> onClass,
-        Function<KeepInstanceOfPattern, T> onInstanceOf) {
+        Function<KeepClassPattern, T> onClass) {
       return onPrimitive.apply(type);
     }
   }
 
   private static class ClassType extends KeepTypePattern {
-    private final KeepQualifiedClassNamePattern type;
+    private final KeepClassPattern type;
 
-    public ClassType(KeepQualifiedClassNamePattern type) {
+    public ClassType(KeepClassPattern type) {
       this.type = type;
     }
 
@@ -179,8 +170,7 @@
         Supplier<T> onAny,
         Function<KeepPrimitiveTypePattern, T> onPrimitive,
         Function<KeepArrayTypePattern, T> onArray,
-        Function<KeepQualifiedClassNamePattern, T> onClass,
-        Function<KeepInstanceOfPattern, T> onInstanceOf) {
+        Function<KeepClassPattern, T> onClass) {
       return onClass.apply(type);
     }
 
@@ -219,8 +209,7 @@
         Supplier<T> onAny,
         Function<KeepPrimitiveTypePattern, T> onPrimitive,
         Function<KeepArrayTypePattern, T> onArray,
-        Function<KeepQualifiedClassNamePattern, T> onClass,
-        Function<KeepInstanceOfPattern, T> onInstanceOf) {
+        Function<KeepClassPattern, T> onClass) {
       return onArray.apply(type);
     }
 
@@ -247,24 +236,6 @@
     }
   }
 
-  private static class KeepInstanceOf extends KeepTypePattern {
-    private final KeepInstanceOfPattern instanceOf;
-
-    private KeepInstanceOf(KeepInstanceOfPattern instanceOf) {
-      this.instanceOf = instanceOf;
-    }
-
-    @Override
-    public <T> T apply(
-        Supplier<T> onAny,
-        Function<KeepPrimitiveTypePattern, T> onPrimitive,
-        Function<KeepArrayTypePattern, T> onArray,
-        Function<KeepQualifiedClassNamePattern, T> onClass,
-        Function<KeepInstanceOfPattern, T> onInstanceOf) {
-      return onInstanceOf.apply(instanceOf);
-    }
-  }
-
   public static KeepTypePattern fromProto(TypePattern typeProto) {
     if (typeProto.hasPrimitive()) {
       return KeepTypePattern.fromPrimitive(
@@ -273,13 +244,8 @@
     if (typeProto.hasArray()) {
       return KeepTypePattern.fromArray(KeepArrayTypePattern.fromProto(typeProto.getArray()));
     }
-    if (typeProto.hasClazz()) {
-      return KeepTypePattern.fromClass(
-          KeepQualifiedClassNamePattern.fromProto(typeProto.getClazz()));
-    }
-    if (typeProto.hasInstanceOf()) {
-      return KeepTypePattern.fromInstanceOf(
-          KeepInstanceOfPattern.fromProto(typeProto.getInstanceOf()));
+    if (typeProto.hasClassPattern()) {
+      return KeepTypePattern.fromClass(KeepClassPattern.fromProto(typeProto.getClassPattern()));
     }
     return KeepTypePattern.any();
   }
@@ -292,17 +258,7 @@
         },
         primitive -> builder.setPrimitive(primitive.buildProto()),
         array -> builder.setArray(array.buildProto()),
-        clazz -> builder.setClazz(clazz.buildProto()),
-        instanceOf -> {
-          if (instanceOf.isAny()) {
-            // Note that an "any" instance-of pattern should match any class-type
-            // TODO(b/350647134): This should become evident when introducing a class-pattern.
-            //  When doing so, consider also if/how to match a general reference type.
-            builder.setClazz(KeepQualifiedClassNamePattern.any().buildProto());
-          } else {
-            instanceOf.buildProto(builder::setInstanceOf);
-          }
-        });
+        clazz -> builder.setClassPattern(clazz.buildProto()));
     return builder;
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ModifierPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ModifierPattern.java
index 1afe19e..0d8ef29 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ModifierPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ModifierPattern.java
@@ -73,7 +73,7 @@
     return System.identityHashCode(this);
   }
 
-  public void buildProto(Consumer<KeepSpecProtos.ModifierPattern.Builder> callback) {
+  public void buildProtoIfNotAny(Consumer<KeepSpecProtos.ModifierPattern.Builder> callback) {
     if (isOnlyPositive()) {
       callback.accept(KeepSpecProtos.ModifierPattern.newBuilder().setValue(true));
     } else if (isOnlyNegative()) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 585eee6..168ff22 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -6,7 +6,6 @@
 import com.android.tools.r8.keepanno.ast.KeepAttribute;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
-import com.android.tools.r8.keepanno.ast.KeepBindings.Binding;
 import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
 import com.android.tools.r8.keepanno.ast.KeepCheck;
 import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
@@ -81,32 +80,31 @@
     if (!extractorOptions.hasCheckDiscardSupport()) {
       return Collections.emptyList();
     }
-    KeepItemPattern itemPattern = check.getItemPattern();
+    KeepBindingReference itemReference = check.getItemReference();
+    KeepBindings bindings = check.getBindings();
+
     boolean isRemovedPattern = check.getKind() == KeepCheckKind.REMOVED;
     List<PgRule> rules = new ArrayList<>(isRemovedPattern ? 2 : 1);
     Holder holder;
     Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns;
     List<KeepBindingSymbol> targetMembers;
-    KeepBindings.Builder builder = KeepBindings.builder();
     KeepBindingSymbol symbol;
-    if (itemPattern.isClassItemPattern()) {
-      symbol = builder.generateFreshSymbol("CLASS");
-      builder.addBinding(symbol, itemPattern);
+    if (itemReference.isClassType()) {
+      symbol = itemReference.getName();
       memberPatterns = Collections.emptyMap();
       targetMembers = Collections.emptyList();
     } else {
-      KeepMemberItemPattern memberItemPattern = itemPattern.asMemberItemPattern();
+      KeepMemberItemPattern memberItemPattern =
+          bindings.getMemberItem(itemReference.asMemberBindingReference());
       KeepClassBindingReference classReference = memberItemPattern.getClassReference();
-      Binding binding = check.getBindings().get(classReference);
       symbol = classReference.getName();
-      builder.addBinding(symbol, binding.getItem());
       KeepMemberPattern memberPattern = memberItemPattern.getMemberPattern();
       // This does not actually allocate a binding as the mapping is maintained in 'memberPatterns'.
-      KeepBindingSymbol memberSymbol = new KeepBindingSymbol("MEMBERS");
+      KeepBindingSymbol memberSymbol = itemReference.getName();
       memberPatterns = Collections.singletonMap(memberSymbol, memberPattern);
       targetMembers = Collections.singletonList(memberSymbol);
     }
-    holder = Holder.create(symbol, builder.build());
+    holder = Holder.create(symbol, bindings);
     // Add a -checkdiscard rule for the class or members.
     rules.add(
         new PgUnconditionalRule(
@@ -121,7 +119,7 @@
     // moving/inlining the items.
     if (isRemovedPattern) {
       KeepOptions allowShrinking = KeepOptions.allow(KeepOption.SHRINKING);
-      if (itemPattern.isClassItemPattern()) {
+      if (itemReference.isClassType()) {
         // A check removal on a class means that the entire class is removed, thus soft-pin the
         // class and *all* of its members.
         KeepBindingSymbol memberSymbol = new KeepBindingSymbol("MEMBERS");
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 38a768f..9289894 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
@@ -21,6 +21,7 @@
 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.KeepPackageComponentPattern;
 import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepPrimitiveTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
@@ -221,13 +222,13 @@
         printer::appendTripleStar,
         primitivePattern -> printPrimitiveType(printer, primitivePattern),
         arrayTypePattern -> printArrayType(printer, arrayTypePattern),
-        classTypePattern -> printClassName(classTypePattern, printer),
-        instanceOfPattern -> printInstanceOf(instanceOfPattern, printer));
-  }
-
-  private static RulePrinter printInstanceOf(
-      KeepInstanceOfPattern instanceOfPattern, RulePrinter printer) {
-    throw new Unimplemented();
+        classTypePattern -> {
+          if (!classTypePattern.getInstanceOfPattern().isAny()) {
+            throw new KeepEdgeException(
+                "Type patterns with instance-of are not supported in rule extraction");
+          }
+          return printClassName(classTypePattern.getClassNamePattern(), printer);
+        });
   }
 
   private static RulePrinter printPrimitiveType(
@@ -334,8 +335,14 @@
     if (packagePattern.isTop()) {
       return builder;
     }
-    assert packagePattern.isExact();
-    return builder.append(packagePattern.getExactPackageAsString()).append(".");
+    for (KeepPackageComponentPattern component : packagePattern.getComponents()) {
+      if (component.isZeroOrMore()) {
+        throw new KeepEdgeException("Unsupported use of zero-or-more package pattern");
+      }
+      printStringPattern(builder, component.getSinglePattern());
+      builder.append(".");
+    }
+    return builder;
   }
 
   private static RulePrinter printSimpleClassName(
diff --git a/src/keepanno/proto/keepspec.proto b/src/keepanno/proto/keepspec.proto
index fd52a93..9760fe4 100644
--- a/src/keepanno/proto/keepspec.proto
+++ b/src/keepanno/proto/keepspec.proto
@@ -158,9 +158,13 @@
 }
 
 message ClassItemPattern {
+  optional ClassPattern class_pattern = 1;
+  optional AnnotatedByPattern annotated_by = 2;
+}
+
+message ClassPattern {
   optional ClassNamePattern class_name = 1;
   optional InstanceOfPattern instance_of = 2;
-  optional AnnotatedByPattern annotated_by = 3;
 }
 
 message InstanceOfPattern {
@@ -174,18 +178,13 @@
 }
 
 message PackagePattern {
-  oneof package_oneof {
-    // An unset oneof implies any package (including multiple package parts).
-    StringPattern name = 1;
-    PackageNode node = 2;
-    // TODO(b/343389186): Rewrite package pattern AST to the tree structure.
-    string exact_package_hack = 3;
-  }
+  // No components matches any package.
+  repeated PackageComponentPattern components = 1;
 }
 
-message PackageNode {
-  PackagePattern lhs = 1;
-  PackagePattern rhs = 2;
+message PackageComponentPattern {
+  // An absent single component matches any number of components.
+  optional StringPattern single_component = 1;
 }
 
 message StringPattern {
@@ -305,8 +304,7 @@
     // Unset type denotes any type.
     TypePatternPrimitive primitive = 1;
     TypePatternArray array = 2;
-    ClassNamePattern clazz = 3;
-    InstanceOfPattern instance_of = 4;
+    ClassPattern class_pattern = 3;
   }
 }
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 3bd0ac5..a5caffa 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -485,6 +485,19 @@
     }
 
     /**
+     * Used to disable that keep rules with no member rules are implicitly converted into rules that
+     * keep the default instance constructor.
+     *
+     * <p>This currently defaults to true in stable versions.
+     */
+    public Builder setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+      parserOptionsBuilder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+          enableEmptyMemberRulesToDefaultInitRuleConversion);
+      return this;
+    }
+
+    /**
      * Used to specify if the application is using isolated splits, i.e., if split APKs installed
      * for this application are loaded into their own Context objects.
      *
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 6b9743a..d55ada3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -1607,13 +1607,14 @@
   }
 
   public boolean verifyValidPositionInfo(boolean debug) {
-    assert position != null;
-    assert !debug || getPosition().isSome();
+    assert position != null : toString();
+    assert !debug || getPosition().isSome() : toString();
     assert !instructionTypeCanThrow()
-        || isConstString()
-        || isDexItemBasedConstString()
-        || getPosition().isSome()
-        || getPosition().isSyntheticNone();
+            || isConstString()
+            || isDexItemBasedConstString()
+            || getPosition().isSome()
+            || getPosition().isSyntheticNone()
+        : toString();
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index a6f39aa..7bafe1b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -200,6 +200,15 @@
     return operands;
   }
 
+  public boolean hasOperandThatMatches(Predicate<Value> predicate) {
+    for (Value operand : operands) {
+      if (predicate.test(operand)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public void removeOperand(int index) {
     removeOperand(index, null, alwaysFalse());
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index eb90225..c5e7aaa 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -110,7 +110,7 @@
                 next.inValues()
                     .forEach(
                         value -> {
-                          if (value.hasSingleUniqueUser()) {
+                          if (value.hasSingleUniqueUser() && !value.hasPhiUsers()) {
                             additionalToRemove.add(value.getDefinition());
                           }
                         });
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index e72d7ec..c73dd4e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -179,7 +179,8 @@
       if (isNumberAgainstConstNumberIf(theIf)
           && nonConstNumberOperand.isPhi()
           && nonConstNumberOperand.hasSingleUniqueUser()
-          && !nonConstNumberOperand.hasPhiUsers()) {
+          && !nonConstNumberOperand.hasPhiUsers()
+          && nonConstNumberOperand.getBlock() == theIf.getBlock()) {
         candidates.add(block);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
index 3935555..d63ef16 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
@@ -137,6 +137,12 @@
           if (!canDetachValueIsNullTarget) {
             continue;
           }
+          if (!block.hasEquivalentCatchHandlers(valueIsNullTarget)) {
+            // The new null check is at the if position, while the throw null/NPE is in the
+            // following block. Both should have the same catch handlers for the rewriting to be
+            // correct.
+            continue;
+          }
 
           insertNotNullCheck(
               block,
diff --git a/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java b/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java
index 0b52815..6b68d25 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java
@@ -37,17 +37,23 @@
 import com.android.tools.r8.lightir.LirCode.TryCatchTable;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.verticalclassmerging.VerticalClassMergerGraphLens;
+import com.google.common.collect.ImmutableSet;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class LirLensCodeRewriter<EV> extends LirParsedInstructionCallback<EV> {
 
+  private static final Set<DexMethod> NO_INVOKES_TO_REWRITE = ImmutableSet.of();
+
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final ProgramMethod context;
   private final DexMethod contextReference;
@@ -58,6 +64,7 @@
   private final boolean isNonStartupInStartupOutlinerLens;
 
   private int numberOfInvokeOpcodeChanges = 0;
+  private Set<DexMethod> invokesToRewrite = NO_INVOKES_TO_REWRITE;
   private Map<LirConstant, LirConstant> constantPoolMapping = null;
 
   private boolean hasNonTrivialRewritings = false;
@@ -144,7 +151,7 @@
       numberOfInvokeOpcodeChanges++;
     } else {
       // All non-type dependent mappings are just rewritten in the content pool.
-      addRewrittenMapping(method, newMethod);
+      addRewrittenMethodMapping(method, newMethod);
     }
   }
 
@@ -174,17 +181,31 @@
     return false;
   }
 
+  private void addRewrittenMethodMapping(DexMethod method, DexMethod rewrittenMethod) {
+    getOrCreateConstantPoolMapping()
+        .compute(
+            method,
+            (unusedKey, otherRewrittenMethod) -> {
+              if (otherRewrittenMethod == null || otherRewrittenMethod == rewrittenMethod) {
+                return rewrittenMethod;
+              } else {
+                // Two invokes with the same symbolic method reference but different invoke types
+                // are rewritten to two different symbolic method references. Record that the
+                // invokes need to be processed.
+                if (invokesToRewrite == NO_INVOKES_TO_REWRITE) {
+                  invokesToRewrite = new HashSet<>();
+                }
+                invokesToRewrite.add(method);
+                return method;
+              }
+            });
+  }
+
   private void addRewrittenMapping(LirConstant item, LirConstant rewrittenItem) {
     if (item == rewrittenItem) {
       return;
     }
-    if (constantPoolMapping == null) {
-      constantPoolMapping =
-          new IdentityHashMap<>(
-              // Avoid using initial capacity larger than the number of actual constants.
-              Math.min(getCode().getConstantPool().length, 32));
-    }
-    LirConstant old = constantPoolMapping.put(item, rewrittenItem);
+    LirConstant old = getOrCreateConstantPoolMapping().put(item, rewrittenItem);
     if (old != null && old != rewrittenItem) {
       throw new Unreachable(
           "Unexpected rewriting of item: "
@@ -196,6 +217,16 @@
     }
   }
 
+  private Map<LirConstant, LirConstant> getOrCreateConstantPoolMapping() {
+    if (constantPoolMapping == null) {
+      constantPoolMapping =
+          new IdentityHashMap<>(
+              // Avoid using initial capacity larger than the number of actual constants.
+              Math.min(getCode().getConstantPool().length, 32));
+    }
+    return constantPoolMapping;
+  }
+
   @Override
   public void onDexItemBasedConstString(
       DexReference item, NameComputationInfo<?> nameComputationInfo) {
@@ -284,39 +315,43 @@
     onInvoke(method, InvokeType.INTERFACE, true);
   }
 
-  private InvokeType getInvokeTypeThatMayChange(int opcode) {
+  private boolean isInvokeThatMaybeRequiresRewriting(int opcode) {
+    assert LirOpcodeUtils.isInvokeMethod(opcode);
+    if (!invokesToRewrite.isEmpty()) {
+      return true;
+    }
     if (codeLens.isIdentityLens() && LirOpcodeUtils.isInvokeMethod(opcode)) {
-      return LirOpcodeUtils.getInvokeType(opcode);
+      return true;
     }
     if (opcode == LirOpcodes.INVOKEVIRTUAL) {
-      return InvokeType.VIRTUAL;
+      return true;
     }
     if (opcode == LirOpcodes.INVOKEINTERFACE) {
-      return InvokeType.INTERFACE;
+      return true;
     }
     if (isNonStartupInStartupOutlinerLens) {
       if (LirOpcodeUtils.isInvokeDirect(opcode)) {
-        return InvokeType.DIRECT;
+        return true;
       }
       if (LirOpcodeUtils.isInvokeInterface(opcode)) {
-        return InvokeType.INTERFACE;
+        return true;
       }
       if (LirOpcodeUtils.isInvokeSuper(opcode)) {
-        return InvokeType.SUPER;
+        return true;
       }
       if (LirOpcodeUtils.isInvokeVirtual(opcode)) {
-        return InvokeType.VIRTUAL;
+        return true;
       }
     }
     if (graphLens.isVerticalClassMergerLens()) {
       if (opcode == LirOpcodes.INVOKESTATIC_ITF) {
-        return InvokeType.STATIC;
+        return true;
       }
       if (opcode == LirOpcodes.INVOKESUPER) {
-        return InvokeType.SUPER;
+        return true;
       }
     }
-    return null;
+    return false;
   }
 
   public LirCode<EV> rewrite() {
@@ -474,7 +509,7 @@
   }
 
   private LirCode<EV> rewriteInstructionsWithInvokeTypeChanges(LirCode<EV> code) {
-    if (numberOfInvokeOpcodeChanges == 0) {
+    if (numberOfInvokeOpcodeChanges == 0 && invokesToRewrite.isEmpty()) {
       return code;
     }
     // Build a small map from method refs to index in case the type-dependent methods are already
@@ -498,8 +533,7 @@
         lirWriter.writeOneByteInstruction(opcode);
         continue;
       }
-      InvokeType type = getInvokeTypeThatMayChange(opcode);
-      if (type == null) {
+      if (!LirOpcodeUtils.isInvokeMethod(opcode) || !isInvokeThatMaybeRequiresRewriting(opcode)) {
         int size = view.getRemainingOperandSizeInBytes();
         lirWriter.writeInstruction(opcode, size);
         while (size-- > 0) {
@@ -507,9 +541,12 @@
         }
         continue;
       }
-      // This is potentially an invoke with a type change, in such cases the method is mapped with
+      // If this is either (i) an invoke with a type change or (ii) an invoke to a method M where
+      // there exists another invoke in the current method to M, and the two invokes are mapped to
+      // two different methods (one-to-many constant pool mapping), then the method is mapped with
       // the instruction updated to the new type. The constant pool is amended with the mapped
       // method if needed.
+      InvokeType type = LirOpcodeUtils.getInvokeType(opcode);
       int constantIndex = view.getNextConstantOperand();
       DexMethod method = (DexMethod) code.getConstantItem(constantIndex);
       MethodLookupResult result =
@@ -517,8 +554,7 @@
       boolean newIsInterface = lookupIsInterface(method, opcode, result);
       InvokeType newType = result.getType();
       int newOpcode = newType.getLirOpcode(newIsInterface);
-      if (newOpcode != opcode) {
-        --numberOfInvokeOpcodeChanges;
+      if (newOpcode != opcode || invokesToRewrite.contains(method)) {
         constantIndex =
             methodIndices.computeIfAbsent(
                 result.getReference(),
@@ -526,6 +562,7 @@
                   methodsToAppend.add(ref);
                   return rewrittenConstants.length + methodsToAppend.size() - 1;
                 });
+        numberOfInvokeOpcodeChanges -= BooleanUtils.intValue(newOpcode != opcode);
       }
       int constantIndexSize = ByteUtils.intEncodingSize(constantIndex);
       int remainingSize = view.getRemainingOperandSizeInBytes();
@@ -539,11 +576,9 @@
     // Note that since we assume 'null' in the mapping is identity this may end up with a stale
     // reference to a no longer used method. That is not an issue as it will be pruned when
     // building IR again, it is just a small and size overhead.
-    LirCode<EV> newCode =
-        code.copyWithNewConstantsAndInstructions(
-            ArrayUtils.appendElements(code.getConstantPool(), methodsToAppend),
-            byteWriter.toByteArray());
-    return newCode;
+    return code.copyWithNewConstantsAndInstructions(
+        ArrayUtils.appendElements(code.getConstantPool(), methodsToAppend),
+        byteWriter.toByteArray());
   }
 
   // TODO(b/157111832): This should be part of the graph lens lookup result.
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
index 60b1176..14f21b3e 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
@@ -13,12 +13,14 @@
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Iterables;
@@ -47,8 +49,10 @@
     if (invokeType.isDirect()) {
       return original;
     }
-
-    if (invokeType.isSuper() && options.canHaveSuperInvokeBug()) {
+    if ((invokeType.isSuper() && options.canHaveSuperInvokeBug())
+        || (invokeType.isVirtual()
+            && options.canHaveDalvikVerifyErrorOnVirtualInvokeWithMissingClasses()
+            && canBreakDalvikWithRebinding(resolutionResult))) {
       // To preserve semantics we should find the first library method on the boundary.
       DexType firstLibraryTarget =
           firstLibraryClassOrFirstInterfaceTarget(
@@ -113,6 +117,36 @@
     return newHolder != null ? original.withHolder(newHolder, appView.dexItemFactory()) : original;
   }
 
+  // On dalvik we can have hard verification errors if we rebind to known library classes that
+  // are super classes unknown library classes.
+  private boolean canBreakDalvikWithRebinding(SingleResolutionResult<?> resolutionResult) {
+    assert !resolutionResult.getResolvedHolder().isProgramClass();
+
+    DexClass current = resolutionResult.getInitialResolutionHolder();
+    while (current != null && !current.isLibraryClass()) {
+      current = appView.definitionFor(current.getSuperType());
+    }
+    if (current == null) {
+      assert resolutionResult.getResolvedHolder().isInterface();
+      return false;
+    }
+
+    DexLibraryClass currentLibraryClass = current.asLibraryClass();
+    while (currentLibraryClass != null) {
+      if (!AndroidApiLevelUtils.isApiSafeForReference(currentLibraryClass, appView)) {
+        return true;
+      }
+      if (!currentLibraryClass.hasSuperType()) {
+        // Object
+        break;
+      }
+      currentLibraryClass =
+          DexLibraryClass.asLibraryClassOrNull(
+              appView.definitionFor(currentLibraryClass.getSuperType()));
+    }
+    return false;
+  }
+
   private boolean canRebindDirectlyToLibraryMethod(
       DexClassAndMethod resolvedMethod,
       SingleResolutionResult<?> resolutionResult,
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
index 80cf637..45848ad 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -7,6 +7,8 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramField;
@@ -17,6 +19,8 @@
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
@@ -26,7 +30,6 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -35,6 +38,7 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -44,6 +48,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 public class DefaultFieldValueJoiner {
 
@@ -69,6 +74,9 @@
     // Find all the fields where we need to determine if each field read is guaranteed to be
     // dominated by a write.
     Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = getFieldsOfInterest();
+    if (fieldsOfInterest.isEmpty()) {
+      return Collections.emptyMap();
+    }
 
     // If constructor inlining is disabled, then we focus on whether each instance initializer
     // definitely assigns the given field before it is read. We do the same for final and static
@@ -94,12 +102,12 @@
     // constructor inlining, we find all new-instance instructions (including subtype allocations)
     // and check if the field is written on each allocation before it is possibly read.
     analyzeNewInstanceInstructions(
-        fieldsNotSubjectToInitializerAnalysis, fieldsWithLiveDefaultValue::add);
+        fieldsNotSubjectToInitializerAnalysis, fieldsWithLiveDefaultValue::add, executorService);
 
     return updateFlowGraphs(fieldsWithLiveDefaultValue, executorService);
   }
 
-  private Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
+  protected Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
     Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = new IdentityHashMap<>();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       clazz.forEachProgramField(
@@ -240,13 +248,101 @@
 
   private void analyzeNewInstanceInstructions(
       Map<DexType, ProgramFieldSet> nonFinalInstanceFields,
-      Consumer<ProgramField> liveDefaultValueConsumer) {
-    // Conservatively treat all fields as maybe read before written.
-    // TODO(b/296030319): Implement analysis by building IR for all methods that instantiate the
-    //  relevant classes and analyzing the puts to the newly created instances.
-    for (ProgramField field : IterableUtils.flatten(nonFinalInstanceFields.values())) {
-      liveDefaultValueConsumer.accept(field);
+      Consumer<ProgramField> liveDefaultValueConsumer,
+      ExecutorService executorService)
+      throws ExecutionException {
+    // To simplify the analysis, we currently bail out for non-final classes.
+    // TODO(b/296030319): Handle non-final classes.
+    MapUtils.removeIf(
+        nonFinalInstanceFields,
+        (holderType, fields) -> {
+          assert !fields.isEmpty();
+          DexProgramClass holder = fields.iterator().next().getHolder();
+          // If the class is kept it could be instantiated directly, in which case all default field
+          // values could be live.
+          if (appView.getKeepInfo(holder).isPinned(appView.options())) {
+            fields.forEach(liveDefaultValueConsumer);
+            return true;
+          }
+          if (holder.isFinal() || !appView.appInfo().isInstantiatedIndirectly(holder)) {
+            // When the class is not explicitly marked final, the class could in principle have
+            // injected subclasses if it is pinned. However, none of the fields are pinned, so we
+            // should be allowed to reason about the field assignments in the program.
+            assert fields.stream()
+                .allMatch(
+                    field -> appView.getKeepInfo(field).isValuePropagationAllowed(appView, field));
+            return false;
+          }
+          fields.forEach(liveDefaultValueConsumer);
+          return true;
+        });
+
+    // We analyze all allocations of the classes that declare one of the given fields.
+    ThreadUtils.processMethods(
+        appView,
+        method ->
+            analyzeNewInstanceInstructionsInMethod(
+                nonFinalInstanceFields, liveDefaultValueConsumer, method),
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private void analyzeNewInstanceInstructionsInMethod(
+      Map<DexType, ProgramFieldSet> nonFinalInstanceFields,
+      Consumer<ProgramField> liveDefaultValueConsumer,
+      ProgramMethod method) {
+    if (!maybeHasNewInstanceThatMatches(method, nonFinalInstanceFields::containsKey)) {
+      return;
     }
+    IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
+    for (NewInstance newInstance : code.<NewInstance>instructions(Instruction::isNewInstance)) {
+      ProgramFieldSet fieldsOfInterest = nonFinalInstanceFields.get(newInstance.getType());
+      if (fieldsOfInterest == null) {
+        continue;
+      }
+      FieldReadBeforeWriteDfsAnalysis analysis =
+          new FieldReadBeforeWriteDfsAnalysis(appView, code, fieldsOfInterest, newInstance) {
+
+            @Override
+            public AnalysisContinuation acceptFieldMaybeReadBeforeWrite(ProgramField field) {
+              // Remove this field from the `fieldsOfInterest`, so that we do not spend more time
+              // analyzing it.
+              if (fieldsOfInterest.remove(field)) {
+                liveDefaultValueConsumer.accept(field);
+              }
+              return AnalysisContinuation.abortIf(fieldsOfInterest.isEmpty());
+            }
+          };
+      analysis.run();
+      if (fieldsOfInterest.isEmpty()) {
+        nonFinalInstanceFields.remove(newInstance.getType());
+      }
+    }
+  }
+
+  private boolean maybeHasNewInstanceThatMatches(
+      ProgramMethod method, Predicate<DexType> predicate) {
+    Code code = method.getDefinition().getCode();
+    if (code == null || code.isSharedCodeObject()) {
+      return false;
+    }
+    if (code.isLirCode()) {
+      return code.asLirCode()
+          .hasConstantItemThatMatches(
+              constant -> constant instanceof DexType && predicate.test((DexType) constant));
+    }
+    assert appView.isCfByteCodePassThrough(method);
+    assert code.isCfCode();
+    return method.registerCodeReferencesWithResult(
+        new DefaultUseRegistryWithResult<>(appView, method, false) {
+
+          @Override
+          public void registerNewInstance(DexType type) {
+            if (predicate.test(type)) {
+              setResult(true);
+            }
+          }
+        });
   }
 
   private Map<FlowGraph, Deque<FlowGraphNode>> updateFlowGraphs(
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
new file mode 100644
index 0000000..1fdfb27
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
@@ -0,0 +1,387 @@
+// 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.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
+import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.GOTO;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
+import static com.android.tools.r8.ir.code.Opcodes.RETURN;
+import static com.android.tools.r8.ir.code.Opcodes.THROW;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.ir.code.Assume;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockInstructionIterator;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.Goto;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.collections.ProgramFieldSet;
+
+/**
+ * Analysis that is given an allocation site (a {@link NewInstance} instruction) and a set of fields
+ * that belong to that newly allocated instance.
+ *
+ * <p>The analysis computes the subset of the given fields which are maybe read before they are
+ * written. The default value of these fields is potentially read, whereas the default value of the
+ * complement field set are guaranteed to never be read.
+ *
+ * <p>The analysis works by exploring all possible paths starting from the given allocation site to
+ * the normal and the exceptional exits of the method, keeping track of which fields are definitely
+ * written before they are read and which fields have maybe been read.
+ */
+public abstract class FieldReadBeforeWriteDfsAnalysis extends FieldReadBeforeWriteDfsAnalysisState {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final IRCode code;
+  private final DexItemFactory dexItemFactory;
+  // The set of fields to consider. Note that this is a concurrent set and that the caller may
+  // concurrently remove fields from the set. This may happen if we concurrently find a
+  // read-before-write of one of the fields.
+  private final ProgramFieldSet fields;
+  private final WorkList<WorkItem> worklist = WorkList.newIdentityWorkList();
+
+  private final FieldReadBeforeWriteDfsAnalysis self = this;
+
+  public FieldReadBeforeWriteDfsAnalysis(
+      AppView<AppInfoWithLiveness> appView,
+      IRCode code,
+      ProgramFieldSet fields,
+      NewInstance newInstance) {
+    super(newInstance);
+    this.appView = appView;
+    this.code = code;
+    this.dexItemFactory = appView.dexItemFactory();
+    this.fields = fields;
+  }
+
+  // Returns ABORT if all fields of interest are now maybe-read-before-written.
+  // Otherwise returns CONTINUE.
+  public abstract AnalysisContinuation acceptFieldMaybeReadBeforeWrite(ProgramField field);
+
+  public void run() {
+    worklist.addIfNotSeen(new InitialWorkItem());
+    worklist.run(WorkItem::process);
+  }
+
+  public enum AnalysisContinuation {
+    // Signals to abort the analysis completely (i.e., to break out of the DFS). This is used when
+    // we've reported all fields as being maybe read before written.
+    ABORT,
+    // Signals to continue the current DFS.
+    CONTINUE,
+    // Signals that all fields have been written before they are read on the current program path,
+    // meaning that the algorithm does not need to explore any further. The algorithm should instead
+    // backtrack and explore any other program paths.
+    BREAK;
+
+    static AnalysisContinuation abortIf(boolean condition) {
+      if (condition) {
+        return ABORT;
+      }
+      return CONTINUE;
+    }
+
+    boolean isAbort() {
+      return this == ABORT;
+    }
+
+    boolean isAbortOrContinue() {
+      return isAbort() || isContinue();
+    }
+
+    boolean isBreak() {
+      return this == BREAK;
+    }
+
+    boolean isContinue() {
+      return this == CONTINUE;
+    }
+
+    TraversalContinuation<?, ?> toTraversalContinuation() {
+      assert isAbortOrContinue();
+      return TraversalContinuation.breakIf(isAbort());
+    }
+  }
+
+  abstract class WorkItem {
+
+    abstract TraversalContinuation<?, ?> process();
+
+    void applyPhis(BasicBlock block) {
+      // TODO(b/339210038): When adding support for non-linear control flow, we need to implement
+      //  backtracking of this (i.e., we should remove the out value from the object aliases again).
+      for (Phi phi : block.getPhis()) {
+        if (phi.hasOperandThatMatches(self::isMaybeInstance)) {
+          addInstanceAlias(phi);
+        }
+      }
+    }
+
+    AnalysisContinuation applyInstructions(BasicBlockInstructionIterator instructionIterator) {
+      while (instructionIterator.hasNext()) {
+        Instruction instruction = instructionIterator.next();
+        assert !instruction.hasOutValue() || !isMaybeInstance(instruction.outValue());
+        AnalysisContinuation continuation;
+        // TODO(b/339210038): Extend this to many other instructions, such as ConstClass,
+        //  InstanceOf, *Binop, etc.
+        switch (instruction.opcode()) {
+          case ASSUME:
+            continuation = applyAssumeInstruction(instruction.asAssume());
+            break;
+          case CHECK_CAST:
+            continuation = applyCheckCastInstruction(instruction.asCheckCast());
+            break;
+          case CONST_NUMBER:
+            continuation = applyConstNumber(instruction.asConstNumber());
+            break;
+          case CONST_STRING:
+            continuation = applyConstString(instruction.asConstString());
+            break;
+          case GOTO:
+            continuation = applyGotoInstruction(instruction.asGoto());
+            break;
+          case INSTANCE_PUT:
+            continuation = applyInstancePut(instruction.asInstancePut());
+            break;
+          case INVOKE_DIRECT:
+          case INVOKE_INTERFACE:
+          case INVOKE_STATIC:
+          case INVOKE_SUPER:
+          case INVOKE_VIRTUAL:
+            continuation = applyInvokeMethodInstruction(instruction.asInvokeMethod());
+            break;
+          case RETURN:
+            continuation = applyReturnInstruction(instruction.asReturn());
+            break;
+          case THROW:
+            continuation = applyThrowInstruction(instruction.asThrow());
+            break;
+          default:
+            continuation = applyUnhandledInstruction();
+            break;
+        }
+        if (continuation.isAbort()) {
+          return continuation;
+        }
+        if (continuation.isBreak()) {
+          break;
+        }
+      }
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    // TODO(b/339210038): When adding support for non-linear control flow, we need to implement
+    //  backtracking of this (i.e., we should remove the out value from the object aliases again).
+    private AnalysisContinuation applyAssumeInstruction(Assume assume) {
+      if (isMaybeInstance(assume.src())) {
+        addInstanceAlias(assume.outValue());
+      }
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    // TODO(b/339210038): When adding support for non-linear control flow, we need to implement
+    //  backtracking of this (i.e., we should remove the out value from the object aliases again).
+    private AnalysisContinuation applyCheckCastInstruction(CheckCast checkCast) {
+      if (isMaybeInstance(checkCast.object())) {
+        addInstanceAlias(checkCast.outValue());
+      }
+      // If the instance has escaped to the heap and this check-cast instruction throws, then it is
+      // possible that the instance is retrieved from the heap and all fields are read.
+      return markRemainingFieldsAsMaybeReadBeforeWrittenIfInstanceIsEscaped();
+    }
+
+    private AnalysisContinuation applyConstNumber(ConstNumber unusedConstNumber) {
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    private AnalysisContinuation applyConstString(ConstString unusedConstString) {
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    private AnalysisContinuation applyGotoInstruction(Goto gotoInstruction) {
+      BasicBlock targetBlock = gotoInstruction.getTarget();
+      if (isBlockOnStack(targetBlock)) {
+        // Bail out in case of cycles.
+        return markRemainingFieldsAsMaybeReadBeforeWritten();
+      } else {
+        // Continue exploration into the successor block.
+        worklist.addIgnoringSeenSet(new ProcessBlockWorkItem(targetBlock));
+        return AnalysisContinuation.CONTINUE;
+      }
+    }
+
+    private AnalysisContinuation applyInstancePut(InstancePut instancePut) {
+      // If the instance has escaped and this instance-put instruction can throw, then the program
+      // can get the instance from the heap and read any field. Give up in this case.
+      if (isEscaped() && instancePut.instructionInstanceCanThrow(appView, code.context())) {
+        return markRemainingFieldsAsMaybeReadBeforeWritten();
+      }
+
+      // Record if this is a definite write to one of the fields of interest.
+      if (isDefinitelyInstance(instancePut.object())) {
+        ProgramField resolvedField =
+            instancePut.resolveField(appView, code.context()).getProgramField();
+        if (resolvedField != null && fields.contains(resolvedField)) {
+          addWrittenBeforeRead(resolvedField);
+        }
+
+        // If all fields of interest are written before read, then stop the exploration of the
+        // current program path (but continue to explore any program paths from previous unexplored
+        // branches).
+        if (fields.allMatch(self::isWrittenBeforeRead)) {
+          return AnalysisContinuation.BREAK;
+        }
+      }
+
+      // Record if the instance has escaped as a result of this instance-put.
+      if (!isEscaped() && isMaybeInstance(instancePut.value())) {
+        setEscaped(instancePut);
+      }
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    private AnalysisContinuation applyInvokeMethodInstruction(InvokeMethod invoke) {
+      // Allow calls to java.lang.Object.<init>().
+      // TODO(b/339210038): Generalize this to other constructors.
+      if (invoke.isInvokeConstructor(dexItemFactory)
+          && isDefinitelyInstance(invoke.getFirstArgument())) {
+        DexClassAndMethod resolvedMethod =
+            invoke.resolveMethod(appView, code.context()).getResolutionPair();
+        if (resolvedMethod != null
+            && resolvedMethod
+                .getReference()
+                .isIdenticalTo(dexItemFactory.objectMembers.constructor)) {
+          return AnalysisContinuation.CONTINUE;
+        }
+      }
+
+      // Conservatively treat calls as reading any field if the receiver has escaped or is escaping.
+      if (!isEscaped()
+          && invoke.hasInValueThatMatches(self::isMaybeInstance)
+          && invoke.instructionMayHaveSideEffects(appView, code.context())) {
+        setEscaped(invoke);
+      }
+
+      if (isEscaped()) {
+        return markRemainingFieldsAsMaybeReadBeforeWritten();
+      }
+
+      // Otherwise, this is a call to a method where none of the arguments is an alias of the
+      // instance, and the instance has not escaped. Therefore, this call cannot read any of fields
+      // from the instance.
+      return AnalysisContinuation.CONTINUE;
+    }
+
+    private AnalysisContinuation applyReturnInstruction(Return unusedReturnInstruction) {
+      return markRemainingFieldsAsMaybeReadBeforeWritten();
+    }
+
+    private AnalysisContinuation applyThrowInstruction(Throw unusedThrowInstruction) {
+      return markRemainingFieldsAsMaybeReadBeforeWrittenIfInstanceIsEscaped();
+    }
+
+    private AnalysisContinuation applyUnhandledInstruction() {
+      return markRemainingFieldsAsMaybeReadBeforeWritten();
+    }
+
+    AnalysisContinuation markRemainingFieldsAsMaybeReadBeforeWritten() {
+      for (ProgramField field : fields) {
+        if (!isWrittenBeforeRead(field)) {
+          AnalysisContinuation continuation = acceptFieldMaybeReadBeforeWrite(field);
+          assert continuation.isAbortOrContinue();
+          if (continuation.isAbort()) {
+            return continuation;
+          }
+        }
+      }
+      // At this point we could also CONTINUE, but we check if the fields of interest have become
+      // empty as a result of concurrent modification.
+      return AnalysisContinuation.abortIf(fields.isEmpty());
+    }
+
+    AnalysisContinuation markRemainingFieldsAsMaybeReadBeforeWrittenIfInstanceIsEscaped() {
+      if (isEscaped()) {
+        return markRemainingFieldsAsMaybeReadBeforeWritten();
+      }
+      return AnalysisContinuation.CONTINUE;
+    }
+  }
+
+  class InitialWorkItem extends WorkItem {
+
+    @Override
+    TraversalContinuation<?, ?> process() {
+      // We start the analysis from the unique constructor invoke instead of from the NewInstance
+      // instruction, since no instructions before the constructor call can read any fields from the
+      // uninitialized this.
+      // TODO(b/339210038): In principle it may be possible for the NewInstance value to flow into a
+      //  phi before the unique constructor invoke. If this happens we would not record the phi as
+      //  an alias when starting the analysis from the invoke-direct.
+      InvokeDirect uniqueConstructorInvoke =
+          getNewInstance().getUniqueConstructorInvoke(dexItemFactory);
+      if (uniqueConstructorInvoke == null) {
+        return markRemainingFieldsAsMaybeReadBeforeWritten().toTraversalContinuation();
+      }
+      BasicBlock block = uniqueConstructorInvoke.getBlock();
+      // TODO(b/339210038): Maybe allow exceptional control flow.
+      if (block.hasCatchHandlers()) {
+        return markRemainingFieldsAsMaybeReadBeforeWritten().toTraversalContinuation();
+      }
+      addBlockToStack(block);
+      addInstanceAlias(getNewInstance().outValue());
+      BasicBlockInstructionIterator instructionIterator = block.iterator(uniqueConstructorInvoke);
+      // Start the analysis from the invoke-direct instruction. This is important if we can tell
+      // that the constructor definitely writes some fields.
+      instructionIterator.previous();
+      return applyInstructions(instructionIterator).toTraversalContinuation();
+    }
+  }
+
+  class ProcessBlockWorkItem extends WorkItem {
+
+    private final BasicBlock block;
+
+    ProcessBlockWorkItem(BasicBlock block) {
+      this.block = block;
+    }
+
+    @Override
+    TraversalContinuation<?, ?> process() {
+      // TODO(b/339210038): Maybe allow exceptional control flow.
+      if (block.hasCatchHandlers()) {
+        return TraversalContinuation.breakIf(
+            markRemainingFieldsAsMaybeReadBeforeWritten().isAbort());
+      }
+      addBlockToStack(block);
+      applyPhis(block);
+      AnalysisContinuation continuation = applyInstructions(block.iterator());
+      assert continuation.isAbortOrContinue();
+      return TraversalContinuation.breakIf(continuation.isAbort());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysisState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysisState.java
new file mode 100644
index 0000000..7e24c03
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysisState.java
@@ -0,0 +1,84 @@
+// 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.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.collections.ProgramFieldSet;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+/**
+ * The state we track during the field-maybe-read-before-write/field-never-read-before-written
+ * analysis.
+ */
+public class FieldReadBeforeWriteDfsAnalysisState {
+
+  // The current allocation we are analyzing.
+  private final NewInstance newInstance;
+
+  // The first instruction on the current program path starting from the `newInstance` instruction
+  // from which the `newInstance` value escapes.
+  private Instruction escape = null;
+
+  // The set of values that *may* be aliases of the `newInstance` value.
+  private final Set<Value> instanceAliases = Sets.newIdentityHashSet();
+
+  // The set of blocks on the current program path.
+  private final Set<BasicBlock> stack = Sets.newIdentityHashSet();
+
+  // The set of fields that are guaranteed to be written before they are read on the current program
+  // path.
+  private final ProgramFieldSet writtenBeforeRead = ProgramFieldSet.create();
+
+  FieldReadBeforeWriteDfsAnalysisState(NewInstance newInstance) {
+    this.newInstance = newInstance;
+  }
+
+  void addInstanceAlias(Value instanceAlias) {
+    boolean changed = instanceAliases.add(instanceAlias);
+    assert changed;
+  }
+
+  void addBlockToStack(BasicBlock block) {
+    boolean changed = stack.add(block);
+    assert changed;
+  }
+
+  void addWrittenBeforeRead(ProgramField field) {
+    writtenBeforeRead.add(field);
+  }
+
+  NewInstance getNewInstance() {
+    return newInstance;
+  }
+
+  boolean isBlockOnStack(BasicBlock block) {
+    return stack.contains(block);
+  }
+
+  boolean isEscaped() {
+    return escape != null;
+  }
+
+  boolean isDefinitelyInstance(Value value) {
+    return value.getAliasedValue() == newInstance.outValue();
+  }
+
+  boolean isMaybeInstance(Value value) {
+    return instanceAliases.contains(value);
+  }
+
+  boolean isWrittenBeforeRead(ProgramField field) {
+    return writtenBeforeRead.contains(field);
+  }
+
+  void setEscaped(Instruction escape) {
+    assert !isEscaped();
+    this.escape = escape;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
index d62328d..81e3424 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
@@ -46,7 +46,7 @@
   final AppView<AppInfoWithLiveness> appView;
   final Set<DexProgramClass> classesWithSingleCallerInlinedInstanceInitializers;
   final IRConverter converter;
-  final FieldStateCollection fieldStates;
+  protected final FieldStateCollection fieldStates;
   final MethodStateCollectionByReference methodStates;
 
   public InFlowPropagator(
@@ -113,12 +113,15 @@
 
   private Map<FlowGraph, Deque<FlowGraphNode>> includeDefaultValuesInFieldStates(
       List<FlowGraph> flowGraphs, ExecutorService executorService) throws ExecutionException {
-    DefaultFieldValueJoiner joiner =
-        new DefaultFieldValueJoiner(
-            appView, classesWithSingleCallerInlinedInstanceInitializers, fieldStates, flowGraphs);
+    DefaultFieldValueJoiner joiner = createDefaultFieldValueJoiner(flowGraphs);
     return joiner.joinDefaultFieldValuesForFieldsWithReadBeforeWrite(executorService);
   }
 
+  protected DefaultFieldValueJoiner createDefaultFieldValueJoiner(List<FlowGraph> flowGraphs) {
+    return new DefaultFieldValueJoiner(
+        appView, classesWithSingleCallerInlinedInstanceInitializers, fieldStates, flowGraphs);
+  }
+
   private void processFlowGraphs(List<FlowGraph> flowGraphs, ExecutorService executorService)
       throws ExecutionException {
     ThreadUtils.processItems(
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
index 8d7498c..22ed377 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
@@ -33,6 +34,8 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.DefaultFieldValueJoiner;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.FlowGraph;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.IterableUtils;
@@ -42,6 +45,8 @@
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -92,7 +97,22 @@
 
     InFlowPropagator inFlowPropagator =
         new InFlowPropagator(
-            appView, null, converter, codeScanner.getFieldStates(), codeScanner.getMethodStates());
+            appView, null, converter, codeScanner.getFieldStates(), codeScanner.getMethodStates()) {
+
+          @Override
+          protected DefaultFieldValueJoiner createDefaultFieldValueJoiner(
+              List<FlowGraph> flowGraphs) {
+            return new DefaultFieldValueJoiner(appView, null, fieldStates, flowGraphs) {
+
+              @Override
+              protected Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
+                // We do not rely on the optimization of any fields in the Composable optimization
+                // pass.
+                return Collections.emptyMap();
+              }
+            };
+          }
+        };
     inFlowPropagator.run(executorService);
 
     ArgumentPropagatorOptimizationInfoPopulator optimizationInfoPopulator =
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index d4fd18a..1766f40 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2462,6 +2462,7 @@
   private void enqueueHolderWithDependentInstanceConstructor(
       ProgramMethod instanceInitializer, Set<ProguardKeepRuleBase> reasons) {
     DexProgramClass holder = instanceInitializer.getHolder();
+    applyMinimumKeepInfoWhenLive(holder, KeepClassInfo.newEmptyJoiner().disallowOptimization());
     enqueueKeepRuleInstantiatedType(holder, reasons, instanceInitializer.getDefinition());
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 8cffa28..fc42207 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -846,8 +846,7 @@
           .setStart(start);
       parseRuleTypeAndModifiers(keepRuleBuilder);
       parseClassSpec(keepRuleBuilder);
-      if (configurationBuilder.isForceProguardCompatibility()
-          || options.isForceEmptyMemberRulesToDefaultInitRuleConversionEnabled()) {
+      if (options.isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder)) {
         if (keepRuleBuilder.getMemberRules().isEmpty()
             && keepRuleBuilder.getKeepRuleType()
                 != ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
index acdd37c..157be68 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
@@ -8,30 +8,36 @@
 
 public class ProguardConfigurationParserOptions {
 
+  private final boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
   private final boolean enableExperimentalCheckEnumUnboxed;
   private final boolean enableExperimentalConvertCheckNotNull;
   private final boolean enableExperimentalWhyAreYouNotInlining;
   private final boolean enableTestingOptions;
-  private final boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
 
   ProguardConfigurationParserOptions(
+      boolean enableEmptyMemberRulesToDefaultInitRuleConversion,
       boolean enableExperimentalCheckEnumUnboxed,
       boolean enableExperimentalConvertCheckNotNull,
       boolean enableExperimentalWhyAreYouNotInlining,
-      boolean enableTestingOptions,
-      boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion) {
+      boolean enableTestingOptions) {
     this.enableExperimentalCheckEnumUnboxed = enableExperimentalCheckEnumUnboxed;
     this.enableExperimentalConvertCheckNotNull = enableExperimentalConvertCheckNotNull;
     this.enableExperimentalWhyAreYouNotInlining = enableExperimentalWhyAreYouNotInlining;
     this.enableTestingOptions = enableTestingOptions;
-    this.forceEnableEmptyMemberRulesToDefaultInitRuleConversion =
-        forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
+    this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+        enableEmptyMemberRulesToDefaultInitRuleConversion;
   }
 
   public static Builder builder() {
     return new Builder();
   }
 
+  public boolean isEmptyMemberRulesToDefaultInitRuleConversionEnabled(
+      ProguardConfiguration.Builder configurationBuilder) {
+    return enableEmptyMemberRulesToDefaultInitRuleConversion
+        || configurationBuilder.isForceProguardCompatibility();
+  }
+
   public boolean isExperimentalCheckEnumUnboxedEnabled() {
     return enableExperimentalCheckEnumUnboxed;
   }
@@ -44,23 +50,22 @@
     return enableExperimentalWhyAreYouNotInlining;
   }
 
-  public boolean isForceEmptyMemberRulesToDefaultInitRuleConversionEnabled() {
-    return forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
-  }
-
   public boolean isTestingOptionsEnabled() {
     return enableTestingOptions;
   }
 
   public static class Builder {
 
+    private boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
     private boolean enableExperimentalCheckEnumUnboxed;
     private boolean enableExperimentalConvertCheckNotNull;
     private boolean enableExperimentalWhyAreYouNotInlining;
     private boolean enableTestingOptions;
-    private boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
 
     public Builder readEnvironment() {
+      enableEmptyMemberRulesToDefaultInitRuleConversion =
+          parseSystemPropertyOrDefault(
+              "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion", false);
       enableExperimentalCheckEnumUnboxed =
           parseSystemPropertyOrDefault(
               "com.android.tools.r8.experimental.enablecheckenumunboxed", false);
@@ -72,9 +77,13 @@
               "com.android.tools.r8.experimental.enablewhyareyounotinlining", false);
       enableTestingOptions =
           parseSystemPropertyOrDefault("com.android.tools.r8.allowTestProguardOptions", false);
-      forceEnableEmptyMemberRulesToDefaultInitRuleConversion =
-          parseSystemPropertyOrDefault(
-              "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion", false);
+      return this;
+    }
+
+    public Builder setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+      this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+          enableEmptyMemberRulesToDefaultInitRuleConversion;
       return this;
     }
 
@@ -103,11 +112,11 @@
 
     public ProguardConfigurationParserOptions build() {
       return new ProguardConfigurationParserOptions(
+          enableEmptyMemberRulesToDefaultInitRuleConversion,
           enableExperimentalCheckEnumUnboxed,
           enableExperimentalConvertCheckNotNull,
           enableExperimentalWhyAreYouNotInlining,
-          enableTestingOptions,
-          forceEnableEmptyMemberRulesToDefaultInitRuleConversion);
+          enableTestingOptions);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
index 052c9ad..c45a64d 100644
--- a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
@@ -472,7 +472,7 @@
           check -> {
             preconditionClassesCount = 0;
             preconditionMembersCount = 0;
-            consequences.add(defineItemPattern(check.getItemPattern()));
+            consequences.add(defineBindingReference(check.getItemReference()));
           });
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
index dfe3f51..9587d94 100644
--- a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
 import com.android.tools.r8.keepanno.ast.KeepArrayTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepClassPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
@@ -296,8 +297,13 @@
         () -> true,
         p -> matchesPrimitiveType(type, p),
         p -> matchesArrayType(type, p, appInfo),
-        p -> matchesClassType(type, p),
-        p -> matchesInstanceOfPattern(type, p, appInfo));
+        p -> matchesClassPattern(type, p, appInfo));
+  }
+
+  public boolean matchesClassPattern(
+      DexType type, KeepClassPattern pattern, AppInfoWithClassHierarchy appInfo) {
+    return matchesClassType(type, pattern.getClassNamePattern())
+        && matchesInstanceOfPattern(type, pattern.getInstanceOfPattern(), appInfo);
   }
 
   public boolean matchesClassType(DexType type, KeepQualifiedClassNamePattern pattern) {
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 14efa1d..8cee41d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2053,8 +2053,12 @@
                   try {
                     ZipUtils.iter(
                         Paths.get(lib),
-                        (entry, input) -> consumer.accept(extractClassDescriptor(input)));
-                  } catch (IOException e) {
+                        (entry, input) -> {
+                          if (ZipUtils.isClassFile(entry.getName())) {
+                            consumer.accept(extractClassDescriptor(input));
+                          }
+                        });
+                  } catch (Exception e) {
                     throw new CompilationError("Failed to read extension library " + lib, e);
                   }
                 });
@@ -3284,4 +3288,11 @@
   public boolean canInitNewInstanceUsingSuperclassConstructor() {
     return isGeneratingDex() && minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.L);
   }
+
+  // b/302826300 Dalvik can vms gives hard verification errors when we rebind program types to
+  // library types that are superclasses of unavailable classes on the specific api level.
+  // Instead, we should always rebind to just the first library class.
+  public boolean canHaveDalvikVerifyErrorOnVirtualInvokeWithMissingClasses() {
+    return canHaveBugPresentUntilExclusive(AndroidApiLevel.L);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
index c778df3..891593a 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
@@ -8,14 +8,15 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramField;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 public class ProgramFieldSet implements Iterable<ProgramField> {
@@ -54,6 +55,10 @@
     backing.putAll(fields.backing);
   }
 
+  public boolean allMatch(Predicate<? super ProgramField> predicate) {
+    return Iterables.all(this, predicate);
+  }
+
   public boolean createAndAdd(DexProgramClass clazz, DexEncodedField definition) {
     return add(new ProgramField(clazz, definition));
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java
index 7d38449..4bb94ee 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFieldResolutionChangesPolicy.java
@@ -42,8 +42,12 @@
       FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
       Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
       for (DexType interfaceType : target.getInterfaces()) {
-        DexClass clazz = appView.definitionFor(interfaceType);
-        for (DexEncodedField staticField : clazz.staticFields()) {
+        DexClass itf = appView.definitionFor(interfaceType);
+        if (itf == null) {
+          // See b/353475583.
+          return true;
+        }
+        for (DexEncodedField staticField : itf.staticFields()) {
           staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
         }
       }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
index 0d912b3..fa46f4d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -35,7 +36,9 @@
         .assertSuccessWithOutputLines("foo", "hello", "5", "foobar")
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(C.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(C.class),
+                  isAbsentIf(parameters.canInitNewInstanceUsingSuperclassConstructor()));
               assertThat(codeInspector.clazz(D.class), isPresent());
               assertThat(codeInspector.clazz(E.class), isPresent());
             });
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/Regress343136777Test.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/Regress343136777Test.java
index 2adf213..1b8c4ce 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ifs/Regress343136777Test.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/Regress343136777Test.java
@@ -4,9 +4,6 @@
 
 package com.android.tools.r8.ir.optimize.ifs;
 
-import static org.junit.Assert.assertThrows;
-
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -45,32 +42,24 @@
   @Test
   public void testD8Release() throws Exception {
     parameters.assumeDexRuntime();
-    // TODO(b/343136777: This should not fail.
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForD8(parameters.getBackend())
-                .addProgramClasses(TestClass.class)
-                .setMinApi(parameters)
-                .release()
-                .compile()
-                .run(parameters.getRuntime(), TestClass.class)
-                .assertSuccessWithOutput(EXPECTED_OUTPUT));
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .setMinApi(parameters)
+        .release()
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/343136777: This should not fail.
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForR8(parameters.getBackend())
-                .addProgramClasses(TestClass.class)
-                .setMinApi(parameters)
-                .addKeepMainRule(TestClass.class)
-                .compile()
-                .run(parameters.getRuntime(), TestClass.class)
-                .assertSuccessWithOutput(EXPECTED_OUTPUT));
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .setMinApi(parameters)
+        .addKeepMainRule(TestClass.class)
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldWithConstructorInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldWithConstructorInliningTest.java
index f952fa9..142d565 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldWithConstructorInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldWithConstructorInliningTest.java
@@ -3,10 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.membervaluepropagation;
 
+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.isPresentIf;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.TestBase;
@@ -42,17 +42,11 @@
         .inspect(
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
-              // TODO(b/339210038): Should always be absent.
-              assertThat(
-                  aClassSubject,
-                  isPresentIf(parameters.canInitNewInstanceUsingSuperclassConstructor()));
+              assertThat(aClassSubject, isAbsent());
 
               MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
               assertThat(mainMethodSubject, isPresent());
-              // TODO(b/339210038): Should always contain 42.
-              assertEquals(
-                  parameters.canInitNewInstanceUsingSuperclassConstructor(),
-                  mainMethodSubject.streamInstructions().noneMatch(i -> i.isConstNumber(42)));
+              assertTrue(mainMethodSubject.streamInstructions().anyMatch(i -> i.isConstNumber(42)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("42");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/LiveDefaultFieldValueOfReflectivelyInstantiatedClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/LiveDefaultFieldValueOfReflectivelyInstantiatedClassTest.java
new file mode 100644
index 0000000..358227d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/LiveDefaultFieldValueOfReflectivelyInstantiatedClassTest.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.membervaluepropagation;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class LiveDefaultFieldValueOfReflectivelyInstantiatedClassTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndDefaultConstructor(BooleanBox.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("false");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) throws Exception {
+      Class<?> clazz = System.currentTimeMillis() > 0 ? BooleanBox.class : Object.class;
+      BooleanBox box = (BooleanBox) clazz.getDeclaredConstructor().newInstance();
+      System.out.println(box.value);
+      box.set();
+    }
+  }
+
+  static class BooleanBox {
+
+    boolean value;
+
+    BooleanBox() {}
+
+    @NeverInline
+    void set() {
+      value = true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantArgumentMultipleConstructorsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantArgumentMultipleConstructorsTest.java
index fca4473..862fbc9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantArgumentMultipleConstructorsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantArgumentMultipleConstructorsTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.membervaluepropagation.fields;
 
+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.isPresentIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -18,21 +18,20 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class FieldInitializedByConstantArgumentMultipleConstructorsTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public FieldInitializedByConstantArgumentMultipleConstructorsTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
@@ -51,10 +50,7 @@
     ClassSubject testClassSubject = inspector.clazz(TestClass.class);
     assertThat(testClassSubject, isPresent());
     assertThat(testClassSubject.uniqueMethodWithOriginalName("live"), isPresent());
-    // TODO(b/280275115): Constructor inlining regresses instance field value analysis.
-    assertThat(
-        testClassSubject.uniqueMethodWithOriginalName("dead"),
-        isPresentIf(parameters.canInitNewInstanceUsingSuperclassConstructor()));
+    assertThat(testClassSubject.uniqueMethodWithOriginalName("dead"), isAbsent());
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index 516cccd..cf7edc4 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -70,6 +70,11 @@
 
   public abstract KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException;
 
+  public final KeepAnnoTestBuilder addProgramClasses(Class<?>... programClasses)
+      throws IOException {
+    return addProgramClasses(Arrays.asList(programClasses));
+  }
+
   public abstract KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses)
       throws IOException;
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionMemberInlingingAllowed.java b/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionMemberInlingingAllowed.java
new file mode 100644
index 0000000..b5d6f00
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionMemberInlingingAllowed.java
@@ -0,0 +1,86 @@
+// 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.isPresentAndNotRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+@RunWith(Parameterized.class)
+public class UsedByReflectionMemberInlingingAllowed extends KeepAnnoTestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world!");
+
+  @Parameter public KeepAnnoParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<KeepAnnoParameters> data() {
+    return createParameters(
+        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForKeepAnno(parameters)
+        .addProgramClasses(getInputClasses())
+        .setExcludedOuterClass(getClass())
+        .run(TestClass.class)
+        .inspect(
+            codeInspector -> {
+              ClassSubject clazz = codeInspector.clazz(TestClass.class);
+              assertThat(clazz, isPresent());
+              MethodSubject main = clazz.uniqueMethodWithOriginalName("main");
+              assertThat(main, isPresentAndNotRenamed());
+              if (parameters.isShrinker()) {
+                // We should just have the void return left in main.
+                assertTrue(main.streamInstructions().allMatch(InstructionSubject::isReturnVoid));
+              }
+              assertThat(
+                  clazz.uniqueMethodWithOriginalName("isFalse"),
+                  parameters.isShrinker() ? isAbsent() : isPresent());
+              assertThat(
+                  clazz.uniqueFieldWithOriginalName("IS_FALSE"),
+                  parameters.isShrinker() ? isAbsent() : isPresent());
+            })
+        .assertSuccessWithEmptyOutput();
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class);
+  }
+
+  static class TestClass {
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_MEMBERS)
+    public static void main(String[] args) {
+      if (IS_FALSE) {
+        System.out.println("IS_FALSE");
+      }
+      if (isFalse()) {
+        System.out.println("isFalse");
+      }
+    }
+
+    private static boolean isFalse() {
+      return false;
+    }
+
+    private static final boolean IS_FALSE = false;
+  }
+}
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 b2beb2e..3c46771 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
@@ -4,6 +4,7 @@
 package com.android.tools.r8.keepanno.ast;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -228,6 +229,38 @@
         extract(edge));
   }
 
+  @Test
+  public void testKeepZeroOrMorePackage() {
+    BindingsHelper helper = new BindingsHelper();
+    KeepClassBindingReference clazz =
+        helper.freshClassBinding(
+            KeepClassItemPattern.builder()
+                .setClassNamePattern(
+                    KeepQualifiedClassNamePattern.builder()
+                        .setPackagePattern(
+                            KeepPackagePattern.builder()
+                                .append(KeepPackageComponentPattern.exact("a"))
+                                .append(KeepPackageComponentPattern.zeroOrMore())
+                                .append(KeepPackageComponentPattern.exact("b"))
+                                .build())
+                        .build())
+                .build());
+    KeepEdge edge =
+        KeepEdge.builder()
+            .setConsequences(KeepConsequences.builder().addTarget(target(clazz)).build())
+            .setBindings(helper.build())
+            .build();
+    // Extraction fails due to the package structure.
+    try {
+      extract(edge);
+    } catch (KeepEdgeException e) {
+      if (e.getMessage().contains("Unsupported use of zero-or-more package pattern")) {
+        return;
+      }
+    }
+    fail("Expected extraction to fail");
+  }
+
   private KeepClassBindingReference classItemBinding(KeepBindingSymbol bindingName) {
     return KeepBindingReference.forClass(bindingName);
   }
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/GenericSignaturePrinter.java b/src/test/java/com/android/tools/r8/keepanno/doctests/GenericSignaturePrinter.java
new file mode 100644
index 0000000..c81a27f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/GenericSignaturePrinter.java
@@ -0,0 +1,87 @@
+// 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.doctests;
+
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import java.lang.reflect.Type;
+
+/* INCLUDE DOC: GenericSignaturePrinter
+
+Imagine we had code that is making use of the template parameters for implementations of a
+generic interface. The code below assumes direct implementations of the `WrappedValue` interface
+and simply prints the type parameter used.
+
+Since we are reflecting on the class structure of implementations of `WrappedValue` we need to
+keep it and any instance of it.
+
+We must also preserve the generic signatures of these classes. We add the
+`@KeepConstraint#GENERIC_SIGNATURE` constraint by using the `@KeepTarget#constraintAdditions`
+property. This ensures that the default constraints are still in place in addition to the
+constraint on generic signatures.
+
+INCLUDE END */
+
+// INCLUDE CODE: GenericSignaturePrinter
+public class GenericSignaturePrinter {
+
+  interface WrappedValue<T> {
+    T getValue();
+  }
+
+  @UsesReflection(
+      @KeepTarget(
+          instanceOfClassConstant = WrappedValue.class,
+          constraintAdditions = KeepConstraint.GENERIC_SIGNATURE))
+  public static void printSignature(WrappedValue<?> obj) {
+    Class<? extends WrappedValue> clazz = obj.getClass();
+    for (Type iface : clazz.getGenericInterfaces()) {
+      String typeName = iface.getTypeName();
+      String param = typeName.substring(typeName.lastIndexOf('<') + 1, typeName.lastIndexOf('>'));
+      System.out.println(clazz.getName() + " uses type " + param);
+    }
+  }
+
+  // INCLUDE END
+
+  static class MyBool implements WrappedValue<Boolean> {
+
+    private boolean value;
+
+    public MyBool(boolean value) {
+      this.value = value;
+    }
+
+    @Override
+    public Boolean getValue() {
+      return value;
+    }
+  }
+
+  static class MyString implements WrappedValue<String> {
+    private String value;
+
+    public MyString(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public String getValue() {
+      return value;
+    }
+  }
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      GenericSignaturePrinter.printSignature(new MyString("foo"));
+      GenericSignaturePrinter.printSignature(new MyBool(true));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/SignatureConstraintsDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/SignatureConstraintsDocumentationTest.java
new file mode 100644
index 0000000..2ab401c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/SignatureConstraintsDocumentationTest.java
@@ -0,0 +1,41 @@
+// 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.doctests;
+
+import com.android.tools.r8.keepanno.KeepAnnoParameters;
+import com.android.tools.r8.keepanno.KeepAnnoTestBase;
+import com.android.tools.r8.keepanno.doctests.GenericSignaturePrinter.TestClass;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+@RunWith(Parameterized.class)
+public class SignatureConstraintsDocumentationTest extends KeepAnnoTestBase {
+
+  static final String EXPECTED =
+      StringUtils.lines(
+          typeName(GenericSignaturePrinter.class) + "$MyString uses type java.lang.String",
+          typeName(GenericSignaturePrinter.class) + "$MyBool uses type java.lang.Boolean");
+
+  @Parameter public KeepAnnoParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<KeepAnnoParameters> data() {
+    return createParameters(
+        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForKeepAnno(parameters)
+        .addProgramClasses(GenericSignaturePrinter.class)
+        .addInnerClasses(GenericSignaturePrinter.class)
+        .run(TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
index 98a2f0d..95a8d5b 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -34,6 +34,7 @@
           EXPECTED_FIELD_EXAMPLE,
           EXPECTED_FIELD_EXAMPLE,
           EXPECTED_FIELD_EXAMPLE,
+          EXPECTED_FIELD_EXAMPLE,
           EXPECTED_FIELD_EXAMPLE);
 
   private final TestParameters parameters;
@@ -70,7 +71,12 @@
 
   public List<Class<?>> getExampleClasses() {
     return ImmutableList.of(
-        Example1.class, Example2.class, Example3.class, Example4.class, Example5.class);
+        Example1.class,
+        Example2.class,
+        Example2WithConstraints.class,
+        Example3.class,
+        Example4.class,
+        Example5.class);
   }
 
   static class Example1 {
@@ -105,12 +111,11 @@
     // INCLUDE CODE: UsesReflectionOnVirtualMethod
     public class MyHiddenMethodCaller {
 
-      @UsesReflection({
-        @KeepTarget(
-            instanceOfClassConstant = BaseClass.class,
-            methodName = "hiddenMethod",
-            methodParameters = {})
-      })
+      @UsesReflection(
+          @KeepTarget(
+              instanceOfClassConstant = BaseClass.class,
+              methodName = "hiddenMethod",
+              methodParameters = {}))
       public void callHiddenMethod(BaseClass base) throws Exception {
         base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
       }
@@ -142,22 +147,59 @@
     of the object.
 
     The `@KeepTarget` describes these field targets. Since the printing only cares about preserving
-    the fields, the `@KeepTarget#kind` is set to `@KeepItemKind#ONLY_FIELDS`. Also, since printing
-    the field names and values only requires looking up the field, printing its name and getting
-    its value the `@KeepTarget#constraints` are set to just `@KeepConstraint#LOOKUP`,
+    the fields, the `@KeepTarget#kind` is set to `@KeepItemKind#ONLY_FIELDS`.
+    INCLUDE END */
+
+    public
+    // INCLUDE CODE: UsesReflectionFieldPrinter
+    static class MyFieldValuePrinter {
+
+      @UsesReflection(
+          @KeepTarget(
+              instanceOfClassConstant = PrintableFieldInterface.class,
+              kind = KeepItemKind.ONLY_FIELDS))
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      new MyFieldValuePrinter().printFieldValues(new ClassWithFields());
+    }
+  }
+
+  static class Example2WithConstraints {
+
+    interface PrintableFieldInterface {}
+
+    static class ClassWithFields implements PrintableFieldInterface {
+      final int intField = 42;
+      String stringField = "Hello!";
+    }
+
+    /* INCLUDE DOC: UsesReflectionFieldPrinterWithConstraints
+    Let us revisit the example reflectively accessing the fields on a class.
+
+    Notice that printing the field names and values only requires looking up the field, printing
+    its name and getting its value. It does not require setting a new value on the field.
+    We can thus use a more restrictive set of constraints
+    by setting the `@KeepTarget#constraints` property to just `@KeepConstraint#LOOKUP`,
     `@KeepConstraint#NAME` and `@KeepConstraint#FIELD_GET`.
     INCLUDE END */
 
-    static
-    // INCLUDE CODE: UsesReflectionFieldPrinter
-    public class MyFieldValuePrinter {
+    public
+    // INCLUDE CODE: UsesReflectionFieldPrinterWithConstraints
+    static class MyFieldValuePrinter {
 
-      @UsesReflection({
-        @KeepTarget(
-            instanceOfClassConstant = PrintableFieldInterface.class,
-            kind = KeepItemKind.ONLY_FIELDS,
-            constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-      })
+      @UsesReflection(
+          @KeepTarget(
+              instanceOfClassConstant = PrintableFieldInterface.class,
+              kind = KeepItemKind.ONLY_FIELDS,
+              constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET}))
       public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
         for (Field field : objectWithFields.getClass().getDeclaredFields()) {
           System.out.println(field.getName() + " = " + field.get(objectWithFields));
@@ -182,9 +224,6 @@
     In this example, the `MyClassWithFields` is a class you are passing to the
     field-printing utility of the library. Since the library is reflectively accessing each field
     we annotate them with the `@UsedByReflection` annotation.
-
-    We could additionally add the `@UsedByReflection#constraints` property as we did previously.
-    We elide it here for brevity.
     INCLUDE END */
 
     static
@@ -220,17 +259,12 @@
     similar to the `@KeepTarget`. The `@UsedByReflection#kind` specifies that only the fields are
     used reflectively. In particular, the "field printer" example we are considering here does not
     make reflective assumptions about the holder class, so we should not constrain it.
-
-    To be more precise let's add the `@UsedByReflection#constraints` property now. This specifies
-    that the fields are looked up, their names are used/assumed and their values are read.
     INCLUDE END */
 
     static
     // INCLUDE CODE: UsedByReflectionFieldPrinterOnClass
-    @UsedByReflection(
-        kind = KeepItemKind.ONLY_FIELDS,
-        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-    public class MyClassWithFields implements PrintableFieldInterface {
+    @UsedByReflection(kind = KeepItemKind.ONLY_FIELDS) public class MyClassWithFields
+        implements PrintableFieldInterface {
       final int intField = 42;
       String stringField = "Hello!";
     }
@@ -275,9 +309,8 @@
               classConstant = FieldValuePrinterLibrary.class,
               methodName = "printFieldValues")
         },
-        kind = KeepItemKind.ONLY_FIELDS,
-        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-    public class MyClassWithFields implements PrintableFieldInterface {
+        kind = KeepItemKind.ONLY_FIELDS) public class MyClassWithFields
+        implements PrintableFieldInterface {
       final int intField = 42;
       String stringField = "Hello!";
     }
@@ -303,6 +336,7 @@
     public static void main(String[] args) throws Exception {
       Example1.run();
       Example2.run();
+      Example2WithConstraints.run();
       Example3.run();
       Example4.run();
       Example5.run();
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index 5a45f17..9b6d615 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -30,6 +30,7 @@
 
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
+import com.android.tools.r8.keepanno.doctests.GenericSignaturePrinter;
 import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
@@ -112,7 +113,8 @@
         UsesReflectionDocumentationTest.class,
         UsesReflectionAnnotationsDocumentationTest.class,
         ForApiDocumentationTest.class,
-        MainMethodsDocumentationTest.class);
+        MainMethodsDocumentationTest.class,
+        GenericSignaturePrinter.class);
   }
 
   private static String getPrefix(ClassReference annoType) {
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 e6d0cf9..0ad61b0 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
@@ -1620,7 +1620,7 @@
                     docEnumLink(KIND_ONLY_CLASS) + " if no member patterns are defined;",
                     docEnumLink(KIND_CLASS_AND_METHODS) + " if method patterns are defined;",
                     docEnumLink(KIND_CLASS_AND_FIELDS) + " if field patterns are defined;",
-                    docEnumLink(KIND_CLASS_AND_MEMBERS) + "otherwise.")
+                    docEnumLink(KIND_CLASS_AND_MEMBERS) + " otherwise.")
                 .addParagraph(
                     "When annotating a method the default kind is: "
                         + docEnumLink(KIND_ONLY_METHODS))
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemGeneratedFilesTest.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemGeneratedFilesTest.java
index 28f1c57..f9e8f41 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemGeneratedFilesTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemGeneratedFilesTest.java
@@ -22,7 +22,10 @@
             String expectedContent = FileUtils.readTextFile(file, StandardCharsets.UTF_8);
             assertEquals(expectedContent, content);
           } catch (IOException e) {
-            throw new RuntimeException(e);
+            throw new RuntimeException(
+                "Documentation out of sync, you might need to run main in"
+                    + " KeepItemAnnotationGenerator.",
+                e);
           }
         });
   }
diff --git a/src/test/java/com/android/tools/r8/lightir/LirLensRewritingWithOneToManyMappingTest.java b/src/test/java/com/android/tools/r8/lightir/LirLensRewritingWithOneToManyMappingTest.java
new file mode 100644
index 0000000..d4f27cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/lightir/LirLensRewritingWithOneToManyMappingTest.java
@@ -0,0 +1,87 @@
+// 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.lightir;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class LirLensRewritingWithOneToManyMappingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, Baz.class, Qux.class)
+        .addKeepMainRule(Main.class)
+        .addLibraryClasses(Foo.class, Bar.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .apply(setMockApiLevelForClass(Foo.class, AndroidApiLevel.B))
+        .apply(setMockApiLevelForMethod(Foo.class.getDeclaredMethod("method"), AndroidApiLevel.B))
+        .apply(setMockApiLevelForClass(Bar.class, AndroidApiLevel.B))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .addRunClasspathClasses(Foo.class, Bar.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Foo", "Foo");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new Qux().method();
+    }
+  }
+
+  // Library.
+
+  static class Foo {
+
+    public void method() {
+      System.out.println("Foo");
+    }
+  }
+
+  static class Bar extends Foo {}
+
+  // Program.
+
+  @NeverClassInline
+  @NoVerticalClassMerging
+  static class Baz extends Bar {}
+
+  @NeverClassInline
+  static class Qux extends Baz {
+
+    @NeverInline
+    @Override
+    public void method() {
+      super.method();
+      new Baz().method();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java b/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
index 90d4305..53740e7 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
@@ -136,6 +136,11 @@
       // If we are compiling to an old runtime with a new android.jar, we should rebind to LibraryB.
       return LibraryB.class;
     }
+    // On pre L we don't rebind above classes that we can't find in the api database
+    if (!parameters.isDexRuntime() || parameters.getApiLevel().isLessThan(AndroidApiLevel.L)) {
+      return LibraryB.class;
+    }
+
     // Otherwise, we are compiling to an old android.jar, in which case we should rebind to
     // LibraryI.
     return LibraryI.class;
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java
new file mode 100644
index 0000000..c9bec48
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.memberrebinding;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithHolderAndName;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoMethodStaticizing;
+import com.android.tools.r8.NoParameterTypeStrengthening;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MemberRebindingWithApiDatabaseLookupTest extends TestBase {
+
+  static final String EXPECTED = "foobar";
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public MemberRebindingWithApiDatabaseLookupTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(Foo.class, Bar.class, ProgramClassExtendsLibrary.class)
+        .addLibraryClasses(LibraryClass.class, LibraryBaseClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoMethodStaticizingAnnotations()
+        .enableConstantArgumentAnnotations()
+        .enableNoParameterTypeStrengtheningAnnotations()
+        .addKeepMainRule(Foo.class)
+        .addKeepClassRules(ProgramClassExtendsLibrary.class)
+        .apply(setMockApiLevelForClass(LibraryBaseClass.class, AndroidApiLevel.B))
+        .apply(setMockApiLevelForClass(LibraryClass.class, AndroidApiLevel.L))
+        .apply(
+            setMockApiLevelForMethod(
+                LibraryBaseClass.class.getDeclaredMethod("z"), AndroidApiLevel.B))
+        .compile()
+        .addRunClasspathClasses(LibraryBaseClass.class)
+        .run(parameters.getRuntime(), Foo.class)
+        .assertSuccessWithOutputLines(EXPECTED)
+        .inspect(
+            codeInspector -> {
+              // We should not have rewritten the invoke virtual in X to the base most library class
+              // if we are not on dalvik, otherwise the first library class.
+              Class expectedInvokeClass =
+                  parameters.getApiLevel().isLessThan(AndroidApiLevel.L)
+                      ? LibraryClass.class
+                      : LibraryBaseClass.class;
+              MethodSubject xMethod =
+                  codeInspector.clazz(Bar.class).uniqueMethodWithOriginalName("x");
+              assertThat(
+                  xMethod, invokesMethodWithHolderAndName(expectedInvokeClass.getTypeName(), "z"));
+            });
+  }
+
+  public static class Foo {
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        new Bar().x(null);
+      } else {
+        new Bar().y("foobar");
+      }
+    }
+  }
+
+  @NeverClassInline
+  public static class Bar {
+    @NeverInline
+    @NoMethodStaticizing
+    @KeepConstantArguments
+    @NoParameterTypeStrengthening
+    public void x(ProgramClassExtendsLibrary value) {
+      // We need to ensure that we don't rebind this to LibraryBaseClass as that will make dalvik
+      // (incorrectly) hard fail verification even when we don't call this method.
+      // Since the superclass of value is not available at runtime dalvik will lower value to
+      // type object, which we can't use for the invoke virtual on the known library class
+      // LibraryBaseClass.
+      value.z();
+    }
+
+    @NeverInline
+    public void y(String s) {
+      System.out.println(s);
+    }
+  }
+
+  public static class ProgramClassExtendsLibrary extends LibraryClass {}
+
+  public static class LibraryClass extends LibraryBaseClass {}
+
+  public static class LibraryBaseClass {
+    public void z() {
+      System.out.println("z()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/B354625681Test.java b/src/test/java/com/android/tools/r8/regress/B354625681Test.java
new file mode 100644
index 0000000..338ca7f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/B354625681Test.java
@@ -0,0 +1,70 @@
+// 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.regress;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class B354625681Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final List<String> EXPECTED_OUTPUT = ImmutableList.of("over");
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  static class TestClass {
+
+    void mainTest() {
+      int i2 = 171;
+      String s3 = "sssssssssssssssssssss";
+      String s2 = "s";
+      String[][] ss = {{"s"}};
+      for (int i = -6; i < 6; i += 1) {
+        s2 = s2 + s3;
+        try {
+          if (ss[i2] == null) {}
+        } catch (Throwable e) {
+        }
+      }
+    }
+
+    public static void main(String[] strArr) {
+      TestClass _instance = new TestClass();
+      _instance.mainTest();
+      System.out.println("over");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/B354625682Test.java b/src/test/java/com/android/tools/r8/regress/B354625682Test.java
new file mode 100644
index 0000000..50e2902
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/B354625682Test.java
@@ -0,0 +1,71 @@
+// 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.regress;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class B354625682Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final List<String> EXPECTED_OUTPUT = ImmutableList.of("over");
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  static class TestClass {
+
+    void a(String[] b) {
+      ByteBuffer byteBuffer = null;
+      SocketAddress socketAddress = null;
+      DatagramChannel datagramChannel = null;
+      try {
+        if (socketAddress == null) {
+          datagramChannel.receive(byteBuffer);
+        }
+      } catch (Throwable $) {
+      }
+    }
+
+    public static void main(String[] c) {
+      TestClass e = new TestClass();
+      e.a(c);
+      System.out.println("over");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/Regress302826300.java b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
index f87eb90..d8cda04 100644
--- a/src/test/java/com/android/tools/r8/regress/Regress302826300.java
+++ b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
@@ -31,6 +31,9 @@
 
   @Test
   public void testD8() throws Exception {
+    // This is illustrating the problem from b/302826300 using just D8 (in which case we don't
+    // introduce the error, the error is in the build setup). The actual bug is introduced by
+    // R8, see MemberRebindingWithApiDatabaseLookupTest.
     D8TestRunResult run =
         testForD8(parameters.getBackend())
             // We simply pass LibraryBaseClass as program, but could also compile it separately and
diff --git a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
new file mode 100644
index 0000000..bdb0a85
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EmptyMemberRulesToDefaultInitRuleConversionTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, convert: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        getTestParameters().withDefaultRuntimes().withMinimumApiLevel().build());
+  }
+
+  @Test
+  public void testCompat() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(Main.class)
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(
+            enableEmptyMemberRulesToDefaultInitRuleConversion)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(inspector -> assertThat(inspector.clazz(Main.class).init(), isPresent()));
+  }
+
+  @Test
+  public void testFull() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(Main.class)
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(
+            enableEmptyMemberRulesToDefaultInitRuleConversion)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector ->
+                assertThat(
+                    inspector.clazz(Main.class).init(),
+                    isPresentIf(enableEmptyMemberRulesToDefaultInitRuleConversion)));
+  }
+
+  static class Main {}
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 247cb6a..d1a45c1 100644
--- a/src/test/testbase/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.base.Charsets;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -245,7 +246,11 @@
       if (FileUtils.isJarFile(file)) {
         programJars.add(file);
       } else {
-        throw new Unimplemented("No support for adding paths directly");
+        try {
+          addProgramClassFileData(Files.readAllBytes(file));
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
       }
     }
     return self();
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 4d7338d..3309783 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -76,6 +76,7 @@
 
   private AllowedDiagnosticMessages allowedDiagnosticMessages = AllowedDiagnosticMessages.NONE;
   private boolean allowUnusedProguardConfigurationRules = false;
+  private boolean enableEmptyMemberRulesToDefaultInitRuleConversion = false;
   private boolean enableIsolatedSplits = false;
   private boolean enableMissingLibraryApiModeling = true;
   private boolean enableStartupLayoutOptimization = true;
@@ -139,6 +140,8 @@
     ToolHelper.addSyntheticProguardRulesConsumerForTesting(
         builder, rules -> box.syntheticProguardRules = rules);
     libraryDesugaringTestConfiguration.configure(builder);
+    builder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        enableEmptyMemberRulesToDefaultInitRuleConversion);
     builder.setEnableIsolatedSplits(enableIsolatedSplits);
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
     builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
@@ -882,6 +885,13 @@
     return self();
   }
 
+  public T enableEmptyMemberRulesToDefaultInitRuleConversion(
+      boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+    this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+        enableEmptyMemberRulesToDefaultInitRuleConversion;
+    return self();
+  }
+
   public T enableIsolatedSplits(boolean enableIsolatedSplits) {
     this.enableIsolatedSplits = enableIsolatedSplits;
     return self();
diff --git a/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/DesugaredLibraryJDK11Undesugarer.java b/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/DesugaredLibraryJDK11Undesugarer.java
index da24386..c18df07 100644
--- a/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/DesugaredLibraryJDK11Undesugarer.java
+++ b/src/test/testbase/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/DesugaredLibraryJDK11Undesugarer.java
@@ -42,7 +42,7 @@
 
   public static void main(String[] args) {
     if (!Files.exists(Paths.get(args[0]))) {
-      throw new RuntimeException("Undesugarer source not found");
+      throw new RuntimeException("Undesugarer source not found: " + args[0]);
     }
     if (Files.exists(Paths.get(args[1]))) {
       throw new RuntimeException("Undesugarer destination already exists");
diff --git a/tools/archive_desugar_jdk_libs.py b/tools/archive_desugar_jdk_libs.py
index ead0bdd..1a9dd57 100755
--- a/tools/archive_desugar_jdk_libs.py
+++ b/tools/archive_desugar_jdk_libs.py
@@ -268,7 +268,9 @@
 
 def Undesugar(variant, maven_zip, version, undesugared_maven_zip):
     gradle.RunGradle([
-        utils.GRADLE_TASK_R8, utils.GRADLE_TASK_TEST_JAR,
+        utils.GRADLE_TASK_R8,
+        utils.GRADLE_TASK_TEST_BASE_JAR,
+        utils.GRADLE_TASK_TEST_JAR,
         utils.GRADLE_TASK_TEST_DEPS_JAR, '-Pno_internal'
     ])
     with utils.TempDir() as tmp:
@@ -282,8 +284,8 @@
         buildLibs = os.path.join(defines.REPO_ROOT, 'build', 'libs')
         cmd = [
             jdk.GetJavaExecutable(), '-cp',
-            '%s:%s:%s' %
-            (utils.R8_JAR, utils.R8_TESTS_JAR, utils.R8_TESTS_DEPS_JAR),
+            '%s:%s:%s:%s' %
+            (utils.R8_JAR, utils.R8_TESTBASE_JAR, utils.R8_TESTS_JAR, utils.R8_TESTS_DEPS_JAR),
             'com.android.tools.r8.desugar.desugaredlibrary.jdk11.DesugaredLibraryJDK11Undesugarer',
             desugar_jdk_libs_jar, undesugared_jar
         ]
diff --git a/tools/desugar_jdk_libs_repository.py b/tools/desugar_jdk_libs_repository.py
index 36268c4..dfe4ed6 100755
--- a/tools/desugar_jdk_libs_repository.py
+++ b/tools/desugar_jdk_libs_repository.py
@@ -88,7 +88,7 @@
     configuration = None
     conversions = None
     implementation = None
-    version_file = None
+    version_file_name = None
     implementation_build_target = None
     implementation_maven_zip = None
     release_archive_location = None
@@ -99,7 +99,7 @@
             configuration = utils.DESUGAR_CONFIGURATION
             conversions = utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP
             implementation = utils.DESUGAR_IMPLEMENTATION
-            version_file = 'VERSION.txt'
+            version_file_name = 'VERSION.txt'
             implementation_build_target = ':maven_release'
             implementation_maven_zip = 'desugar_jdk_libs.zip'
             release_archive_location = 'desugar_jdk_libs'
@@ -109,7 +109,7 @@
             configuration = utils.DESUGAR_CONFIGURATION_JDK11_LEGACY
             conversions = utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP
             implementation = utils.DESUGAR_IMPLEMENTATION_JDK11
-            version_file = 'VERSION_JDK11_LEGACY.txt'
+            version_file_name = 'VERSION_JDK11_LEGACY.txt'
             implementation_build_target = ':maven_release_jdk11_legacy'
             implementation_maven_zip = 'desugar_jdk_libs_jdk11_legacy.zip'
             release_archive_location = 'desugar_jdk_libs'
@@ -119,7 +119,7 @@
             configuration = utils.DESUGAR_CONFIGURATION_JDK11_MINIMAL
             conversions = utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP
             implementation = utils.DESUGAR_IMPLEMENTATION_JDK11
-            version_file = 'VERSION_JDK11_MINIMAL.txt'
+            version_file_name = 'VERSION_JDK11_MINIMAL.txt'
             implementation_build_target = ':maven_release_jdk11_minimal'
             implementation_maven_zip = 'desugar_jdk_libs_jdk11_minimal.zip'
             release_archive_location = 'desugar_jdk_libs_minimal'
@@ -129,7 +129,7 @@
             configuration = utils.DESUGAR_CONFIGURATION_JDK11
             conversions = utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP
             implementation = utils.DESUGAR_IMPLEMENTATION_JDK11
-            version_file = 'VERSION_JDK11.txt'
+            version_file_name = 'VERSION_JDK11.txt'
             implementation_build_target = ':maven_release_jdk11'
             implementation_maven_zip = 'desugar_jdk_libs_jdk11.zip'
             release_archive_location = 'desugar_jdk_libs'
@@ -139,7 +139,7 @@
             configuration = utils.DESUGAR_CONFIGURATION_JDK11_NIO
             conversions = utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP
             implementation = utils.DESUGAR_IMPLEMENTATION_JDK11
-            version_file = 'VERSION_JDK11_NIO.txt'
+            version_file_name = 'VERSION_JDK11_NIO.txt'
             implementation_build_target = ':maven_release_jdk11_nio'
             implementation_maven_zip = 'desugar_jdk_libs_jdk11_nio.zip'
             release_archive_location = 'desugar_jdk_libs_nio'
@@ -171,8 +171,9 @@
                     'git', '-C', checkout_dir, 'checkout',
                     args.desugar_jdk_libs_revision
                 ])
+        if not args.release_version:
             with utils.ChangedWorkingDirectory(checkout_dir):
-                with open(version_file) as version_file:
+                with open(version_file_name) as version_file:
                     version_file_lines = version_file.readlines()
                     for line in version_file_lines:
                         if not line.startswith('#'):
@@ -180,9 +181,13 @@
                             if (version != desugar_jdk_libs_version):
                                 raise Exception(
                                     "Version mismatch. Configuration has version '"
-                                    + version +
-                                    "', and desugar_jdk_libs has version '" +
-                                    desugar_jdk_libs_version + "'")
+                                    + version
+                                    + "', and desugar_jdk_libs has version '"
+                                    + desugar_jdk_libs_version
+                                    + " in '" + version_file_name
+                                    + "'. If testing a new version use "
+                                    + "--desugar-jdk-libs-checkout with updated "
+                                    + "VERSION_*.txt and DEPENDENCIES_*.txt files.")
 
         # Build desugared library configuration.
         print("Building desugared library configuration " + version)
diff --git a/tools/utils.py b/tools/utils.py
index 2b77ed5..86d7e8d 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -55,6 +55,7 @@
 GRADLE_TASK_ALL_TESTS_WITH_APPLY_MAPPING_JAR = ':test:rewriteTestsForR8LibWithRelocatedDeps'
 GRADLE_TASK_TESTBASE_WITH_APPLY_MAPPING_JAR = ':test:rewriteTestBaseForR8LibWithRelocatedDeps'
 GRADLE_TASK_TEST_DEPS_JAR = ':test:packageTestDeps'
+GRADLE_TASK_TEST_BASE_JAR = ':test:relocateTestBaseForR8LibWithRelocatedDeps'
 GRADLE_TASK_TEST_JAR = ':test:relocateTestsForR8LibWithRelocatedDeps'
 GRADLE_TASK_TEST_UNZIP_TESTBASE = ':test:unzipTestBase'