Merge commit 'd8c78b6ead20b88d8e6619f2447120154e6356d9' into dev-release
diff --git a/build.gradle b/build.gradle
index 7123eb7..f5838d6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 
+
 import desugaredlibrary.CustomConversionAsmRewriterTask
 import dx.DexMergerTask
 import dx.DxTask
@@ -1091,7 +1092,6 @@
 task rawBuildLibraryDesugarConversions(type: Zip, dependsOn: downloadDeps) {
     from sourceSets.libraryDesugarConversions.output
     include "java/**/*.class"
-    include "desugar/sun/nio/fs/DesugarAndroid*.class"
     baseName 'library_desugar_conversions_raw'
     destinationDir file('build/tmp/desugaredlibrary')
 }
diff --git a/doc/retrace.md b/doc/retrace.md
index ea6a151..ae22ff1 100644
--- a/doc/retrace.md
+++ b/doc/retrace.md
@@ -209,7 +209,7 @@
 should carry the reported position to the next frame and use the
 `outlineCallsite` to obtain the correct position.
 
-### Outline Call Site (Introduced at version 2.0)
+### Outline Call Site (Introduced at version 2.0, updated at 2.2)
 
 A position in an outline can correspond to multiple different positions
 depending on the context. The information can be stored in the mapping file with
@@ -221,10 +221,14 @@
         'outline_pos_1': callsite_pos_1,
         'outline_pos_2': callsite_pos_2,
          ...
-     }
+     },
+     'outline': 'outline':'La;a()I'
   }
 ```
 
+The `outline` key was added in 2.2 and should be the residual descriptor of the
+outline.
+
 The retracer should when seeing the `outline` information carry the line number
 to the next frame. The position should be rewritten by using the positions map
 before using the resulting position for further retracing. Here is an example:
@@ -240,7 +244,8 @@
   5:5:int outlineCaller(int):100:100 -> s
   27:27:int outlineCaller(int):0:0 -> s
 # { 'id':'com.android.tools.r8.outlineCallsite',
-    'positions': { '1': 4, '2': 5 } }
+    'positions': { '1': 4, '2': 5 },
+    'outline':'La;a()I'}
 ```
 
 Retracing the following stack trace lines:
@@ -273,4 +278,30 @@
 ```
 
 It does not matter if the mapping is an inline frame. Catch all ranges should
-never be used for overloads.
\ No newline at end of file
+never be used for overloads.
+
+### Residual signature (Introduced at 2.2)
+
+The residual signature information was added to mitigate the problem with no
+right hand side signature in mapping files. The information should be placed
+directly under the first occurrence of a field or method.
+
+```
+com.bar -> a:
+com.foo -> b:
+  com.bar m1(int) -> m2,
+  # { id: 'com.android.tools.r8.residualsignature', signature:'(Z)a' }
+```
+
+Similar for fields:
+```
+com.bar -> a:
+com.foo -> b:
+  com.bar f1 -> f2,
+  # { id: 'com.android.tools.r8.residualsignature', signature:'a' }
+```
+
+If the residual definition has changed arguments or return type then the
+signature should be emitted. The residual signature has no effect on retracing
+stack traces but they are necessary when interacting with residual signatures
+through the Retrace Api or for composing mapping files.
\ No newline at end of file
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
new file mode 100644
index 0000000..b17a3e1
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+/**
+ * A binding of a keep item.
+ *
+ * <p>A binding allows referencing the exact instance of a match from a condition in other
+ * conditions and/or targets. It can also be used to reduce duplication of targets by sharing
+ * patterns.
+ *
+ * <p>See KeepTarget for documentation on specifying an item pattern.
+ */
+public @interface KeepBinding {
+
+  /** Name with which other bindings, conditions or targets can reference the bound item pattern. */
+  String bindingName();
+
+  String classFromBinding() default "";
+
+  String className() default "";
+
+  Class<?> classConstant() default Object.class;
+
+  String extendsClassName() default "";
+
+  Class<?> extendsClassConstant() default Object.class;
+
+  String methodName() default "";
+
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {""};
+
+  String fieldName() default "";
+
+  String fieldType() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index 2d5eb60..a0dbcf4 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -11,20 +11,14 @@
 /**
  * A condition for a keep edge.
  *
- * <p>The condition denotes a keep item:
- *
- * <ul>
- *   <li>a class, or pattern on classes;
- *   <li>a method, or pattern on methods; or
- *   <li>a field, or pattern on fields.
- * </ul>
- *
- * <p>The structure of a condition item is the same as for a target item but without a notion of
- * "keep options".
+ * <p>See KeepTarget for documentation on specifying an item pattern.
  */
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepCondition {
+
+  String classFromBinding() default "";
+
   String className() default "";
 
   Class<?> classConstant() default Object.class;
@@ -33,6 +27,8 @@
 
   Class<?> extendsClassConstant() default Object.class;
 
+  String memberFromBinding() default "";
+
   String methodName() default "";
 
   String methodReturnType() default "";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
index 02eb5df..1c7d3a5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -33,6 +33,7 @@
     public static final Class<KeepEdge> CLASS = KeepEdge.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
     public static final String description = "description";
+    public static final String bindings = "bindings";
     public static final String preconditions = "preconditions";
     public static final String consequences = "consequences";
   }
@@ -48,6 +49,9 @@
 
   // Implicit hidden item which is "super type" of Condition and Target.
   public static final class Item {
+    public static final String classFromBinding = "classFromBinding";
+    public static final String memberFromBinding = "memberFromBinding";
+
     public static final String className = "className";
     public static final String classConstant = "classConstant";
 
@@ -78,6 +82,12 @@
     public static final String fieldTypeDefaultValue = "";
   }
 
+  public static final class Binding {
+    public static final Class<KeepBinding> CLASS = KeepBinding.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String bindingName = "bindingName";
+  }
+
   public static final class Condition {
     public static final Class<KeepCondition> CLASS = KeepCondition.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
index 329b153..fb2c560 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
@@ -13,6 +13,8 @@
 public @interface KeepEdge {
   String description() default "";
 
+  KeepBinding[] bindings() default {};
+
   KeepCondition[] preconditions() default {};
 
   KeepTarget[] consequences();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index e4cb263..be61b4a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -14,9 +14,9 @@
  * <p>The target denotes a keep item along with options for what to keep:
  *
  * <ul>
- *   <li>a class, or pattern on classes;
- *   <li>a method, or pattern on methods; or
- *   <li>a field, or pattern on fields.
+ *   <li>a pattern on classes;
+ *   <li>a pattern on methods; or
+ *   <li>a pattern on fields.
  * </ul>
  *
  * <p>The structure of a target item is the same as for a condition item but has the additional keep
@@ -26,29 +26,119 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepTarget {
 
-  // KeepTarget only content (keep options) =========
-
+  /**
+   * Define the options that do not need to be preserved for the target.
+   *
+   * <p>Mutually exclusive with `disallow`.
+   *
+   * <p>If none are specified the default is "allow none" / "disallow all".
+   */
   KeepOption[] allow() default {};
 
+  /**
+   * Define the options that *must* be preserved for the target.
+   *
+   * <p>Mutually exclusive with `allow`.
+   *
+   * <p>If none are specified the default is "allow none" / "disallow all".
+   */
   KeepOption[] disallow() default {};
 
-  // Shared KeepItem content ========================
+  /**
+   * Define the class-name pattern by reference to a binding.
+   *
+   * <p>Mutually exclusive with `className` and `classConstant`.
+   *
+   * <p>If none are specified the default is to match any class.
+   */
+  String classFromBinding() default "";
 
+  /**
+   * Define the class-name pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with `classFromBinding` and `classConstant`.
+   *
+   * <p>If none are specified the default is to match any class.
+   */
   String className() default "";
 
+  /**
+   * Define the class-name pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with `classFromBinding` and `className`.
+   *
+   * <p>If none are specified the default is to match any class.
+   */
   Class<?> classConstant() default Object.class;
 
+  /**
+   * Define the extends pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with `extendsClassConstant`.
+   *
+   * <p>If none are specified the default is to match any extends clause.
+   */
   String extendsClassName() default "";
 
+  /**
+   * Define the extends pattern by Class constant.
+   *
+   * <p>Mutually exclusive with `extendsClassName`.
+   *
+   * <p>If none are specified the default is to match any extends clause.
+   */
   Class<?> extendsClassConstant() default Object.class;
 
+  /**
+   * Define the member pattern in full by a reference to a binding.
+   *
+   * <p>Mutually exclusive with all other pattern properties. When a member binding is referenced
+   * this item is defined to be that item, including its class and member patterns.
+   */
+  String memberFromBinding() default "";
+
+  /**
+   * Define the method-name pattern by an exact method name.
+   *
+   * <p>Mutually exclusive with any field properties.
+   *
+   * <p>If none and other properties define this as a method the default matches any method name.
+   */
   String methodName() default "";
 
+  /**
+   * Define the method return-type pattern by a fully qualified type or 'void'.
+   *
+   * <p>Mutually exclusive with any field properties.
+   *
+   * <p>If none and other properties define this as a method the default matches any return type.
+   */
   String methodReturnType() default "";
 
+  /**
+   * Define the method parameters pattern by a list of fully qualified types.
+   *
+   * <p>Mutually exclusive with any field properties.
+   *
+   * <p>If none and other properties define this as a method the default matches any parameters.
+   */
   String[] methodParameters() default {""};
 
+  /**
+   * Define the field-name pattern by an exact field name.
+   *
+   * <p>Mutually exclusive with any method properties.
+   *
+   * <p>If none and other properties define this as a field the default matches any field name.
+   */
   String fieldName() default "";
 
+  /**
+   * Define the field-type pattern by a fully qualified type.
+   *
+   * <p>Mutually exclusive with any method properties.
+   *
+   * <p>If none and other properties define this as a field the default matches any field type.
+   */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 5d23d5d..9fdf1f4 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -4,11 +4,14 @@
 package com.android.tools.r8.keepanno.asm;
 
 import com.android.tools.r8.keepanno.annotations.KeepConstants;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Binding;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Condition;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Item;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Option;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
+import com.android.tools.r8.keepanno.ast.KeepBindings;
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
@@ -19,6 +22,7 @@
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
@@ -278,6 +282,9 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.bindings)) {
+        return new KeepBindingsVisitor(builder::setBindings);
+      }
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(builder::setPreconditions);
       }
@@ -304,7 +311,7 @@
         Consumer<KeepEdgeMetaInfo.Builder> addContext,
         KeepItemPattern context) {
       this.parent = parent;
-      preconditions.addCondition(KeepCondition.builder().setItem(context).build());
+      preconditions.addCondition(KeepCondition.builder().setItemPattern(context).build());
       addContext.accept(metaInfoBuilder);
     }
 
@@ -341,6 +348,28 @@
     }
   }
 
+  private static class KeepBindingsVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepBindings> parent;
+    private final KeepBindings.Builder builder = KeepBindings.builder();
+
+    public KeepBindingsVisitor(Parent<KeepBindings> parent) {
+      this.parent = parent;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      if (descriptor.equals(KeepConstants.Binding.DESCRIPTOR)) {
+        return new KeepBindingVisitor(builder);
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(builder.build());
+    }
+  }
+
   private static class KeepPreconditionsVisitor extends AnnotationVisitorBase {
     private final Parent<KeepPreconditions> parent;
     private final KeepPreconditions.Builder builder = KeepPreconditions.builder();
@@ -461,24 +490,31 @@
     }
   }
 
-  private static class ClassDeclaration extends SingleDeclaration<KeepQualifiedClassNamePattern> {
+  private static class ClassDeclaration extends SingleDeclaration<KeepClassReference> {
     @Override
     String kind() {
       return "class";
     }
 
-    @Override
-    KeepQualifiedClassNamePattern getDefaultValue() {
-      return KeepQualifiedClassNamePattern.any();
+    KeepClassReference wrap(KeepQualifiedClassNamePattern namePattern) {
+      return KeepClassReference.fromClassNamePattern(namePattern);
     }
 
     @Override
-    KeepQualifiedClassNamePattern parse(String name, Object value) {
+    KeepClassReference getDefaultValue() {
+      return wrap(KeepQualifiedClassNamePattern.any());
+    }
+
+    @Override
+    KeepClassReference parse(String name, Object value) {
+      if (name.equals(Item.classFromBinding) && value instanceof String) {
+        return KeepClassReference.fromBindingReference((String) value);
+      }
       if (name.equals(Item.classConstant) && value instanceof Type) {
-        return KeepQualifiedClassNamePattern.exact(((Type) value).getClassName());
+        return wrap(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()));
       }
       if (name.equals(Item.className) && value instanceof String) {
-        return KeepQualifiedClassNamePattern.exact(((String) value));
+        return wrap(KeepQualifiedClassNamePattern.exact(((String) value)));
       }
       return null;
     }
@@ -659,17 +695,30 @@
   }
 
   private abstract static class KeepItemVisitorBase extends AnnotationVisitorBase {
-    private final Parent<KeepItemPattern> parent;
+    private Parent<KeepItemReference> parent;
+    private String memberBindingReference = null;
     private final ClassDeclaration classDeclaration = new ClassDeclaration();
     private final ExtendsDeclaration extendsDeclaration = new ExtendsDeclaration();
     private final MemberDeclaration memberDeclaration = new MemberDeclaration();
 
-    public KeepItemVisitorBase(Parent<KeepItemPattern> parent) {
+    public KeepItemVisitorBase(Parent<KeepItemReference> parent) {
+      setParent(parent);
+    }
+
+    public KeepItemVisitorBase() {}
+
+    void setParent(Parent<KeepItemReference> parent) {
+      assert parent != null;
+      assert this.parent == null;
       this.parent = parent;
     }
 
     @Override
     public void visit(String name, Object value) {
+      if (name.equals(Item.memberFromBinding) && value instanceof String) {
+        memberBindingReference = (String) value;
+        return;
+      }
       if (classDeclaration.tryParse(name, value)
           || extendsDeclaration.tryParse(name, value)
           || memberDeclaration.tryParse(name, value)) {
@@ -689,12 +738,63 @@
 
     @Override
     public void visitEnd() {
-      parent.accept(
-          KeepItemPattern.builder()
-              .setClassPattern(classDeclaration.getValue())
-              .setExtendsPattern(extendsDeclaration.getValue())
-              .setMemberPattern(memberDeclaration.getValue())
-              .build());
+      if (memberBindingReference != null) {
+        if (!classDeclaration.getValue().equals(classDeclaration.getDefaultValue())
+            || !memberDeclaration.getValue().isNone()
+            || !extendsDeclaration.getValue().isAny()) {
+          throw new KeepEdgeException(
+              "Cannot define an item explicitly and via a member-binding reference");
+        }
+        parent.accept(KeepItemReference.fromBindingReference(memberBindingReference));
+      } else {
+        parent.accept(
+            KeepItemReference.fromItemPattern(
+                KeepItemPattern.builder()
+                    .setClassReference(classDeclaration.getValue())
+                    .setExtendsPattern(extendsDeclaration.getValue())
+                    .setMemberPattern(memberDeclaration.getValue())
+                    .build()));
+      }
+    }
+  }
+
+  private static class KeepBindingVisitor extends KeepItemVisitorBase {
+
+    private final KeepBindings.Builder builder;
+    private String bindingName;
+    private KeepItemPattern item;
+
+    public KeepBindingVisitor(KeepBindings.Builder builder) {
+      this.builder = builder;
+      setParent(
+          item -> {
+            // The language currently disallows aliasing bindings, thus a binding should directly be
+            // defined by a reference to another binding.
+            if (item.isBindingReference()) {
+              throw new KeepEdgeException(
+                  "Invalid binding reference to '"
+                      + item.asBindingReference()
+                      + "' in binding definition of '"
+                      + bindingName
+                      + "'");
+            }
+            this.item = item.asItemPattern();
+          });
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Binding.bindingName) && value instanceof String) {
+        bindingName = (String) value;
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnd() {
+      super.visitEnd();
+      builder.addBinding(bindingName, item);
     }
   }
 
@@ -765,7 +865,7 @@
     }
 
     private KeepTargetVisitor(Parent<KeepTarget> parent, KeepTarget.Builder builder) {
-      super(item -> parent.accept(builder.setItem(item).build()));
+      super(item -> parent.accept(builder.setItemReference(item).build()));
       this.builder = builder;
     }
 
@@ -782,7 +882,7 @@
   private static class KeepConditionVisitor extends KeepItemVisitorBase {
 
     public KeepConditionVisitor(Parent<KeepCondition> parent) {
-      super(item -> parent.accept(KeepCondition.builder().setItem(item).build()));
+      super(item -> parent.accept(KeepCondition.builder().setItemReference(item).build()));
     }
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index a8f631a..774ea58 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Item;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
@@ -53,7 +54,10 @@
         condition -> {
           AnnotationVisitor conditionVisitor =
               arrayVisitor.visitAnnotation(ignoredArrayValueName, Condition.DESCRIPTOR);
-          writeItem(conditionVisitor, condition.getItemPattern());
+          if (condition.getItem().isBindingReference()) {
+            throw new Unimplemented();
+          }
+          writeItem(conditionVisitor, condition.getItem().asItemPattern());
         });
     arrayVisitor.visitEnd();
   }
@@ -70,16 +74,26 @@
           if (!target.getOptions().isKeepAll()) {
             throw new Unimplemented();
           }
-          writeItem(targetVisitor, target.getItem());
+          if (target.getItem().isBindingReference()) {
+            throw new Unimplemented();
+          }
+          writeItem(targetVisitor, target.getItem().asItemPattern());
         });
     arrayVisitor.visitEnd();
   }
 
   private void writeItem(AnnotationVisitor itemVisitor, KeepItemPattern item) {
-    if (item.isAny()) {
+    if (item.isAny(
+        binding -> {
+          throw new Unimplemented();
+        })) {
       throw new Unimplemented();
     }
-    KeepQualifiedClassNamePattern namePattern = item.getClassNamePattern();
+    KeepClassReference classReference = item.getClassReference();
+    if (classReference.isBindingReference()) {
+      throw new Unimplemented();
+    }
+    KeepQualifiedClassNamePattern namePattern = classReference.asClassNamePattern();
     if (namePattern.isExact()) {
       Type typeConstant = Type.getType(namePattern.getExactDescriptor());
       itemVisitor.visit(KeepConstants.Item.classConstant, typeConstant);
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java
new file mode 100644
index 0000000..21692c9
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepBindings.java
@@ -0,0 +1,169 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public class KeepBindings {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private static final KeepBindings NONE_INSTANCE = new KeepBindings(Collections.emptyMap());
+
+  private final Map<String, Binding> bindings;
+
+  private KeepBindings(Map<String, Binding> bindings) {
+    assert bindings != null;
+    this.bindings = bindings;
+  }
+
+  public static KeepBindings none() {
+    return NONE_INSTANCE;
+  }
+
+  public Binding get(String bindingReference) {
+    return bindings.get(bindingReference);
+  }
+
+  public int size() {
+    return bindings.size();
+  }
+
+  public boolean isEmpty() {
+    return bindings.isEmpty();
+  }
+
+  public void forEach(BiConsumer<String, KeepItemPattern> fn) {
+    bindings.forEach((name, binding) -> fn.accept(name, binding.getItem()));
+  }
+
+  public boolean isAny(KeepItemReference itemReference) {
+    return itemReference.isBindingReference()
+        ? isAny(get(itemReference.asBindingReference()).getItem())
+        : isAny(itemReference.asItemPattern());
+  }
+
+  public boolean isAny(KeepItemPattern itemPattern) {
+    return itemPattern.isAny(this::isAnyClassNamePattern);
+  }
+
+  // If the outer-most item has been judged to be "any" then we internally only need to check
+  // that the class-name pattern itself is "any". The class-name could potentially reference names
+  // of other item bindings so this is a recursive search.
+  private boolean isAnyClassNamePattern(String bindingName) {
+    KeepClassReference classReference = get(bindingName).getItem().getClassReference();
+    return classReference.isBindingReference()
+        ? isAnyClassNamePattern(classReference.asBindingReference())
+        : classReference.asClassNamePattern().isAny();
+  }
+
+  @Override
+  public String toString() {
+    return "{"
+        + bindings.entrySet().stream()
+            .map(e -> e.getKey() + "=" + e.getValue())
+            .collect(Collectors.joining(", "));
+  }
+
+  /**
+   * A unique binding.
+   *
+   * <p>The uniqueness / identity of a binding is critical as a binding denotes a concrete match in
+   * the precondition of a rule. In terms of proguard keep rules it provides the difference of:
+   *
+   * <pre>
+   *   -if class *Foo -keep class *Foo { void <init>(...); }
+   * </pre>
+   *
+   * and
+   *
+   * <pre>
+   *   -if class *Foo -keep class <1>Foo { void <init>(...); }
+   * </pre>
+   *
+   * The first case will keep all classes matching *Foo and there default constructors if any single
+   * live class matches. The second will keep the default constructors of the live classes that
+   * match.
+   *
+   * <p>This wrapper ensures that pattern equality does not imply binding equality.
+   */
+  public static class Binding {
+    private final KeepItemPattern item;
+
+    public Binding(KeepItemPattern item) {
+      this.item = item;
+    }
+
+    public KeepItemPattern getItem() {
+      return item;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return item.toString();
+    }
+  }
+
+  public static class Builder {
+    private final Map<String, KeepItemPattern> bindings = new HashMap<>();
+
+    public Builder addBinding(String name, KeepItemPattern itemPattern) {
+      if (name == null || itemPattern == null) {
+        throw new KeepEdgeException("Invalid binding of '" + name + "'");
+      }
+      KeepItemPattern old = bindings.put(name, itemPattern);
+      if (old != null) {
+        throw new KeepEdgeException("Multiple definitions for binding '" + name + "'");
+      }
+      return this;
+    }
+
+    public KeepBindings build() {
+      if (bindings.isEmpty()) {
+        return NONE_INSTANCE;
+      }
+      Map<String, Binding> definitions = new HashMap<>(bindings.size());
+      for (String name : bindings.keySet()) {
+        definitions.put(name, verifyAndCreateBinding(name));
+      }
+      return new KeepBindings(definitions);
+    }
+
+    private Binding verifyAndCreateBinding(String bindingDefinitionName) {
+      KeepItemPattern pattern = bindings.get(bindingDefinitionName);
+      for (String bindingReference : pattern.getBindingReferences()) {
+        // Currently, it is not possible to define mutually recursive items, so we only need
+        // to check against self.
+        if (bindingReference.equals(bindingDefinitionName)) {
+          throw new KeepEdgeException("Recursive binding for name '" + bindingReference + "'");
+        }
+        if (!bindings.containsKey(bindingReference)) {
+          throw new KeepEdgeException(
+              "Undefined binding for name '"
+                  + bindingReference
+                  + "' referenced in binding of '"
+                  + bindingDefinitionName
+                  + "'");
+        }
+      }
+      return new Binding(pattern);
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java
new file mode 100644
index 0000000..7397be7
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassReference.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Predicate;
+
+public abstract class KeepClassReference {
+
+  public static KeepClassReference fromBindingReference(String bindingReference) {
+    return new BindingReference(bindingReference);
+  }
+
+  public static KeepClassReference fromClassNamePattern(
+      KeepQualifiedClassNamePattern classNamePattern) {
+    return new SomeItem(classNamePattern);
+  }
+
+  public boolean isBindingReference() {
+    return asBindingReference() != null;
+  }
+
+  public boolean isClassNamePattern() {
+    return asClassNamePattern() != null;
+  }
+
+  public String asBindingReference() {
+    return null;
+  }
+
+  public KeepQualifiedClassNamePattern asClassNamePattern() {
+    return null;
+  }
+
+  public abstract Collection<String> getBindingReferences();
+
+  public boolean isAny(Predicate<String> onReference) {
+    return isBindingReference()
+        ? onReference.test(asBindingReference())
+        : asClassNamePattern().isAny();
+  }
+
+  private static class BindingReference extends KeepClassReference {
+    private final String bindingReference;
+
+    private BindingReference(String bindingReference) {
+      assert bindingReference != null;
+      this.bindingReference = bindingReference;
+    }
+
+    @Override
+    public String asBindingReference() {
+      return bindingReference;
+    }
+
+    @Override
+    public Collection<String> getBindingReferences() {
+      return Collections.singletonList(bindingReference);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      BindingReference that = (BindingReference) o;
+      return bindingReference.equals(that.bindingReference);
+    }
+
+    @Override
+    public int hashCode() {
+      return bindingReference.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return bindingReference;
+    }
+  }
+
+  private static class SomeItem extends KeepClassReference {
+    private final KeepQualifiedClassNamePattern classNamePattern;
+
+    private SomeItem(KeepQualifiedClassNamePattern classNamePattern) {
+      assert classNamePattern != null;
+      this.classNamePattern = classNamePattern;
+    }
+
+    @Override
+    public KeepQualifiedClassNamePattern asClassNamePattern() {
+      return classNamePattern;
+    }
+
+    @Override
+    public Collection<String> getBindingReferences() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SomeItem someItem = (SomeItem) o;
+      return classNamePattern.equals(someItem.classNamePattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return classNamePattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return classNamePattern.toString();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
index 9909097..de4b742 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
@@ -19,29 +19,33 @@
 
   public static class Builder {
 
-    private KeepItemPattern itemPattern;
+    private KeepItemReference item;
 
     private Builder() {}
 
-    public Builder setItem(KeepItemPattern itemPattern) {
-      this.itemPattern = itemPattern;
+    public Builder setItemReference(KeepItemReference item) {
+      this.item = item;
       return this;
     }
 
+    public Builder setItemPattern(KeepItemPattern itemPattern) {
+      return setItemReference(KeepItemReference.fromItemPattern(itemPattern));
+    }
+
     public KeepCondition build() {
-      return new KeepCondition(itemPattern);
+      return new KeepCondition(item);
     }
   }
 
-  private final KeepItemPattern itemPattern;
+  private final KeepItemReference item;
 
-  private KeepCondition(KeepItemPattern itemPattern) {
-    assert itemPattern != null;
-    this.itemPattern = itemPattern;
+  private KeepCondition(KeepItemReference item) {
+    assert item != null;
+    this.item = item;
   }
 
-  public KeepItemPattern getItemPattern() {
-    return itemPattern;
+  public KeepItemReference getItem() {
+    return item;
   }
 
   @Override
@@ -53,16 +57,16 @@
       return false;
     }
     KeepCondition that = (KeepCondition) o;
-    return itemPattern.equals(that.itemPattern);
+    return item.equals(that.item);
   }
 
   @Override
   public int hashCode() {
-    return itemPattern.hashCode();
+    return item.hashCode();
   }
 
   @Override
   public String toString() {
-    return itemPattern.toString();
+    return item.toString();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index dbe52b0..8c890df 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -19,27 +19,40 @@
  * <p>TODO(b/248408342): Update the BNF and AST to be complete.
  *
  * <pre>
- *   EDGE ::= METAINFO PRECONDITIONS -> CONSEQUENCES
- *   METAINFO = [CONTEXT] [DESCRIPTION]
- *   CONTEXT = class-descriptor | method-descriptor | field-descriptor
- *   DESCRIPTION = string-content
+ *   EDGE ::= METAINFO BINDINGS PRECONDITIONS -> CONSEQUENCES
+ *   METAINFO ::= [CONTEXT] [DESCRIPTION]
+ *   CONTEXT ::= class-descriptor | method-descriptor | field-descriptor
+ *   DESCRIPTION ::= string-content
+ *
+ *   BINDINGS ::= (BINDING_NAME = ITEM_PATTERN)*
+ *   BINDING_NAME ::= string-content
+ *   BINDING_REFERENCE ::= BINDING_NAME
  *
  *   PRECONDITIONS ::= always | CONDITION+
- *   CONDITION ::= ITEM_PATTERN
+ *   CONDITION ::= ITEM_REFERENCE
  *
  *   CONSEQUENCES ::= TARGET+
- *   TARGET ::= OPTIONS ITEM_PATTERN
+ *   TARGET ::= OPTIONS ITEM_REFERENCE
  *   OPTIONS ::= keep-all | OPTION+
  *   OPTION ::= shrinking | optimizing | obfuscating | access-modification | annotation-removal
  *
+ *   ITEM_REFERENCE  ::= BINDING_REFERENCE | ITEM_PATTERN
+ *   CLASS_REFERENCE ::= BINDING_REFERENCE | QUALIFIED_CLASS_NAME_PATTERN
+ *
  *   ITEM_PATTERN
  *     ::= any
- *       | class QUALIFIED_CLASS_NAME_PATTERN extends EXTENDS_PATTERN { MEMBER_PATTERN }
+ *       | class CLASS_REFERENCE extends EXTENDS_PATTERN { MEMBER_PATTERN }
  *
  *   TYPE_PATTERN ::= any | exact type-descriptor
  *   PACKAGE_PATTERN ::= any | exact package-name
- *   QUALIFIED_CLASS_NAME_PATTERN ::= any | PACKAGE_PATTERN | UNQUALIFIED_CLASS_NAME_PATTERN
+ *
+ *   QUALIFIED_CLASS_NAME_PATTERN
+ *     ::= any
+ *       | PACKAGE_PATTERN UNQUALIFIED_CLASS_NAME_PATTERN
+ *       | BINDING_REFERENCE
+ *
  *   UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
+ *
  *   EXTENDS_PATTERN ::= any | QUALIFIED_CLASS_NAME_PATTERN
  *
  *   MEMBER_PATTERN ::= none | all | FIELD_PATTERN | METHOD_PATTERN
@@ -65,11 +78,22 @@
 
   public static class Builder {
     private KeepEdgeMetaInfo metaInfo = KeepEdgeMetaInfo.none();
+    private KeepBindings bindings = KeepBindings.none();
     private KeepPreconditions preconditions = KeepPreconditions.always();
     private KeepConsequences consequences;
 
     private Builder() {}
 
+    public Builder setMetaInfo(KeepEdgeMetaInfo metaInfo) {
+      this.metaInfo = metaInfo;
+      return this;
+    }
+
+    public Builder setBindings(KeepBindings bindings) {
+      this.bindings = bindings;
+      return this;
+    }
+
     public Builder setPreconditions(KeepPreconditions preconditions) {
       this.preconditions = preconditions;
       return this;
@@ -80,16 +104,11 @@
       return this;
     }
 
-    public Builder setMetaInfo(KeepEdgeMetaInfo metaInfo) {
-      this.metaInfo = metaInfo;
-      return this;
-    }
-
     public KeepEdge build() {
       if (consequences.isEmpty()) {
         throw new KeepEdgeException("KeepEdge must have non-empty set of consequences.");
       }
-      return new KeepEdge(preconditions, consequences, metaInfo);
+      return new KeepEdge(metaInfo, bindings, preconditions, consequences);
     }
   }
 
@@ -98,23 +117,33 @@
   }
 
   private final KeepEdgeMetaInfo metaInfo;
+  private final KeepBindings bindings;
   private final KeepPreconditions preconditions;
   private final KeepConsequences consequences;
 
   private KeepEdge(
-      KeepPreconditions preconditions, KeepConsequences consequences, KeepEdgeMetaInfo metaInfo) {
+      KeepEdgeMetaInfo metaInfo,
+      KeepBindings bindings,
+      KeepPreconditions preconditions,
+      KeepConsequences consequences) {
+    assert metaInfo != null;
+    assert bindings != null;
     assert preconditions != null;
     assert consequences != null;
-    assert metaInfo != null;
+    this.metaInfo = metaInfo;
+    this.bindings = bindings;
     this.preconditions = preconditions;
     this.consequences = consequences;
-    this.metaInfo = metaInfo;
   }
 
   public KeepEdgeMetaInfo getMetaInfo() {
     return metaInfo;
   }
 
+  public KeepBindings getBindings() {
+    return bindings;
+  }
+
   public KeepPreconditions getPreconditions() {
     return preconditions;
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
index 7f84090..6dfe7e4 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
@@ -3,7 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import java.util.Collection;
 import java.util.Objects;
+import java.util.function.Predicate;
 
 /**
  * A pattern for matching items in the program.
@@ -18,35 +20,52 @@
 public class KeepItemPattern {
 
   public static KeepItemPattern any() {
-    KeepItemPattern any = builder().any().build();
-    assert any.isAny();
-    return any;
+    return builder().any().build();
   }
 
   public static Builder builder() {
     return new Builder();
   }
 
+  public boolean isClassItemPattern() {
+    return memberPattern.isNone();
+  }
+
+  public boolean isMemberItemPattern() {
+    return !memberPattern.isNone();
+  }
+
   public static class Builder {
 
-    private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
+    private KeepClassReference classReference =
+        KeepClassReference.fromClassNamePattern(KeepQualifiedClassNamePattern.any());
     private KeepExtendsPattern extendsPattern = KeepExtendsPattern.any();
     private KeepMemberPattern memberPattern = KeepMemberPattern.none();
 
     private Builder() {}
 
+    public Builder copyFrom(KeepItemPattern pattern) {
+      return setClassReference(pattern.getClassReference())
+          .setExtendsPattern(pattern.getExtendsPattern())
+          .setMemberPattern(pattern.getMemberPattern());
+    }
+
     public Builder any() {
-      classNamePattern = KeepQualifiedClassNamePattern.any();
+      classReference = KeepClassReference.fromClassNamePattern(KeepQualifiedClassNamePattern.any());
       extendsPattern = KeepExtendsPattern.any();
       memberPattern = KeepMemberPattern.all();
       return this;
     }
 
-    public Builder setClassPattern(KeepQualifiedClassNamePattern qualifiedClassNamePattern) {
-      this.classNamePattern = qualifiedClassNamePattern;
+    public Builder setClassReference(KeepClassReference classReference) {
+      this.classReference = classReference;
       return this;
     }
 
+    public Builder setClassPattern(KeepQualifiedClassNamePattern qualifiedClassNamePattern) {
+      return setClassReference(KeepClassReference.fromClassNamePattern(qualifiedClassNamePattern));
+    }
+
     public Builder setExtendsPattern(KeepExtendsPattern extendsPattern) {
       this.extendsPattern = extendsPattern;
       return this;
@@ -58,33 +77,33 @@
     }
 
     public KeepItemPattern build() {
-      return new KeepItemPattern(classNamePattern, extendsPattern, memberPattern);
+      return new KeepItemPattern(classReference, extendsPattern, memberPattern);
     }
   }
 
-  private final KeepQualifiedClassNamePattern qualifiedClassPattern;
+  private final KeepClassReference classReference;
   private final KeepExtendsPattern extendsPattern;
   private final KeepMemberPattern memberPattern;
   // TODO: class annotations
 
   private KeepItemPattern(
-      KeepQualifiedClassNamePattern qualifiedClassPattern,
+      KeepClassReference classReference,
       KeepExtendsPattern extendsPattern,
       KeepMemberPattern memberPattern) {
-    assert qualifiedClassPattern != null;
+    assert classReference != null;
     assert extendsPattern != null;
     assert memberPattern != null;
-    this.qualifiedClassPattern = qualifiedClassPattern;
+    this.classReference = classReference;
     this.extendsPattern = extendsPattern;
     this.memberPattern = memberPattern;
   }
 
-  public boolean isAny() {
-    return qualifiedClassPattern.isAny() && extendsPattern.isAny() && memberPattern.isAll();
+  public boolean isAny(Predicate<String> onReference) {
+    return extendsPattern.isAny() && memberPattern.isAll() && classReference.isAny(onReference);
   }
 
-  public KeepQualifiedClassNamePattern getClassNamePattern() {
-    return qualifiedClassPattern;
+  public KeepClassReference getClassReference() {
+    return classReference;
   }
 
   public KeepExtendsPattern getExtendsPattern() {
@@ -95,6 +114,10 @@
     return memberPattern;
   }
 
+  public Collection<String> getBindingReferences() {
+    return classReference.getBindingReferences();
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
@@ -104,21 +127,21 @@
       return false;
     }
     KeepItemPattern that = (KeepItemPattern) obj;
-    return qualifiedClassPattern.equals(that.qualifiedClassPattern)
+    return classReference.equals(that.classReference)
         && extendsPattern.equals(that.extendsPattern)
         && memberPattern.equals(that.memberPattern);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(qualifiedClassPattern, extendsPattern, memberPattern);
+    return Objects.hash(classReference, extendsPattern, memberPattern);
   }
 
   @Override
   public String toString() {
     return "KeepClassPattern{"
-        + "qualifiedClassPattern="
-        + qualifiedClassPattern
+        + "classReference="
+        + classReference
         + ", extendsPattern="
         + extendsPattern
         + ", memberPattern="
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java
new file mode 100644
index 0000000..63a111b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemReference.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+public abstract class KeepItemReference {
+
+  public static KeepItemReference fromBindingReference(String bindingReference) {
+    return new BindingReference(bindingReference);
+  }
+
+  public static KeepItemReference fromItemPattern(KeepItemPattern itemPattern) {
+    return new SomeItem(itemPattern);
+  }
+
+  public boolean isBindingReference() {
+    return asBindingReference() != null;
+  }
+
+  public boolean isItemPattern() {
+    return asItemPattern() != null;
+  }
+
+  public String asBindingReference() {
+    return null;
+  }
+
+  public KeepItemPattern asItemPattern() {
+    return null;
+  }
+
+  public abstract KeepItemPattern lookupItemPattern(KeepBindings bindings);
+
+  private static class BindingReference extends KeepItemReference {
+    private final String bindingReference;
+
+    private BindingReference(String bindingReference) {
+      assert bindingReference != null;
+      this.bindingReference = bindingReference;
+    }
+
+    @Override
+    public String asBindingReference() {
+      return bindingReference;
+    }
+
+    @Override
+    public KeepItemPattern lookupItemPattern(KeepBindings bindings) {
+      return bindings.get(bindingReference).getItem();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      BindingReference that = (BindingReference) o;
+      return bindingReference.equals(that.bindingReference);
+    }
+
+    @Override
+    public int hashCode() {
+      return bindingReference.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return "reference='" + bindingReference + "'";
+    }
+  }
+
+  private static class SomeItem extends KeepItemReference {
+    private final KeepItemPattern itemPattern;
+
+    private SomeItem(KeepItemPattern itemPattern) {
+      assert itemPattern != null;
+      this.itemPattern = itemPattern;
+    }
+
+    @Override
+    public KeepItemPattern asItemPattern() {
+      return itemPattern;
+    }
+
+    @Override
+    public KeepItemPattern lookupItemPattern(KeepBindings bindings) {
+      return asItemPattern();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SomeItem someItem = (SomeItem) o;
+      return itemPattern.equals(someItem.itemPattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return itemPattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return itemPattern.toString();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
index 6b7a212..30af390 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
@@ -9,16 +9,20 @@
 
   public static class Builder {
 
-    private KeepItemPattern item;
+    private KeepItemReference item;
     private KeepOptions options = KeepOptions.keepAll();
 
     private Builder() {}
 
-    public Builder setItem(KeepItemPattern item) {
+    public Builder setItemReference(KeepItemReference item) {
       this.item = item;
       return this;
     }
 
+    public Builder setItemPattern(KeepItemPattern itemPattern) {
+      return setItemReference(KeepItemReference.fromItemPattern(itemPattern));
+    }
+
     public Builder setOptions(KeepOptions options) {
       this.options = options;
       return this;
@@ -32,10 +36,10 @@
     }
   }
 
-  private final KeepItemPattern item;
+  private final KeepItemReference item;
   private final KeepOptions options;
 
-  private KeepTarget(KeepItemPattern item, KeepOptions options) {
+  private KeepTarget(KeepItemReference item, KeepOptions options) {
     assert item != null;
     assert options != null;
     this.item = item;
@@ -46,7 +50,7 @@
     return new Builder();
   }
 
-  public KeepItemPattern getItem() {
+  public KeepItemReference getItem() {
     return item;
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeBindingMinimizer.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeBindingMinimizer.java
new file mode 100644
index 0000000..0d0b853
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeBindingMinimizer.java
@@ -0,0 +1,139 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.keeprules;
+
+import com.android.tools.r8.keepanno.ast.KeepBindings;
+import com.android.tools.r8.keepanno.ast.KeepBindings.Builder;
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepCondition;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepPreconditions;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Compute the minimal set of unique bindings.
+ *
+ * <p>This will check if two bindings define the same exact type in which case they can and will use
+ * the same binding definition.
+ *
+ * <p>TODO(b/248408342): Consider extending this to also identify aliased members.
+ */
+public class KeepEdgeBindingMinimizer {
+
+  public static KeepEdge run(KeepEdge edge) {
+    KeepEdgeBindingMinimizer minimizer = new KeepEdgeBindingMinimizer();
+    return minimizer.minimize(edge);
+  }
+
+  Map<String, List<String>> descriptorToUniqueBindings = new HashMap<>();
+  Map<String, String> aliases = new HashMap<>();
+
+  private KeepEdge minimize(KeepEdge edge) {
+    computeAliases(edge);
+    if (aliases.isEmpty()) {
+      return edge;
+    }
+    return KeepEdge.builder()
+        .setMetaInfo(edge.getMetaInfo())
+        .setBindings(computeNewBindings(edge.getBindings()))
+        .setPreconditions(computeNewPreconditions(edge.getPreconditions()))
+        .setConsequences(computeNewConsequences(edge.getConsequences()))
+        .build();
+  }
+
+  private void computeAliases(KeepEdge edge) {
+    edge.getBindings()
+        .forEach(
+            (name, pattern) -> {
+              if (pattern.isClassItemPattern()
+                  && pattern.getClassReference().asClassNamePattern().isExact()) {
+                String descriptor =
+                    pattern.getClassReference().asClassNamePattern().getExactDescriptor();
+                List<String> others =
+                    descriptorToUniqueBindings.computeIfAbsent(descriptor, k -> new ArrayList<>());
+                String alias = findEqualBinding(pattern, others, edge);
+                if (alias != null) {
+                  aliases.put(name, alias);
+                } else {
+                  others.add(name);
+                }
+              }
+            });
+  }
+
+  private String findEqualBinding(KeepItemPattern pattern, List<String> others, KeepEdge edge) {
+    for (String otherName : others) {
+      KeepItemPattern otherItem = edge.getBindings().get(otherName).getItem();
+      if (pattern.equals(otherItem)) {
+        return otherName;
+      }
+    }
+    return null;
+  }
+
+  private String getBinding(String bindingName) {
+    return aliases.getOrDefault(bindingName, bindingName);
+  }
+
+  private KeepBindings computeNewBindings(KeepBindings bindings) {
+    Builder builder = KeepBindings.builder();
+    bindings.forEach(
+        (name, item) -> {
+          if (!aliases.containsKey(name)) {
+            builder.addBinding(name, computeNewItemPattern(item));
+          }
+        });
+    return builder.build();
+  }
+
+  private KeepPreconditions computeNewPreconditions(KeepPreconditions preconditions) {
+    if (preconditions.isAlways()) {
+      return preconditions;
+    }
+    KeepPreconditions.Builder builder = KeepPreconditions.builder();
+    preconditions.forEach(
+        condition ->
+            builder.addCondition(
+                KeepCondition.builder()
+                    .setItemReference(computeNewItemReference(condition.getItem()))
+                    .build()));
+    return builder.build();
+  }
+
+  private KeepConsequences computeNewConsequences(KeepConsequences consequences) {
+    KeepConsequences.Builder builder = KeepConsequences.builder();
+    consequences.forEachTarget(
+        target ->
+            builder.addTarget(
+                KeepTarget.builder()
+                    .setOptions(target.getOptions())
+                    .setItemReference(computeNewItemReference(target.getItem()))
+                    .build()));
+    return builder.build();
+  }
+
+  private KeepItemReference computeNewItemReference(KeepItemReference item) {
+    return item.isBindingReference()
+        ? KeepItemReference.fromBindingReference(getBinding(item.asBindingReference()))
+        : KeepItemReference.fromItemPattern(computeNewItemPattern(item.asItemPattern()));
+  }
+
+  private KeepItemPattern computeNewItemPattern(KeepItemPattern pattern) {
+    String classBinding = pattern.getClassReference().asBindingReference();
+    if (classBinding == null) {
+      return pattern;
+    }
+    return KeepItemPattern.builder()
+        .copyFrom(pattern)
+        .setClassReference(KeepClassReference.fromBindingReference(getBinding(classBinding)))
+        .build();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
new file mode 100644
index 0000000..5922be3
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
@@ -0,0 +1,164 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.keeprules;
+
+import com.android.tools.r8.keepanno.ast.KeepBindings;
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepCondition;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepPreconditions;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Normalize a keep edge with respect to its bindings. This will systematically introduce a binding
+ * for each item in the edge. It will also introduce a class binding for the holder of any member
+ * item. By introducing a binding for each item the binding can be used as item identity.
+ */
+public class KeepEdgeNormalizer {
+
+  private static final String syntheticBindingPrefix = "SyntheticBinding";
+  private static final char syntheticBindingSuffix = 'X';
+
+  public static KeepEdge normalize(KeepEdge edge) {
+    // Check that all referenced bindings are defined.
+    KeepEdgeNormalizer normalizer = new KeepEdgeNormalizer(edge);
+    KeepEdge normalized = normalizer.run();
+    KeepEdge minimized = KeepEdgeBindingMinimizer.run(normalized);
+    return minimized;
+  }
+
+  private final KeepEdge edge;
+
+  private String freshBindingNamePrefix;
+  private int nextFreshBindingNameIndex = 1;
+
+  private final KeepBindings.Builder bindingsBuilder = KeepBindings.builder();
+  private final KeepPreconditions.Builder preconditionsBuilder = KeepPreconditions.builder();
+  private final KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
+
+  private KeepEdgeNormalizer(KeepEdge edge) {
+    this.edge = edge;
+    findValidFreshBindingPrefix();
+  }
+
+  private void findValidFreshBindingPrefix() {
+    List<String> existingSuffixes = new ArrayList<>();
+    edge.getBindings()
+        .forEach(
+            (name, ignore) -> {
+              if (name.startsWith(syntheticBindingPrefix)) {
+                existingSuffixes.add(name.substring(syntheticBindingPrefix.length()));
+              }
+            });
+    if (!existingSuffixes.isEmpty()) {
+      int suffixLength = 0;
+      for (String existingSuffix : existingSuffixes) {
+        suffixLength = Math.max(suffixLength, getRepeatedSuffixLength(existingSuffix));
+      }
+      StringBuilder suffix = new StringBuilder();
+      for (int i = 0; i <= suffixLength; i++) {
+        suffix.append(syntheticBindingSuffix);
+      }
+      freshBindingNamePrefix = syntheticBindingPrefix + suffix;
+    } else {
+      freshBindingNamePrefix = syntheticBindingPrefix;
+    }
+  }
+
+  private int getRepeatedSuffixLength(String string) {
+    int i = 0;
+    while (i < string.length() && string.charAt(i) == syntheticBindingSuffix) {
+      i++;
+    }
+    return i;
+  }
+
+  private String nextFreshBindingName() {
+    return freshBindingNamePrefix + (nextFreshBindingNameIndex++);
+  }
+
+  private KeepEdge run() {
+    edge.getBindings()
+        .forEach(
+            (name, pattern) -> {
+              bindingsBuilder.addBinding(name, normalizeItemPattern(pattern));
+            });
+    // TODO(b/248408342): Normalize the preconditions by identifying vacuously true conditions.
+    edge.getPreconditions()
+        .forEach(
+            condition ->
+                preconditionsBuilder.addCondition(
+                    KeepCondition.builder()
+                        .setItemReference(normalizeItem(condition.getItem()))
+                        .build()));
+    edge.getConsequences()
+        .forEachTarget(
+            target -> {
+              consequencesBuilder.addTarget(
+                  KeepTarget.builder()
+                      .setOptions(target.getOptions())
+                      .setItemReference(normalizeItem(target.getItem()))
+                      .build());
+            });
+    return KeepEdge.builder()
+        .setMetaInfo(edge.getMetaInfo())
+        .setBindings(bindingsBuilder.build())
+        .setPreconditions(preconditionsBuilder.build())
+        .setConsequences(consequencesBuilder.build())
+        .build();
+  }
+
+  private KeepItemReference normalizeItem(KeepItemReference item) {
+    if (item.isBindingReference()) {
+      return item;
+    }
+    KeepItemPattern newItemPattern = normalizeItemPattern(item.asItemPattern());
+    String bindingName = nextFreshBindingName();
+    bindingsBuilder.addBinding(bindingName, newItemPattern);
+    return KeepItemReference.fromBindingReference(bindingName);
+  }
+
+  private KeepItemPattern normalizeItemPattern(KeepItemPattern pattern) {
+    // If the pattern is just a class pattern it is in normal form.
+    if (pattern.isClassItemPattern()) {
+      return pattern;
+    }
+    KeepClassReference bindingReference = bindingForClassItem(pattern);
+    return getMemberItemPattern(pattern, bindingReference);
+  }
+
+  private KeepClassReference bindingForClassItem(KeepItemPattern pattern) {
+    KeepClassReference classReference = pattern.getClassReference();
+    if (classReference.isBindingReference()) {
+      // If the class is already defined via a binding then no need to introduce a new one and
+      // change the item.
+      return classReference;
+    }
+    String bindingName = nextFreshBindingName();
+    KeepClassReference bindingReference = KeepClassReference.fromBindingReference(bindingName);
+    KeepItemPattern newClassPattern = getClassItemPattern(pattern);
+    bindingsBuilder.addBinding(bindingName, newClassPattern);
+    return bindingReference;
+  }
+
+  private KeepItemPattern getClassItemPattern(KeepItemPattern fromPattern) {
+    return KeepItemPattern.builder()
+        .setClassReference(fromPattern.getClassReference())
+        .setExtendsPattern(fromPattern.getExtendsPattern())
+        .build();
+  }
+
+  private KeepItemPattern getMemberItemPattern(
+      KeepItemPattern fromPattern, KeepClassReference classReference) {
+    return KeepItemPattern.builder()
+        .setClassReference(classReference)
+        .setMemberPattern(fromPattern.getMemberPattern())
+        .build();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeSplitter.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeSplitter.java
new file mode 100644
index 0000000..f565635
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeSplitter.java
@@ -0,0 +1,377 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.keeprules;
+
+import com.android.tools.r8.keepanno.ast.KeepBindings;
+import com.android.tools.r8.keepanno.ast.KeepCondition;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
+import com.android.tools.r8.keepanno.ast.KeepOptions;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgConditionalClassRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgConditionalMemberRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgDependentClassRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgDependentMembersRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgUnconditionalClassRule;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/** Split a keep edge into multiple PG rules that over-approximate it. */
+public class KeepEdgeSplitter {
+
+  public static Collection<PgRule> split(KeepEdge edge) {
+    return doSplit(KeepEdgeNormalizer.normalize(edge));
+  }
+
+  /**
+   * Utility to package up a class binding with its name and item pattern.
+   *
+   * <p>This is useful as the normalizer will have introduced class reference indirections so a
+   * given item may need to.
+   */
+  public static class Holder {
+    final KeepItemPattern itemPattern;
+    final KeepQualifiedClassNamePattern namePattern;
+
+    static Holder create(String bindingName, KeepBindings bindings) {
+      KeepItemPattern itemPattern = bindings.get(bindingName).getItem();
+      assert itemPattern.isClassItemPattern();
+      KeepQualifiedClassNamePattern namePattern = getClassNamePattern(itemPattern, bindings);
+      return new Holder(itemPattern, namePattern);
+    }
+
+    private Holder(KeepItemPattern itemPattern, KeepQualifiedClassNamePattern namePattern) {
+      this.itemPattern = itemPattern;
+      this.namePattern = namePattern;
+    }
+  }
+
+  private static class BindingUsers {
+
+    final Holder holder;
+    final Set<String> conditionRefs = new HashSet<>();
+    final Map<KeepOptions, Set<String>> targetRefs = new HashMap<>();
+
+    static BindingUsers create(String bindingName, KeepBindings bindings) {
+      return new BindingUsers(Holder.create(bindingName, bindings));
+    }
+
+    private BindingUsers(Holder holder) {
+      this.holder = holder;
+    }
+
+    public void addCondition(KeepCondition condition) {
+      assert condition.getItem().isBindingReference();
+      conditionRefs.add(condition.getItem().asBindingReference());
+    }
+
+    public void addTarget(KeepTarget target) {
+      assert target.getItem().isBindingReference();
+      targetRefs
+          .computeIfAbsent(target.getOptions(), k -> new HashSet<>())
+          .add(target.getItem().asBindingReference());
+    }
+  }
+
+  private static Collection<PgRule> doSplit(KeepEdge edge) {
+    List<PgRule> rules = new ArrayList<>();
+
+    // First step after normalizing is to group up all conditions and targets on their target class.
+    // Here we use the normalized binding as the notion of identity on a class.
+    KeepBindings bindings = edge.getBindings();
+    Map<String, BindingUsers> bindingUsers = new HashMap<>();
+    edge.getPreconditions()
+        .forEach(
+            condition -> {
+              String classReference = getClassItemBindingReference(condition.getItem(), bindings);
+              assert classReference != null;
+              bindingUsers
+                  .computeIfAbsent(classReference, k -> BindingUsers.create(k, bindings))
+                  .addCondition(condition);
+            });
+    edge.getConsequences()
+        .forEachTarget(
+            target -> {
+              String classReference = getClassItemBindingReference(target.getItem(), bindings);
+              assert classReference != null;
+              bindingUsers
+                  .computeIfAbsent(classReference, k -> BindingUsers.create(k, bindings))
+                  .addTarget(target);
+            });
+
+    bindingUsers.forEach(
+        (targetBindingName, users) -> {
+          Holder targetHolder = users.holder;
+          if (!users.conditionRefs.isEmpty() && !users.targetRefs.isEmpty()) {
+            // The targets depend on the condition and thus we generate just the dependent edges.
+            users.targetRefs.forEach(
+                (options, targets) -> {
+                  createDependentRules(
+                      rules,
+                      targetHolder,
+                      edge.getMetaInfo(),
+                      bindings,
+                      options,
+                      users.conditionRefs,
+                      targets);
+                });
+          } else if (!users.targetRefs.isEmpty()) {
+            // The targets don't have a binding relation to any conditions, so we generate a rule
+            // per condition, or a single unconditional edge if no conditions exist.
+            if (edge.getPreconditions().isAlways()) {
+              users.targetRefs.forEach(
+                  ((options, targets) -> {
+                    createUnconditionalRules(
+                        rules, targetHolder, edge.getMetaInfo(), bindings, options, targets);
+                  }));
+            } else {
+              users.targetRefs.forEach(
+                  ((options, targets) -> {
+                    // Note that here we iterate over *all* non-empty conditions and create rules.
+                    bindingUsers.forEach(
+                        (conditionBindingName, conditionUsers) -> {
+                          if (!conditionUsers.conditionRefs.isEmpty()) {
+                            createConditionalRules(
+                                rules,
+                                edge.getMetaInfo(),
+                                conditionUsers.holder,
+                                targetHolder,
+                                bindings,
+                                options,
+                                conditionUsers.conditionRefs,
+                                targets);
+                          }
+                        });
+                  }));
+            }
+          }
+        });
+
+    assert !rules.isEmpty();
+    return rules;
+  }
+
+  private static List<String> computeConditions(
+      Set<String> conditions,
+      KeepBindings bindings,
+      Map<String, KeepMemberPattern> memberPatterns) {
+    List<String> conditionMembers = new ArrayList<>();
+    conditions.forEach(
+        conditionReference -> {
+          KeepItemPattern item = bindings.get(conditionReference).getItem();
+          if (item.isMemberItemPattern()) {
+            KeepMemberPattern old = memberPatterns.put(conditionReference, item.getMemberPattern());
+            conditionMembers.add(conditionReference);
+            assert old == null;
+          }
+        });
+    return conditionMembers;
+  }
+
+  private static void computeTargets(
+      Set<String> targets,
+      KeepBindings bindings,
+      Map<String, KeepMemberPattern> memberPatterns,
+      Runnable onKeepClass,
+      BiConsumer<Map<String, KeepMemberPattern>, List<String>> onKeepMembers) {
+    List<String> targetMembers = new ArrayList<>();
+    boolean keepClassTarget = false;
+    for (String targetReference : targets) {
+      KeepItemPattern item = bindings.get(targetReference).getItem();
+      if (item.isClassItemPattern() || bindings.isAny(item)) {
+        keepClassTarget = true;
+      }
+      if (item.isMemberItemPattern()) {
+        memberPatterns.putIfAbsent(targetReference, item.getMemberPattern());
+        targetMembers.add(targetReference);
+      }
+    }
+    if (keepClassTarget) {
+      onKeepClass.run();
+    }
+    if (!targetMembers.isEmpty()) {
+      onKeepMembers.accept(memberPatterns, targetMembers);
+    }
+  }
+
+  private static void createUnconditionalRules(
+      List<PgRule> rules,
+      Holder holder,
+      KeepEdgeMetaInfo metaInfo,
+      KeepBindings bindings,
+      KeepOptions options,
+      Set<String> targets) {
+    computeTargets(
+        targets,
+        bindings,
+        new HashMap<>(),
+        () -> {
+          rules.add(new PgUnconditionalClassRule(metaInfo, options, holder));
+        },
+        (memberPatterns, targetMembers) -> {
+          // Members are still dependent on the class, so they go to the implicitly dependent rule.
+          rules.add(
+              new PgDependentMembersRule(
+                  metaInfo,
+                  holder,
+                  options,
+                  memberPatterns,
+                  Collections.emptyList(),
+                  targetMembers));
+        });
+  }
+
+  private static void createConditionalRules(
+      List<PgRule> rules,
+      KeepEdgeMetaInfo metaInfo,
+      Holder conditionHolder,
+      Holder targetHolder,
+      KeepBindings bindings,
+      KeepOptions options,
+      Set<String> conditions,
+      Set<String> targets) {
+
+    Map<String, KeepMemberPattern> memberPatterns = new HashMap<>();
+    List<String> conditionMembers = computeConditions(conditions, bindings, memberPatterns);
+
+    computeTargets(
+        targets,
+        bindings,
+        memberPatterns,
+        () ->
+            rules.add(
+                new PgConditionalClassRule(
+                    metaInfo,
+                    options,
+                    conditionHolder,
+                    targetHolder,
+                    memberPatterns,
+                    conditionMembers)),
+        (ignore, targetMembers) ->
+            rules.add(
+                new PgConditionalMemberRule(
+                    metaInfo,
+                    options,
+                    conditionHolder,
+                    targetHolder,
+                    memberPatterns,
+                    conditionMembers,
+                    targetMembers)));
+  }
+
+  // For a conditional and dependent edge (e.g., the condition and target both reference holder X),
+  // we can assume the general form of:
+  //
+  //   { X, memberConds } -> { X, memberTargets }
+  //
+  // First, we assume that if memberConds=={} then X is in the conditions, otherwise the conditions
+  // are empty (i.e. always true) and this is not a dependent edge.
+  //
+  // Without change in meaning we can always assume X in conditions as it either was and if not then
+  // the condition on a member implicitly entails a condition on the holder.
+  //
+  // Next we can split any such edge into two edges:
+  //
+  //   { X, memberConds } -> { X }
+  //   { X, memberConds } -> { memberTargets }
+  //
+  // The first edge, if present, gives rise to the rule:
+  //
+  //   -if class X { memberConds } -keep class <1>
+  //
+  // The second rule only pertains to keeping member targets and those targets are kept as a
+  // -keepclassmembers such that they are still conditional on the holder being referenced/live.
+  // If the only precondition is the holder, then it can omitted, thus we generate:
+  // If memberConds={}:
+  //   -keepclassmembers class X { memberTargets }
+  // else:
+  //   -if class X { memberConds } -keepclassmembers X { memberTargets }
+  //
+  private static void createDependentRules(
+      List<PgRule> rules,
+      Holder holder,
+      KeepEdgeMetaInfo metaInfo,
+      KeepBindings bindings,
+      KeepOptions options,
+      Set<String> conditions,
+      Set<String> targets) {
+    Map<String, KeepMemberPattern> memberPatterns = new HashMap<>();
+    List<String> conditionMembers = computeConditions(conditions, bindings, memberPatterns);
+    computeTargets(
+        targets,
+        bindings,
+        memberPatterns,
+        () ->
+            rules.add(
+                new PgDependentClassRule(
+                    metaInfo, holder, options, memberPatterns, conditionMembers)),
+        (ignore, targetMembers) ->
+            rules.add(
+                new PgDependentMembersRule(
+                    metaInfo, holder, options, memberPatterns, conditionMembers, targetMembers)));
+  }
+
+  private static KeepQualifiedClassNamePattern getClassNamePattern(
+      KeepItemPattern itemPattern, KeepBindings bindings) {
+    return itemPattern.getClassReference().isClassNamePattern()
+        ? itemPattern.getClassReference().asClassNamePattern()
+        : getClassNamePattern(
+            bindings.get(itemPattern.getClassReference().asBindingReference()).getItem(), bindings);
+  }
+
+  private static String getClassItemBindingReference(
+      KeepItemReference itemReference, KeepBindings bindings) {
+    String classReference = null;
+    for (String reference : getTransitiveBindingReferences(itemReference, bindings)) {
+      if (bindings.get(reference).getItem().isClassItemPattern()) {
+        if (classReference != null) {
+          throw new KeepEdgeException("Unexpected reference to multiple class bindings");
+        }
+        classReference = reference;
+      }
+    }
+    return classReference;
+  }
+
+  private static Set<String> getTransitiveBindingReferences(
+      KeepItemReference itemReference, KeepBindings bindings) {
+    Set<String> references = new HashSet<>(2);
+    Deque<String> worklist = new ArrayDeque<>();
+    worklist.addAll(getBindingReference(itemReference));
+    while (!worklist.isEmpty()) {
+      String bindingReference = worklist.pop();
+      if (references.add(bindingReference)) {
+        worklist.addAll(getBindingReference(bindings.get(bindingReference).getItem()));
+      }
+    }
+    return references;
+  }
+
+  private static Collection<String> getBindingReference(KeepItemReference itemReference) {
+    if (itemReference.isBindingReference()) {
+      return Collections.singletonList(itemReference.asBindingReference());
+    }
+    return getBindingReference(itemReference.asItemPattern());
+  }
+
+  private static Collection<String> getBindingReference(KeepItemPattern itemPattern) {
+    return itemPattern.getClassReference().isBindingReference()
+        ? Collections.singletonList(itemPattern.getClassReference().asBindingReference())
+        : Collections.emptyList();
+  }
+}
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 f5739e1..39ae46d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.keeprules;
 
-import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
@@ -21,17 +21,16 @@
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
-import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.utils.Unimplemented;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+/** Extract out a sequence of Proguard keep rules that give a conservative over-approximation. */
 public class KeepRuleExtractor {
 
   private final Consumer<String> ruleConsumer;
@@ -41,17 +40,16 @@
   }
 
   public void extract(KeepEdge edge) {
-    List<ItemRule> consequentRules = getConsequentRules(edge.getConsequences());
-    printConditionalRules(consequentRules, edge.getPreconditions(), edge.getMetaInfo());
+    Collection<PgRule> rules = KeepEdgeSplitter.split(edge);
+    StringBuilder builder = new StringBuilder();
+    for (PgRule rule : rules) {
+      rule.printRule(builder);
+      builder.append("\n");
+    }
+    ruleConsumer.accept(builder.toString());
   }
 
-  private List<ItemRule> getConsequentRules(KeepConsequences consequences) {
-    List<ItemRule> consequentItems = new ArrayList<>();
-    consequences.forEachTarget(target -> consequentItems.add(new ItemRule(target)));
-    return consequentItems;
-  }
-
-  private void printHeader(StringBuilder builder, KeepEdgeMetaInfo metaInfo) {
+  public static void printHeader(StringBuilder builder, KeepEdgeMetaInfo metaInfo) {
     if (metaInfo.hasContext()) {
       builder.append("# context: ").append(metaInfo.getContextDescriptorString()).append('\n');
     }
@@ -61,7 +59,7 @@
     }
   }
 
-  private String escapeChar(char c) {
+  public static String escapeChar(char c) {
     if (c == '\n') {
       return "\\n";
     }
@@ -71,7 +69,7 @@
     return null;
   }
 
-  private String escapeLineBreaks(String string) {
+  public static String escapeLineBreaks(String string) {
     char[] charArray = string.toCharArray();
     for (int i = 0; i < charArray.length; i++) {
       // We don't expect escape chars, so wait with constructing a new string until found.
@@ -92,86 +90,37 @@
     return string;
   }
 
-  private void printConditionalRules(
-      List<ItemRule> consequentRules, KeepPreconditions preconditions, KeepEdgeMetaInfo metaInfo) {
-    boolean[] hasAtLeastOneConditionalClause = new boolean[1];
-    preconditions.forEach(
-        condition -> {
-          KeepItemPattern conditionItem = condition.getItemPattern();
-          // If the conditions is "any" then we ignore it for now (identity of conjunction).
-          if (conditionItem.isAny()) {
-            return;
-          }
-          hasAtLeastOneConditionalClause[0] = true;
-          consequentRules.forEach(
-              consequentItem -> {
-                // Since conjunctions are not supported in keep rules, we expand them into
-                // disjunctions so conservatively we keep the consequences if any one of
-                // the preconditions hold.
-                StringBuilder builder = new StringBuilder();
-                printHeader(builder, metaInfo);
-                if (!consequentItem.isMemberOnlyConsequent()
-                    || !conditionItem
-                        .getClassNamePattern()
-                        .equals(consequentItem.getHolderPattern())) {
-                  builder.append("-if ");
-                  printItem(builder, conditionItem);
-                  builder.append(' ');
-                }
-                printConsequentRule(builder, consequentItem);
-                ruleConsumer.accept(builder.toString());
-              });
-        });
-    assert !(preconditions.isAlways() && hasAtLeastOneConditionalClause[0]);
-    if (!hasAtLeastOneConditionalClause[0]) {
-      // If there are no preconditions, print each consequent as is.
-      consequentRules.forEach(
-          r -> {
-            StringBuilder builder = new StringBuilder();
-            printHeader(builder, metaInfo);
-            ruleConsumer.accept(printConsequentRule(builder, r).toString());
-          });
-    }
-  }
-
-  private static StringBuilder printConsequentRule(StringBuilder builder, ItemRule rule) {
-    if (rule.isMemberOnlyConsequent()) {
-      builder.append("-keepclassmembers");
-    } else {
-      builder.append("-keep");
-    }
+  public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
     for (KeepOption option : KeepOption.values()) {
-      if (rule.options.isAllowed(option)) {
+      if (options.isAllowed(option)) {
         builder.append(",allow").append(getOptionString(option));
       }
     }
-    return builder.append(" ").append(rule.getKeepRuleForItem());
   }
 
-  private static StringBuilder printItem(StringBuilder builder, KeepItemPattern clazzPattern) {
+  public static StringBuilder printClassHeader(
+      StringBuilder builder,
+      KeepItemPattern classPattern,
+      BiConsumer<StringBuilder, KeepClassReference> printClassReference) {
     builder.append("class ");
-    printClassName(builder, clazzPattern.getClassNamePattern());
-    KeepExtendsPattern extendsPattern = clazzPattern.getExtendsPattern();
+    printClassReference.accept(builder, classPattern.getClassReference());
+    KeepExtendsPattern extendsPattern = classPattern.getExtendsPattern();
     if (!extendsPattern.isAny()) {
       builder.append(" extends ");
       printClassName(builder, extendsPattern.asClassNamePattern());
     }
-    KeepMemberPattern member = clazzPattern.getMemberPattern();
-    if (member.isNone()) {
-      return builder;
-    }
+    return builder;
+  }
+
+  public static StringBuilder printMemberClause(StringBuilder builder, KeepMemberPattern member) {
     if (member.isAll()) {
-      return builder.append(" { *; }");
+      return builder.append("*;");
     }
     if (member.isMethod()) {
-      builder.append(" {");
-      printMethod(builder.append(' '), member.asMethod());
-      return builder.append(" }");
+      return printMethod(builder, member.asMethod());
     }
     if (member.isField()) {
-      builder.append(" {");
-      printField(builder.append(' '), member.asField());
-      return builder.append(" }");
+      return printField(builder, member.asField());
     }
     throw new Unimplemented();
   }
@@ -259,7 +208,7 @@
     throw new Unimplemented();
   }
 
-  private static StringBuilder printClassName(
+  public static StringBuilder printClassName(
       StringBuilder builder, KeepQualifiedClassNamePattern classNamePattern) {
     if (classNamePattern.isAny()) {
       return builder.append('*');
@@ -369,33 +318,4 @@
     }
     throw new KeepEdgeException("Invalid array descriptor: " + descriptor);
   }
-
-  private static class ItemRule {
-    private final KeepTarget target;
-    private final KeepOptions options;
-    private String ruleLine = null;
-
-    public ItemRule(KeepTarget target) {
-      this.target = target;
-      this.options = target.getOptions();
-    }
-
-    public boolean isMemberOnlyConsequent() {
-      KeepItemPattern item = target.getItem();
-      return !item.isAny() && !item.getMemberPattern().isNone();
-    }
-
-    public KeepQualifiedClassNamePattern getHolderPattern() {
-      return target.getItem().getClassNamePattern();
-    }
-
-    public String getKeepRuleForItem() {
-      if (ruleLine == null) {
-        KeepItemPattern item = target.getItem();
-        ruleLine =
-            item.isAny() ? "class * { *; }" : printItem(new StringBuilder(), item).toString();
-      }
-      return ruleLine;
-    }
-  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
new file mode 100644
index 0000000..9a9ccc0
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
@@ -0,0 +1,488 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.keeprules;
+
+import com.android.tools.r8.keepanno.ast.KeepClassReference;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
+import com.android.tools.r8.keepanno.ast.KeepOptions;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.keeprules.KeepEdgeSplitter.Holder;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+public abstract class PgRule {
+  private final KeepEdgeMetaInfo metaInfo;
+  private final KeepOptions options;
+
+  private PgRule(KeepEdgeMetaInfo metaInfo, KeepOptions options) {
+    this.metaInfo = metaInfo;
+    this.options = options;
+  }
+
+  // Helper to print the class-name pattern in a class-item.
+  // The item is assumed to either be a binding (where the binding is a class with
+  // the supplied class-name pattern), or a class-item that has the class-name pattern itself (e.g.,
+  // without a binding indirection).
+  public static BiConsumer<StringBuilder, KeepClassReference> classReferencePrinter(
+      KeepQualifiedClassNamePattern classNamePattern) {
+    return (StringBuilder builder, KeepClassReference classReference) -> {
+      assert classReference.isBindingReference()
+          || classReference.asClassNamePattern().equals(classNamePattern);
+      KeepRuleExtractor.printClassName(builder, classNamePattern);
+    };
+  }
+
+  void printKeepOptions(StringBuilder builder) {
+    KeepRuleExtractor.printKeepOptions(builder, options);
+  }
+
+  public void printRule(StringBuilder builder) {
+    KeepRuleExtractor.printHeader(builder, metaInfo);
+    printCondition(builder);
+    printConsequence(builder);
+  }
+
+  void printCondition(StringBuilder builder) {
+    if (hasCondition()) {
+      builder.append("-if ");
+      printConditionHolder(builder);
+      List<String> members = getConditionMembers();
+      if (!members.isEmpty()) {
+        builder.append(" {");
+        for (String member : members) {
+          builder.append(' ');
+          printConditionMember(builder, member);
+        }
+        builder.append(" }");
+      }
+      builder.append(' ');
+    }
+  }
+
+  void printConsequence(StringBuilder builder) {
+    builder.append(getConsequenceKeepType());
+    printKeepOptions(builder);
+    builder.append(' ');
+    printTargetHolder(builder);
+    List<String> members = getTargetMembers();
+    if (!members.isEmpty()) {
+      builder.append(" {");
+      for (String member : members) {
+        builder.append(' ');
+        printTargetMember(builder, member);
+      }
+      builder.append(" }");
+    }
+  }
+
+  boolean hasCondition() {
+    return false;
+  }
+  ;
+
+  List<String> getConditionMembers() {
+    throw new KeepEdgeException("Unreachable");
+  }
+
+  abstract String getConsequenceKeepType();
+
+  abstract List<String> getTargetMembers();
+
+  void printConditionHolder(StringBuilder builder) {
+    throw new KeepEdgeException("Unreachable");
+  }
+
+  void printConditionMember(StringBuilder builder, String member) {
+    throw new KeepEdgeException("Unreachable");
+  }
+
+  abstract void printTargetHolder(StringBuilder builder);
+
+  abstract void printTargetMember(StringBuilder builder, String member);
+
+  /**
+   * Representation of an unconditional rule to keep a class.
+   *
+   * <pre>
+   *   -keep class <holder>
+   * </pre>
+   *
+   * and with no dependencies / back-references.
+   */
+  static class PgUnconditionalClassRule extends PgRule {
+    final KeepQualifiedClassNamePattern holderNamePattern;
+    final KeepItemPattern holderPattern;
+
+    public PgUnconditionalClassRule(KeepEdgeMetaInfo metaInfo, KeepOptions options, Holder holder) {
+      super(metaInfo, options);
+      this.holderNamePattern = holder.namePattern;
+      this.holderPattern = holder.itemPattern;
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return "-keep";
+    }
+
+    @Override
+    List<String> getTargetMembers() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    void printTargetHolder(StringBuilder builder) {
+      KeepRuleExtractor.printClassHeader(
+          builder, holderPattern, classReferencePrinter(holderNamePattern));
+    }
+
+    @Override
+    void printTargetMember(StringBuilder builder, String memberReference) {
+      throw new KeepEdgeException("Unreachable");
+    }
+  }
+
+  abstract static class PgConditionalRuleBase extends PgRule {
+    final KeepItemPattern classCondition;
+    final KeepItemPattern classTarget;
+    final Map<String, KeepMemberPattern> memberPatterns;
+    final List<String> memberConditions;
+
+    public PgConditionalRuleBase(
+        KeepEdgeMetaInfo metaInfo,
+        KeepOptions options,
+        Holder classCondition,
+        Holder classTarget,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions) {
+      super(metaInfo, options);
+      this.classCondition = classCondition.itemPattern;
+      this.classTarget = classTarget.itemPattern;
+      this.memberPatterns = memberPatterns;
+      this.memberConditions = memberConditions;
+    }
+
+    @Override
+    boolean hasCondition() {
+      return true;
+    }
+
+    @Override
+    List<String> getConditionMembers() {
+      return memberConditions;
+    }
+
+    @Override
+    void printConditionHolder(StringBuilder builder) {
+      KeepRuleExtractor.printClassHeader(builder, classCondition, this::printClassName);
+    }
+
+    @Override
+    void printConditionMember(StringBuilder builder, String member) {
+      KeepMemberPattern memberPattern = memberPatterns.get(member);
+      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+    }
+
+    @Override
+    void printTargetHolder(StringBuilder builder) {
+      KeepRuleExtractor.printClassHeader(builder, classTarget, this::printClassName);
+    }
+
+    void printClassName(StringBuilder builder, KeepClassReference clazz) {
+      KeepRuleExtractor.printClassName(builder, clazz.asClassNamePattern());
+    }
+  }
+
+  /**
+   * Representation of conditional rules but without dependencies between condition and target.
+   *
+   * <pre>
+   *   -if class <class-condition> { <member-conditions> }
+   *   -keep class <class-target>
+   * </pre>
+   *
+   * and with no dependencies / back-references.
+   */
+  static class PgConditionalClassRule extends PgConditionalRuleBase {
+
+    public PgConditionalClassRule(
+        KeepEdgeMetaInfo metaInfo,
+        KeepOptions options,
+        Holder classCondition,
+        Holder classTarget,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions) {
+      super(metaInfo, options, classCondition, classTarget, memberPatterns, memberConditions);
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return "-keep";
+    }
+
+    @Override
+    List<String> getTargetMembers() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    void printTargetMember(StringBuilder builder, String member) {
+      throw new KeepEdgeException("Unreachable");
+    }
+  }
+
+  /**
+   * Representation of conditional rules but without dependencies between condition and target.
+   *
+   * <pre>
+   *   -if class <class-condition> { <member-conditions> }
+   *   -keep[classmembers] class <class-target> { <member-targets> }
+   * </pre>
+   *
+   * and with no dependencies / back-references.
+   */
+  static class PgConditionalMemberRule extends PgConditionalRuleBase {
+
+    private final List<String> memberTargets;
+
+    public PgConditionalMemberRule(
+        KeepEdgeMetaInfo metaInfo,
+        KeepOptions options,
+        Holder classCondition,
+        Holder classTarget,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions,
+        List<String> memberTargets) {
+      super(metaInfo, options, classCondition, classTarget, memberPatterns, memberConditions);
+      this.memberTargets = memberTargets;
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return "-keepclassmembers";
+    }
+
+    @Override
+    List<String> getTargetMembers() {
+      return memberTargets;
+    }
+
+    @Override
+    void printTargetMember(StringBuilder builder, String member) {
+      KeepMemberPattern memberPattern = memberPatterns.get(member);
+      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+    }
+  }
+
+  abstract static class PgDependentRuleBase extends PgRule {
+
+    final KeepQualifiedClassNamePattern holderNamePattern;
+    final KeepItemPattern holderPattern;
+    final Map<String, KeepMemberPattern> memberPatterns;
+    final List<String> memberConditions;
+
+    public PgDependentRuleBase(
+        KeepEdgeMetaInfo metaInfo,
+        Holder holder,
+        KeepOptions options,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions) {
+      super(metaInfo, options);
+      this.holderNamePattern = holder.namePattern;
+      this.holderPattern = holder.itemPattern;
+      this.memberPatterns = memberPatterns;
+      this.memberConditions = memberConditions;
+    }
+
+    int nextBackReferenceNumber = 1;
+    String holderBackReferencePattern;
+    // TODO(b/248408342): Support back-ref to members too.
+
+    private StringBuilder addBackRef(StringBuilder backReferenceBuilder) {
+      return backReferenceBuilder.append('<').append(nextBackReferenceNumber++).append('>');
+    }
+
+    @Override
+    List<String> getConditionMembers() {
+      return memberConditions;
+    }
+
+    @Override
+    void printConditionHolder(StringBuilder b) {
+      KeepRuleExtractor.printClassHeader(
+          b,
+          holderPattern,
+          (builder, classReference) -> {
+            StringBuilder backReference = new StringBuilder();
+            if (holderNamePattern.isAny()) {
+              addBackRef(backReference);
+              builder.append('*');
+            } else {
+              printPackagePrefix(builder, holderNamePattern.getPackagePattern(), backReference);
+              printSimpleClassName(builder, holderNamePattern.getNamePattern(), backReference);
+            }
+            holderBackReferencePattern = backReference.toString();
+          });
+    }
+
+    @Override
+    void printConditionMember(StringBuilder builder, String member) {
+      // TODO(b/248408342): Support back-ref to member instances too.
+      KeepMemberPattern memberPattern = memberPatterns.get(member);
+      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+    }
+
+    @Override
+    void printTargetHolder(StringBuilder builder) {
+      KeepRuleExtractor.printClassHeader(
+          builder,
+          holderPattern,
+          (b, reference) -> {
+            assert reference.isBindingReference()
+                || reference.asClassNamePattern().equals(holderNamePattern);
+            b.append(holderBackReferencePattern);
+          });
+    }
+
+    private StringBuilder printPackagePrefix(
+        StringBuilder builder,
+        KeepPackagePattern packagePattern,
+        StringBuilder backReferenceBuilder) {
+      if (packagePattern.isAny()) {
+        addBackRef(backReferenceBuilder).append('.');
+        return builder.append("**.");
+      }
+      if (packagePattern.isTop()) {
+        return builder;
+      }
+      assert packagePattern.isExact();
+      String exactPackage = packagePattern.getExactPackageAsString();
+      backReferenceBuilder.append(exactPackage).append('.');
+      return builder.append(exactPackage).append('.');
+    }
+
+    private StringBuilder printSimpleClassName(
+        StringBuilder builder,
+        KeepUnqualfiedClassNamePattern namePattern,
+        StringBuilder backReferenceBuilder) {
+      if (namePattern.isAny()) {
+        addBackRef(backReferenceBuilder);
+        return builder.append('*');
+      }
+      assert namePattern.isExact();
+      String exactName = namePattern.asExact().getExactNameAsString();
+      backReferenceBuilder.append(exactName);
+      return builder.append(exactName);
+    }
+  }
+
+  /**
+   * Representation of a conditional class rule that is match/instance dependent.
+   *
+   * <pre>
+   *   -if class <class-pattern> { <member-condition>* }
+   *   -keep class <class-backref>
+   * </pre>
+   */
+  static class PgDependentClassRule extends PgDependentRuleBase {
+
+    public PgDependentClassRule(
+        KeepEdgeMetaInfo metaInfo,
+        Holder holder,
+        KeepOptions options,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions) {
+      super(metaInfo, holder, options, memberPatterns, memberConditions);
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return "-keep";
+    }
+
+    @Override
+    boolean hasCondition() {
+      return true;
+    }
+
+    @Override
+    List<String> getTargetMembers() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    void printTargetMember(StringBuilder builder, String member) {
+      throw new KeepEdgeException("Unreachable");
+    }
+  }
+
+  /**
+   * Representation of a conditional member rule that is match/instance dependent.
+   *
+   * <pre>
+   *   -if class <class-pattern> { <member-condition>* }
+   *   -keepclassmembers class <class-backref> { <member-target | member-backref>* }
+   * </pre>
+   *
+   * or if the only condition is the class itself, just:
+   *
+   * <pre>
+   *   -keepclassmembers <class-pattern> { <member-target> }
+   * </pre>
+   */
+  static class PgDependentMembersRule extends PgDependentRuleBase {
+
+    final List<String> memberTargets;
+
+    public PgDependentMembersRule(
+        KeepEdgeMetaInfo metaInfo,
+        Holder holder,
+        KeepOptions options,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberConditions,
+        List<String> memberTargets) {
+      super(metaInfo, holder, options, memberPatterns, memberConditions);
+      assert !memberTargets.isEmpty();
+      this.memberTargets = memberTargets;
+    }
+
+    @Override
+    boolean hasCondition() {
+      return !memberConditions.isEmpty();
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return "-keepclassmembers";
+    }
+
+    @Override
+    List<String> getTargetMembers() {
+      return memberTargets;
+    }
+
+    @Override
+    void printTargetHolder(StringBuilder builder) {
+      if (hasCondition()) {
+        super.printTargetHolder(builder);
+      } else {
+        KeepRuleExtractor.printClassHeader(
+            builder, holderPattern, classReferencePrinter(holderNamePattern));
+      }
+    }
+
+    @Override
+    void printTargetMember(StringBuilder builder, String member) {
+      // TODO(b/248408342): Support back-ref to member instances too.
+      KeepMemberPattern memberPattern = memberPatterns.get(member);
+      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
index fe49466..80a6ec5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
@@ -165,13 +165,13 @@
   private void processCondition(KeepCondition.Builder builder, AnnotationMirror mirror) {
     KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
     processItem(itemBuilder, mirror);
-    builder.setItem(itemBuilder.build());
+    builder.setItemPattern(itemBuilder.build());
   }
 
   private void processTarget(KeepTarget.Builder builder, AnnotationMirror mirror) {
     KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
     processItem(itemBuilder, mirror);
-    builder.setItem(itemBuilder.build());
+    builder.setItemPattern(itemBuilder.build());
   }
 
   private void processItem(KeepItemPattern.Builder builder, AnnotationMirror mirror) {
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidBasicFileAttributeView.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidBasicFileAttributeView.java
deleted file mode 100644
index b8be0e2..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidBasicFileAttributeView.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Map;
-
-class DesugarAndroidBasicFileAttributeView extends DesugarBasicFileAttributeView {
-
-  private final Path path;
-
-  public DesugarAndroidBasicFileAttributeView(Path path) {
-    super(path);
-    this.path = path;
-  }
-
-  @Override
-  public BasicFileAttributes readAttributes() throws IOException {
-    path.getFileSystem().provider().checkAccess(path);
-    return super.readAttributes();
-  }
-
-  @Override
-  public Map<String, Object> readAttributes(String[] requested) throws IOException {
-    path.getFileSystem().provider().checkAccess(path);
-    return super.readAttributes(requested);
-  }
-}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidDefaultFileSystemProvider.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidDefaultFileSystemProvider.java
deleted file mode 100644
index 8de3f76..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidDefaultFileSystemProvider.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.net.URI;
-import java.nio.file.FileSystem;
-import java.nio.file.spi.FileSystemProvider;
-
-public class DesugarAndroidDefaultFileSystemProvider {
-  private static final FileSystemProvider INSTANCE = DesugarAndroidFileSystemProvider.create();
-
-  private DesugarAndroidDefaultFileSystemProvider() {}
-
-  /** Returns the platform's default file system provider. */
-  public static FileSystemProvider instance() {
-    return INSTANCE;
-  }
-
-  /** Returns the platform's default file system. */
-  public static FileSystem theFileSystem() {
-    return INSTANCE.getFileSystem(URI.create("file:///"));
-  }
-}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidFileSystemProvider.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidFileSystemProvider.java
deleted file mode 100644
index 6b4a062..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarAndroidFileSystemProvider.java
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.adapter.AndroidVersionTest;
-import java.io.IOException;
-import java.nio.channels.DesugarChannels;
-import java.nio.channels.FileChannel;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.file.CopyOption;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.FileAttribute;
-import java.nio.file.attribute.FileAttributeView;
-import java.nio.file.spi.FileSystemProvider;
-import java.util.Map;
-import java.util.Set;
-
-/** Linux implementation of {@link FileSystemProvider} for desugar support. */
-public class DesugarAndroidFileSystemProvider
-    extends desugar.sun.nio.fs.DesugarLinuxFileSystemProvider {
-
-  public static DesugarAndroidFileSystemProvider create() {
-    return new DesugarAndroidFileSystemProvider(System.getProperty("user.dir"), "/");
-  }
-
-  DesugarAndroidFileSystemProvider(String userDir, String rootDir) {
-    super(userDir, rootDir);
-  }
-
-  @Override
-  public void copy(Path source, Path target, CopyOption... options) throws IOException {
-    if (!containsCopyOption(options, StandardCopyOption.REPLACE_EXISTING) && Files.exists(target)) {
-      throw new FileAlreadyExistsException(target.toString());
-    }
-    if (containsCopyOption(options, StandardCopyOption.ATOMIC_MOVE)) {
-      throw new UnsupportedOperationException("Unsupported copy option");
-    }
-    super.copy(source, target, options);
-  }
-
-  @Override
-  public void move(Path source, Path target, CopyOption... options) throws IOException {
-    if (!containsCopyOption(options, StandardCopyOption.REPLACE_EXISTING) && Files.exists(target)) {
-      throw new FileAlreadyExistsException(target.toString());
-    }
-    if (containsCopyOption(options, StandardCopyOption.COPY_ATTRIBUTES)) {
-      throw new UnsupportedOperationException("Unsupported copy option");
-    }
-    super.move(source, target, options);
-  }
-
-  private boolean containsCopyOption(CopyOption[] options, CopyOption option) {
-    for (CopyOption copyOption : options) {
-      if (copyOption == option) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
-    if (dir.getParent() != null && !Files.exists(dir.getParent())) {
-      throw new NoSuchFileException(dir.toString());
-    }
-    super.createDirectory(dir, attrs);
-  }
-
-  @Override
-  public <V extends FileAttributeView> V getFileAttributeView(
-      Path path, Class<V> type, LinkOption... options) {
-    if (type == null) {
-      throw new NullPointerException();
-    }
-    if (type == BasicFileAttributeView.class) {
-      return type.cast(new DesugarAndroidBasicFileAttributeView(path));
-    }
-    return null;
-  }
-
-  @Override
-  public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options)
-      throws IOException {
-    int attributesTypeIndexEnd = attributes.indexOf(":");
-    final Class<? extends BasicFileAttributeView> attributeViewType;
-    final String[] requestedAttributes;
-    if (attributesTypeIndexEnd == -1) {
-      attributeViewType = BasicFileAttributeView.class;
-      requestedAttributes = attributes.split(",");
-    } else {
-      String attributeTypeSpec = attributes.substring(0, attributesTypeIndexEnd);
-      if ("basic".equals(attributeTypeSpec)) {
-        attributeViewType = BasicFileAttributeView.class;
-      } else {
-        throw new UnsupportedOperationException(
-            String.format("Requested attribute type for: %s is not available.", attributeTypeSpec));
-      }
-      requestedAttributes = attributes.substring(attributesTypeIndexEnd + 1).split(",");
-    }
-    if (attributeViewType == BasicFileAttributeView.class) {
-      DesugarBasicFileAttributeView attrView = new DesugarAndroidBasicFileAttributeView(path);
-      return attrView.readAttributes(requestedAttributes);
-    }
-    throw new AssertionError("Unexpected View '" + attributeViewType + "' requested");
-  }
-
-  private boolean exists(Path file) {
-    try {
-      checkAccess(file);
-      return true;
-    } catch (IOException ioe) {
-      return false;
-    }
-  }
-
-  @Override
-  public void delete(Path path) throws IOException {
-    if (exists(path)) {
-      deleteIfExists(path);
-      return;
-    }
-    throw new NoSuchFileException(path.toString());
-  }
-
-  @Override
-  public SeekableByteChannel newByteChannel(
-      Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
-    if (path.toFile().isDirectory()) {
-      throw new UnsupportedOperationException(
-          "The desugar library does not support creating a file channel on a directory: " + path);
-    }
-    // A FileChannel is a SeekableByteChannel.
-    return newFileChannel(path, options, attrs);
-  }
-
-  @Override
-  public FileChannel newFileChannel(
-      Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
-    if (AndroidVersionTest.is26OrAbove) {
-      throw new RuntimeException("Above Api 26, the platform FileSystemProvider should be used.");
-    }
-    return DesugarChannels.openEmulatedFileChannel(path, options, attrs);
-  }
-
-  @Override
-  public boolean isSameFile(Path path, Path path2) throws IOException {
-    // If the paths are equals, then it answers true even if they do not exist.
-    if (path.equals(path2)) {
-      return true;
-    }
-    // If the paths are not equal, they could still be equal due to symbolic link and so on, but
-    // in that case accessibility is checked.
-    checkAccess(path);
-    checkAccess(path2);
-    return super.isSameFile(path, path2);
-  }
-}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributeView.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributeView.java
deleted file mode 100644
index c62a952..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributeView.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-import java.util.Map;
-
-public class DesugarBasicFileAttributeView {
-
-  public DesugarBasicFileAttributeView(Path path) {}
-
-  public BasicFileAttributes readAttributes() throws IOException {
-    return null;
-  }
-
-  public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime)
-      throws IOException {}
-
-  public Map<String, Object> readAttributes(String[] requested) throws IOException {
-    return null;
-  }
-}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributes.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributes.java
deleted file mode 100644
index 8121b78..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarBasicFileAttributes.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.io.File;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-
-public class DesugarBasicFileAttributes implements BasicFileAttributes {
-
-  static DesugarBasicFileAttributes create(File file) {
-    return null;
-  }
-
-  @Override
-  public FileTime lastModifiedTime() {
-    return null;
-  }
-
-  @Override
-  public FileTime lastAccessTime() {
-    return null;
-  }
-
-  @Override
-  public FileTime creationTime() {
-    return null;
-  }
-
-  @Override
-  public boolean isRegularFile() {
-    return false;
-  }
-
-  @Override
-  public boolean isDirectory() {
-    return false;
-  }
-
-  @Override
-  public boolean isSymbolicLink() {
-    return false;
-  }
-
-  @Override
-  public boolean isOther() {
-    return false;
-  }
-
-  @Override
-  public long size() {
-    return 0;
-  }
-
-  @Override
-  public Object fileKey() {
-    return null;
-  }
-}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileSystemProvider.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileSystemProvider.java
new file mode 100644
index 0000000..ae066b2
--- /dev/null
+++ b/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileSystemProvider.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package desugar.sun.nio.fs;
+
+import java.nio.file.FileSystem;
+import java.nio.file.spi.FileSystemProvider;
+
+/** Creates this platform's default FileSystemProvider. */
+public class DesugarDefaultFileSystemProvider {
+
+  /** Returns the platform's default file system provider. */
+  public static FileSystemProvider instance() {
+    return null;
+  }
+
+  /** Returns the platform's default file system. */
+  public static FileSystem theFileSystem() {
+    return null;
+  }
+}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileTypeDetector.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileTypeDetector.java
index 916d6e0..1871b85 100644
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileTypeDetector.java
+++ b/src/library_desugar/java/desugar/sun/nio/fs/DesugarDefaultFileTypeDetector.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
@@ -7,6 +7,7 @@
 import java.nio.file.spi.FileTypeDetector;
 
 public class DesugarDefaultFileTypeDetector {
+
   public static FileTypeDetector create() {
     return null;
   }
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarFileChannel.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarFileChannel.java
new file mode 100644
index 0000000..b92f2a3
--- /dev/null
+++ b/src/library_desugar/java/desugar/sun/nio/fs/DesugarFileChannel.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package desugar.sun.nio.fs;
+
+import java.nio.channels.FileChannel;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
+import java.util.Set;
+
+public class DesugarFileChannel {
+
+  public static FileChannel wrap(FileChannel raw) {
+    return null;
+  }
+
+  public static FileChannel openEmulatedFileChannel(
+      Path path, Set<? extends OpenOption> openOptions, FileAttribute<?>... attrs) {
+    return null;
+  }
+}
diff --git a/src/library_desugar/java/desugar/sun/nio/fs/DesugarLinuxFileSystemProvider.java b/src/library_desugar/java/desugar/sun/nio/fs/DesugarLinuxFileSystemProvider.java
deleted file mode 100644
index 566bf12..0000000
--- a/src/library_desugar/java/desugar/sun/nio/fs/DesugarLinuxFileSystemProvider.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package desugar.sun.nio.fs;
-
-import java.io.IOException;
-import java.net.URI;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.file.AccessMode;
-import java.nio.file.CopyOption;
-import java.nio.file.DirectoryStream;
-import java.nio.file.DirectoryStream.Filter;
-import java.nio.file.FileStore;
-import java.nio.file.FileSystem;
-import java.nio.file.LinkOption;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileAttribute;
-import java.nio.file.attribute.FileAttributeView;
-import java.nio.file.spi.FileSystemProvider;
-import java.util.Map;
-import java.util.Set;
-
-public class DesugarLinuxFileSystemProvider extends FileSystemProvider {
-
-  DesugarLinuxFileSystemProvider(String userDir, String rootDir) {
-    super();
-  }
-
-  @Override
-  public String getScheme() {
-    return null;
-  }
-
-  @Override
-  public FileSystem newFileSystem(URI uri, Map<String, ?> map) throws IOException {
-    return null;
-  }
-
-  @Override
-  public FileSystem getFileSystem(URI uri) {
-    return null;
-  }
-
-  @Override
-  public Path getPath(URI uri) {
-    return null;
-  }
-
-  @Override
-  public SeekableByteChannel newByteChannel(
-      Path path, Set<? extends OpenOption> set, FileAttribute<?>... fileAttributes)
-      throws IOException {
-    return null;
-  }
-
-  @Override
-  public DirectoryStream<Path> newDirectoryStream(Path path, Filter<? super Path> filter)
-      throws IOException {
-    return null;
-  }
-
-  @Override
-  public void createDirectory(Path path, FileAttribute<?>... fileAttributes) throws IOException {}
-
-  @Override
-  public void delete(Path path) throws IOException {}
-
-  @Override
-  public void copy(Path path, Path path1, CopyOption... copyOptions) throws IOException {}
-
-  @Override
-  public void move(Path path, Path path1, CopyOption... copyOptions) throws IOException {}
-
-  @Override
-  public boolean isSameFile(Path path, Path path1) throws IOException {
-    return false;
-  }
-
-  @Override
-  public boolean isHidden(Path path) throws IOException {
-    return false;
-  }
-
-  @Override
-  public FileStore getFileStore(Path path) throws IOException {
-    return null;
-  }
-
-  @Override
-  public void checkAccess(Path path, AccessMode... accessModes) throws IOException {}
-
-  @Override
-  public <V extends FileAttributeView> V getFileAttributeView(
-      Path path, Class<V> aClass, LinkOption... linkOptions) {
-    return null;
-  }
-
-  @Override
-  public <A extends BasicFileAttributes> A readAttributes(
-      Path path, Class<A> aClass, LinkOption... linkOptions) throws IOException {
-    return null;
-  }
-
-  @Override
-  public Map<String, Object> readAttributes(Path path, String s, LinkOption... linkOptions)
-      throws IOException {
-    return null;
-  }
-
-  @Override
-  public void setAttribute(Path path, String s, Object o, LinkOption... linkOptions)
-      throws IOException {}
-}
diff --git a/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java b/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
index eea074f..bd3b845 100644
--- a/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
+++ b/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
@@ -6,7 +6,7 @@
 
 import android.os.StrictMode;
 import android.os.StrictMode.ThreadPolicy;
-import desugar.sun.nio.fs.DesugarAndroidDefaultFileSystemProvider;
+import desugar.sun.nio.fs.DesugarDefaultFileSystemProvider;
 import j$.nio.file.FileSystems;
 import java.net.URI;
 import java.nio.file.FileSystem;
@@ -34,7 +34,7 @@
       // We cannot set the ThreadPolicy in headless and it should not matter.
       setThreadPolicy();
     }
-    return DesugarAndroidDefaultFileSystemProvider.instance();
+    return DesugarDefaultFileSystemProvider.instance();
   }
 
   private static void setThreadPolicy() {
diff --git a/src/library_desugar/java/java/nio/channels/DesugarChannels.java b/src/library_desugar/java/java/nio/channels/DesugarChannels.java
index 15040ef..85d1627 100644
--- a/src/library_desugar/java/java/nio/channels/DesugarChannels.java
+++ b/src/library_desugar/java/java/nio/channels/DesugarChannels.java
@@ -4,17 +4,11 @@
 
 package java.nio.channels;
 
+import desugar.sun.nio.fs.DesugarFileChannel;
 import java.adapter.AndroidVersionTest;
 import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.ByteBuffer;
-import java.nio.MappedByteBuffer;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.FileAttribute;
 import java.util.Collections;
 import java.util.HashSet;
@@ -33,174 +27,7 @@
     if (AndroidVersionTest.is24OrAbove) {
       return raw;
     }
-    return WrappedFileChannel.wrap(raw);
-  }
-
-  /**
-   * All FileChannels below 24 are wrapped to support the new interface SeekableByteChannel.
-   * FileChannels between 24 and 26 are wrapped only to improve the emulation of program opened
-   * FileChannels, especially with the append and delete on close options.
-   */
-  static class WrappedFileChannel extends FileChannel implements SeekableByteChannel {
-
-    final FileChannel delegate;
-    final boolean deleteOnClose;
-    final boolean appendMode;
-    final Path path;
-
-    public static FileChannel wrap(FileChannel channel) {
-      if (channel instanceof WrappedFileChannel) {
-        return channel;
-      }
-      return new WrappedFileChannel(channel, false, false, null);
-    }
-
-    public static FileChannel withExtraOptions(
-        FileChannel channel, Set<? extends OpenOption> options, Path path) {
-      FileChannel raw =
-          channel instanceof WrappedFileChannel ? ((WrappedFileChannel) channel).delegate : channel;
-      return new WrappedFileChannel(
-          raw,
-          options.contains(StandardOpenOption.DELETE_ON_CLOSE),
-          options.contains(StandardOpenOption.APPEND),
-          path);
-    }
-
-    private WrappedFileChannel(
-        FileChannel delegate, boolean deleteOnClose, boolean appendMode, Path path) {
-      this.delegate = delegate;
-      this.deleteOnClose = deleteOnClose;
-      this.appendMode = appendMode;
-      this.path = deleteOnClose ? path : null;
-    }
-
-    @Override
-    public int read(ByteBuffer dst) throws IOException {
-      return delegate.read(dst);
-    }
-
-    @Override
-    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
-      return delegate.read(dsts, offset, length);
-    }
-
-    @Override
-    public int write(ByteBuffer src) throws IOException {
-      if (appendMode) {
-        return delegate.write(src, size());
-      }
-      return delegate.write(src);
-    }
-
-    @Override
-    public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
-      return delegate.write(srcs, offset, length);
-    }
-
-    @Override
-    public long position() throws IOException {
-      return delegate.position();
-    }
-
-    @Override
-    public FileChannel position(long newPosition) throws IOException {
-      return WrappedFileChannel.wrap(delegate.position(newPosition));
-    }
-
-    @Override
-    public long size() throws IOException {
-      return delegate.size();
-    }
-
-    @Override
-    public FileChannel truncate(long size) throws IOException {
-      return WrappedFileChannel.wrap(delegate.truncate(size));
-    }
-
-    @Override
-    public void force(boolean metaData) throws IOException {
-      delegate.force(metaData);
-    }
-
-    @Override
-    public long transferTo(long position, long count, WritableByteChannel target)
-        throws IOException {
-      return delegate.transferTo(position, count, target);
-    }
-
-    @Override
-    public long transferFrom(ReadableByteChannel src, long position, long count)
-        throws IOException {
-      return delegate.transferFrom(src, position, count);
-    }
-
-    @Override
-    public int read(ByteBuffer dst, long position) throws IOException {
-      return delegate.read(dst, position);
-    }
-
-    @Override
-    public int write(ByteBuffer src, long position) throws IOException {
-      return delegate.write(src, position);
-    }
-
-    @Override
-    public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
-      return delegate.map(mode, position, size);
-    }
-
-    // When using lock or tryLock the extra options are lost.
-    @Override
-    public FileLock lock(long position, long size, boolean shared) throws IOException {
-      return wrapLock(delegate.lock(position, size, shared));
-    }
-
-    @Override
-    public FileLock tryLock(long position, long size, boolean shared) throws IOException {
-      return wrapLock(delegate.tryLock(position, size, shared));
-    }
-
-    private FileLock wrapLock(FileLock lock) {
-      if (lock == null) {
-        return null;
-      }
-      return new WrappedFileChannelFileLock(lock, this);
-    }
-
-    @Override
-    public void implCloseChannel() throws IOException {
-      // We cannot call the protected method, this should be effectively equivalent.
-      delegate.close();
-      if (deleteOnClose) {
-        Files.deleteIfExists(path);
-      }
-    }
-  }
-
-  /**
-   * The FileLock state is final and duplicated in the wrapper, besides the FileChannel where the
-   * wrapped file channel is used. All methods in FileLock, even channel(), use the duplicated and
-   * corrected state. Only 2 methods require to dispatch to the delegate which is effectively
-   * holding the lock.
-   */
-  static class WrappedFileChannelFileLock extends FileLock {
-
-    private final FileLock delegate;
-
-    WrappedFileChannelFileLock(FileLock delegate, WrappedFileChannel wrappedFileChannel) {
-      super(wrappedFileChannel, delegate.position(), delegate.size(), delegate.isShared());
-      this.delegate = delegate;
-    }
-
-    @Override
-    public boolean isValid() {
-      return delegate.isValid();
-    }
-
-    @Override
-    public void release() throws IOException {
-      delegate.release();
-    }
+    return DesugarFileChannel.wrap(raw);
   }
 
   /** The 2 open methods are present to be retargeted from FileChannel#open. */
@@ -214,79 +41,10 @@
       Path path, Set<? extends OpenOption> openOptions, FileAttribute<?>... attrs)
       throws IOException {
     if (AndroidVersionTest.is26OrAbove) {
+      // This uses the library version of the method, the call is not rewritten.
       return FileChannel.open(path, openOptions, attrs);
     }
-    return openEmulatedFileChannel(path, openOptions, attrs);
+    return DesugarFileChannel.openEmulatedFileChannel(path, openOptions, attrs);
   }
 
-  /**
-   * All FileChannel creation go through the FileSystemProvider which then comes here if the Api is
-   * strictly below 26, and to the plaform FileSystemProvider if the Api is above or equal to 26.
-   *
-   * <p>Below Api 26 there is no way to create a FileChannel, so we create instead an emulated
-   * version using RandomAccessFile which tries, with a best effort, to support all settings.
-   *
-   * <p>The FileAttributes are ignored.
-   */
-  public static FileChannel openEmulatedFileChannel(
-      Path path, Set<? extends OpenOption> openOptions, FileAttribute<?>... attrs)
-      throws IOException {
-
-    validateOpenOptions(path, openOptions);
-
-    RandomAccessFile randomAccessFile =
-        new RandomAccessFile(path.toFile(), getFileAccessModeText(openOptions));
-    // TRUNCATE_EXISTING is ignored if the file is not writable.
-    // TRUNCATE_EXISTING is not compatible with APPEND, so we just need to check for WRITE.
-    if (openOptions.contains(StandardOpenOption.TRUNCATE_EXISTING)
-        && openOptions.contains(StandardOpenOption.WRITE)) {
-      randomAccessFile.setLength(0);
-    }
-
-    if (!openOptions.contains(StandardOpenOption.APPEND)
-        && !openOptions.contains(StandardOpenOption.DELETE_ON_CLOSE)) {
-      // This one may be retargeted, below 24, to support SeekableByteChannel.
-      return randomAccessFile.getChannel();
-    }
-
-    return WrappedFileChannel.withExtraOptions(randomAccessFile.getChannel(), openOptions, path);
-  }
-
-  private static void validateOpenOptions(Path path, Set<? extends OpenOption> openOptions)
-      throws IOException {
-    if (Files.exists(path)) {
-      if (openOptions.contains(StandardOpenOption.CREATE_NEW)
-          && openOptions.contains(StandardOpenOption.WRITE)) {
-        throw new FileAlreadyExistsException(path.toString());
-      }
-    } else {
-      if (!(openOptions.contains(StandardOpenOption.CREATE)
-          || openOptions.contains(StandardOpenOption.CREATE_NEW))) {
-        throw new NoSuchFileException(path.toString());
-      }
-    }
-    // Validations that resemble sun.nio.fs.UnixChannelFactory#newFileChannel.
-    if (openOptions.contains(StandardOpenOption.READ)
-        && openOptions.contains(StandardOpenOption.APPEND)) {
-      throw new IllegalArgumentException("READ + APPEND not allowed");
-    }
-    if (openOptions.contains(StandardOpenOption.APPEND)
-        && openOptions.contains(StandardOpenOption.TRUNCATE_EXISTING)) {
-      throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed");
-    }
-  }
-
-  private static String getFileAccessModeText(Set<? extends OpenOption> options) {
-    if (!options.contains(StandardOpenOption.WRITE)
-        && !options.contains(StandardOpenOption.APPEND)) {
-      return "r";
-    }
-    if (options.contains(StandardOpenOption.SYNC)) {
-      return "rws";
-    }
-    if (options.contains(StandardOpenOption.DSYNC)) {
-      return "rwd";
-    }
-    return "rw";
-  }
 }
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index f3d0764..d6649f7 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -24,7 +24,8 @@
     {
       "api_level_below_or_equal": 32,
       "rewrite_prefix": {
-        "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit"
+        "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit",
+        "java.util.concurrent.ConcurrentLinkedQueue": "j$.util.concurrent.ConcurrentLinkedQueue"
       },
       "retarget_method": {
         "java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit#of(java.time.temporal.ChronoUnit)": "java.util.concurrent.DesugarTimeUnit",
@@ -300,6 +301,12 @@
   ],
   "library_flags": [
     {
+      "api_level_below_or_equal": 32,
+      "rewrite_prefix": {
+        "java.util.concurrent.Helpers": "j$.util.concurrent.Helpers"
+      }
+    },
+    {
       "api_level_below_or_equal": 30,
       "rewrite_prefix": {
         "jdk.internal.": "j$.jdk.internal.",
@@ -342,7 +349,6 @@
         "java.util.KeyValueHolder": "j$.util.KeyValueHolder",
         "java.util.SortedSet$1": "j$.util.SortedSet$1",
         "java.util.Tripwire": "j$.util.Tripwire",
-        "java.util.concurrent.Helpers": "j$.util.concurrent.Helpers",
         "java.util.ConversionRuntimeException": "j$.util.ConversionRuntimeException"
       },
       "rewrite_derived_prefix": {
@@ -381,6 +387,9 @@
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$TreeBin { int lockState; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap { int sizeCtl; int transferIndex; long baseCount; int cellsBusy; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$CounterCell { long value; }",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue { j$.util.concurrent.ConcurrentLinkedQueue$Node head; j$.util.concurrent.ConcurrentLinkedQueue$Node tail; }",
+    "-keep,allowshrinking class j$.util.concurrent.ConcurrentLinkedQueue$Node",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue$Node { j$.util.concurrent.ConcurrentLinkedQueue$Node next; java.lang.Object item; }",
     "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); public static final !synthetic <fields>; }",
     "-keeppackagenames java.**",
     "-keeppackagenames j$.**",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
index 119b674..eb7db99 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
@@ -6,6 +6,12 @@
   "support_all_callbacks_from_library": false,
   "common_flags": [
     {
+      "api_level_below_or_equal": 32,
+      "rewrite_prefix": {
+        "java.util.concurrent.ConcurrentLinkedQueue": "j$.util.concurrent.ConcurrentLinkedQueue"
+      }
+    },
+    {
       "api_level_below_or_equal": 23,
       "maintain_prefix": [
         "java.util.function.",
@@ -14,8 +20,19 @@
     }
   ],
   "program_flags": [],
-  "library_flags": [],
+  "library_flags": [
+    {
+      "api_level_below_or_equal": 32,
+      "rewrite_prefix": {
+        "java.util.concurrent.Helpers": "j$.util.concurrent.Helpers"
+      }
+    }
+  ],
   "shrinker_config": [
+    "-keepclassmembers class j$.** extends java.io.Serializable { void <init>(); private static final java.io.ObjectStreamField[] serialPersistentFields; static final long serialVersionUID; java.lang.Object readResolve(); java.lang.Object writeReplace(); private void readObject(java.io.ObjectInputStream); private void writeObject(java.io.ObjectOutputStream); private void readObjectNoData(); }",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue { j$.util.concurrent.ConcurrentLinkedQueue$Node head; j$.util.concurrent.ConcurrentLinkedQueue$Node tail; }",
+    "-keep,allowshrinking class j$.util.concurrent.ConcurrentLinkedQueue$Node",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue$Node { j$.util.concurrent.ConcurrentLinkedQueue$Node next; java.lang.Object item; }",
     "-keeppackagenames java.**",
     "-keeppackagenames j$.**",
     "-keepattributes Signature",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index 5269d86..98054a7 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -34,7 +34,8 @@
         "java.net.URLDecoder": "j$.net.URLDecoder",
         "java.net.URLEncoder": "j$.net.URLEncoder",
         "java.io.DesugarInputStream": "j$.io.DesugarInputStream",
-        "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit"
+        "java.util.concurrent.DesugarTimeUnit": "j$.util.concurrent.DesugarTimeUnit",
+        "java.util.concurrent.ConcurrentLinkedQueue": "j$.util.concurrent.ConcurrentLinkedQueue"
       },
       "retarget_method": {
         "java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit#of(java.time.temporal.ChronoUnit)": "java.util.concurrent.DesugarTimeUnit",
@@ -225,6 +226,7 @@
         "java.nio.channels.SeekableByteChannel java.nio.file.spi.FileSystemProvider#newByteChannel(java.nio.file.Path, java.util.Set, java.nio.file.attribute.FileAttribute[])" : [1, "java.util.Set java.nio.file.FileApiFlips#flipOpenOptionSet(java.util.Set)"],
         "java.nio.channels.FileChannel java.nio.file.spi.FileSystemProvider#newFileChannel(java.nio.file.Path, java.util.Set, java.nio.file.attribute.FileAttribute[])" : [1, "java.util.Set java.nio.file.FileApiFlips#flipOpenOptionSet(java.util.Set)"],
         "java.util.List java.nio.file.WatchKey#pollEvents()": [-1, "java.util.List java.nio.file.FileApiFlips#flipWatchEventList(java.util.List)"],
+        "java.lang.Object java.nio.file.WatchEvent#context()": [-1, "java.lang.Object java.nio.file.PathApiFlips#convertPath(java.lang.Object)"],
         "boolean java.nio.file.FileStore#supportsFileAttributeView(java.lang.Class)": [0, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributeView(java.lang.Class)"],
         "java.nio.file.attribute.FileAttributeView java.nio.file.spi.FileSystemProvider#getFileAttributeView(java.nio.file.Path, java.lang.Class, java.nio.file.LinkOption[])": [1, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributeView(java.lang.Class)"],
         "java.nio.file.attribute.BasicFileAttributes java.nio.file.spi.FileSystemProvider#readAttributes(java.nio.file.Path, java.lang.Class, java.nio.file.LinkOption[])": [1, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributes(java.lang.Class)"],
@@ -476,7 +478,8 @@
       "rewrite_prefix": {
         "desugar.": "j$.desugar.",
         "libcore.": "j$.libcore.",
-        "sun.security.action.": "j$.sun.security.action."
+        "sun.security.action.": "j$.sun.security.action.",
+        "java.util.concurrent.Helpers": "j$.util.concurrent.Helpers"
       }
     },
     {
@@ -588,6 +591,9 @@
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$TreeBin { int lockState; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap { int sizeCtl; int transferIndex; long baseCount; int cellsBusy; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$CounterCell { long value; }",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue { j$.util.concurrent.ConcurrentLinkedQueue$Node head; j$.util.concurrent.ConcurrentLinkedQueue$Node tail; }",
+    "-keep,allowshrinking class j$.util.concurrent.ConcurrentLinkedQueue$Node",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentLinkedQueue$Node { j$.util.concurrent.ConcurrentLinkedQueue$Node next; java.lang.Object item; }",
     "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); public static final !synthetic <fields>; }",
     "-keeppackagenames java.**",
     "-keeppackagenames j$.**",
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index aea1c45..1acbd0c 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -294,13 +294,6 @@
               appView.setNamingLens(
                   RecordRewritingNamingLens.createRecordRewritingNamingLens(appView)));
 
-      timing.time(
-          "Create MethodHandle.Lookup rewriting lens",
-          () ->
-              appView.setNamingLens(
-                  VarHandleDesugaringRewritingNamingLens
-                      .createVarHandleDesugaringRewritingNamingLens(appView)));
-
       if (options.isGeneratingDex()
           && hasDexResources
           && hasClassResources
@@ -336,6 +329,14 @@
 
       finalizeApplication(appView, executor, timing);
 
+      // Add the VarHandle naming lens after synthetic finalization.
+      timing.time(
+          "Create MethodHandle.Lookup rewriting lens",
+          () ->
+              appView.setNamingLens(
+                  VarHandleDesugaringRewritingNamingLens
+                      .createVarHandleDesugaringRewritingNamingLens(appView)));
+
       timing.end(); // post-converter
 
       if (options.isGeneratingClassFiles()) {
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 1dddf3f..56558ed 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryAmender;
 import com.android.tools.r8.jar.CfApplicationWriter;
 import com.android.tools.r8.naming.PrefixRewritingNamingLens;
+import com.android.tools.r8.naming.VarHandleDesugaringRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.origin.CommandLineOrigin;
 import com.android.tools.r8.shaking.AnnotationRemover;
@@ -102,12 +103,15 @@
             options.enableSwitchRewriting = false;
             assert options.enableStringSwitchConversion;
             options.enableStringSwitchConversion = false;
+            assert !options.enableVarHandleDesugaring;
+            options.enableVarHandleDesugaring = true;
 
             desugar(app, options, executorService);
 
             options.forceAnnotateSynthetics = false;
             options.enableSwitchRewriting = true;
             options.enableStringSwitchConversion = true;
+            options.enableVarHandleDesugaring = false;
           });
       if (shrink) {
         R8.run(r8Command, executorService);
@@ -140,6 +144,9 @@
       SyntheticFinalization.finalize(appView, timing, executor);
 
       appView.setNamingLens(PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView));
+      appView.setNamingLens(
+          VarHandleDesugaringRewritingNamingLens.createVarHandleDesugaringRewritingNamingLens(
+              appView));
       new GenericSignatureRewriter(appView).run(appView.appInfo().classes(), executor);
 
       new CfApplicationWriter(appView, options.getMarker(Tool.L8))
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3102f0d..88f233f 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -318,6 +318,7 @@
 
       List<ProguardConfigurationRule> synthesizedProguardRules = new ArrayList<>();
       timing.begin("Strip unused code");
+      timing.begin("Before enqueuer");
       RuntimeTypeCheckInfo.Builder classMergingEnqueuerExtensionBuilder =
           new RuntimeTypeCheckInfo.Builder(appView);
       try {
@@ -356,6 +357,8 @@
 
         AnnotationRemover.Builder annotationRemoverBuilder =
             options.isShrinking() ? AnnotationRemover.builder(Mode.INITIAL_TREE_SHAKING) : null;
+        timing.end();
+        timing.begin("Enqueuer");
         AppView<AppInfoWithLiveness> appViewWithLiveness =
             runEnqueuer(
                 annotationRemoverBuilder,
@@ -363,7 +366,8 @@
                 appView,
                 subtypingInfo,
                 classMergingEnqueuerExtensionBuilder);
-
+        timing.end();
+        timing.begin("After enqueuer");
         assert appView.rootSet().verifyKeptFieldsAreAccessedAndLive(appViewWithLiveness);
         assert appView.rootSet().verifyKeptMethodsAreTargetedAndLive(appViewWithLiveness);
         assert appView.rootSet().verifyKeptTypesAreLive(appViewWithLiveness);
@@ -429,6 +433,7 @@
 
           assert appView.checkForTesting(() -> allReferencesAssignedApiLevel(appViewWithLiveness));
         }
+        timing.end();
       } finally {
         timing.end();
       }
@@ -982,6 +987,7 @@
       SubtypingInfo subtypingInfo,
       RuntimeTypeCheckInfo.Builder classMergingEnqueuerExtensionBuilder)
       throws ExecutionException {
+    timing.begin("Set up enqueuer");
     Enqueuer enqueuer =
         EnqueuerFactory.createForInitialTreeShaking(appView, executorService, subtypingInfo);
     enqueuer.setAnnotationRemoverBuilder(annotationRemoverBuilder);
@@ -999,9 +1005,12 @@
     if (options.isClassMergingExtensionRequired(enqueuer.getMode())) {
       classMergingEnqueuerExtensionBuilder.attach(enqueuer);
     }
-
+    timing.end();
+    timing.begin("Trace application");
     EnqueuerResult enqueuerResult =
         enqueuer.traceApplication(appView.rootSet(), executorService, timing);
+    timing.end();
+    timing.begin("Finalize enqueuer result");
     AppView<AppInfoWithLiveness> appViewWithLiveness =
         appView.setAppInfo(enqueuerResult.getAppInfo());
     if (InternalOptions.assertionsEnabled()) {
@@ -1015,6 +1024,7 @@
         shrinker ->
             shrinker.rewriteDeadBuilderReferencesFromDynamicMethods(
                 appViewWithLiveness, executorService, timing));
+    timing.end();
     return appViewWithLiveness;
   }
 
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
index 60f5fe4..35eb379 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
@@ -43,6 +43,14 @@
   public abstract ComputedApiLevel computeApiLevelForDefinition(
       Iterable<DexType> types, ComputedApiLevel unknownValue);
 
+  public ComputedApiLevel computeApiLevelForLibraryReference(DexReference reference) {
+    return computeApiLevelForLibraryReference(reference, ComputedApiLevel.unknown());
+  }
+
+  public ComputedApiLevel computeApiLevelForDefinition(Iterable<DexType> types) {
+    return computeApiLevelForDefinition(types, ComputedApiLevel.unknown());
+  }
+
   public abstract boolean isEnabled();
 
   public void reportUnknownApiReferences() {
@@ -74,16 +82,6 @@
     return new KnownApiLevel(options.getMinApiLevel());
   }
 
-  // TODO(b/213552119): This should not be necessary if we have an api computation that returns min
-  //  api if we have platform.
-  @Deprecated
-  public ComputedApiLevel getPlatformApiLevelOrUnknown(AppView<?> appView) {
-    if (appView.options().getMinApiLevel() == AndroidApiLevel.ANDROID_PLATFORM) {
-      return ComputedApiLevel.platform();
-    }
-    return ComputedApiLevel.unknown();
-  }
-
   public static class NoAndroidApiLevelCompute extends AndroidApiLevelCompute {
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index fff7a6e..82e4b48 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -399,6 +399,23 @@
     return true;
   }
 
+  public Position getPreamblePosition() {
+    Position preamble = null;
+    for (CfInstruction instruction : instructions) {
+      if (instruction.isLabel()) {
+        continue;
+      }
+      if (instruction.isPosition()) {
+        Position candidate = instruction.asPosition().getPosition();
+        if (candidate.getLine() == 0) {
+          preamble = candidate;
+        }
+      }
+      break;
+    }
+    return preamble;
+  }
+
   private static class PrunePreambleMethodVisitor extends MethodVisitor {
 
     private final AppView<?> appView;
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index bc0c72b..c70e226 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -187,7 +187,7 @@
     throw new Unreachable();
   }
 
-  public Position newInlineePosition(
+  public static Position newInlineePosition(
       Position callerPosition, Position oldPosition, boolean isCalleeD8R8Synthesized) {
     Position outermostCaller = oldPosition.getOutermostCaller();
     if (!isCalleeD8R8Synthesized) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 49bfc86..311a287 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -219,6 +219,25 @@
       assert newProgramClasses != null;
       this.programClasses.clear();
       this.programClasses.addAll(newProgramClasses);
+
+      DexApplicationReadFlags.Builder builder = DexApplicationReadFlags.builder();
+      builder.setHasReadProgramClassFromDex(this.flags.hasReadProgramClassFromDex());
+      builder.setHasReadProgramClassFromCf(this.flags.hasReadProgramClassFromCf());
+      this.programClasses.forEach(
+          clazz -> {
+            DexType type = clazz.getType();
+            if (flags.getRecordWitnesses().contains(type)) {
+              builder.addRecordWitness(type);
+            }
+            if (flags.getVarHandleWitnesses().contains(type)) {
+              builder.addVarHandleWitness(type);
+            }
+            if (flags.getMethodHandlesLookupWitnesses().contains(type)) {
+              builder.addMethodHandlesLookupWitness(type);
+            }
+          });
+      this.flags = builder.build();
+
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 85f928d..59bff83 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -32,7 +31,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
@@ -59,10 +57,7 @@
   private OptionalBool isResolvable = OptionalBool.unknown();
 
   /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedField[] staticFields = DexEncodedField.EMPTY_ARRAY;
-
-  /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedField[] instanceFields = DexEncodedField.EMPTY_ARRAY;
+  protected final FieldCollection fieldCollection;
 
   /** Access has to be synchronized during concurrent collection/writing phase. */
   protected final MethodCollection methodCollection;
@@ -112,8 +107,7 @@
     this.accessFlags = accessFlags;
     this.superType = superType;
     this.type = type;
-    setStaticFields(staticFields);
-    setInstanceFields(instanceFields);
+    this.fieldCollection = FieldCollection.create(this, staticFields, instanceFields);
     this.methodCollection = methodCollectionFactory.create(this);
     this.nestHost = nestHost;
     this.nestMembers = nestMembers;
@@ -222,9 +216,7 @@
   }
 
   public Iterable<DexEncodedField> fields(final Predicate<? super DexEncodedField> predicate) {
-    return Iterables.concat(
-        Iterables.filter(Arrays.asList(instanceFields), predicate::test),
-        Iterables.filter(Arrays.asList(staticFields), predicate::test));
+    return fieldCollection.fields(predicate);
   }
 
   public Iterable<DexEncodedMember<?, ?>> members() {
@@ -235,6 +227,10 @@
     return Iterables.concat(fields(predicate), methods(predicate));
   }
 
+  public FieldCollection getFieldCollection() {
+    return fieldCollection;
+  }
+
   @Override
   public MethodCollection getMethodCollection() {
     return methodCollection;
@@ -326,6 +322,10 @@
     methodCollection.forEachMethod(consumer);
   }
 
+  public List<DexEncodedField> allFieldsSorted() {
+    return fieldCollection.allFieldsSorted();
+  }
+
   public List<DexEncodedMethod> allMethodsSorted() {
     return methodCollection.allMethodsSorted();
   }
@@ -387,11 +387,7 @@
   }
 
   public List<DexEncodedField> staticFields() {
-    assert staticFields != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(staticFields));
-    }
-    return Arrays.asList(staticFields);
+    return fieldCollection.staticFieldsAsList();
   }
 
   public Iterable<DexEncodedField> staticFields(Predicate<DexEncodedField> predicate) {
@@ -399,159 +395,71 @@
   }
 
   public void appendStaticField(DexEncodedField field) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length + 1];
-    System.arraycopy(staticFields, 0, newFields, 0, staticFields.length);
-    newFields[staticFields.length] = field;
-    staticFields = newFields;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendStaticField(field);
   }
 
   public void appendStaticFields(Collection<DexEncodedField> fields) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length + fields.size()];
-    System.arraycopy(staticFields, 0, newFields, 0, staticFields.length);
-    int i = staticFields.length;
-    for (DexEncodedField field : fields) {
-      newFields[i] = field;
-      i++;
-    }
-    staticFields = newFields;
-    assert verifyCorrectnessOfFieldHolders(fields);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendStaticFields(fields);
   }
 
   public DexEncodedField[] clearStaticFields() {
-    DexEncodedField[] previousFields = staticFields;
-    setStaticFields(DexEncodedField.EMPTY_ARRAY);
-    return previousFields;
-  }
-
-  public void removeStaticField(int index) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length - 1];
-    System.arraycopy(staticFields, 0, newFields, 0, index);
-    System.arraycopy(staticFields, index + 1, newFields, index, staticFields.length - index - 1);
-    staticFields = newFields;
-  }
-
-  public void setStaticField(int index, DexEncodedField field) {
-    staticFields[index] = field;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    List<DexEncodedField> previousFields = staticFields();
+    fieldCollection.clearStaticFields();
+    return previousFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
   public void setStaticFields(DexEncodedField[] fields) {
-    staticFields = MoreObjects.firstNonNull(fields, DexEncodedField.EMPTY_ARRAY);
-    assert verifyCorrectnessOfFieldHolders(staticFields());
-    assert verifyNoDuplicateFields();
+    fieldCollection.setStaticFields(fields);
   }
 
   public void setStaticFields(Collection<DexEncodedField> fields) {
     setStaticFields(fields.toArray(DexEncodedField.EMPTY_ARRAY));
   }
 
-  public boolean definesStaticField(DexField field) {
-    for (DexEncodedField encodedField : staticFields()) {
-      if (encodedField.getReference() == field) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   public List<DexEncodedField> instanceFields() {
-    assert instanceFields != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(instanceFields));
-    }
-    return Arrays.asList(instanceFields);
+    return fieldCollection.instanceFieldsAsList();
   }
 
   public Iterable<DexEncodedField> instanceFields(Predicate<? super DexEncodedField> predicate) {
-    return Iterables.filter(Arrays.asList(instanceFields), predicate::test);
+    return Iterables.filter(instanceFields(), predicate::test);
   }
 
   public void appendInstanceField(DexEncodedField field) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length + 1];
-    System.arraycopy(instanceFields, 0, newFields, 0, instanceFields.length);
-    newFields[instanceFields.length] = field;
-    instanceFields = newFields;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendInstanceField(field);
   }
 
   public void appendInstanceFields(Collection<DexEncodedField> fields) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length + fields.size()];
-    System.arraycopy(instanceFields, 0, newFields, 0, instanceFields.length);
-    int i = instanceFields.length;
-    for (DexEncodedField field : fields) {
-      newFields[i] = field;
-      i++;
-    }
-    instanceFields = newFields;
-    assert verifyCorrectnessOfFieldHolders(fields);
-    assert verifyNoDuplicateFields();
-  }
-
-  public void removeInstanceField(int index) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length - 1];
-    System.arraycopy(instanceFields, 0, newFields, 0, index);
-    System.arraycopy(
-        instanceFields, index + 1, newFields, index, instanceFields.length - index - 1);
-    instanceFields = newFields;
-  }
-
-  public void setInstanceField(int index, DexEncodedField field) {
-    instanceFields[index] = field;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendInstanceFields(fields);
   }
 
   public void setInstanceFields(DexEncodedField[] fields) {
-    instanceFields = MoreObjects.firstNonNull(fields, DexEncodedField.EMPTY_ARRAY);
-    assert verifyCorrectnessOfFieldHolders(instanceFields());
-    assert verifyNoDuplicateFields();
+    fieldCollection.setInstanceFields(fields);
   }
 
   public DexEncodedField[] clearInstanceFields() {
-    DexEncodedField[] previousFields = instanceFields;
-    instanceFields = DexEncodedField.EMPTY_ARRAY;
-    return previousFields;
+    List<DexEncodedField> previousFields = instanceFields();
+    fieldCollection.clearInstanceFields();
+    return previousFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
-  private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
-    assert field.getHolderType() == type
-        : "Expected field `"
-            + field.getReference().toSourceString()
-            + "` to have holder `"
-            + type.toSourceString()
-            + "`";
-    return true;
+  /** Find method in this class matching {@param method}. */
+  public DexClassAndField lookupClassField(DexField field) {
+    return toClassFieldOrNull(lookupField(field));
   }
 
-  private boolean verifyCorrectnessOfFieldHolders(Iterable<DexEncodedField> fields) {
-    for (DexEncodedField field : fields) {
-      assert verifyCorrectnessOfFieldHolder(field);
-    }
-    return true;
-  }
-
-  private boolean verifyNoDuplicateFields() {
-    Set<DexField> unique = Sets.newIdentityHashSet();
-    for (DexEncodedField field : fields()) {
-      boolean changed = unique.add(field.getReference());
-      assert changed : "Duplicate field `" + field.getReference().toSourceString() + "`";
-    }
-    return true;
+  /** Find field in this class matching {@param field}. */
+  public DexEncodedField lookupField(DexField field) {
+    return fieldCollection.lookupField(field);
   }
 
   /** Find static field in this class matching {@param field}. */
   public DexEncodedField lookupStaticField(DexField field) {
-    return lookupTarget(staticFields, field);
+    return fieldCollection.lookupStaticField(field);
   }
 
   /** Find instance field in this class matching {@param field}. */
   public DexEncodedField lookupInstanceField(DexField field) {
-    return lookupTarget(instanceFields, field);
+    return fieldCollection.lookupInstanceField(field);
   }
 
   public DexEncodedField lookupUniqueInstanceFieldWithName(DexString name) {
@@ -576,21 +484,10 @@
     return result;
   }
 
-  /** Find method in this class matching {@param method}. */
-  public DexClassAndField lookupClassField(DexField field) {
-    return toClassFieldOrNull(lookupField(field));
-  }
-
   private DexClassAndField toClassFieldOrNull(DexEncodedField field) {
     return field != null ? DexClassAndField.create(this, field) : null;
   }
 
-  /** Find field in this class matching {@param field}. */
-  public DexEncodedField lookupField(DexField field) {
-    DexEncodedField result = lookupInstanceField(field);
-    return result == null ? lookupStaticField(field) : result;
-  }
-
   /** Find direct method in this class matching {@param method}. */
   public DexEncodedMethod lookupDirectMethod(DexMethod method) {
     return methodCollection.getDirectMethod(method);
@@ -676,16 +573,6 @@
         && method.getReference().proto.parameters.values[0] == factory.objectArrayType;
   }
 
-  private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> D lookupTarget(
-      D[] items, R descriptor) {
-    for (D entry : items) {
-      if (descriptor.match(entry)) {
-        return entry;
-      }
-    }
-    return null;
-  }
-
   public boolean canBeInstantiatedByNewInstance() {
     return !isAbstract() && !isAnnotation() && !isInterface();
   }
@@ -1238,22 +1125,11 @@
   }
 
   public boolean hasStaticFields() {
-    return staticFields.length > 0;
+    return fieldCollection.hasStaticFields();
   }
 
   public boolean hasInstanceFields() {
-    return instanceFields.length > 0;
-  }
-
-  public boolean hasInstanceFieldsDirectlyOrIndirectly(AppView<?> appView) {
-    if (superType == null || type == appView.dexItemFactory().objectType) {
-      return false;
-    }
-    if (hasInstanceFields()) {
-      return true;
-    }
-    DexClass superClass = appView.definitionFor(superType);
-    return superClass == null || superClass.hasInstanceFieldsDirectlyOrIndirectly(appView);
+    return fieldCollection.hasInstanceFields();
   }
 
   public List<DexEncodedField> getDirectAndIndirectInstanceFields(AppView<?> appView) {
@@ -1269,8 +1145,7 @@
   public boolean isValid(InternalOptions options) {
     assert verifyNoAbstractMethodsOnNonAbstractClasses(virtualMethods(), options);
     assert !isInterface() || !getMethodCollection().hasVirtualMethods(DexEncodedMethod::isFinal);
-    assert verifyCorrectnessOfFieldHolders(fields());
-    assert verifyNoDuplicateFields();
+    assert fieldCollection.verify();
     assert methodCollection.verify();
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
index 43c2532..aca2a14 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
@@ -161,8 +161,7 @@
         .withItem(DexDefinition::annotations)
         // TODO(b/158159959): Make signatures structural.
         .withAssert(c -> c.classSignature == ClassSignature.noSignature())
-        .withItemArray(c -> c.staticFields)
-        .withItemArray(c -> c.instanceFields)
+        .withItemCollection(DexClass::allFieldsSorted)
         .withItemCollection(DexClass::allMethodsSorted);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 5dcc091..c7e251e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.graph.DexDebugEventBuilder.addDefaultEventWithAdvancePcIfNecessary;
+import static com.android.tools.r8.utils.DexDebugUtils.computePreamblePosition;
 import static com.android.tools.r8.utils.DexDebugUtils.verifySetPositionFramesFollowedByDefaultEvent;
 
 import com.android.tools.r8.dex.CodeToKeep;
@@ -15,6 +17,7 @@
 import com.android.tools.r8.dex.code.DexReturnVoid;
 import com.android.tools.r8.dex.code.DexSwitchPayload;
 import com.android.tools.r8.graph.DexCode.TryHandler.TypeAddrPair;
+import com.android.tools.r8.graph.DexDebugEvent.AdvanceLine;
 import com.android.tools.r8.graph.DexDebugEvent.Default;
 import com.android.tools.r8.graph.DexDebugEvent.SetPositionFrame;
 import com.android.tools.r8.graph.DexDebugEvent.StartLocal;
@@ -35,7 +38,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.ThrowingMethodConversionOptions;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ArrayUtils;
-import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.DexDebugUtils.PositionInfo;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.structural.Equatable;
@@ -47,11 +50,13 @@
 import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import java.nio.ShortBuffer;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -297,7 +302,8 @@
 
   private DexDebugInfo debugInfoAsInlining(
       DexMethod caller, DexMethod callee, boolean isCalleeD8R8Synthesized, DexItemFactory factory) {
-    Position callerPosition = SyntheticPosition.builder().setLine(0).setMethod(caller).build();
+    Position callerPosition =
+        SyntheticPosition.builder().setLine(0).setMethod(caller).setIsD8R8Synthesized(true).build();
     EventBasedDebugInfo eventBasedInfo = DexDebugInfo.convertToEventBased(this, factory);
     if (eventBasedInfo == null) {
       // If the method has no debug info we generate a preamble position to denote the inlining.
@@ -318,55 +324,78 @@
             factory.createPositionFrame(preamblePosition), factory.zeroChangeDefaultEvent
           });
     }
-    // The inline position should match the first actual callee position, so either its actual line
-    // at first instruction or it is a synthetic preamble.
-    int lineAtPcZero = findLineAtPcZero(callee, eventBasedInfo);
-    PositionBuilder<?, ?> frameBuilder =
-        lineAtPcZero == -1
-            ? SyntheticPosition.builder().setLine(0)
-            : SourcePosition.builder().setLine(lineAtPcZero);
+    // At this point we know we had existing debug information:
+    // 1) There is an already existing SET_POSITION_FRAME before a default event and the default
+    //    event sets a position for PC 0
+    //    => Nothing to do except append caller.
+    // 2) There is no SET_POSITION_FRAME before a default event and a default event covers PC 0.
+    //    => Insert a SET_POSITION_FRAME
+    // 3) There is a SET_POSITION_FRAME and no default event setting a position for PC 0.
+    //    => Insert a default event and potentially advance line.
+    // 4) There is no SET_POSITION_FRAME and no default event setting a position for PC 0..
+    //    => Insert a SET_POSITION_FRAME and a default event and potentially advance line.
+    PositionInfo positionInfo = computePreamblePosition(callee, eventBasedInfo);
     DexDebugEvent[] oldEvents = eventBasedInfo.events;
-    DexDebugEvent[] newEvents = new DexDebugEvent[oldEvents.length + 1];
-    int i = 0;
-    newEvents[i++] =
-        new SetPositionFrame(
-            isCalleeD8R8Synthesized
-                ? callerPosition
-                : frameBuilder.setMethod(callee).setCallerPosition(callerPosition).build());
+    boolean adjustStartPosition =
+        !positionInfo.hasLinePositionAtPcZero() && debugInfo.getStartLine() > 0;
+    List<DexDebugEvent> newEvents =
+        new ArrayList<>(
+            oldEvents.length
+                + (positionInfo.hasFramePosition() ? 0 : 1)
+                + (positionInfo.hasLinePositionAtPcZero() ? 0 : 1)
+                + (adjustStartPosition ? 1 : 0)); // Potentially an advance line.
+    if (!positionInfo.hasFramePosition()) {
+      PositionBuilder<?, ?> calleePositionBuilder =
+          isCalleeD8R8Synthesized ? SyntheticPosition.builder() : SourcePosition.builder();
+      newEvents.add(
+          factory.createPositionFrame(
+              newInlineePosition(
+                  callerPosition,
+                  calleePositionBuilder
+                      .setLine(
+                          positionInfo.hasLinePositionAtPcZero()
+                              ? positionInfo.getLinePositionAtPcZero()
+                              : 0)
+                      .setMethod(callee)
+                      .setIsD8R8Synthesized(isCalleeD8R8Synthesized)
+                      .build(),
+                  isCalleeD8R8Synthesized)));
+    }
+    if (!positionInfo.hasLinePositionAtPcZero()) {
+      newEvents.add(factory.zeroChangeDefaultEvent);
+    }
     for (DexDebugEvent event : oldEvents) {
-      if (event instanceof SetPositionFrame) {
-        SetPositionFrame oldFrame = (SetPositionFrame) event;
+      if (event.isAdvanceLine() && adjustStartPosition) {
+        AdvanceLine advanceLine = event.asAdvanceLine();
+        newEvents.add(factory.createAdvanceLine(debugInfo.getStartLine() + advanceLine.delta));
+        adjustStartPosition = false;
+      } else if (event.isDefaultEvent() && adjustStartPosition) {
+        Default oldDefaultEvent = event.asDefaultEvent();
+        addDefaultEventWithAdvancePcIfNecessary(
+            oldDefaultEvent.getLineDelta() + debugInfo.getStartLine(),
+            oldDefaultEvent.getPCDelta(),
+            newEvents,
+            factory);
+        adjustStartPosition = false;
+      } else if (event.isPositionFrame()) {
+        SetPositionFrame oldFrame = event.asSetPositionFrame();
         assert oldFrame.getPosition() != null;
-        newEvents[i++] =
+        newEvents.add(
             new SetPositionFrame(
                 newInlineePosition(
-                    callerPosition, oldFrame.getPosition(), isCalleeD8R8Synthesized));
+                    callerPosition, oldFrame.getPosition(), isCalleeD8R8Synthesized)));
       } else {
-        newEvents[i++] = event;
+        newEvents.add(event);
       }
     }
-    return new EventBasedDebugInfo(eventBasedInfo.startLine, eventBasedInfo.parameters, newEvents);
-  }
-
-  private static int findLineAtPcZero(DexMethod method, EventBasedDebugInfo debugInfo) {
-    IntBox lineAtPcZero = new IntBox(-1);
-    DexDebugPositionState visitor =
-        new DexDebugPositionState(debugInfo.startLine, method) {
-          @Override
-          public void visit(Default defaultEvent) {
-            super.visit(defaultEvent);
-            if (getCurrentPc() == 0) {
-              lineAtPcZero.set(getCurrentLine());
-            }
-          }
-        };
-    for (DexDebugEvent event : debugInfo.events) {
-      event.accept(visitor);
-      if (visitor.getCurrentPc() > 0) {
-        break;
-      }
+    if (adjustStartPosition) {
+      // This only happens if we have no default event and the debug start line is > 0.
+      newEvents.add(factory.createAdvanceLine(debugInfo.getStartLine()));
     }
-    return lineAtPcZero.get();
+    return new EventBasedDebugInfo(
+        positionInfo.hasLinePositionAtPcZero() ? eventBasedInfo.getStartLine() : 0,
+        eventBasedInfo.parameters,
+        newEvents.toArray(DexDebugEvent.EMPTY_ARRAY));
   }
 
   public static int getLargestPrefix(DexItemFactory factory, DexString name) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
index cd157e3..737df40 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -97,10 +97,22 @@
     return false;
   }
 
+  public boolean isAdvanceLine() {
+    return false;
+  }
+
   public SetPositionFrame asSetPositionFrame() {
     return null;
   }
 
+  public Default asDefaultEvent() {
+    return null;
+  }
+
+  public AdvanceLine asAdvanceLine() {
+    return null;
+  }
+
   public static class AdvancePC extends DexDebugEvent {
 
     public final int delta;
@@ -267,6 +279,16 @@
     }
 
     @Override
+    public boolean isAdvanceLine() {
+      return true;
+    }
+
+    @Override
+    public AdvanceLine asAdvanceLine() {
+      return this;
+    }
+
+    @Override
     public void internalWriteOn(
         DebugBytecodeWriter writer, ObjectToOffsetMapping mapping, GraphLens graphLens) {
       writer.putByte(Constants.DBG_ADVANCE_LINE);
@@ -648,6 +670,11 @@
     }
 
     @Override
+    public Default asDefaultEvent() {
+      return this;
+    }
+
+    @Override
     boolean isWritableEvent() {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java b/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java
new file mode 100644
index 0000000..2f22c1a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.utils.structural.Equatable;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
+import java.util.Objects;
+
+public class DexFieldSignature implements StructuralItem<DexFieldSignature> {
+
+  private final DexString name;
+  private final DexType type;
+
+  private static void specify(StructuralSpecification<DexFieldSignature, ?> spec) {
+    spec.withItem(DexFieldSignature::getName).withItem(DexFieldSignature::getType);
+  }
+
+  public static DexFieldSignature fromField(DexField field) {
+    return new DexFieldSignature(field.getName(), field.getType());
+  }
+
+  private DexFieldSignature(DexString name, DexType type) {
+    this.name = name;
+    this.type = type;
+  }
+
+  public DexString getName() {
+    return name;
+  }
+
+  public DexType getType() {
+    return type;
+  }
+
+  public boolean match(DexField field) {
+    return getName().equals(field.getName()) && getType().equals(field.getType());
+  }
+
+  @Override
+  public DexFieldSignature self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexFieldSignature> getStructuralMapping() {
+    return DexFieldSignature::specify;
+  }
+
+  @Override
+  public boolean isEqualTo(DexFieldSignature other) {
+    return getName() == other.getName() && getType() == other.getType();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return Equatable.equalsImpl(this, o);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 4df3ab0..1ba1122 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -80,6 +80,8 @@
   public static final String varHandleDescriptorString = "Ljava/lang/invoke/VarHandle;";
   public static final String desugarMethodHandlesLookupDescriptorString =
       "Lcom/android/tools/r8/DesugarMethodHandlesLookup;";
+  public static final String methodHandlesLookupDescriptorString =
+      "Ljava/lang/invoke/MethodHandles$Lookup;";
   public static final String dalvikAnnotationOptimizationPrefixString =
       "Ldalvik/annotation/optimization/";
 
@@ -275,7 +277,7 @@
   public final DexString methodHandleDescriptor = createString("Ljava/lang/invoke/MethodHandle;");
   public final DexString methodHandlesDescriptor = createString("Ljava/lang/invoke/MethodHandles;");
   public final DexString methodHandlesLookupDescriptor =
-      createString("Ljava/lang/invoke/MethodHandles$Lookup;");
+      createString(methodHandlesLookupDescriptorString);
   public final DexString methodTypeDescriptor = createString("Ljava/lang/invoke/MethodType;");
   public final DexString invocationHandlerDescriptor =
       createString("Ljava/lang/reflect/InvocationHandler;");
@@ -2708,9 +2710,10 @@
     if (result == null) {
       result = new DexType(descriptor);
       assert result.isArrayType()
-          || result.isClassType()
-          || result.isPrimitiveType()
-          || result.isVoidType();
+              || result.isClassType()
+              || result.isPrimitiveType()
+              || result.isVoidType()
+          : descriptor.toString();
       assert !isInternalSentinel(result);
       types.put(descriptor, result);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 34d5646..0cb6881 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -244,8 +244,7 @@
         .withItem(DexDefinition::annotations)
         // TODO(b/158159959): Make signatures structural.
         .withAssert(c -> c.classSignature == ClassSignature.noSignature())
-        .withItemArray(c -> c.staticFields)
-        .withItemArray(c -> c.instanceFields)
+        .withItemCollection(DexClass::allFieldsSorted)
         .withItemCollection(DexClass::allMethodsSorted);
   }
 
@@ -475,8 +474,7 @@
     if (hasMethodsOrFields()) {
       collector.add(this);
       methodCollection.forEachMethod(m -> m.collectMixedSectionItems(collector));
-      collectAll(collector, staticFields);
-      collectAll(collector, instanceFields);
+      fieldCollection.forEachField(f -> f.collectMixedSectionItems(collector));
     }
     annotations().collectMixedSectionItems(collector);
     if (interfaces != null) {
@@ -643,7 +641,7 @@
   }
 
   public boolean hasFields() {
-    return instanceFields.length + staticFields.length > 0;
+    return fieldCollection.size() > 0;
   }
 
   public boolean hasMethods() {
@@ -658,19 +656,16 @@
   public boolean hasClassOrMemberAnnotations() {
     return !annotations().isEmpty()
         || hasAnnotations(methodCollection)
-        || hasAnnotations(staticFields)
-        || hasAnnotations(instanceFields);
+        || hasAnnotations(fieldCollection);
   }
 
   boolean hasOnlyInternalizableAnnotations() {
-    return !hasAnnotations(methodCollection)
-        && !hasAnnotations(staticFields)
-        && !hasAnnotations(instanceFields);
+    return !hasAnnotations(methodCollection) && !hasAnnotations(fieldCollection);
   }
 
-  private boolean hasAnnotations(DexEncodedField[] fields) {
+  private boolean hasAnnotations(FieldCollection fields) {
     synchronized (fields) {
-      return Arrays.stream(fields).anyMatch(DexEncodedField::hasAnnotations);
+      return fields.hasAnnotations();
     }
   }
 
@@ -685,13 +680,12 @@
     if (!hasNonDefaultStaticFieldValues()) {
       return null;
     }
-    DexEncodedField[] fields = staticFields;
-    Arrays.sort(
-        fields, (a, b) -> a.getReference().compareToWithNamingLens(b.getReference(), namingLens));
+    List<DexEncodedField> fields = new ArrayList<>(staticFields());
+    fields.sort((a, b) -> a.getReference().compareToWithNamingLens(b.getReference(), namingLens));
     int length = 0;
-    List<DexValue> values = new ArrayList<>(fields.length);
-    for (int i = 0; i < fields.length; i++) {
-      DexEncodedField field = fields[i];
+    List<DexValue> values = new ArrayList<>(fields.size());
+    for (int i = 0; i < fields.size(); i++) {
+      DexEncodedField field = fields.get(i);
       DexValue staticValue = field.getStaticValue();
       assert staticValue != null;
       values.add(staticValue);
@@ -705,7 +699,7 @@
   }
 
   private boolean hasNonDefaultStaticFieldValues() {
-    for (DexEncodedField field : staticFields) {
+    for (DexEncodedField field : staticFields()) {
       DexValue value = field.getStaticValue();
       if (value != null && !value.isDefault(field.getReference().type)) {
         return true;
diff --git a/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
new file mode 100644
index 0000000..4b782b3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
@@ -0,0 +1,243 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import static com.google.common.base.Predicates.alwaysTrue;
+
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldArrayBacking extends FieldCollectionBacking {
+
+  private DexEncodedField[] staticFields;
+  private DexEncodedField[] instanceFields;
+
+  public static FieldCollectionBacking fromArrays(
+      DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    return new FieldArrayBacking(staticFields, instanceFields);
+  }
+
+  private FieldArrayBacking(DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    assert staticFields != null;
+    assert instanceFields != null;
+    this.staticFields = staticFields;
+    this.instanceFields = instanceFields;
+  }
+
+  @Override
+  boolean verify() {
+    assert verifyNoDuplicateFields();
+    return true;
+  }
+
+  private boolean verifyNoDuplicateFields() {
+    Set<DexField> unique = Sets.newIdentityHashSet();
+    for (DexEncodedField field : fields(alwaysTrue())) {
+      boolean changed = unique.add(field.getReference());
+      assert changed : "Duplicate field `" + field.getReference().toSourceString() + "`";
+    }
+    return true;
+  }
+
+  @Override
+  int numberOfStaticFields() {
+    return staticFields.length;
+  }
+
+  @Override
+  int numberOfInstanceFields() {
+    return instanceFields.length;
+  }
+
+  @Override
+  int size() {
+    return staticFields.length + instanceFields.length;
+  }
+
+  @Override
+  TraversalContinuation<?, ?> traverse(Function<DexEncodedField, TraversalContinuation<?, ?>> fn) {
+    for (int i = 0; i < staticFields.length; i++) {
+      if (fn.apply(staticFields[i]).shouldBreak()) {
+        return TraversalContinuation.doBreak();
+      }
+    }
+    for (int i = 0; i < instanceFields.length; i++) {
+      if (fn.apply(instanceFields[i]).shouldBreak()) {
+        return TraversalContinuation.doBreak();
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  @Override
+  Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return Iterables.concat(
+        Iterables.filter(Arrays.asList(instanceFields), predicate::test),
+        Iterables.filter(Arrays.asList(staticFields), predicate::test));
+  }
+
+  @Override
+  List<DexEncodedField> staticFieldsAsList() {
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(staticFields));
+    }
+    return Arrays.asList(staticFields);
+  }
+
+  @Override
+  void appendStaticField(DexEncodedField field) {
+    staticFields = appendFieldHelper(staticFields, field);
+  }
+
+  @Override
+  void appendStaticFields(Collection<DexEncodedField> fields) {
+    staticFields = appendFieldsHelper(staticFields, fields);
+  }
+
+  @Override
+  void clearStaticFields() {
+    staticFields = DexEncodedField.EMPTY_ARRAY;
+  }
+
+  @Override
+  public void setStaticFields(DexEncodedField[] fields) {
+    assert fields != null;
+    staticFields = fields;
+  }
+
+  @Override
+  List<DexEncodedField> instanceFieldsAsList() {
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(instanceFields));
+    }
+    return Arrays.asList(instanceFields);
+  }
+
+  @Override
+  void appendInstanceField(DexEncodedField field) {
+    instanceFields = appendFieldHelper(instanceFields, field);
+  }
+
+  @Override
+  void appendInstanceFields(Collection<DexEncodedField> fields) {
+    instanceFields = appendFieldsHelper(instanceFields, fields);
+  }
+
+  @Override
+  void clearInstanceFields() {
+    instanceFields = DexEncodedField.EMPTY_ARRAY;
+  }
+
+  @Override
+  void setInstanceFields(DexEncodedField[] fields) {
+    assert fields != null;
+    instanceFields = fields;
+  }
+
+  @Override
+  DexEncodedField lookupField(DexField field) {
+    DexEncodedField result = lookupInstanceField(field);
+    return result == null ? lookupStaticField(field) : result;
+  }
+
+  @Override
+  DexEncodedField lookupStaticField(DexField field) {
+    return lookupFieldHelper(staticFields, field);
+  }
+
+  @Override
+  DexEncodedField lookupInstanceField(DexField field) {
+    return lookupFieldHelper(instanceFields, field);
+  }
+
+  @Override
+  void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    staticFields =
+        replaceFieldsHelper(
+            staticFields,
+            replacement,
+            FieldCollectionBacking::belongsInStaticPool,
+            this::appendInstanceFields);
+    instanceFields =
+        replaceFieldsHelper(
+            instanceFields,
+            replacement,
+            FieldCollectionBacking::belongsInInstancePool,
+            this::appendStaticFields);
+  }
+
+  private static DexEncodedField[] appendFieldHelper(
+      DexEncodedField[] existingItems, DexEncodedField itemToAppend) {
+    DexEncodedField[] newFields = new DexEncodedField[existingItems.length + 1];
+    System.arraycopy(existingItems, 0, newFields, 0, existingItems.length);
+    newFields[existingItems.length] = itemToAppend;
+    return newFields;
+  }
+
+  private static DexEncodedField[] appendFieldsHelper(
+      DexEncodedField[] existingItems, Collection<DexEncodedField> itemsToAppend) {
+    DexEncodedField[] newFields = new DexEncodedField[existingItems.length + itemsToAppend.size()];
+    System.arraycopy(existingItems, 0, newFields, 0, existingItems.length);
+    int i = existingItems.length;
+    for (DexEncodedField field : itemsToAppend) {
+      newFields[i] = field;
+      i++;
+    }
+    return newFields;
+  }
+
+  private static DexEncodedField lookupFieldHelper(DexEncodedField[] items, DexField reference) {
+    for (int i = 0; i < items.length; i++) {
+      DexEncodedField item = items[i];
+      if (reference.match(item)) {
+        return item;
+      }
+    }
+    return null;
+  }
+
+  private static DexEncodedField[] replaceFieldsHelper(
+      DexEncodedField[] fields,
+      Function<DexEncodedField, DexEncodedField> replacement,
+      Predicate<DexEncodedField> inThisPool,
+      Consumer<List<DexEncodedField>> onMovedToOtherPool) {
+    List<DexEncodedField> movedToOtherPool = new ArrayList<>();
+    for (int i = 0; i < fields.length; i++) {
+      DexEncodedField existingField = fields[i];
+      assert inThisPool.test(existingField);
+      DexEncodedField newField = replacement.apply(existingField);
+      assert newField != null;
+      if (existingField != newField) {
+        if (inThisPool.test(newField)) {
+          fields[i] = newField;
+        } else {
+          fields[i] = null;
+          movedToOtherPool.add(newField);
+        }
+      }
+    }
+    if (movedToOtherPool.isEmpty()) {
+      return fields;
+    }
+    onMovedToOtherPool.accept(movedToOtherPool);
+    return ArrayUtils.filter(
+        fields,
+        Objects::nonNull,
+        DexEncodedField.EMPTY_ARRAY,
+        fields.length - movedToOtherPool.size());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollection.java b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
new file mode 100644
index 0000000..e2cf122
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
@@ -0,0 +1,180 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldCollection {
+
+  // Threshold between using an array and a map for the backing store.
+  // The choice of 30 is just a copy from the method backing threshold.
+  private static final int ARRAY_BACKING_THRESHOLD = 30;
+
+  private final DexClass holder;
+  private FieldCollectionBacking backing;
+
+  private FieldCollection(DexClass holder, FieldCollectionBacking backing) {
+    this.holder = holder;
+    this.backing = backing;
+  }
+
+  public static FieldCollection create(
+      DexClass holder, DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    int fieldCount = staticFields.length + instanceFields.length;
+    FieldCollectionBacking backing;
+    if (fieldCount > ARRAY_BACKING_THRESHOLD) {
+      backing = FieldMapBacking.createLinked(fieldCount);
+      backing.setStaticFields(staticFields);
+      backing.setInstanceFields(instanceFields);
+    } else {
+      backing = FieldArrayBacking.fromArrays(staticFields, instanceFields);
+    }
+    return createInternal(holder, backing);
+  }
+
+  private static FieldCollection createInternal(DexClass holder, FieldCollectionBacking backing) {
+    // Internal create mirrors MethodCollection in case of adding a concurrency checker.
+    return new FieldCollection(holder, backing);
+  }
+
+  public int size() {
+    return backing.size();
+  }
+
+  public void forEachField(Consumer<DexEncodedField> fn) {
+    backing.traverse(
+        field -> {
+          fn.accept(field);
+          return TraversalContinuation.doContinue();
+        });
+  }
+
+  public Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return backing.fields(predicate);
+  }
+
+  public boolean verify() {
+    forEachField(
+        field -> {
+          assert verifyCorrectnessOfFieldHolder(field);
+        });
+    assert backing.verify();
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
+    assert field.getHolderType() == holder.type
+        : "Expected field `"
+            + field.getReference().toSourceString()
+            + "` to have holder `"
+            + holder.type.toSourceString()
+            + "`";
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfFieldHolders(Iterable<DexEncodedField> fields) {
+    for (DexEncodedField field : fields) {
+      assert verifyCorrectnessOfFieldHolder(field);
+    }
+    return true;
+  }
+
+  public boolean hasStaticFields() {
+    return backing.numberOfStaticFields() > 0;
+  }
+
+  public List<DexEncodedField> staticFieldsAsList() {
+    return backing.staticFieldsAsList();
+  }
+
+  public void appendStaticField(DexEncodedField field) {
+    assert verifyCorrectnessOfFieldHolder(field);
+    backing.appendStaticField(field);
+    assert backing.verify();
+  }
+
+  public void appendStaticFields(Collection<DexEncodedField> fields) {
+    assert verifyCorrectnessOfFieldHolders(fields);
+    backing.appendStaticFields(fields);
+    assert backing.verify();
+  }
+
+  public void clearStaticFields() {
+    backing.clearStaticFields();
+  }
+
+  public void setStaticFields(DexEncodedField[] fields) {
+    backing.setStaticFields(fields);
+    assert backing.verify();
+  }
+
+  public boolean hasInstanceFields() {
+    return backing.numberOfInstanceFields() > 0;
+  }
+
+  public List<DexEncodedField> instanceFieldsAsList() {
+    return backing.instanceFieldsAsList();
+  }
+
+  public void appendInstanceField(DexEncodedField field) {
+    assert verifyCorrectnessOfFieldHolder(field);
+    backing.appendInstanceField(field);
+    assert backing.verify();
+  }
+
+  public void appendInstanceFields(Collection<DexEncodedField> fields) {
+    assert verifyCorrectnessOfFieldHolders(fields);
+    backing.appendInstanceFields(fields);
+    assert backing.verify();
+  }
+
+  public void clearInstanceFields() {
+    backing.clearInstanceFields();
+  }
+
+  public void setInstanceFields(DexEncodedField[] fields) {
+    backing.setInstanceFields(fields);
+    assert backing.verify();
+  }
+
+  public DexEncodedField lookupField(DexField field) {
+    return backing.lookupField(field);
+  }
+
+  public DexEncodedField lookupStaticField(DexField field) {
+    return backing.lookupStaticField(field);
+  }
+
+  public DexEncodedField lookupInstanceField(DexField field) {
+    return backing.lookupInstanceField(field);
+  }
+
+  public void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    backing.replaceFields(replacement);
+  }
+
+  public List<DexEncodedField> allFieldsSorted() {
+    List<DexEncodedField> sorted = new ArrayList<>(size());
+    forEachField(sorted::add);
+    sorted.sort(Comparator.comparing(DexEncodedMember::getReference));
+    return sorted;
+  }
+
+  public boolean hasAnnotations() {
+    return backing
+        .traverse(
+            field ->
+                field.hasAnnotations()
+                    ? TraversalContinuation.doBreak()
+                    : TraversalContinuation.doContinue())
+        .shouldBreak();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
new file mode 100644
index 0000000..570996f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public abstract class FieldCollectionBacking {
+
+  // Internal consistency.
+
+  static boolean belongsInStaticPool(DexEncodedField field) {
+    return field.isStatic();
+  }
+
+  static boolean belongsInInstancePool(DexEncodedField field) {
+    return !belongsInStaticPool(field);
+  }
+
+  abstract boolean verify();
+
+  // Traversal methods.
+
+  abstract TraversalContinuation<?, ?> traverse(
+      Function<DexEncodedField, TraversalContinuation<?, ?>> fn);
+
+  // Collection methods.
+
+  abstract int size();
+
+  abstract Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate);
+
+  // Specialized to static fields.
+
+  abstract int numberOfStaticFields();
+
+  abstract List<DexEncodedField> staticFieldsAsList();
+
+  abstract void appendStaticField(DexEncodedField field);
+
+  abstract void appendStaticFields(Collection<DexEncodedField> fields);
+
+  abstract void clearStaticFields();
+
+  abstract void setStaticFields(DexEncodedField[] fields);
+
+  // Specialized to instance fields.
+
+  abstract int numberOfInstanceFields();
+
+  abstract List<DexEncodedField> instanceFieldsAsList();
+
+  abstract void appendInstanceField(DexEncodedField field);
+
+  abstract void appendInstanceFields(Collection<DexEncodedField> fields);
+
+  abstract void clearInstanceFields();
+
+  abstract void setInstanceFields(DexEncodedField[] fields);
+
+  abstract DexEncodedField lookupField(DexField field);
+
+  abstract DexEncodedField lookupStaticField(DexField field);
+
+  abstract DexEncodedField lookupInstanceField(DexField field);
+
+  abstract void replaceFields(Function<DexEncodedField, DexEncodedField> replacement);
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
new file mode 100644
index 0000000..3bac4e0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
@@ -0,0 +1,219 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldMapBacking extends FieldCollectionBacking {
+
+  private SortedMap<DexFieldSignature, DexEncodedField> fieldMap;
+
+  public static FieldMapBacking createLinked(int capacity) {
+    return new FieldMapBacking(createdLinkedMap(capacity));
+  }
+
+  private static SortedMap<DexFieldSignature, DexEncodedField> createdLinkedMap(int capacity) {
+    return new Object2ReferenceLinkedOpenHashMap<>(capacity);
+  }
+
+  private FieldMapBacking(SortedMap<DexFieldSignature, DexEncodedField> fieldMap) {
+    this.fieldMap = fieldMap;
+  }
+
+  // Internal map allocation that shall preserve the map-backing type.
+  // Only the linked map exists for fields currently.
+  private SortedMap<DexFieldSignature, DexEncodedField> internalCreateMap(int capacity) {
+    return createdLinkedMap(capacity);
+  }
+
+  @Override
+  boolean verify() {
+    fieldMap.forEach(
+        (signature, field) -> {
+          assert signature.match(field.getReference());
+        });
+    return true;
+  }
+
+  @Override
+  TraversalContinuation<?, ?> traverse(Function<DexEncodedField, TraversalContinuation<?, ?>> fn) {
+    for (DexEncodedField field : fieldMap.values()) {
+      TraversalContinuation<?, ?> result = fn.apply(field);
+      if (result.shouldBreak()) {
+        return result;
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  @Override
+  int size() {
+    return fieldMap.size();
+  }
+
+  @Override
+  Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return IterableUtils.filter(fieldMap.values(), predicate);
+  }
+
+  @Override
+  int numberOfStaticFields() {
+    return numberOfFieldsHelper(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  List<DexEncodedField> staticFieldsAsList() {
+    return fieldsAsListHelper(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  void appendStaticField(DexEncodedField field) {
+    assert belongsInStaticPool(field);
+    DexEncodedField old = fieldMap.put(getSignature(field), field);
+    assert old == null;
+  }
+
+  @Override
+  void appendStaticFields(Collection<DexEncodedField> fields) {
+    fields.forEach(this::appendStaticField);
+  }
+
+  @Override
+  void clearStaticFields() {
+    fieldMap.values().removeIf(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  void setStaticFields(DexEncodedField[] fields) {
+    setFieldsInPoolHelper(fields, FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  int numberOfInstanceFields() {
+    return numberOfFieldsHelper(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  List<DexEncodedField> instanceFieldsAsList() {
+    return fieldsAsListHelper(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  void appendInstanceField(DexEncodedField field) {
+    assert belongsInInstancePool(field);
+    DexEncodedField old = fieldMap.put(getSignature(field), field);
+    assert old == null;
+  }
+
+  @Override
+  void appendInstanceFields(Collection<DexEncodedField> fields) {
+    fields.forEach(this::appendInstanceField);
+  }
+
+  @Override
+  void clearInstanceFields() {
+    fieldMap.values().removeIf(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  void setInstanceFields(DexEncodedField[] fields) {
+    setFieldsInPoolHelper(fields, FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  DexEncodedField lookupField(DexField field) {
+    return fieldMap.get(getSignature(field));
+  }
+
+  @Override
+  DexEncodedField lookupStaticField(DexField field) {
+    DexEncodedField result = lookupField(field);
+    return result != null && belongsInStaticPool(result) ? result : null;
+  }
+
+  @Override
+  DexEncodedField lookupInstanceField(DexField field) {
+    DexEncodedField result = lookupField(field);
+    return result != null && belongsInInstancePool(result) ? result : null;
+  }
+
+  @Override
+  void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    // The code assumes that when replacement.apply(field) is called, the map is up-to-date with
+    // the previously replaced fields. We therefore cannot postpone the map updates to the end of
+    // the replacement.
+    ArrayList<DexEncodedField> initialValues = new ArrayList<>(fieldMap.values());
+    for (DexEncodedField field : initialValues) {
+      DexEncodedField newField = replacement.apply(field);
+      if (newField != field) {
+        DexFieldSignature oldSignature = getSignature(field);
+        DexFieldSignature newSignature = getSignature(newField);
+        if (!newSignature.isEqualTo(oldSignature)) {
+          if (fieldMap.get(oldSignature) == field) {
+            fieldMap.remove(oldSignature);
+          }
+        }
+        fieldMap.put(newSignature, newField);
+      }
+    }
+  }
+
+  private DexFieldSignature getSignature(DexEncodedField field) {
+    return getSignature(field.getReference());
+  }
+
+  private DexFieldSignature getSignature(DexField field) {
+    return DexFieldSignature.fromField(field);
+  }
+
+  private int numberOfFieldsHelper(Predicate<DexEncodedField> predicate) {
+    int count = 0;
+    for (DexEncodedField field : fieldMap.values()) {
+      if (predicate.test(field)) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  private List<DexEncodedField> fieldsAsListHelper(Predicate<DexEncodedField> predicate) {
+    List<DexEncodedField> result = new ArrayList<>(fieldMap.size());
+    fieldMap.forEach(
+        (signature, field) -> {
+          if (predicate.test(field)) {
+            result.add(field);
+          }
+        });
+    return Collections.unmodifiableList(result);
+  }
+
+  private void setFieldsInPoolHelper(
+      DexEncodedField[] fields, Predicate<DexEncodedField> inThisPool) {
+    if (fields.length == 0 && fieldMap.isEmpty()) {
+      return;
+    }
+    SortedMap<DexFieldSignature, DexEncodedField> newMap =
+        internalCreateMap(size() + fields.length);
+    fieldMap.forEach(
+        (signature, field) -> {
+          if (!inThisPool.test(field)) {
+            newMap.put(signature, field);
+          }
+        });
+    for (DexEncodedField field : fields) {
+      assert inThisPool.test(field);
+      newMap.put(getSignature(field), field);
+    }
+    fieldMap = newMap;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
index 30a6dfa..b1ca241 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
@@ -97,8 +97,6 @@
         .getDefinition()
         .setApiLevelForDefinition(
             apiCompute.computeApiLevelForDefinition(
-                member.getReference(),
-                appView.dexItemFactory(),
-                apiCompute.getPlatformApiLevelOrUnknown(appView)));
+                member.getReference(), appView.dexItemFactory(), ComputedApiLevel.unknown()));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index 25933dd..195a8fc 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeUtils;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
 import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -22,6 +24,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
+import java.util.function.Predicate;
 
 public interface ClassInstanceFieldsMerger {
 
@@ -135,6 +138,8 @@
 
     private DexEncodedField classIdField;
 
+    private final Set<DexField> committedFields = Sets.newIdentityHashSet();
+
     private ClassInstanceFieldsMergerImpl(
         AppView<? extends AppInfoWithClassHierarchy> appView,
         HorizontalClassMergerGraphLens.Builder lensBuilder,
@@ -155,12 +160,17 @@
       List<DexEncodedField> newFields = new ArrayList<>();
       if (classIdField != null) {
         newFields.add(classIdField);
+        committedFields.add(classIdField.getReference());
       }
       group
           .getInstanceFieldMap()
           .forEachManyToOneMapping(
-              (sourceFields, targetField) ->
-                  newFields.add(mergeSourceFieldsToTargetField(targetField, sourceFields)));
+              (sourceFields, targetField) -> {
+                DexEncodedField newField =
+                    mergeSourceFieldsToTargetField(targetField, sourceFields);
+                newFields.add(newField);
+                committedFields.add(newField.getReference());
+              });
       return newFields.toArray(DexEncodedField.EMPTY_ARRAY);
     }
 
@@ -184,6 +194,19 @@
         newField = targetField;
       }
 
+      if (committedFields.contains(newField.getReference())) {
+        newField =
+            targetField.toTypeSubstitutedField(
+                appView,
+                appView
+                    .dexItemFactory()
+                    .createFreshFieldNameWithoutHolder(
+                        newField.getHolderType(),
+                        newField.getType(),
+                        newField.getName().toString(),
+                        Predicate.not(committedFields::contains)));
+      }
+
       lensBuilder.recordNewFieldSignature(
           Iterables.transform(
               IterableUtils.append(sourceFields, targetField), DexEncodedField::getReference),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index b6095d1..94577c8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -57,6 +57,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields;
 import com.android.tools.r8.horizontalclassmerging.policies.SameMainDexGroup;
 import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
+import com.android.tools.r8.horizontalclassmerging.policies.SamePackageForApiOutline;
 import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
 import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.VerifyMultiClassPolicyAlwaysSatisfied;
@@ -264,6 +265,7 @@
         new SameNestHost(appView),
         new SameParentClass(),
         new SyntheticItemsPolicy(appView, mode),
+        new SamePackageForApiOutline(appView, mode),
         new NoDifferentApiReferenceLevel(appView),
         new LimitClassGroups(appView));
     assert verifyMultiClassPoliciesIrrelevantForMergingSyntheticsInD8(appView, mode, builder);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ComputeApiLevelOfSyntheticClass.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ComputeApiLevelOfSyntheticClass.java
index 3568c17..9038a57 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ComputeApiLevelOfSyntheticClass.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ComputeApiLevelOfSyntheticClass.java
@@ -195,9 +195,7 @@
 
     private void setMaxApiReferenceLevel(DexReference reference) {
       maxApiReferenceLevel =
-          maxApiReferenceLevel.max(
-              apiLevelCompute.computeApiLevelForLibraryReference(
-                  reference, apiLevelCompute.getPlatformApiLevelOrUnknown(appView)));
+          maxApiReferenceLevel.max(apiLevelCompute.computeApiLevelForLibraryReference(reference));
     }
 
     public ComputedApiLevel getMaxApiReferenceLevel() {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDifferentApiReferenceLevel.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDifferentApiReferenceLevel.java
index 8605caa..a021a28 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDifferentApiReferenceLevel.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDifferentApiReferenceLevel.java
@@ -16,7 +16,6 @@
 
   private final AndroidApiLevelCompute apiLevelCompute;
   private final AppView<?> appView;
-  // TODO(b/188388130): Remove when stabilized.
   private final boolean enableApiCallerIdentification;
 
   public NoDifferentApiReferenceLevel(AppView<?> appView) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForApiOutline.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForApiOutline.java
new file mode 100644
index 0000000..30a3200
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SamePackageForApiOutline.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SamePackageForApiOutline extends MultiClassPolicy {
+
+  private final AppView<AppInfo> appView;
+  private final Mode mode;
+
+  public SamePackageForApiOutline(AppView<AppInfo> appView, Mode mode) {
+    this.appView = appView;
+    this.mode = mode;
+  }
+
+  /** Sort unrestricted classes into restricted classes if they are in the same package. */
+  private void tryFindRestrictedPackage(
+      MergeGroup unrestrictedClasses, Map<String, MergeGroup> restrictedClasses) {
+    unrestrictedClasses.removeIf(
+        clazz -> {
+          MergeGroup restrictedPackage = restrictedClasses.get(clazz.type.getPackageDescriptor());
+          if (restrictedPackage != null) {
+            restrictedPackage.add(clazz);
+            return true;
+          }
+          return false;
+        });
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    Map<String, MergeGroup> restrictedClasses = new LinkedHashMap<>();
+    MergeGroup unrestrictedClasses = new MergeGroup();
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+
+    // Sort all restricted classes into packages.
+    for (DexProgramClass clazz : group) {
+      if (syntheticItems.isSyntheticOfKind(clazz.getType(), k -> k.API_MODEL_OUTLINE)) {
+        restrictedClasses
+            .computeIfAbsent(
+                clazz.getType().getPackageDescriptor(), ignoreArgument(MergeGroup::new))
+            .add(clazz);
+      } else {
+        unrestrictedClasses.add(clazz);
+      }
+    }
+
+    tryFindRestrictedPackage(unrestrictedClasses, restrictedClasses);
+    removeTrivialGroups(restrictedClasses.values());
+
+    Collection<MergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
+    if (unrestrictedClasses.size() > 1) {
+      groups.add(unrestrictedClasses);
+    }
+    groups.addAll(restrictedClasses.values());
+    return groups;
+  }
+
+  @Override
+  public String getName() {
+    return "SamePackageForApiOutline";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java b/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java
index 85467b7..bde37fd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.Position.SourcePosition;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
@@ -28,26 +29,23 @@
       Position callerPosition,
       int expectedPositionsCount,
       DexMethod method,
-      boolean methodIsSynthesized) {
+      boolean methodIsSynthesized,
+      Position preamblePosition) {
     canonicalPositions =
         new HashMap<>(1 + (callerPosition == null ? 0 : 1) + expectedPositionsCount);
+    if (preamblePosition == null) {
+      preamblePosition = SyntheticPosition.builder().setLine(0).setMethod(method).build();
+    }
     if (callerPosition != null) {
       this.callerPosition = getCanonical(callerPosition);
       isCompilerSynthesizedInlinee = methodIsSynthesized;
-      preamblePosition =
-          methodIsSynthesized
-              ? callerPosition
-              : getCanonical(
-                  SourcePosition.builder()
-                      .setLine(0)
-                      .setMethod(method)
-                      .setCallerPosition(callerPosition)
-                      .build());
+      this.preamblePosition =
+          getCanonical(
+              Code.newInlineePosition(callerPosition, preamblePosition, methodIsSynthesized));
     } else {
       this.callerPosition = null;
       isCompilerSynthesizedInlinee = false;
-      preamblePosition =
-          getCanonical(SyntheticPosition.builder().setLine(0).setMethod(method).build());
+      this.preamblePosition = getCanonical(preamblePosition);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index bffd1fe..60d5807 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -252,7 +252,8 @@
             callerPosition,
             cfPositionCount,
             originalMethod,
-            method.getDefinition().isD8R8Synthesized());
+            method.getDefinition().isD8R8Synthesized(),
+            code.getPreamblePosition());
     internalOutputMode = appView.options().getInternalOutputMode();
 
     needsGeneratedMethodSynchronization =
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 3242779..229fcfc 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -44,6 +44,7 @@
 import com.android.tools.r8.ir.code.CanonicalPositions;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.utils.DexDebugUtils;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -94,7 +95,8 @@
             callerPosition,
             debugEntries == null ? 0 : debugEntries.size(),
             originalMethod,
-            method.getDefinition().isD8R8Synthesized());
+            method.getDefinition().isD8R8Synthesized(),
+            DexDebugUtils.computePreamblePosition(originalMethod, info).getFramePosition());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 42497b9..dc31133 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -626,6 +626,14 @@
         int limit = 11;
         for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
           if (!clazz.type.descriptor.startsWith(neverMergePrefix)) {
+            boolean hasExceptionPrefix = false;
+            for (DexString exceptionPrefix : neverMerge.getExceptionPrefixes()) {
+              hasExceptionPrefix =
+                  hasExceptionPrefix | clazz.type.descriptor.startsWith(exceptionPrefix);
+            }
+            if (hasExceptionPrefix) {
+              continue;
+            }
             if (limit-- < 0) {
               message.append("..");
               break;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index 98e481d..bc11f33 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -207,7 +207,8 @@
         }
       }
       if (options.getMinApiLevel().isLessThan(AndroidApiLevel.R)) {
-        if (typeIsPresentWithoutBackportsFrom(factory.setType, AndroidApiLevel.R)) {
+        if (options.testing.alwaysBackportListSetMapMethods
+            || typeIsPresentWithoutBackportsFrom(factory.setType, AndroidApiLevel.R)) {
           initializeAndroidRSetListMapMethodProviders(factory);
         }
         if (typeIsAbsentOrPresentWithoutBackportsFrom(factory.objectsType, AndroidApiLevel.R)) {
@@ -219,7 +220,8 @@
       }
       if (options.getMinApiLevel().isLessThan(AndroidApiLevel.S)) {
         initializeAndroidSMethodProviders(factory);
-        if (typeIsPresentWithoutBackportsFrom(factory.setType, AndroidApiLevel.S)) {
+        if (options.testing.alwaysBackportListSetMapMethods
+            || typeIsPresentWithoutBackportsFrom(factory.setType, AndroidApiLevel.S)) {
           initializeAndroidSSetListMapMethodProviders(factory);
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 1288379..753b6ce 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -207,8 +207,7 @@
                         .setApiLevelForDefinition(appView.computedMinApiLevel())
                         .setApiLevelForCode(
                             apiLevelCompute.computeApiLevelForDefinition(
-                                ListUtils.map(classes, clazz -> clazz.type),
-                                apiLevelCompute.getPlatformApiLevelOrUnknown(appView)))
+                                ListUtils.map(classes, clazz -> clazz.type)))
                         .setCode(
                             m ->
                                 ServiceLoaderSourceCode.generate(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 44eed32..77100b5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -126,8 +126,7 @@
         localUtilityClass.getDefinition().setStaticFields(localUtilityFields);
       } else {
         clazz.getMethodCollection().replaceMethods(this::fixupEncodedMethod);
-        fixupFields(clazz.staticFields(), clazz::setStaticField);
-        fixupFields(clazz.instanceFields(), clazz::setInstanceField);
+        clazz.getFieldCollection().replaceFields(this::fixupEncodedField);
       }
     }
 
@@ -613,32 +612,27 @@
     return newMethod;
   }
 
-  private void fixupFields(List<DexEncodedField> fields, DexClass.FieldSetter setter) {
-    if (fields == null) {
-      return;
+  private DexEncodedField fixupEncodedField(DexEncodedField encodedField) {
+    DexField field = encodedField.getReference();
+    DexType newType = fixupType(field.type);
+    if (newType == field.type) {
+      return encodedField;
     }
-    for (int i = 0; i < fields.size(); i++) {
-      DexEncodedField encodedField = fields.get(i);
-      DexField field = encodedField.getReference();
-      DexType newType = fixupType(field.type);
-      if (newType != field.type) {
-        DexField newField = field.withType(newType, factory);
-        lensBuilder.move(field, newField);
-        DexEncodedField newEncodedField =
-            encodedField.toTypeSubstitutedField(
-                appView,
-                newField,
-                builder ->
-                    builder.setAbstractValue(
-                        encodedField.getOptimizationInfo().getAbstractValue(), appView));
-        setter.setField(i, newEncodedField);
-        if (encodedField.isStatic() && encodedField.hasExplicitStaticValue()) {
-          assert encodedField.getStaticValue() == DexValue.DexValueNull.NULL;
-          newEncodedField.setStaticValue(DexValue.DexValueInt.DEFAULT);
-          // TODO(b/150593449): Support conversion from DexValueEnum to DexValueInt.
-        }
-      }
+    DexField newField = field.withType(newType, factory);
+    lensBuilder.move(field, newField);
+    DexEncodedField newEncodedField =
+        encodedField.toTypeSubstitutedField(
+            appView,
+            newField,
+            builder ->
+                builder.setAbstractValue(
+                    encodedField.getOptimizationInfo().getAbstractValue(), appView));
+    if (encodedField.isStatic() && encodedField.hasExplicitStaticValue()) {
+      assert encodedField.getStaticValue() == DexValue.DexValueNull.NULL;
+      newEncodedField.setStaticValue(DexValue.DexValueInt.DEFAULT);
+      // TODO(b/150593449): Support conversion from DexValueEnum to DexValueInt.
     }
+    return newEncodedField;
   }
 
   private DexProto fixupProto(DexProto proto) {
diff --git a/src/main/java/com/android/tools/r8/naming/VarHandleDesugaringRewritingNamingLens.java b/src/main/java/com/android/tools/r8/naming/VarHandleDesugaringRewritingNamingLens.java
index cc9c83f..0d7078f 100644
--- a/src/main/java/com/android/tools/r8/naming/VarHandleDesugaringRewritingNamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/VarHandleDesugaringRewritingNamingLens.java
@@ -13,6 +13,8 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.NamingLens.NonIdentityNamingLens;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.IdentityHashMap;
+import java.util.Map;
 
 // Naming lens for VarHandle desugaring rewriting. Rewriting java.lang.invoke.MethodHandles$Lookup
 // to com.android.tools.r8.DesugarMethodHandlesLookup.
@@ -20,26 +22,80 @@
 
   private final DexItemFactory factory;
   private final NamingLens namingLens;
+  private final Map<DexType, DexString> mapping;
 
   public static NamingLens createVarHandleDesugaringRewritingNamingLens(AppView<?> appView) {
+    DexItemFactory factory = appView.dexItemFactory();
     if (appView.options().shouldDesugarVarHandle()
-        && (appView
-                    .appInfo()
-                    .definitionForWithoutExistenceAssert(appView.dexItemFactory().lookupType)
-                != null
-            || appView
-                    .appInfo()
-                    .definitionForWithoutExistenceAssert(appView.dexItemFactory().varHandleType)
+        && (appView.appInfo().definitionForWithoutExistenceAssert(factory.lookupType) != null
+            || appView.appInfo().definitionForWithoutExistenceAssert(factory.varHandleType)
                 != null)) {
-      return new VarHandleDesugaringRewritingNamingLens(appView);
+
+      // Prune all inner classes attributes referring to MethodHandles$Lookup, as that is rewritten
+      // to the toplevel class DesugarMethodHandlesLookup.
+      appView
+          .appInfo()
+          .classes()
+          .forEach(
+              clazz -> {
+                clazz.removeInnerClasses(
+                    innerClassAttribute -> innerClassAttribute.getInner() == factory.lookupType);
+              });
+
+      // Function to prefix type namespace, e.g. rename L... to Lj$/...
+      Map<DexType, DexString> mapping = new IdentityHashMap<>();
+      addRewritingForGlobalSynthetic(
+          appView, factory.lookupType, factory.desugarMethodHandlesLookupType, mapping);
+      addRewritingForGlobalSynthetic(
+          appView, factory.varHandleType, factory.desugarVarHandleType, mapping);
+      return new VarHandleDesugaringRewritingNamingLens(appView, mapping);
     }
     return appView.getNamingLens();
   }
 
-  public VarHandleDesugaringRewritingNamingLens(AppView<?> appView) {
+  private static void addRewritingForGlobalSynthetic(
+      AppView<?> appView,
+      DexType globalSynthetic,
+      DexType desugaredGlobalSynthetic,
+      Map<DexType, DexString> mapping) {
+    DexItemFactory factory = appView.dexItemFactory();
+    // The VarHandle global synthetics and synthetics derived from them are rewritten to use the
+    // desugared name.
+    assert appView.appInfo().getSyntheticItems().isFinalized();
+    String globalSyntheticString = globalSynthetic.descriptor.toString();
+    DexString currentPrefix =
+        factory.createString(
+            globalSyntheticString.substring(0, globalSyntheticString.length() - 1));
+    String desugaredGlobalSyntheticString = desugaredGlobalSynthetic.descriptor.toString();
+    DexString newPrefix =
+        appView.options().synthesizedClassPrefix.isEmpty()
+            ? factory.createString(
+                "L"
+                    + desugaredGlobalSyntheticString.substring(
+                        1, desugaredGlobalSyntheticString.length() - 1))
+            : factory.createString(
+                "L"
+                    + appView.options().synthesizedClassPrefix
+                    + desugaredGlobalSyntheticString.substring(
+                        1, desugaredGlobalSyntheticString.length() - 1));
+    // Rewrite the global synthetic in question and all the synthetics derived from it.
+    appView
+        .appInfo()
+        .getSyntheticItems()
+        .collectSyntheticsFromContext(globalSynthetic)
+        .forEach(
+            synthetic ->
+                mapping.put(
+                    synthetic,
+                    synthetic.descriptor.withNewPrefix(currentPrefix, newPrefix, factory)));
+  }
+
+  private VarHandleDesugaringRewritingNamingLens(
+      AppView<?> appView, Map<DexType, DexString> mapping) {
     super(appView.dexItemFactory());
     this.factory = appView.dexItemFactory();
     this.namingLens = appView.getNamingLens();
+    this.mapping = mapping;
   }
 
   private boolean isRenamed(DexType type) {
@@ -49,12 +105,7 @@
   private DexString getRenaming(DexType type) {
     assert type != factory.desugarMethodHandlesLookupType;
     assert type != factory.desugarVarHandleType;
-    if (type == factory.lookupType) {
-      return factory.desugarMethodHandlesLookupType.descriptor;
-    } else if (type == factory.varHandleType) {
-      return factory.desugarVarHandleType.descriptor;
-    }
-    return null;
+    return mapping.get(type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
index d779378..67e5054 100644
--- a/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
@@ -155,9 +155,7 @@
     if (isEnabled) {
       if (reference.isDexType()) {
         maxApiReferenceLevel =
-            maxApiReferenceLevel.max(
-                apiLevelCompute.computeApiLevelForLibraryReference(
-                    reference, apiLevelCompute.getPlatformApiLevelOrUnknown(appView)));
+            maxApiReferenceLevel.max(apiLevelCompute.computeApiLevelForLibraryReference(reference));
       } else if (!reference.getContextType().isClassType()) {
         maxApiReferenceLevel = maxApiReferenceLevel.max(appView.computedMinApiLevel());
       } else {
@@ -169,11 +167,7 @@
                       appView, appInfoWithClassHierarchy, holder, reference.asDexMember())
                   .getSecond();
         }
-        maxApiReferenceLevel =
-            maxApiReferenceLevel.max(
-                referenceApiLevel.isUnknownApiLevel()
-                    ? apiLevelCompute.getPlatformApiLevelOrUnknown(appView)
-                    : referenceApiLevel);
+        maxApiReferenceLevel = maxApiReferenceLevel.max(referenceApiLevel);
       }
     }
   }
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 6c5c07b..59b7ae3 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3606,6 +3606,7 @@
     this.rootSet = rootSet;
     rootSet.pendingMethodMoveInverse.forEach(pendingMethodMoveInverse::put);
     // Translate the result of root-set computation into enqueuer actions.
+    timing.begin("Register analysis");
     if (mode.isTreeShaking()
         && appView.options().hasProguardConfiguration()
         && !options.kotlinOptimizationOptions().disableKotlinSpecificOptimizations) {
@@ -3620,14 +3621,20 @@
     if (options.apiModelingOptions().enableLibraryApiModeling) {
       registerAnalysis(new ApiModelAnalysis(appView));
     }
+    timing.end();
 
     // Transfer the minimum keep info from the root set into the Enqueuer state.
+    timing.begin("Transfer minimum keep info");
     includeMinimumKeepInfo(rootSet);
+    timing.end();
 
     if (mode.isInitialTreeShaking()) {
       // Amend library methods with covariant return types.
+      timing.begin("Model library");
       modelLibraryMethodsWithCovariantReturnTypes();
+      timing.end();
     } else if (appView.getKeepInfo() != null) {
+      timing.begin("Retain keep info");
       EnqueuerEvent preconditionEvent = UnconditionalKeepInfoEvent.get();
       appView
           .getKeepInfo()
@@ -3638,24 +3645,38 @@
               (field, minimumKeepInfo) ->
                   applyMinimumKeepInfoWhenLive(field, minimumKeepInfo, preconditionEvent),
               this::applyMinimumKeepInfoWhenLiveOrTargeted);
+      timing.end();
     }
+    timing.begin("Enqueue all");
     enqueueAllIfNotShrinking();
+    timing.end();
+    timing.begin("Trace");
     trace(executorService, timing);
+    timing.end();
     options.reporter.failIfPendingErrors();
+    timing.begin("Finalize library override");
     finalizeLibraryMethodOverrideInformation();
+    timing.end();
+    timing.begin("Finish analysis");
     analyses.forEach(analyses -> analyses.done(this));
+    timing.end();
     assert verifyKeptGraph();
+    timing.begin("Finish compat building");
     if (mode.isInitialTreeShaking() && forceProguardCompatibility) {
       appView.setProguardCompatibilityActions(proguardCompatibilityActionsBuilder.build());
     } else {
       assert proguardCompatibilityActionsBuilder == null;
     }
+    timing.end();
     if (mode.isWhyAreYouKeeping()) {
       // For why are you keeping the information is reported through the kept graph callbacks and
       // no AppInfo is returned.
       return null;
     }
-    return createEnqueuerResult(appInfo);
+    timing.begin("Create result");
+    EnqueuerResult result = createEnqueuerResult(appInfo, timing);
+    timing.end();
+    return result;
   }
 
   private void includeMinimumKeepInfo(RootSetBase rootSet) {
@@ -4163,38 +4184,47 @@
     return true;
   }
 
-  private EnqueuerResult createEnqueuerResult(AppInfoWithClassHierarchy appInfo)
+  private EnqueuerResult createEnqueuerResult(AppInfoWithClassHierarchy appInfo, Timing timing)
       throws ExecutionException {
+    timing.begin("Remove dead protos");
     // Compute the set of dead proto types.
     deadProtoTypeCandidates.removeIf(this::isTypeLive);
     Set<DexType> deadProtoTypes =
         SetUtils.newIdentityHashSet(deadProtoTypeCandidates.size() + initialDeadProtoTypes.size());
     deadProtoTypeCandidates.forEach(deadProtoType -> deadProtoTypes.add(deadProtoType.type));
     deadProtoTypes.addAll(initialDeadProtoTypes);
+    timing.end();
 
     // Remove the temporary mappings that have been inserted into the field access info collection
     // and verify that the mapping is then one-to-one.
+    timing.begin("Prune field access mappings");
     fieldAccessInfoCollection.removeIf(
         (field, info) -> field != info.getField() || info == MISSING_FIELD_ACCESS_INFO);
     assert fieldAccessInfoCollection.verifyMappingIsOneToOne();
+    timing.end();
 
     // Verify all references on the input app before synthesizing definitions.
     assert verifyReferences(appInfo.app());
 
     // Prune the root set items that turned out to be dead.
     // TODO(b/150736225): Pruning of dead root set items is still incomplete.
-    rootSet.pruneDeadItems(appView, this);
+    timing.begin("Prune dead items");
+    rootSet.pruneDeadItems(appView, this, timing);
     if (mode.isTreeShaking() && appView.hasMainDexRootSet()) {
       assert rootSet != appView.getMainDexRootSet();
-      appView.getMainDexRootSet().pruneDeadItems(appView, this);
+      appView.getMainDexRootSet().pruneDeadItems(appView, this, timing);
     }
+    timing.end();
 
     // Ensure references from all hard coded factory items.
+    timing.begin("Ensure static factory references");
     appView
         .dexItemFactory()
         .forEachPossiblyCompilerSynthesizedType(this::recordCompilerSynthesizedTypeReference);
+    timing.end();
 
     // Rebuild a new app only containing referenced types.
+    timing.begin("Rebuild application");
     Set<DexLibraryClass> libraryClasses = Sets.newIdentityHashSet();
     Set<DexClasspathClass> classpathClasses = Sets.newIdentityHashSet();
     // Ensure all referenced non program types have their hierarchy built as live.
@@ -4220,6 +4250,7 @@
             .replaceLibraryClasses(libraryClasses)
             .replaceClasspathClasses(classpathClasses)
             .build();
+    timing.end();
 
     // Verify the references on the pruned application after type synthesis.
     assert verifyReferences(app);
@@ -4232,7 +4263,10 @@
               : ImmutableSet.of(syntheticClass.getType());
         };
     amendKeepInfoWithCompanionMethods();
+    timing.begin("Rewrite with deferred results");
     deferredTracing.rewriteApplication(executorService);
+    timing.end();
+    timing.begin("Create app info with liveness");
     AppInfoWithLiveness appInfoWithLiveness =
         new AppInfoWithLiveness(
             appInfo.getSyntheticItems().commit(app),
@@ -4275,6 +4309,7 @@
             lockCandidates,
             initClassReferences,
             recordFieldValuesReferences);
+    timing.end();
     appInfo.markObsolete();
     if (options.testing.enqueuerInspector != null) {
       options.testing.enqueuerInspector.accept(appInfoWithLiveness, mode);
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 9e1f28f..a728063 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -71,6 +71,7 @@
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
 import com.google.common.base.Equivalence.Wrapper;
@@ -1918,12 +1919,17 @@
               minimumKeepInfoForDefinition -> !minimumKeepInfoForDefinition.isShrinkingAllowed());
     }
 
-    public void pruneDeadItems(DexDefinitionSupplier definitions, Enqueuer enqueuer) {
+    public void pruneDeadItems(
+        DexDefinitionSupplier definitions, Enqueuer enqueuer, Timing timing) {
+      timing.begin("Prune keep info");
       getDependentMinimumKeepInfo().pruneDeadItems(definitions, enqueuer);
+      timing.end();
+      timing.begin("Prune others");
       pruneDeadReferences(noUnusedInterfaceRemoval, definitions, enqueuer);
       pruneDeadReferences(noVerticalClassMerging, definitions, enqueuer);
       pruneDeadReferences(noHorizontalClassMerging, definitions, enqueuer);
       pruneDeadReferences(alwaysInline, definitions, enqueuer);
+      timing.end();
     }
 
     private static void pruneDeadReferences(
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 4ea11dc..bf4029e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -49,6 +49,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -206,6 +207,33 @@
   private final ContextsForGlobalSynthetics globalContexts;
   private final GlobalSyntheticsStrategy globalSyntheticsStrategy;
 
+  public Set<DexType> collectSyntheticsFromContext(DexType context) {
+    Set<DexType> result = Sets.newIdentityHashSet();
+    committed
+        .getMethods()
+        .forEach(
+            (synthetic, methodReferences) -> {
+              methodReferences.forEach(
+                  methodReference -> {
+                    if (methodReference.getContext().getSynthesizingContextType() == context) {
+                      result.add(synthetic);
+                    }
+                  });
+            });
+    committed
+        .getClasses()
+        .forEach(
+            (synthetic, classReferences) -> {
+              classReferences.forEach(
+                  classReference -> {
+                    if (classReference.getContext().getSynthesizingContextType() == context) {
+                      result.add(synthetic);
+                    }
+                  });
+            });
+    return result;
+  }
+
   public SyntheticNaming getNaming() {
     return naming;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 63ed0ca..841bf9a 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -68,9 +68,7 @@
       AppView<?> appView, AndroidApiLevelCompute apiLevelCompute, DexProgramClass clazz) {
     // The api level of a class is the max level of it's members, super class and interfaces.
     return getMembersApiReferenceLevelForMerging(
-        clazz,
-        apiLevelCompute.computeApiLevelForDefinition(
-            clazz.allImmediateSupertypes(), apiLevelCompute.getPlatformApiLevelOrUnknown(appView)));
+        clazz, apiLevelCompute.computeApiLevelForDefinition(clazz.allImmediateSupertypes()));
   }
 
   private static ComputedApiLevel getMembersApiReferenceLevelForMerging(
diff --git a/src/main/java/com/android/tools/r8/utils/DexDebugUtils.java b/src/main/java/com/android/tools/r8/utils/DexDebugUtils.java
index 1dc89fd..adfda77 100644
--- a/src/main/java/com/android/tools/r8/utils/DexDebugUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DexDebugUtils.java
@@ -5,7 +5,13 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.graph.DexDebugEvent;
+import com.android.tools.r8.graph.DexDebugEvent.SetPositionFrame;
 import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexDebugInfo.EventBasedDebugInfo;
+import com.android.tools.r8.graph.DexDebugPositionState;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.utils.DexDebugUtils.PositionInfo.PositionInfoBuilder;
 import java.util.List;
 
 public class DexDebugUtils {
@@ -29,4 +35,81 @@
     }
     return true;
   }
+
+  public static PositionInfo computePreamblePosition(
+      DexMethod method, EventBasedDebugInfo debugInfo) {
+    if (debugInfo == null) {
+      return PositionInfo.builder().build();
+    }
+    Box<Position> existingPositionFrame = new Box<>();
+    DexDebugPositionState visitor =
+        new DexDebugPositionState(debugInfo.startLine, method) {
+          @Override
+          public void visit(SetPositionFrame setPositionFrame) {
+            super.visit(setPositionFrame);
+            existingPositionFrame.set(setPositionFrame.getPosition());
+          }
+        };
+    PositionInfoBuilder builder = PositionInfo.builder();
+    for (DexDebugEvent event : debugInfo.events) {
+      event.accept(visitor);
+      if (visitor.getCurrentPc() > 0) {
+        break;
+      }
+      if (event.isDefaultEvent()) {
+        builder.setLinePositionAtPcZero(visitor.getCurrentLine());
+        builder.setFramePosition(existingPositionFrame.get());
+      }
+    }
+    return builder.build();
+  }
+
+  public static class PositionInfo {
+
+    private final Position framePosition;
+    private final int linePositionAtPcZero;
+
+    private PositionInfo(Position framePosition, int linePositionAtPcZero) {
+      this.framePosition = framePosition;
+      this.linePositionAtPcZero = linePositionAtPcZero;
+    }
+
+    public boolean hasFramePosition() {
+      return framePosition != null;
+    }
+
+    public boolean hasLinePositionAtPcZero() {
+      return linePositionAtPcZero > -1;
+    }
+
+    public Position getFramePosition() {
+      return framePosition;
+    }
+
+    public int getLinePositionAtPcZero() {
+      return linePositionAtPcZero;
+    }
+
+    public static PositionInfoBuilder builder() {
+      return new PositionInfoBuilder();
+    }
+
+    public static class PositionInfoBuilder {
+
+      private Position framePosition;
+      private int linePositionAtPcZero = -1;
+
+      public void setFramePosition(Position position) {
+        this.framePosition = position;
+      }
+
+      public void setLinePositionAtPcZero(int linePositionAtPcZero) {
+        this.linePositionAtPcZero = linePositionAtPcZero;
+      }
+
+      public PositionInfo build() {
+        return new PositionInfo(framePosition, linePositionAtPcZero);
+      }
+    }
+  }
 }
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 c3ea9e2..51a5873 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1966,6 +1966,8 @@
 
   public static class TestingOptions {
 
+    // If false, use the desugared library implementation when desugared library is enabled.
+    public boolean alwaysBackportListSetMapMethods = true;
     public boolean neverReuseCfLocalRegisters = false;
     public boolean roundtripThroughLIR = false;
     public boolean checkReceiverAlwaysNullInCallSiteOptimization = true;
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
index a660a37..a6a2a06 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
@@ -87,9 +87,6 @@
     private final List<MappedPosition> mappedPositions;
     private final PositionRemapper positionRemapper;
     private final List<DexDebugEvent> processedEvents;
-    private final DexItemFactory factory;
-
-    private final DexMethod startMethod;
 
     // Keep track of what PC has been emitted.
     private int emittedPc = 0;
@@ -109,8 +106,6 @@
       this.mappedPositions = mappedPositions;
       this.positionRemapper = positionRemapper;
       this.processedEvents = processedEvents;
-      this.factory = factory;
-      this.startMethod = method;
     }
 
     // Force the current PC to emitted.
@@ -124,9 +119,6 @@
     // A default event denotes a line table entry and must always be emitted. Remap its line.
     @Override
     public void visit(Default defaultEvent) {
-      if (hasPreamblePosition(defaultEvent)) {
-        emitPreamblePosition();
-      }
       super.visit(defaultEvent);
       assert getCurrentLine() >= 0;
       Position position = getPosition();
@@ -139,13 +131,6 @@
       emittedPc = getCurrentPc();
     }
 
-    private boolean hasPreamblePosition(Default defaultEvent) {
-      return getCurrentPc() == 0
-          && defaultEvent.getPCDelta() > 0
-          && currentPosition != null
-          && currentPosition.getLine() != getCurrentLine();
-    }
-
     // Non-materializing events use super, ie, AdvancePC, AdvanceLine and SetInlineFrame.
 
     // Materializing events are just amended to the stream.
@@ -184,17 +169,6 @@
       flushPc();
       processedEvents.add(restartLocal);
     }
-
-    public void emitPreamblePosition() {
-      if (currentPosition == null || positionEventEmitter.didEmitLineEvents()) {
-        return;
-      }
-      Position mappedPosition =
-          PositionUtils.remapAndAdd(currentPosition, positionRemapper, mappedPositions);
-      processedEvents.add(factory.createPositionFrame(mappedPosition));
-      currentPosition = null;
-      currentMethod = startMethod;
-    }
   }
 
   private final AppView<?> appView;
@@ -206,7 +180,7 @@
   }
 
   public List<MappedPosition> optimizeDexCodePositions(
-      ProgramMethod method, PositionRemapper positionRemapper, boolean hasOverloads) {
+      ProgramMethod method, PositionRemapper positionRemapper) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexApplication application = appView.appInfo().app();
@@ -237,11 +211,6 @@
       event.accept(visitor);
     }
 
-    // We still need to emit a preamble if we did not materialize any other instructions.
-    if (mappedPositions.isEmpty()) {
-      visitor.emitPreamblePosition();
-    }
-
     EventBasedDebugInfo optimizedDebugInfo =
         new EventBasedDebugInfo(
             positionEventEmitter.didEmitLineEvents() ? positionEventEmitter.getStartLine() : 0,
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
index 914b174..2ada4e3 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
@@ -57,7 +57,7 @@
         int pcEncodingCutoff) {
       return canUseDexPc
           ? pcMapper.optimizeDexCodePositionsForPc(method, positionRemapper, pcEncodingCutoff)
-          : noPcMapper.optimizeDexCodePositions(method, positionRemapper, hasOverloads);
+          : noPcMapper.optimizeDexCodePositions(method, positionRemapper);
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/BackportedMethodListTest.java b/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
index b8a76cc..3bb27c2 100644
--- a/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
+++ b/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
@@ -97,11 +97,8 @@
         apiLevel < AndroidApiLevel.S.getLevel(),
         backports.contains("java/lang/StrictMath#multiplyExact(JI)J"));
     // Java 9, 10 and 11 method added at API level S.
-    // The method is not backported in desugared library JDK 11 (already present).
-    // TODO(b/243679691): Should no use backport but retargeting in between 24 and 33,
     assertEquals(
-        apiLevel < AndroidApiLevel.S.getLevel()
-            && (mode != Mode.LIBRARY_DESUGAR_11 || apiLevel >= AndroidApiLevel.N.getLevel()),
+        apiLevel < AndroidApiLevel.S.getLevel(),
         backports.contains("java/util/List#copyOf(Ljava/util/Collection;)Ljava/util/List;"));
 
     // Java 9, 10 and 11 methods not yet added.
diff --git a/src/test/java/com/android/tools/r8/L8TestBuilder.java b/src/test/java/com/android/tools/r8/L8TestBuilder.java
index 7d2ea31..2a1d0f5 100644
--- a/src/test/java/com/android/tools/r8/L8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/L8TestBuilder.java
@@ -32,6 +32,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public class L8TestBuilder {
 
@@ -247,6 +248,7 @@
     // in the vanilla desugared library.
     // Vanilla desugared library compilation should have no warnings.
     assertTrue(
+        warnings.stream().map(Diagnostic::getDiagnosticMessage).collect(Collectors.joining()),
         warnings.isEmpty()
             || warnings.stream()
                 .allMatch(warn -> warn.getDiagnosticMessage().contains("org.testng.Assert")));
@@ -257,8 +259,9 @@
       // with R8 anyway.
       if (info instanceof UnusedProguardKeepRuleDiagnostic) {
         // The default keep rules on desugared library may be unused. They should all be defined
-        // with keepclassmembers.
-        if (info.getDiagnosticMessage().contains("keepclassmembers")) {
+        // with keepclassmembers or keep,allowshrinking.
+        if (info.getDiagnosticMessage().contains("keepclassmembers")
+            || info.getDiagnosticMessage().contains("keep,allowshrinking")) {
           continue;
         }
         // We allow info regarding the extended version of desugared library for JDK11 testing.
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingPackagePrivateTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingPackagePrivateTest.java
new file mode 100644
index 0000000..85ab556
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingPackagePrivateTest.java
@@ -0,0 +1,175 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelClassMergingPackagePrivateTest extends TestBase {
+
+  private final AndroidApiLevel mockLevel = AndroidApiLevel.T;
+  private final String newPackageBinaryName = "package/a/";
+  private final String newADescriptor = "L" + newPackageBinaryName + "A;";
+  private final String newCallerDescriptor = "L" + newPackageBinaryName + "Caller;";
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public ApiModelClassMergingPackagePrivateTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean isGreaterOrEqualToMockLevel() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(mockLevel);
+  }
+
+  private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+    testBuilder
+        .addProgramClasses(B.class)
+        .addProgramClassFileData(
+            transformer(A.class).setClassDescriptor(newADescriptor).transform(),
+            transformer(Caller.class)
+                .setClassDescriptor(newCallerDescriptor)
+                .replaceClassDescriptorInMembers(descriptor(A.class), newADescriptor)
+                .replaceClassDescriptorInMethodInstructions(descriptor(A.class), newADescriptor)
+                .transform(),
+            transformer(Main.class)
+                .replaceClassDescriptorInMembers(descriptor(A.class), newADescriptor)
+                .replaceClassDescriptorInMethodInstructions(descriptor(A.class), newADescriptor)
+                .replaceClassDescriptorInMembers(descriptor(Caller.class), newCallerDescriptor)
+                .replaceClassDescriptorInMethodInstructions(
+                    descriptor(Caller.class), newCallerDescriptor)
+                .transform())
+        .addLibraryClasses(Api1.class, Api2.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+        .apply(b -> setApiLevels(b, Api1.class))
+        .apply(b -> setApiLevels(b, Api2.class));
+  }
+
+  private void setApiLevels(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder, Class<?> apiClass) {
+    testBuilder
+        .apply(setMockApiLevelForClass(apiClass, mockLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(apiClass, mockLevel))
+        .apply(
+            setMockApiLevelForMethod(
+                Reference.method(
+                    Reference.classFromClass(apiClass), "foo", Collections.emptyList(), null),
+                mockLevel));
+  }
+
+  @Test
+  public void testD8Debug() throws Exception {
+    testForD8(parameters.getBackend())
+        .setMode(CompilationMode.DEBUG)
+        .apply(this::setupTestBuilder)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .compile()
+        .addBootClasspathClasses(Api1.class, Api2.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkOutput);
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    testForD8(parameters.getBackend())
+        .setMode(CompilationMode.RELEASE)
+        .apply(this::setupTestBuilder)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .compile()
+        .addBootClasspathClasses(Api1.class, Api2.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkOutput);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .apply(this::setupTestBuilder)
+        .addKeepMainRule(Main.class)
+        .addDontObfuscate()
+        .addHorizontallyMergedClassesInspectorIf(
+            parameters.isCfRuntime(), HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addHorizontallyMergedClassesInspectorIf(
+            !parameters.isCfRuntime(), this::inspectHorizontallyMergedClasses)
+        .compile()
+        .addBootClasspathClasses(Api1.class, Api2.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkOutput);
+  }
+
+  private void inspectHorizontallyMergedClasses(HorizontallyMergedClassesInspector inspector) {
+    if (isGreaterOrEqualToMockLevel()) {
+      inspector.assertNoClassesMerged();
+    } else {
+      inspector.assertClassReferencesMerged(
+          SyntheticItemsTestUtils.syntheticApiOutlineClass(Reference.classFromClass(Main.class), 0),
+          SyntheticItemsTestUtils.syntheticApiOutlineClass(
+              Reference.classFromDescriptor(newCallerDescriptor), 0));
+    }
+  }
+
+  private void checkOutput(SingleTestRunResult<?> runResult) {
+    runResult.assertSuccessWithOutputLines("Api1::foo", "Api2::foo");
+  }
+
+  public static class Api1 {
+
+    public void foo() {
+      System.out.println("Api1::foo");
+    }
+  }
+
+  public static class Api2 {
+
+    public void foo() {
+      System.out.println("Api2::foo");
+    }
+  }
+
+  static class /* package.A. */ A extends Api1 {}
+
+  public static class /* package.A. */ Caller {
+
+    public static void createAndCallFoo() {
+      new A().foo();
+    }
+  }
+
+  static class B extends Api2 {}
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      Caller.createAndCallFoo();
+      new B().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/varhandle/VarHandleDesugaringTestBase.java b/src/test/java/com/android/tools/r8/cf/varhandle/VarHandleDesugaringTestBase.java
index 833e949..cb6c812 100644
--- a/src/test/java/com/android/tools/r8/cf/varhandle/VarHandleDesugaringTestBase.java
+++ b/src/test/java/com/android/tools/r8/cf/varhandle/VarHandleDesugaringTestBase.java
@@ -111,7 +111,7 @@
     // forwarding of Unsafe.compareAndSwapObject.
     MethodReference firstBackportFromDesugarVarHandle =
         SyntheticItemsTestUtils.syntheticBackportWithForwardingMethod(
-            Reference.classFromDescriptor("Ljava/lang/invoke/VarHandle;"),
+            Reference.classFromDescriptor("Lcom/android/tools/r8/DesugarVarHandle;"),
             0,
             Reference.method(
                 Reference.classFromDescriptor("Lsun/misc/Unsafe;"),
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/RelaxedInstanceFieldCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/RelaxedInstanceFieldCollisionTest.java
new file mode 100644
index 0000000..d35ce82
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/RelaxedInstanceFieldCollisionTest.java
@@ -0,0 +1,148 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.ClassFileTransformer.FieldPredicate;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.MethodVisitor;
+
+/***
+ * This is a regression test for b/263934503.
+ */
+@RunWith(Parameterized.class)
+public class RelaxedInstanceFieldCollisionTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private final String[] EXPECTED =
+      new String[] {"UnrelatedA", "UnrelatedB", "UnrelatedC", "UnrelatedD"};
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(UnrelatedA.class, UnrelatedB.class, UnrelatedC.class, UnrelatedD.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(UnrelatedA.class, UnrelatedB.class, UnrelatedC.class, UnrelatedD.class)
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(UnrelatedA.class, UnrelatedB.class, UnrelatedC.class, UnrelatedD.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertClassesMerged(A.class, B.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private Collection<byte[]> getTransformedClasses() throws Exception {
+    return Arrays.asList(
+        transformer(A.class)
+            .transformFieldInsnInMethod("<init>", RelaxedInstanceFieldCollisionTest::visitFieldInsn)
+            .renameField(FieldPredicate.onName("field2"), "field")
+            .transform(),
+        transformer(B.class)
+            .transformFieldInsnInMethod("<init>", RelaxedInstanceFieldCollisionTest::visitFieldInsn)
+            .renameField(FieldPredicate.onName("field2"), "field")
+            .transform(),
+        transformer(Main.class)
+            .transformFieldInsnInMethod("main", RelaxedInstanceFieldCollisionTest::visitFieldInsn)
+            .transform());
+  }
+
+  private static void visitFieldInsn(
+      int opcode, String owner, String name, String descriptor, MethodVisitor visitor) {
+    visitor.visitFieldInsn(opcode, owner, name.equals("field2") ? "field" : name, descriptor);
+  }
+
+  public static class UnrelatedA {
+
+    @Override
+    public String toString() {
+      return "UnrelatedA";
+    }
+  }
+
+  public static class UnrelatedB {
+
+    @Override
+    public String toString() {
+      return "UnrelatedB";
+    }
+  }
+
+  public static class UnrelatedC {
+
+    @Override
+    public String toString() {
+      return "UnrelatedC";
+    }
+  }
+
+  public static class UnrelatedD {
+
+    @Override
+    public String toString() {
+      return "UnrelatedD";
+    }
+  }
+
+  public static class A {
+
+    public UnrelatedA field;
+    public UnrelatedB field2; /* will be renamed field */
+
+    public A(UnrelatedA unrelatedA, UnrelatedB unrelatedB) {
+      field = unrelatedA;
+      field2 = unrelatedB;
+    }
+  }
+
+  public static class B {
+
+    public UnrelatedC field;
+    public UnrelatedD field2; /* will be renamed field */
+
+    public B(UnrelatedC unrelatedA, UnrelatedD unrelatedB) {
+      field = unrelatedA;
+      field2 = unrelatedB;
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() > 0) {
+        A a = new A(new UnrelatedA(), new UnrelatedB());
+        System.out.println(a.field);
+        System.out.println(a.field2);
+      }
+      if (System.currentTimeMillis() > 0) {
+        B b = new B(new UnrelatedC(), new UnrelatedD());
+        System.out.println(b.field);
+        System.out.println(b.field2);
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CollectionOfTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CollectionOfTest.java
index c9f3f99..44cbf36 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CollectionOfTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CollectionOfTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -54,10 +55,13 @@
     this.compilationSpecification = compilationSpecification;
   }
 
-  private String getExpectedOutput(boolean desugaredLib) {
+  private String getExpectedOutput(boolean desugaredLib, boolean alwaysBackportListSetMapMethods) {
     if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.R)) {
       return EXPECTED_OUTPUT_CORRECT;
     }
+    if (alwaysBackportListSetMapMethods) {
+      return EXPECTED_OUTPUT_BACKPORT;
+    }
     if (desugaredLib && libraryDesugaringSpecification != JDK8) {
       if (parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) {
         return EXPECTED_OUTPUT_CORRECT;
@@ -71,11 +75,14 @@
 
   @Test
   public void testCollectionOf() throws Throwable {
-    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
-        .addProgramFiles(INPUT_JAR)
-        .addKeepMainRule(MAIN_CLASS)
-        .run(parameters.getRuntime(), MAIN_CLASS)
-        .assertSuccessWithOutput(getExpectedOutput(true));
+    for (Boolean value : BooleanUtils.values()) {
+      testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+          .addProgramFiles(INPUT_JAR)
+          .addKeepMainRule(MAIN_CLASS)
+          .addOptionsModification(opt -> opt.testing.alwaysBackportListSetMapMethods = value)
+          .run(parameters.getRuntime(), MAIN_CLASS)
+          .assertSuccessWithOutput(getExpectedOutput(true, value));
+    }
   }
 
   @Test
@@ -87,6 +94,6 @@
         .addProgramFiles(INPUT_JAR)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN_CLASS)
-        .assertSuccessWithOutput(getExpectedOutput(false));
+        .assertSuccessWithOutput(getExpectedOutput(false, true));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ConcurrentLinkedQueueTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ConcurrentLinkedQueueTest.java
new file mode 100644
index 0000000..b4028c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ConcurrentLinkedQueueTest.java
@@ -0,0 +1,149 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.SPECIFICATIONS_WITH_CF2CF;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_MINIMAL;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+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 ConcurrentLinkedQueueTest extends DesugaredLibraryTestBase {
+
+  @Parameter(0)
+  public static TestParameters parameters;
+
+  @Parameter(1)
+  public static LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameter(2)
+  public static CompilationSpecification compilationSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        // TODO(134732760): Support Dalvik VMs, currently fails because libjavacrypto is required
+        // and present only in ART runtimes.
+        getTestParameters()
+            .withDexRuntimesStartingFromIncluding(Version.V5_1_1)
+            .withAllApiLevels()
+            .build(),
+        ImmutableList.of(JDK11_MINIMAL, JDK11, JDK11_PATH),
+        SPECIFICATIONS_WITH_CF2CF);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // Right now we only expect one backport coming out of DesugarVarHandle - the backport with
+    // forwarding of Unsafe.compareAndSwapObject.
+    MethodReference firstBackportFromDesugarVarHandle =
+        SyntheticItemsTestUtils.syntheticBackportWithForwardingMethod(
+            Reference.classFromDescriptor("Lj$/com/android/tools/r8/DesugarVarHandle;"),
+            0,
+            Reference.method(
+                Reference.classFromDescriptor("Lsun/misc/Unsafe;"),
+                "compareAndSwapObject",
+                ImmutableList.of(
+                    Reference.typeFromDescriptor("Ljava/lang/Object;"),
+                    Reference.LONG,
+                    Reference.typeFromDescriptor("Ljava/lang/Object;"),
+                    Reference.typeFromDescriptor("Ljava/lang/Object;")),
+                Reference.BOOL));
+
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(DexItemFactory.varHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(
+                DexItemFactory.methodHandlesLookupDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            "j$." + DescriptorUtils.descriptorToJavaType(DexItemFactory.varHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.methodHandlesLookupDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(DexItemFactory.desugarVarHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(
+                DexItemFactory.desugarMethodHandlesLookupDescriptorString)),
+        not(isPresent()));
+
+    boolean usesNativeVarHandle =
+        parameters.asDexRuntime().getVersion().isNewerThanOrEqual(Version.V13_0_0)
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.T);
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.desugarVarHandleDescriptorString)),
+        usesNativeVarHandle ? not(isPresent()) : isPresent());
+    assertThat(
+        inspector.clazz(firstBackportFromDesugarVarHandle.getHolderClass()),
+        usesNativeVarHandle ? not(isPresent()) : isPresent());
+    // Currently DesugarMethodHandlesLookup this is fully inlined by R8.
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.desugarMethodHandlesLookupDescriptorString)),
+        usesNativeVarHandle || compilationSpecification.isL8Shrink()
+            ? not(isPresent())
+            : isPresent());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Executor.class)
+        .compile()
+        .inspectL8(this::inspect)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      Queue<String> queue = new ConcurrentLinkedQueue<>();
+      queue.add("Hello, world!");
+      System.out.println(queue.poll());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
index fe483f8..f1aacd6 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
@@ -42,6 +42,10 @@
       StringUtils.lines(
           "PRESENT FILE",
           "buffRead:cHello!",
+          "inStream[null]:class java.lang.NullPointerException :: npe",
+          "outStream[null]:class java.lang.NullPointerException :: npe",
+          "buffWrite[null]:class java.lang.NullPointerException :: npe",
+          "newByte[null]:class java.lang.NullPointerException :: npe",
           "inStream[READ]:cHello",
           "outStream[READ]:class java.lang.IllegalArgumentException :: READ not allowed",
           "buffWrite[READ]:class java.lang.IllegalArgumentException :: READ not allowed",
@@ -85,6 +89,10 @@
           "newByte[DSYNC]:c6",
           "ABSENT FILE",
           "buffRead:class java.nio.file.NoSuchFileException :: notExisting",
+          "inStream[null]:class java.lang.NullPointerException :: npe",
+          "outStream[null]:class java.lang.NullPointerException :: npe",
+          "buffWrite[null]:class java.lang.NullPointerException :: npe",
+          "newByte[null]:class java.lang.NullPointerException :: npe",
           "inStream[READ]:class java.nio.file.NoSuchFileException :: notExisting",
           "outStream[READ]:class java.lang.IllegalArgumentException :: READ not allowed",
           "buffWrite[READ]:class java.lang.IllegalArgumentException :: READ not allowed",
@@ -233,6 +241,11 @@
         // really relevant for the test.
         split = new String[] {"index 0, size 0"};
       }
+      if (t instanceof NullPointerException) {
+        // NullPointerException are printed slightly differently across platform and it's not
+        // really relevant for the test.
+        split = new String[] {"npe"};
+      }
       System.out.println(t.getClass() + " :: " + split[0]);
     }
 
@@ -246,50 +259,57 @@
         printError(t);
       }
       Files.deleteIfExists(path);
-      for (StandardOpenOption value : StandardOpenOption.values()) {
-        path = pathSupplier.get();
-        System.out.print("inStream[" + value + "]:");
-        try (InputStream inputStream = Files.newInputStream(path, value)) {
-          System.out.print("c");
-          byte[] read = new byte[5];
-          inputStream.read(read);
-          System.out.println(new String(read));
-        } catch (Throwable t) {
-          printError(t);
-        }
-        Files.deleteIfExists(path);
-        path = pathSupplier.get();
-        System.out.print("outStream[" + value + "]:");
-        try (OutputStream outputStream = Files.newOutputStream(path, value)) {
-          System.out.print("c");
-          outputStream.write("Game over!".getBytes(StandardCharsets.UTF_8));
-          System.out.print("w");
-          System.out.println(Files.readAllLines(path).get(0));
-        } catch (Throwable t) {
-          printError(t);
-        }
-        Files.deleteIfExists(path);
-        path = pathSupplier.get();
-        System.out.print("buffWrite[" + value + "]:");
-        try (BufferedWriter bufferedWriter = Files.newBufferedWriter(path, value)) {
-          System.out.print("c");
-          bufferedWriter.write("Game over!");
-          System.out.print("w");
-          System.out.println(Files.readAllLines(path).get(0));
-        } catch (Throwable t) {
-          printError(t);
-        }
-        Files.deleteIfExists(path);
-        path = pathSupplier.get();
-        System.out.print("newByte[" + value + "]:");
-        try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path, value)) {
-          System.out.print("c");
-          System.out.println(seekableByteChannel.size());
-        } catch (Throwable t) {
-          printError(t);
-        }
-        Files.deleteIfExists(path);
+      testWithOpenOption(pathSupplier, null);
+      for (StandardOpenOption openOption : StandardOpenOption.values()) {
+        testWithOpenOption(pathSupplier, openOption);
       }
     }
+
+    private static void testWithOpenOption(
+        Supplier<Path> pathSupplier, StandardOpenOption openOption) throws IOException {
+      Path path;
+      path = pathSupplier.get();
+      System.out.print("inStream[" + openOption + "]:");
+      try (InputStream inputStream = Files.newInputStream(path, openOption)) {
+        System.out.print("c");
+        byte[] read = new byte[5];
+        inputStream.read(read);
+        System.out.println(new String(read));
+      } catch (Throwable t) {
+        printError(t);
+      }
+      Files.deleteIfExists(path);
+      path = pathSupplier.get();
+      System.out.print("outStream[" + openOption + "]:");
+      try (OutputStream outputStream = Files.newOutputStream(path, openOption)) {
+        System.out.print("c");
+        outputStream.write("Game over!".getBytes(StandardCharsets.UTF_8));
+        System.out.print("w");
+        System.out.println(Files.readAllLines(path).get(0));
+      } catch (Throwable t) {
+        printError(t);
+      }
+      Files.deleteIfExists(path);
+      path = pathSupplier.get();
+      System.out.print("buffWrite[" + openOption + "]:");
+      try (BufferedWriter bufferedWriter = Files.newBufferedWriter(path, openOption)) {
+        System.out.print("c");
+        bufferedWriter.write("Game over!");
+        System.out.print("w");
+        System.out.println(Files.readAllLines(path).get(0));
+      } catch (Throwable t) {
+        printError(t);
+      }
+      Files.deleteIfExists(path);
+      path = pathSupplier.get();
+      System.out.print("newByte[" + openOption + "]:");
+      try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path, openOption)) {
+        System.out.print("c");
+        System.out.println(seekableByteChannel.size());
+      } catch (Throwable t) {
+        printError(t);
+      }
+      Files.deleteIfExists(path);
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesSymLinkTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesSymLinkTest.java
index 9a4521c..aa43822 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesSymLinkTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesSymLinkTest.java
@@ -96,8 +96,7 @@
 
     public static void main(String[] args) throws Throwable {
       Path target = Files.createTempFile("guinea_pig_", ".txt");
-
-      Path link = Paths.get("link");
+      Path link = target.getParent().resolve("link");
       try {
         Files.createLink(link, target);
         System.out.println("link created.");
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
index 58af649..347e0a9 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
@@ -22,6 +22,7 @@
 import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.OpenOption;
@@ -59,6 +60,9 @@
           "/",
           "true",
           "tmpFile",
+          "DirectoryStream created: class"
+              + " java.nio.file.DirectoryIteratorException::java.io.IOException: Here",
+          "%s",
           "This",
           "is",
           "fun!",
@@ -104,21 +108,22 @@
     this.compilationSpecification = compilationSpecification;
   }
 
-  private static String computeExpectedResult(boolean supportPosix, boolean j$nioClasses) {
-    List<String> strings =
-        new ArrayList<>(
-            supportPosix ? EXPECTED_RESULT_POSIX : EXPECTED_RESULT_DESUGARING_NON_POSIX);
-    strings.add(j$nioClasses ? "j$.nio.file.attribute" : "java.nio.file.attribute");
-    return String.format(EXPECTED_RESULT_FORMAT, strings.toArray());
-  }
-
   private String getExpectedResult() {
-    if (libraryDesugaringSpecification.usesPlatformFileSystem(parameters)) {
-      return libraryDesugaringSpecification.hasNioFileDesugaring(parameters)
-          ? computeExpectedResult(true, true)
-          : computeExpectedResult(true, false);
-    }
-    return computeExpectedResult(false, true);
+    List<String> strings = new ArrayList<>();
+    strings.add(
+        libraryDesugaringSpecification.usesPlatformFileSystem(parameters)
+                && libraryDesugaringSpecification.hasNioFileDesugaring(parameters)
+            ? "fail"
+            : "npe caught");
+    strings.addAll(
+        libraryDesugaringSpecification.usesPlatformFileSystem(parameters)
+            ? EXPECTED_RESULT_POSIX
+            : EXPECTED_RESULT_DESUGARING_NON_POSIX);
+    strings.add(
+        libraryDesugaringSpecification.hasNioFileDesugaring(parameters)
+            ? "j$.nio.file.attribute"
+            : "java.nio.file.attribute");
+    return String.format(EXPECTED_RESULT_FORMAT, strings.toArray());
   }
 
   @Test
@@ -224,6 +229,28 @@
       Iterator<Path> theIterator = paths.iterator();
       System.out.println(theIterator.hasNext());
       System.out.println(theIterator.next().getFileName());
+      try {
+        // TODO(b/262190079): In desugared lib the filter is resolved eagerly which leads to the
+        // exception being thrown here and not later.
+        DirectoryStream<Path> pathExceptions =
+            Files.newDirectoryStream(
+                tmpDict,
+                x -> {
+                  throw new IOException("Here");
+                });
+        System.out.print("DirectoryStream created: ");
+        pathExceptions.iterator().next();
+        System.out.println("fail");
+      } catch (Throwable t) {
+        System.out.println(t.getClass() + "::" + t.getMessage());
+      }
+      try {
+        DirectoryStream<Path> thrown =
+            Files.newDirectoryStream(tmpDict, (Filter<? super Path>) null);
+        System.out.println("fail");
+      } catch (NullPointerException npe) {
+        System.out.println("npe caught");
+      }
     }
 
     private static void fspMethodsWithGeneric(Path path) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesWatchEventTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesWatchEventTest.java
index 6f3b986..38ea6d4 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesWatchEventTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesWatchEventTest.java
@@ -36,7 +36,7 @@
 public class FilesWatchEventTest extends DesugaredLibraryTestBase {
 
   private static final String EXPECTED_RESULT =
-      StringUtils.lines("true", "true", "true", "ENTRY_CREATE", "foo", "true");
+      StringUtils.lines("true", "true", "true", "ENTRY_CREATE", "Path", "foo", "foo", "true");
   private static final String EXPECTED_RESULT_DESUGARING =
       StringUtils.lines("class java.lang.UnsupportedOperationException :: null");
 
@@ -110,7 +110,9 @@
 
         WatchEvent<?> event = myKey.pollEvents().iterator().next();
         System.out.println(event.kind());
+        System.out.println(event.kind().type().getSimpleName());
         System.out.println(event.context());
+        System.out.println(((Path) event.context()).getFileName());
 
         System.out.println(myKey.reset());
         Files.delete(file);
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathTest.java
index 104876d..699cd7f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathTest.java
@@ -13,9 +13,12 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -28,14 +31,24 @@
   private final LibraryDesugaringSpecification libraryDesugaringSpecification;
   private final CompilationSpecification compilationSpecification;
 
+  private static final String EXPECTED_RESULT_FORMAT =
+      StringUtils.lines(
+          "x.txt",
+          "dir",
+          "dir/x.txt",
+          "/",
+          "%s",
+          "class java.nio.file.NoSuchFileException :: notExisting",
+          "class java.nio.file.NoSuchFileException :: notExisting",
+          "x.txt",
+          "",
+          "");
+
   private static final String EXPECTED_RESULT_DESUGARING =
-      StringUtils.lines(
-          "x.txt", "dir", "dir/x.txt", "/", "class j$.desugar.sun.nio.fs.DesugarLinuxFileSystem");
+      "class j$.desugar.sun.nio.fs.DesugarLinuxFileSystem";
   private static final String EXPECTED_RESULT_DESUGARING_PLATFORM_FILE_SYSTEM =
-      StringUtils.lines(
-          "x.txt", "dir", "dir/x.txt", "/", "class j$.nio.file.FileSystem$VivifiedWrapper");
-  private static final String EXPECTED_RESULT_NO_DESUGARING =
-      StringUtils.lines("x.txt", "dir", "dir/x.txt", "/", "class sun.nio.fs.LinuxFileSystem");
+      "class j$.nio.file.FileSystem$VivifiedWrapper";
+  private static final String EXPECTED_RESULT_NO_DESUGARING = "class sun.nio.fs.LinuxFileSystem";
 
   @Parameters(name = "{0}, spec: {1}, {2}")
   public static List<Object[]> data() {
@@ -56,13 +69,16 @@
 
   private String getExpectedResult() {
     if (!libraryDesugaringSpecification.hasNioFileDesugaring(parameters)) {
-      return EXPECTED_RESULT_NO_DESUGARING;
+      return String.format(EXPECTED_RESULT_FORMAT, EXPECTED_RESULT_NO_DESUGARING);
     }
-    return libraryDesugaringSpecification.usesPlatformFileSystem(parameters)
-        ? EXPECTED_RESULT_DESUGARING_PLATFORM_FILE_SYSTEM
-        : EXPECTED_RESULT_DESUGARING;
+    return String.format(
+        EXPECTED_RESULT_FORMAT,
+        libraryDesugaringSpecification.usesPlatformFileSystem(parameters)
+            ? EXPECTED_RESULT_DESUGARING_PLATFORM_FILE_SYSTEM
+            : EXPECTED_RESULT_DESUGARING);
   }
 
+  @Ignore("b/265268776")
   @Test
   public void test() throws Throwable {
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
@@ -80,16 +96,41 @@
 
   public static class TestClass {
 
+    private static void printError(Throwable t) {
+      String[] split =
+          t.getMessage() == null ? new String[] {"no-message"} : t.getMessage().split("/");
+      System.out.println(t.getClass() + " :: " + split[split.length - 1]);
+    }
+
     public static void main(String[] args) {
       File file = new File("x.txt");
       Path path1 = file.toPath();
       System.out.println(path1);
-      Path path2 = Paths.get("dir/");
-      System.out.println(path2);
-      Path resolve = path2.resolve(path1);
+      Path dir = Paths.get("dir/");
+      System.out.println(dir);
+      Path resolve = dir.resolve(path1);
       System.out.println(resolve);
       System.out.println(resolve.getFileSystem().getSeparator());
       System.out.println(resolve.getFileSystem().getClass());
+
+      Path notExisting = dir.resolve("notExisting");
+      try {
+        notExisting.toRealPath();
+        System.out.println("IOException not raised!");
+      } catch (IOException e) {
+        printError(e);
+      }
+      try {
+        notExisting.toRealPath(LinkOption.NOFOLLOW_LINKS);
+        System.out.println("IOException not raised!");
+      } catch (IOException e) {
+        printError(e);
+      }
+
+      System.out.println(path1.getFileName());
+      Path relativize = path1.relativize(path1);
+      System.out.println(relativize);
+      System.out.println(relativize.getFileName());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
index a711203..c7d7c3a 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
@@ -13,7 +13,11 @@
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.JAVA_EXTENSION;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
@@ -24,9 +28,16 @@
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.DesugaredLibraryTestCompileResult;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
@@ -71,7 +82,7 @@
             .withApiLevel(AndroidApiLevel.N)
             .build(),
         ImmutableList.of(JDK11_MINIMAL, JDK11, JDK11_PATH),
-        ImmutableList.of(D8_L8DEBUG, D8_L8SHRINK));
+        ImmutableSet.of(D8_L8DEBUG, D8_L8SHRINK));
   }
 
   @BeforeClass
@@ -88,33 +99,81 @@
         new Path[] {jdk11MathTestsDir.resolve(WHITEBOX + CLASS_EXTENSION)};
   }
 
-  private static void ranWithSuccessOrFailures(String testName, SingleTestRunResult result) {
-    // Tests use ThreadLocalRandom, so success or failure is random. Note this is only for
-    // VMs where the internal implementation is not based on JDK11.
-    assertTrue(
-        result.getStdOut().contains(StringUtils.lines(testName + ": SUCCESS"))
-            || result
-                .getStdOut()
-                .contains(StringUtils.lines("Tests result in " + testName + ": FAILURE")));
-    if (result.getStdOut().contains(StringUtils.lines(testName + ": SUCCESS"))) {
-      assertTrue(
-          result.toString(),
-          result.getStdOut().contains("Total tests run: 37, Failures: 0, Skips: 0"));
-    } else {
-      assertTrue(
-          result.toString(),
-          result.getStdOut().contains("Total tests run: 37, Failures: 1, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 2, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 3, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 4, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 5, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 6, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 7, Skips: 0")
-              || result.getStdOut().contains("Total tests run: 37, Failures: 8, Skips: 0"));
-    }
+  private void inspect(CodeInspector inspector) {
+    // Right now we only expect one backport coming out of DesugarVarHandle - the backport with
+    // forwarding of Unsafe.compareAndSwapObject.
+    MethodReference firstBackportFromDesugarVarHandle =
+        SyntheticItemsTestUtils.syntheticBackportWithForwardingMethod(
+            Reference.classFromDescriptor("Lj$/com/android/tools/r8/DesugarVarHandle;"),
+            0,
+            Reference.method(
+                Reference.classFromDescriptor("Lsun/misc/Unsafe;"),
+                "compareAndSwapObject",
+                ImmutableList.of(
+                    Reference.typeFromDescriptor("Ljava/lang/Object;"),
+                    Reference.LONG,
+                    Reference.typeFromDescriptor("Ljava/lang/Object;"),
+                    Reference.typeFromDescriptor("Ljava/lang/Object;")),
+                Reference.BOOL));
+
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(DexItemFactory.varHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(
+                DexItemFactory.methodHandlesLookupDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            "j$." + DescriptorUtils.descriptorToJavaType(DexItemFactory.varHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.methodHandlesLookupDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(DexItemFactory.desugarVarHandleDescriptorString)),
+        not(isPresent()));
+    assertThat(
+        inspector.clazz(
+            DescriptorUtils.descriptorToJavaType(
+                DexItemFactory.desugarMethodHandlesLookupDescriptorString)),
+        not(isPresent()));
+
+    boolean usesNativeVarHandle =
+        parameters.asDexRuntime().getVersion().isNewerThanOrEqual(Version.V13_0_0)
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.T);
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.desugarVarHandleDescriptorString)),
+        usesNativeVarHandle ? not(isPresent()) : isPresent());
+    assertThat(
+        inspector.clazz(firstBackportFromDesugarVarHandle.getHolderClass()),
+        usesNativeVarHandle ? not(isPresent()) : isPresent());
+    // Currently DesugarMethodHandlesLookup this is fully inlined by R8.
+    assertThat(
+        inspector.clazz(
+            "j$."
+                + DescriptorUtils.descriptorToJavaType(
+                    DexItemFactory.desugarMethodHandlesLookupDescriptorString)),
+        usesNativeVarHandle || compilationSpecification.isL8Shrink()
+            ? not(isPresent())
+            : isPresent());
   }
 
   void runTest(List<String> toRun) throws Exception {
+    // Skip test with minimal configuration before API level 24, as the test use stream.
+    assumeTrue(
+        libraryDesugaringSpecification != JDK11_MINIMAL
+            || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N));
+
     String verbosity = "2";
     DesugaredLibraryTestCompileResult<?> compileResult =
         testForDesugaredLibrary(
@@ -125,30 +184,14 @@
             // internal state of the implementation, so desugaring is needed for the program here.
             .addOptionsModification(options -> options.enableVarHandleDesugaring = true)
             .compile()
+            .inspectL8(this::inspect)
             .withArt6Plus64BitsLib();
     for (String success : toRun) {
       SingleTestRunResult<?> result =
           compileResult.run(parameters.getRuntime(), "TestNGMainRunner", verbosity, success);
-      if ((parameters.asDexRuntime().getVersion().equals(Version.V5_1_1)
-              || parameters.asDexRuntime().getVersion().equals(Version.V6_0_1))
-          && libraryDesugaringSpecification == JDK11_MINIMAL) {
-        // Some tests use streams, so which is not desugared with JDK11_MINIMAL. These tests are
-        // somehow skipped by the test runner used in the JDK11 tests.
-        assertTrue(result.getStdOut().contains("Total tests run: 9, Failures: 0, Skips: 7"));
-        assertTrue(result.getStdOut().contains(StringUtils.lines(success + ": SUCCESS")));
-      } else if (parameters.asDexRuntime().getVersion().isOlderThanOrEqual(Version.V12_0_0)) {
-        ranWithSuccessOrFailures(success, result);
-      } else {
-        assertTrue(parameters.asDexRuntime().getVersion().isNewerThanOrEqual(Version.V13_0_0));
-        if (parameters.getApiLevel() == AndroidApiLevel.B) {
-          ranWithSuccessOrFailures(success, result);
-        } else {
-          // No desugaring and JDK11 based runtime implementation.
-          assertTrue(
-              "Failure in " + success + "\n" + result,
-              result.getStdOut().contains(StringUtils.lines(success + ": SUCCESS")));
-        }
-      }
+      assertTrue(
+          "Failure in " + success + "\n" + result,
+          result.getStdOut().contains(StringUtils.lines(success + ": SUCCESS")));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
index 9f3a07b..0f735fe 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
@@ -317,7 +317,7 @@
       // failures.
       int sevenOffset = parameters.getDexRuntimeVersion() == Version.V7_0_0 ? -1 : 0;
       int shrinkOffset = compilationSpecification.isL8Shrink() ? -1 : 0;
-      assertTrue(success >= 18 + sevenOffset + shrinkOffset);
+      assertTrue(success >= 20 + sevenOffset + shrinkOffset);
     } else if (parameters.getApiLevel().isLessThan(AndroidApiLevel.O)) {
       // Desugaring high api level.
       assertEquals(26, success);
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepBindingTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepBindingTest.java
new file mode 100644
index 0000000..c83340f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepBindingTest.java
@@ -0,0 +1,156 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepBinding;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepBindingTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::foo");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepBindingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    List<String> rules = getExtractedKeepRules();
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepClassRules(A.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(i -> checkOutput(i, true));
+  }
+
+  @Test
+  public void testWithRuleExtractionAndNoKeepOnClass() throws Exception {
+    List<String> rules = getExtractedKeepRules();
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(i -> checkOutput(i, false));
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+  }
+
+  public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
+  }
+
+  public List<String> getExtractedKeepRules() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<String> rules = new ArrayList<>();
+    for (Class<?> clazz : classes) {
+      rules.addAll(KeepEdgeAnnotationsTest.getKeepRulesForClass(clazz));
+    }
+    return rules;
+  }
+
+  private void checkOutput(CodeInspector inspector, boolean expectB) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueMethodWithOriginalName("foo"), isPresent());
+    if (expectB) {
+      assertThat(inspector.clazz(B.class), isPresent());
+      assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("foo"), isAbsent());
+    } else {
+      assertThat(inspector.clazz(B.class), isAbsent());
+    }
+    assertThat(inspector.clazz(C.class), isAbsent());
+  }
+
+  static class A {
+    public void foo() throws Exception {
+      System.out.println("A::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  static class B {
+    public void foo() throws Exception {
+      System.out.println("B::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  static class C {
+    public void foo() throws Exception {
+      System.out.println("C::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  /**
+   * This conditional rule expresses that if any class in the program has a live "bar" method then
+   * that same classes "foo" method is to be kept. The binding(s) establishes the relation between
+   * the holder of the two methods.
+   */
+  @KeepEdge(
+      bindings = {
+        @KeepBinding(bindingName = "Holder"),
+        @KeepBinding(bindingName = "BarMethod", classFromBinding = "Holder", methodName = "bar"),
+        @KeepBinding(bindingName = "FooMethod", classFromBinding = "Holder", methodName = "foo")
+      },
+      preconditions = {@KeepCondition(memberFromBinding = "BarMethod")},
+      consequences = {@KeepTarget(memberFromBinding = "FooMethod")})
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarAnyClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarAnyClassTest.java
new file mode 100644
index 0000000..74da176
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarAnyClassTest.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepFooIfBarAnyClassTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::foo");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepFooIfBarAnyClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    List<String> rules = getExtractedKeepRules();
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
+  }
+
+  public List<String> getExtractedKeepRules() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<String> rules = new ArrayList<>();
+    for (Class<?> clazz : classes) {
+      rules.addAll(KeepEdgeAnnotationsTest.getKeepRulesForClass(clazz));
+    }
+    return rules;
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueMethodWithOriginalName("foo"), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("foo"), isPresent());
+  }
+
+  @KeepEdge(consequences = {@KeepTarget(classConstant = A.class)})
+  static class A {
+    public void foo() throws Exception {
+      System.out.println("A::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  @KeepEdge(consequences = {@KeepTarget(classConstant = B.class)})
+  static class B {
+    public void foo() throws Exception {
+      System.out.println("B::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  /**
+   * This conditional rule expresses that if any class in the program has a live "bar" method then
+   * all methods named "foo" on *any class* are to be kept. This most definitely not the rule one
+   * would want but is here to contrast with a rule that expresses the relationship between the
+   * holder or the two methods.
+   */
+  @KeepEdge(
+      preconditions = {@KeepCondition(methodName = "bar")},
+      consequences = {@KeepTarget(methodName = "foo")})
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java
new file mode 100644
index 0000000..e43398a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepBinding;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepFooIfBarSameClassTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::foo");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepFooIfBarSameClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    List<String> rules = getExtractedKeepRules();
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
+  }
+
+  public List<String> getExtractedKeepRules() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<String> rules = new ArrayList<>();
+    for (Class<?> clazz : classes) {
+      rules.addAll(KeepEdgeAnnotationsTest.getKeepRulesForClass(clazz));
+    }
+    return rules;
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(A.class).uniqueMethodWithOriginalName("foo"), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("foo"), isAbsent());
+    assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("bar"), isAbsent());
+    // TODO(b/248408342): R8 full is keeping the default constructor. Avoid that.
+    assertThat(inspector.clazz(B.class).uniqueInstanceInitializer(), isPresent());
+  }
+
+  @KeepEdge(consequences = {@KeepTarget(classConstant = A.class)})
+  static class A {
+    public void foo() throws Exception {
+      System.out.println("A::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  @KeepEdge(consequences = {@KeepTarget(classConstant = B.class)})
+  static class B {
+    public void foo() throws Exception {
+      System.out.println("B::foo");
+    }
+
+    public void bar() throws Exception {
+      getClass().getDeclaredMethod("foo").invoke(this);
+    }
+  }
+
+  /**
+   * This conditional rule expresses that if any class in the program has a live "bar" method then
+   * that same classes "foo" method is to be kept. The binding establishes the relation between the
+   * holder of the two methods.
+   */
+  @KeepEdge(
+      bindings = {@KeepBinding(bindingName = "Holder")},
+      preconditions = {@KeepCondition(classFromBinding = "Holder", methodName = "bar")},
+      consequences = {@KeepTarget(classFromBinding = "Holder", methodName = "foo")})
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
index 33f2531..a9dc1f7 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
@@ -77,7 +77,6 @@
   }
 
   private void checkOutput(CodeInspector inspector) {
-    assertThat(inspector.clazz(A.class), isPresent());
     assertThat(inspector.clazz(B.class), isPresent());
     assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("<init>"), isPresent());
     assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("bar"), isPresent());
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 0386448..b85a41b 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
@@ -33,7 +33,7 @@
 
   public static String extract(KeepEdge edge) {
     StringBuilder builder = new StringBuilder();
-    KeepRuleExtractor extractor = new KeepRuleExtractor(rule -> builder.append(rule).append('\n'));
+    KeepRuleExtractor extractor = new KeepRuleExtractor(builder::append);
     extractor.extract(edge);
     return builder.toString();
   }
@@ -44,10 +44,11 @@
         KeepEdge.builder()
             .setConsequences(
                 KeepConsequences.builder()
-                    .addTarget(KeepTarget.builder().setItem(KeepItemPattern.any()).build())
+                    .addTarget(KeepTarget.builder().setItemPattern(KeepItemPattern.any()).build())
                     .build())
             .build();
-    assertEquals(StringUtils.unixLines("-keep class * { *; }"), extract(edge));
+    assertEquals(
+        StringUtils.unixLines("-keep class *", "-keepclassmembers class * { *; }"), extract(edge));
   }
 
   @Test
@@ -58,7 +59,7 @@
                 KeepConsequences.builder()
                     .addTarget(
                         KeepTarget.builder()
-                            .setItem(KeepItemPattern.any())
+                            .setItemPattern(KeepItemPattern.any())
                             .setOptions(KeepOptions.disallow(KeepOption.OPTIMIZING))
                             .build())
                     .build())
@@ -67,7 +68,13 @@
     List<String> options =
         ImmutableList.of("shrinking", "obfuscation", "accessmodification", "annotationremoval");
     String allows = String.join(",allow", options);
-    assertEquals(StringUtils.unixLines("-keep,allow" + allows + " class * { *; }"), extract(edge));
+    // The "any" item will be split in two rules, one for the targeted types and one for the
+    // targeted members.
+    assertEquals(
+        StringUtils.unixLines(
+            "-keep,allow" + allows + " class *",
+            "-keepclassmembers,allow" + allows + " class * { *; }"),
+        extract(edge));
   }
 
   @Test
@@ -78,7 +85,7 @@
                 KeepConsequences.builder()
                     .addTarget(
                         KeepTarget.builder()
-                            .setItem(KeepItemPattern.any())
+                            .setItemPattern(KeepItemPattern.any())
                             .setOptions(
                                 KeepOptions.allow(KeepOption.OBFUSCATING, KeepOption.SHRINKING))
                             .build())
@@ -86,7 +93,9 @@
             .build();
     // Allow is just the ordered list of options.
     assertEquals(
-        StringUtils.unixLines("-keep,allowshrinking,allowobfuscation class * { *; }"),
+        StringUtils.unixLines(
+            "-keep,allowshrinking,allowobfuscation class *",
+            "-keepclassmembers,allowshrinking,allowobfuscation class * { *; }"),
         extract(edge));
   }
 
@@ -104,7 +113,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
+                    .addCondition(KeepCondition.builder().setItemPattern(classItem(CLASS)).build())
                     .build())
             .setConsequences(
                 KeepConsequences.builder()
@@ -126,7 +135,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
+                    .addCondition(KeepCondition.builder().setItemPattern(classItem(CLASS)).build())
                     .build())
             .setConsequences(KeepConsequences.builder().addTarget(target(classItem(CLASS))).build())
             .build();
@@ -140,7 +149,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
+                    .addCondition(KeepCondition.builder().setItemPattern(classItem(CLASS)).build())
                     .build())
             .setConsequences(
                 KeepConsequences.builder()
@@ -160,7 +169,7 @@
   }
 
   private KeepTarget target(KeepItemPattern item) {
-    return KeepTarget.builder().setItem(item).build();
+    return KeepTarget.builder().setItemPattern(item).build();
   }
 
   private KeepItemPattern classItem(String typeName) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
index d4c7121..8958148 100644
--- a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
@@ -133,11 +133,11 @@
   }
 
   static KeepTarget mkTarget(KeepItemPattern item) {
-    return KeepTarget.builder().setItem(item).build();
+    return KeepTarget.builder().setItemPattern(item).build();
   }
 
   static KeepCondition mkCondition(KeepItemPattern item) {
-    return KeepCondition.builder().setItem(item).build();
+    return KeepCondition.builder().setItemPattern(item).build();
   }
 
   static KeepConsequences mkConsequences(KeepTarget... targets) {
diff --git a/src/test/java/com/android/tools/r8/resolution/packageprivate/PackagePrivateInitialResolutionHolderTest.java b/src/test/java/com/android/tools/r8/resolution/packageprivate/PackagePrivateInitialResolutionHolderTest.java
new file mode 100644
index 0000000..4d37079
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/packageprivate/PackagePrivateInitialResolutionHolderTest.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.resolution.packageprivate;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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.DexType;
+import com.android.tools.r8.graph.LookupMethodTarget;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.OptionalBool;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Collections;
+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 PackagePrivateInitialResolutionHolderTest extends TestBase {
+
+  private final String newCDescriptor = "La/C;";
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testResolution() throws Exception {
+    assumeTrue(parameters.isOrSimulateNoneRuntime());
+    AppView<AppInfoWithLiveness> appView =
+        computeAppViewWithLiveness(
+            buildClasses()
+                .addClassProgramData(getRewrittenResources())
+                .addLibraryFile(parameters.getDefaultRuntimeLibrary())
+                .build(),
+            Main.class);
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod method =
+        buildMethod(
+            Reference.method(
+                Reference.classFromDescriptor(newCDescriptor),
+                "foo",
+                Collections.emptyList(),
+                null),
+            appInfo.dexItemFactory());
+    MethodResolutionResult resolutionResult = appInfo.resolveMethodOnClassHolderLegacy(method);
+    assertTrue(resolutionResult.isSingleResolution());
+    DexProgramClass programClass =
+        appInfo.definitionForProgramType(
+            buildType(
+                Reference.classFromDescriptor(descriptor(Main.class)), appInfo.dexItemFactory()));
+    assertEquals(OptionalBool.FALSE, resolutionResult.isAccessibleFrom(programClass, appInfo));
+    DexType cType =
+        buildType(Reference.classFromDescriptor(newCDescriptor), appInfo.dexItemFactory());
+    DexProgramClass cClass = appView.definitionForProgramType(cType);
+    LookupMethodTarget lookupMethodTarget =
+        resolutionResult.lookupVirtualDispatchTarget(cClass, appInfo);
+    assertEquals(
+        "void " + typeName(B.class) + ".foo()",
+        lookupMethodTarget.getDefinition().toSourceString());
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    boolean hasIllegalAccessError =
+        parameters.isCfRuntime() || parameters.getDexRuntimeVersion().isDalvik();
+    testForRuntime(parameters)
+        .addProgramClassFileData(getRewrittenResources())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrowsIf(hasIllegalAccessError, IllegalAccessError.class)
+        // TODO(b/264523290): Should be IllegalAccessError.
+        .assertSuccessWithOutputLinesIf(!hasIllegalAccessError, "B::foo");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getRewrittenResources())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/264522833): Should be IllegalAccessError.
+        .assertFailureWithErrorThatThrows(AbstractMethodError.class);
+  }
+
+  private Collection<byte[]> getRewrittenResources() throws Exception {
+    String newCBuilderDescriptor = "La/CBuilder;";
+    return ImmutableList.of(
+        transformer(B.class).transform(),
+        transformer(C.class).setClassDescriptor(newCDescriptor).transform(),
+        transformer(CBuilder.class)
+            .replaceClassDescriptorInMethodInstructions(descriptor(C.class), newCDescriptor)
+            .replaceClassDescriptorInMembers(descriptor(C.class), newCDescriptor)
+            .setClassDescriptor(newCBuilderDescriptor)
+            .transform(),
+        transformer(Main.class)
+            .replaceClassDescriptorInMembers(descriptor(C.class), newCDescriptor)
+            .replaceClassDescriptorInMethodInstructions(descriptor(C.class), newCDescriptor)
+            .replaceClassDescriptorInMethodInstructions(
+                descriptor(CBuilder.class), newCBuilderDescriptor)
+            .transform());
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      CBuilder.build().foo();
+    }
+  }
+
+  public static class B {
+
+    public void foo() {
+      System.out.println("B::foo");
+    }
+  }
+
+  static class /* a. */ C extends B {}
+
+  public static class /* a. */ CBuilder {
+
+    public static C build() {
+      return new C();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldBasedOnClassExtends.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldBasedOnClassExtends.java
new file mode 100644
index 0000000..4ca6833
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldBasedOnClassExtends.java
@@ -0,0 +1,153 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking.ifrule;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.ProguardVersion;
+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;
+
+@RunWith(Parameterized.class)
+public class IfRuleWithFieldBasedOnClassExtends extends TestBase {
+
+  static final String EXPECTED = "foobar";
+  public static final String CONDITIONAL_KEEP_RULE =
+      "-if class * extends "
+          + Base.class.getTypeName()
+          + "\n"
+          + " -keepclassmembers class * {\n"
+          + "    <1> *;\n"
+          + "}";
+  public static final String CONDITIONAL_KEEP_RULE_FOR_SAME_CLASS =
+      "-if class * extends "
+          + Extending.class.getTypeName()
+          + "\n"
+          + " -keepclassmembers class * {\n"
+          + "    <1> *;\n"
+          + "}";
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public IfRuleWithFieldBasedOnClassExtends(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // Validate that we keep the field if the conditional rule is used
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassAndMembersRules(Extending.class)
+        .addKeepRules(CONDITIONAL_KEEP_RULE)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(
+            codeInspector -> {
+              assertThat(
+                  codeInspector
+                      .clazz(UsingExtendingAsField.class)
+                      .uniqueFieldWithOriginalName("shouldBeKept"),
+                  isPresent());
+              // The rest of the fields are gone
+              assertEquals(
+                  codeInspector.clazz(UsingExtendingAsField.class).allFields().stream().count(), 1);
+            })
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8NotKept() throws Exception {
+    // Validate that we don't keep the field if the conditional rule is not used
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassAndMembersRules(Extending.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(NoSuchFieldException.class);
+  }
+
+  @Test
+  public void testProguard() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForProguard(ProguardVersion.V7_0_0)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addDontWarn(getClass())
+        .addKeepClassAndMembersRules(Extending.class)
+        .addKeepRules(CONDITIONAL_KEEP_RULE)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  // It would often be easier to write keep rules if a given class was considered to extend itself,
+  // but that is not the case (see also test below for proguard)
+  public void testR8SameClassInCondition() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassAndMembersRules(Extending.class)
+        .addKeepRules(CONDITIONAL_KEEP_RULE_FOR_SAME_CLASS)
+        .allowUnusedProguardConfigurationRules()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(NoSuchFieldException.class);
+  }
+
+  @Test
+  public void testProguardSameClassInCondition() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForProguard(ProguardVersion.V7_0_0)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addDontWarn(getClass())
+        .addKeepClassAndMembersRules(Extending.class)
+        .addKeepRules(CONDITIONAL_KEEP_RULE_FOR_SAME_CLASS)
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(NoSuchFieldException.class);
+  }
+
+  public static class Base {}
+
+  public static class Extending extends Base {
+    String foo = "foo";
+    String bar = "bar";
+  }
+
+  public static class UsingExtendingAsField {
+    Extending shouldBeKept = new Extending();
+    Base baseField = null;
+    String otherField = "never_used";
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
+      UsingExtendingAsField using = new UsingExtendingAsField();
+      Object shouldBeKept = using.getClass().getDeclaredField("shouldBeKept").get(using);
+      System.out.print(shouldBeKept.getClass().getDeclaredField("foo").get(shouldBeKept));
+      System.out.println(shouldBeKept.getClass().getDeclaredField("bar").get(shouldBeKept));
+    }
+  }
+}
diff --git a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1 b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
index a75d743..d52bbc5 100644
--- a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
+++ b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
@@ -1 +1 @@
-baf1112d9c5ee962bf5085a81289f43b9f38697e
\ No newline at end of file
+024c0b5aa78dba1442189dc55367551825cc4085
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index 64a2a31..691bdeb 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -433,8 +433,6 @@
     jar = args.r8_jar if args.r8_jar else download_distribution(version, args.nolib, temp)
     if ':' not in jar and not os.path.exists(jar):
       error("Distribution does not exist: " + jar)
-    prepare_r8_wrapper(jar, temp, jdkhome)
-    prepare_d8_wrapper(jar, temp, jdkhome)
     cmd = [jdk.GetJavaExecutable(jdkhome)]
     cmd.extend(jvmargs)
     if args.debug_agent:
@@ -456,10 +454,12 @@
     cmd.extend(determine_properties(build_properties))
     cmd.extend(['-cp', '%s:%s' % (temp, jar)])
     if compiler == 'd8':
+      prepare_d8_wrapper(jar, temp, jdkhome)
       cmd.append('com.android.tools.r8.utils.CompileDumpD8')
     if compiler == 'l8':
       cmd.append('com.android.tools.r8.L8')
     if compiler.startswith('r8'):
+      prepare_r8_wrapper(jar, temp, jdkhome)
       cmd.append('com.android.tools.r8.utils.CompileDumpCompatR8')
     if compiler == 'r8':
       cmd.append('--compat')