Merge commit 'd293c56c5ce265bb4e423e3beef2418c6074519f' into dev-release
diff --git a/build.gradle b/build.gradle
index 6429c57..f016148 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,8 +44,8 @@
     mockitoVersion = '2.10.0'
     // The kotlin version is only here to specify the kotlin language level,
     // all kotlin compilations are done in tests.
-    kotlinVersion = '1.6.0'
-    kotlinExtMetadataJVMVersion = '0.5.0'
+    kotlinVersion = '1.8.0'
+    kotlinExtMetadataJVMVersion = '0.6.0'
     smaliVersion = '2.2b4'
     errorproneVersion = '2.3.2'
     testngVersion = '6.10'
@@ -285,7 +285,7 @@
     main17Implementation group: 'org.ow2.asm', name: 'asm-tree', version: asmVersion
     main17Implementation group: 'org.ow2.asm', name: 'asm-analysis', version: asmVersion
     main17Implementation group: 'org.ow2.asm', name: 'asm-util', version: asmVersion
-    
+
     examplesTestNGRunnerCompile group: 'org.testng', name: 'testng', version: testngVersion
 
     testCompile sourceSets.examples.output
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index b17a3e1..8872494 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -17,6 +17,8 @@
   /** Name with which other bindings, conditions or targets can reference the bound item pattern. */
   String bindingName();
 
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
   String classFromBinding() default "";
 
   String className() 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 1c7d3a5..c88085a 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
@@ -97,10 +97,21 @@
     public static final Class<KeepTarget> CLASS = KeepTarget.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
 
+    public static final String kind = "kind";
     public static final String allow = "allow";
     public static final String disallow = "disallow";
   }
 
+  public static final class Kind {
+    public static final Class<KeepItemKind> CLASS = KeepItemKind.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+
+    public static final String DEFAULT = "DEFAULT";
+    public static final String ONLY_CLASS = "ONLY_CLASS";
+    public static final String ONLY_MEMBERS = "ONLY_MEMBERS";
+    public static final String CLASS_AND_MEMBERS = "CLASS_AND_MEMBERS";
+  }
+
   public static final class Option {
     public static final Class<KeepOption> CLASS = KeepOption.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
new file mode 100644
index 0000000..7530bf1
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+public enum KeepItemKind {
+  ONLY_CLASS,
+  ONLY_MEMBERS,
+  CLASS_AND_MEMBERS,
+  DEFAULT
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index be61b4a..a2e9c4f 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
@@ -26,6 +26,8 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepTarget {
 
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
   /**
    * Define the options that do not need to be preserved for the target.
    *
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 9fdf1f4..7a6de42 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
@@ -8,6 +8,7 @@
 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.Kind;
 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;
@@ -21,6 +22,7 @@
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepItemKind;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
@@ -697,6 +699,7 @@
   private abstract static class KeepItemVisitorBase extends AnnotationVisitorBase {
     private Parent<KeepItemReference> parent;
     private String memberBindingReference = null;
+    private KeepItemKind kind = null;
     private final ClassDeclaration classDeclaration = new ClassDeclaration();
     private final ExtendsDeclaration extendsDeclaration = new ExtendsDeclaration();
     private final MemberDeclaration memberDeclaration = new MemberDeclaration();
@@ -714,6 +717,29 @@
     }
 
     @Override
+    public void visitEnum(String name, String descriptor, String value) {
+      if (!descriptor.equals(KeepConstants.Kind.DESCRIPTOR)) {
+        super.visitEnum(name, descriptor, value);
+      }
+      switch (value) {
+        case Kind.DEFAULT:
+          // The default value is obtained by not assigning a kind (e.g., null in the builder).
+          break;
+        case Kind.ONLY_CLASS:
+          kind = KeepItemKind.ONLY_CLASS;
+          break;
+        case Kind.ONLY_MEMBERS:
+          kind = KeepItemKind.ONLY_MEMBERS;
+          break;
+        case Kind.CLASS_AND_MEMBERS:
+          kind = KeepItemKind.CLASS_AND_MEMBERS;
+          break;
+        default:
+          super.visitEnum(name, descriptor, value);
+      }
+    }
+
+    @Override
     public void visit(String name, Object value) {
       if (name.equals(Item.memberFromBinding) && value instanceof String) {
         memberBindingReference = (String) value;
@@ -741,18 +767,29 @@
       if (memberBindingReference != null) {
         if (!classDeclaration.getValue().equals(classDeclaration.getDefaultValue())
             || !memberDeclaration.getValue().isNone()
-            || !extendsDeclaration.getValue().isAny()) {
+            || !extendsDeclaration.getValue().isAny()
+            || kind != null) {
           throw new KeepEdgeException(
               "Cannot define an item explicitly and via a member-binding reference");
         }
         parent.accept(KeepItemReference.fromBindingReference(memberBindingReference));
       } else {
+        KeepMemberPattern memberPattern = memberDeclaration.getValue();
+        // If the kind is not set (default) then the content of the members determines the kind.
+        if (kind == null) {
+          kind = memberPattern.isNone() ? KeepItemKind.ONLY_CLASS : KeepItemKind.ONLY_MEMBERS;
+        }
+        // If the kind is a member kind and no member pattern is set then set members to all.
+        if (!kind.equals(KeepItemKind.ONLY_CLASS) && memberPattern.isNone()) {
+          memberPattern = KeepMemberPattern.allMembers();
+        }
         parent.accept(
             KeepItemReference.fromItemPattern(
                 KeepItemPattern.builder()
+                    .setKind(kind)
                     .setClassReference(classDeclaration.getValue())
                     .setExtendsPattern(extendsDeclaration.getValue())
-                    .setMemberPattern(memberDeclaration.getValue())
+                    .setMemberPattern(memberPattern)
                     .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 774ea58..f3e97fa 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
@@ -112,7 +112,7 @@
       // Default is "no methods".
       return;
     }
-    if (memberPattern.isAll()) {
+    if (memberPattern.isAllMembers()) {
       throw new Unimplemented();
     }
     if (memberPattern.isMethod()) {
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 8c890df..f1ed988 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
@@ -41,7 +41,9 @@
  *
  *   ITEM_PATTERN
  *     ::= any
- *       | class CLASS_REFERENCE extends EXTENDS_PATTERN { MEMBER_PATTERN }
+ *       | ITEM_KIND class CLASS_REFERENCE extends EXTENDS_PATTERN { MEMBER_PATTERN }
+ *
+ *   ITEM_KIND ::= ONLY_CLASS | ONLY_MEMBERS | CLASS_AND_MEMBERS
  *
  *   TYPE_PATTERN ::= any | exact type-descriptor
  *   PACKAGE_PATTERN ::= any | exact package-name
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
index 1466fc1..dd104eb 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
@@ -11,6 +11,10 @@
     return new Builder();
   }
 
+  public static KeepFieldPattern allFields() {
+    return builder().build();
+  }
+
   public static class Builder {
 
     private KeepFieldAccessPattern accessPattern = KeepFieldAccessPattern.any();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemKind.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemKind.java
new file mode 100644
index 0000000..0d59769
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemKind.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+public enum KeepItemKind {
+  ONLY_CLASS,
+  ONLY_MEMBERS,
+  CLASS_AND_MEMBERS
+}
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 6dfe7e4..6477a4c 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
@@ -27,16 +27,21 @@
     return new Builder();
   }
 
+  public boolean isClassAndMemberPattern() {
+    return kind == KeepItemKind.CLASS_AND_MEMBERS;
+  }
+
   public boolean isClassItemPattern() {
-    return memberPattern.isNone();
+    return kind == KeepItemKind.ONLY_CLASS;
   }
 
   public boolean isMemberItemPattern() {
-    return !memberPattern.isNone();
+    return kind == KeepItemKind.ONLY_MEMBERS;
   }
 
   public static class Builder {
 
+    private KeepItemKind kind = null;
     private KeepClassReference classReference =
         KeepClassReference.fromClassNamePattern(KeepQualifiedClassNamePattern.any());
     private KeepExtendsPattern extendsPattern = KeepExtendsPattern.any();
@@ -45,15 +50,22 @@
     private Builder() {}
 
     public Builder copyFrom(KeepItemPattern pattern) {
-      return setClassReference(pattern.getClassReference())
+      return setKind(pattern.getKind())
+          .setClassReference(pattern.getClassReference())
           .setExtendsPattern(pattern.getExtendsPattern())
           .setMemberPattern(pattern.getMemberPattern());
     }
 
     public Builder any() {
+      kind = KeepItemKind.CLASS_AND_MEMBERS;
       classReference = KeepClassReference.fromClassNamePattern(KeepQualifiedClassNamePattern.any());
       extendsPattern = KeepExtendsPattern.any();
-      memberPattern = KeepMemberPattern.all();
+      memberPattern = KeepMemberPattern.allMembers();
+      return this;
+    }
+
+    public Builder setKind(KeepItemKind kind) {
+      this.kind = kind;
       return this;
     }
 
@@ -77,29 +89,54 @@
     }
 
     public KeepItemPattern build() {
-      return new KeepItemPattern(classReference, extendsPattern, memberPattern);
+      if (kind == null) {
+        kind = memberPattern.isNone() ? KeepItemKind.ONLY_CLASS : KeepItemKind.ONLY_MEMBERS;
+      }
+      if (kind == KeepItemKind.ONLY_CLASS && !memberPattern.isNone()) {
+        throw new KeepEdgeException(
+            "Invalid kind ONLY_CLASS for item with member pattern: " + memberPattern);
+      }
+      if (kind == KeepItemKind.ONLY_MEMBERS && memberPattern.isNone()) {
+        throw new KeepEdgeException("Invalid kind ONLY_MEMBERS for item with no member pattern");
+      }
+      if (kind == KeepItemKind.CLASS_AND_MEMBERS && memberPattern.isNone()) {
+        throw new KeepEdgeException(
+            "Invalid kind CLASS_AND_MEMBERS for item with no member pattern");
+      }
+      return new KeepItemPattern(kind, classReference, extendsPattern, memberPattern);
     }
   }
 
+  private final KeepItemKind kind;
   private final KeepClassReference classReference;
   private final KeepExtendsPattern extendsPattern;
   private final KeepMemberPattern memberPattern;
   // TODO: class annotations
 
   private KeepItemPattern(
+      KeepItemKind kind,
       KeepClassReference classReference,
       KeepExtendsPattern extendsPattern,
       KeepMemberPattern memberPattern) {
+    assert kind != null;
     assert classReference != null;
     assert extendsPattern != null;
     assert memberPattern != null;
+    this.kind = kind;
     this.classReference = classReference;
     this.extendsPattern = extendsPattern;
     this.memberPattern = memberPattern;
   }
 
   public boolean isAny(Predicate<String> onReference) {
-    return extendsPattern.isAny() && memberPattern.isAll() && classReference.isAny(onReference);
+    return kind.equals(KeepItemKind.CLASS_AND_MEMBERS)
+        && extendsPattern.isAny()
+        && memberPattern.isAllMembers()
+        && classReference.isAny(onReference);
+  }
+
+  public KeepItemKind getKind() {
+    return kind;
   }
 
   public KeepClassReference getClassReference() {
@@ -127,20 +164,23 @@
       return false;
     }
     KeepItemPattern that = (KeepItemPattern) obj;
-    return classReference.equals(that.classReference)
+    return kind.equals(that.kind)
+        && classReference.equals(that.classReference)
         && extendsPattern.equals(that.extendsPattern)
         && memberPattern.equals(that.memberPattern);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(classReference, extendsPattern, memberPattern);
+    return Objects.hash(kind, classReference, extendsPattern, memberPattern);
   }
 
   @Override
   public String toString() {
     return "KeepClassPattern{"
-        + "classReference="
+        + "kind="
+        + kind
+        + ", classReference="
         + classReference
         + ", extendsPattern="
         + extendsPattern
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
index b0f205e..d994d84 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
@@ -9,7 +9,7 @@
     return None.getInstance();
   }
 
-  public static KeepMemberPattern all() {
+  public static KeepMemberPattern allMembers() {
     return All.getInstance();
   }
 
@@ -22,7 +22,7 @@
     }
 
     @Override
-    public boolean isAll() {
+    public boolean isAllMembers() {
       return true;
     }
 
@@ -73,7 +73,7 @@
 
   KeepMemberPattern() {}
 
-  public boolean isAll() {
+  public boolean isAllMembers() {
     return false;
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index f84a4b1..4104392 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -12,10 +12,14 @@
     return new Builder();
   }
 
+  public static KeepMemberPattern allMethods() {
+    return builder().build();
+  }
+
   public static class Builder {
 
     private KeepMethodAccessPattern accessPattern = KeepMethodAccessPattern.any();
-    private KeepMethodNamePattern namePattern = null;
+    private KeepMethodNamePattern namePattern = KeepMethodNamePattern.any();
     private KeepMethodReturnTypePattern returnTypePattern = KeepMethodReturnTypePattern.any();
     private KeepMethodParametersPattern parametersPattern = KeepMethodParametersPattern.any();
 
@@ -50,9 +54,6 @@
     }
 
     public KeepMethodPattern build() {
-      if (namePattern == null) {
-        throw new KeepEdgeException("Method pattern must declar a name pattern");
-      }
       KeepMethodReturnTypePattern returnTypePattern = this.returnTypePattern;
       KeepMethodNameExactPattern exactName = namePattern.asExact();
       if (exactName != null
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
index 5922be3..1664218 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeNormalizer.java
@@ -8,6 +8,7 @@
 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.KeepItemKind;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
@@ -156,7 +157,10 @@
 
   private KeepItemPattern getMemberItemPattern(
       KeepItemPattern fromPattern, KeepClassReference classReference) {
+    assert fromPattern.getKind().equals(KeepItemKind.ONLY_MEMBERS)
+        || fromPattern.getKind().equals(KeepItemKind.CLASS_AND_MEMBERS);
     return KeepItemPattern.builder()
+        .setKind(fromPattern.getKind())
         .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
deleted file mode 100644
index f565635..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepEdgeSplitter.java
+++ /dev/null
@@ -1,377 +0,0 @@
-// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.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 39ae46d..ee80978 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
@@ -1,36 +1,38 @@
-// 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.
 package com.android.tools.r8.keepanno.keeprules;
 
-import com.android.tools.r8.keepanno.ast.KeepClassReference;
+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.KeepExtendsPattern;
-import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
-import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepOptions;
-import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
-import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
-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 com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgConditionalRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgDependentMembersRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.PgUnconditionalRule;
+import com.android.tools.r8.keepanno.keeprules.PgRule.TargetKeepKind;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.function.BiConsumer;
+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.Consumer;
-import java.util.stream.Collectors;
 
-/** Extract out a sequence of Proguard keep rules that give a conservative over-approximation. */
+/** Extract the PG keep rules that over-approximate a keep edge. */
 public class KeepRuleExtractor {
 
   private final Consumer<String> ruleConsumer;
@@ -40,7 +42,7 @@
   }
 
   public void extract(KeepEdge edge) {
-    Collection<PgRule> rules = KeepEdgeSplitter.split(edge);
+    Collection<PgRule> rules = split(edge);
     StringBuilder builder = new StringBuilder();
     for (PgRule rule : rules) {
       rule.printRule(builder);
@@ -49,273 +51,395 @@
     ruleConsumer.accept(builder.toString());
   }
 
-  public static void printHeader(StringBuilder builder, KeepEdgeMetaInfo metaInfo) {
-    if (metaInfo.hasContext()) {
-      builder.append("# context: ").append(metaInfo.getContextDescriptorString()).append('\n');
+  private 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);
     }
-    if (metaInfo.hasDescription()) {
-      String escapedDescription = escapeLineBreaks(metaInfo.getDescriptionString());
-      builder.append("# description: ").append(escapedDescription).append('\n');
+
+    private Holder(KeepItemPattern itemPattern, KeepQualifiedClassNamePattern namePattern) {
+      this.itemPattern = itemPattern;
+      this.namePattern = namePattern;
     }
   }
 
-  public static String escapeChar(char c) {
-    if (c == '\n') {
-      return "\\n";
+  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));
     }
-    if (c == '\r') {
-      return "\\r";
+
+    private BindingUsers(Holder holder) {
+      this.holder = holder;
     }
-    return null;
+
+    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());
+    }
   }
 
-  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.
-      if (escapeChar(charArray[i]) != null) {
-        StringBuilder builder = new StringBuilder(string.substring(0, i));
-        for (int j = i; j < charArray.length; j++) {
-          char c = charArray[j];
-          String escaped = escapeChar(c);
-          if (escaped != null) {
-            builder.append(escaped);
-          } else {
-            builder.append(c);
+  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.
+                    // Doing so over-approximates the matching instances of the edge, but gives
+                    // better stability of the extraction as it avoids picking a particular
+                    // precondition as the "primary" one to act on.
+                    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.isClassItemPattern()) {
+            KeepMemberPattern old = memberPatterns.put(conditionReference, item.getMemberPattern());
+            conditionMembers.add(conditionReference);
+            assert old == null;
+          }
+        });
+    return conditionMembers;
+  }
+
+  @FunctionalInterface
+  private interface OnTargetCallback {
+    void accept(
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> memberTargets,
+        TargetKeepKind keepKind);
+  }
+
+  private static void computeTargets(
+      Set<String> targets,
+      KeepBindings bindings,
+      Map<String, KeepMemberPattern> memberPatterns,
+      OnTargetCallback callback) {
+    boolean keepClassTarget = false;
+    List<String> disjunctiveTargetMembers = new ArrayList<>();
+    List<String> classConjunctiveTargetMembers = new ArrayList<>();
+
+    for (String targetReference : targets) {
+      KeepItemPattern item = bindings.get(targetReference).getItem();
+      if (bindings.isAny(item)) {
+        // If the target is "any item" then it contains any other target pattern.
+        memberPatterns.put(targetReference, item.getMemberPattern());
+        callback.accept(
+            memberPatterns,
+            Collections.singletonList(targetReference),
+            TargetKeepKind.CLASS_OR_MEMBERS);
+        return;
+      }
+      if (item.isClassItemPattern()) {
+        keepClassTarget = true;
+      } else {
+        memberPatterns.putIfAbsent(targetReference, item.getMemberPattern());
+        if (item.isClassAndMemberPattern()) {
+          // If a target is a "class and member" target then it must be added as a separate rule.
+          classConjunctiveTargetMembers.add(targetReference);
+        } else {
+          assert item.isMemberItemPattern();
+          disjunctiveTargetMembers.add(targetReference);
         }
-        return builder.toString();
       }
     }
-    return string;
-  }
 
-  public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
-    for (KeepOption option : KeepOption.values()) {
-      if (options.isAllowed(option)) {
-        builder.append(",allow").append(getOptionString(option));
+    // The class is targeted, so that part of a class-and-member conjunction is satisfied.
+    // The conjunctive members can thus be moved to the disjunctive set.
+    if (keepClassTarget) {
+      disjunctiveTargetMembers.addAll(classConjunctiveTargetMembers);
+      classConjunctiveTargetMembers.clear();
+    }
+
+    if (!disjunctiveTargetMembers.isEmpty()) {
+      TargetKeepKind keepKind =
+          keepClassTarget ? TargetKeepKind.CLASS_OR_MEMBERS : TargetKeepKind.JUST_MEMBERS;
+      callback.accept(memberPatterns, disjunctiveTargetMembers, keepKind);
+    } else if (keepClassTarget) {
+      callback.accept(
+          Collections.emptyMap(), Collections.emptyList(), TargetKeepKind.CLASS_OR_MEMBERS);
+    }
+
+    if (!classConjunctiveTargetMembers.isEmpty()) {
+      assert !keepClassTarget;
+      for (String targetReference : classConjunctiveTargetMembers) {
+        callback.accept(
+            memberPatterns,
+            Collections.singletonList(targetReference),
+            TargetKeepKind.CLASS_AND_MEMBERS);
       }
     }
   }
 
-  public static StringBuilder printClassHeader(
-      StringBuilder builder,
-      KeepItemPattern classPattern,
-      BiConsumer<StringBuilder, KeepClassReference> printClassReference) {
-    builder.append("class ");
-    printClassReference.accept(builder, classPattern.getClassReference());
-    KeepExtendsPattern extendsPattern = classPattern.getExtendsPattern();
-    if (!extendsPattern.isAny()) {
-      builder.append(" extends ");
-      printClassName(builder, extendsPattern.asClassNamePattern());
-    }
-    return builder;
+  private static void createUnconditionalRules(
+      List<PgRule> rules,
+      Holder holder,
+      KeepEdgeMetaInfo metaInfo,
+      KeepBindings bindings,
+      KeepOptions options,
+      Set<String> targets) {
+    computeTargets(
+        targets,
+        bindings,
+        new HashMap<>(),
+        (memberPatterns, targetMembers, targetKeepKind) -> {
+          if (targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS)) {
+            // Members dependent on the class, so they go to the implicitly dependent rule.
+            rules.add(
+                new PgDependentMembersRule(
+                    metaInfo,
+                    holder,
+                    options,
+                    memberPatterns,
+                    Collections.emptyList(),
+                    targetMembers,
+                    targetKeepKind));
+          } else {
+            rules.add(
+                new PgUnconditionalRule(
+                    metaInfo, holder, options, memberPatterns, targetMembers, targetKeepKind));
+          }
+        });
   }
 
-  public static StringBuilder printMemberClause(StringBuilder builder, KeepMemberPattern member) {
-    if (member.isAll()) {
-      return builder.append("*;");
-    }
-    if (member.isMethod()) {
-      return printMethod(builder, member.asMethod());
-    }
-    if (member.isField()) {
-      return printField(builder, member.asField());
-    }
-    throw new Unimplemented();
+  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,
+        (ignore, targetMembers, targetKeepKind) ->
+            rules.add(
+                new PgConditionalRule(
+                    metaInfo,
+                    options,
+                    conditionHolder,
+                    targetHolder,
+                    memberPatterns,
+                    conditionMembers,
+                    targetMembers,
+                    targetKeepKind)));
   }
 
-  private static StringBuilder printField(StringBuilder builder, KeepFieldPattern fieldPattern) {
-    if (fieldPattern.isAnyField()) {
-      return builder.append("<fields>;");
-    }
-    printAccess(builder, " ", fieldPattern.getAccessPattern());
-    printType(builder, fieldPattern.getTypePattern().asType());
-    builder.append(' ');
-    printFieldName(builder, fieldPattern.getNamePattern());
-    return builder.append(';');
+  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,
+        (ignore, targetMembers, targetKeepKind) -> {
+          List<String> nonAllMemberTargets = new ArrayList<>(targetMembers.size());
+          for (String targetMember : targetMembers) {
+            KeepMemberPattern memberPattern = memberPatterns.get(targetMember);
+            if (memberPattern.isAllMembers() && conditionMembers.contains(targetMember)) {
+              // This pattern is a pattern for "any member" and it is bound by a condition.
+              // Since backrefs can't reference a *-member we split this target in two, one for
+              // fields and one for methods.
+              HashMap<String, KeepMemberPattern> copyWithMethod = new HashMap<>(memberPatterns);
+              copyWithMethod.put(targetMember, KeepMethodPattern.allMethods());
+              rules.add(
+                  new PgDependentMembersRule(
+                      metaInfo,
+                      holder,
+                      options,
+                      copyWithMethod,
+                      conditionMembers,
+                      Collections.singletonList(targetMember),
+                      targetKeepKind));
+              HashMap<String, KeepMemberPattern> copyWithField = new HashMap<>(memberPatterns);
+              copyWithField.put(targetMember, KeepFieldPattern.allFields());
+              rules.add(
+                  new PgDependentMembersRule(
+                      metaInfo,
+                      holder,
+                      options,
+                      copyWithField,
+                      conditionMembers,
+                      Collections.singletonList(targetMember),
+                      targetKeepKind));
+            } else {
+              nonAllMemberTargets.add(targetMember);
+            }
+          }
+          if (targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS) && nonAllMemberTargets.isEmpty()) {
+            return;
+          }
+          rules.add(
+              new PgDependentMembersRule(
+                  metaInfo,
+                  holder,
+                  options,
+                  memberPatterns,
+                  conditionMembers,
+                  nonAllMemberTargets,
+                  targetKeepKind));
+        });
   }
 
-  private static StringBuilder printMethod(StringBuilder builder, KeepMethodPattern methodPattern) {
-    if (methodPattern.isAnyMethod()) {
-      return builder.append("<methods>;");
-    }
-    printAccess(builder, " ", methodPattern.getAccessPattern());
-    printReturnType(builder, methodPattern.getReturnTypePattern());
-    builder.append(' ');
-    printMethodName(builder, methodPattern.getNamePattern());
-    printParameters(builder, methodPattern.getParametersPattern());
-    return builder.append(';');
+  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 StringBuilder printParameters(
-      StringBuilder builder, KeepMethodParametersPattern parametersPattern) {
-    if (parametersPattern.isAny()) {
-      return builder.append("(...)");
-    }
-    return builder
-        .append('(')
-        .append(
-            parametersPattern.asList().stream()
-                .map(KeepRuleExtractor::getTypePatternString)
-                .collect(Collectors.joining(", ")))
-        .append(')');
-  }
-
-  private static StringBuilder printFieldName(
-      StringBuilder builder, KeepFieldNamePattern namePattern) {
-    return namePattern.isAny()
-        ? builder.append("*")
-        : builder.append(namePattern.asExact().getName());
-  }
-
-  private static StringBuilder printMethodName(
-      StringBuilder builder, KeepMethodNamePattern namePattern) {
-    return namePattern.isAny()
-        ? builder.append("*")
-        : builder.append(namePattern.asExact().getName());
-  }
-
-  private static StringBuilder printReturnType(
-      StringBuilder builder, KeepMethodReturnTypePattern returnTypePattern) {
-    if (returnTypePattern.isVoid()) {
-      return builder.append("void");
-    }
-    return printType(builder, returnTypePattern.asType());
-  }
-
-  private static StringBuilder printType(StringBuilder builder, KeepTypePattern typePattern) {
-    return builder.append(getTypePatternString(typePattern));
-  }
-
-  private static StringBuilder printAccess(
-      StringBuilder builder, String indent, KeepMethodAccessPattern accessPattern) {
-    if (accessPattern.isAny()) {
-      // No text will match any access pattern.
-      // Don't print the indent in this case.
-      return builder;
-    }
-    throw new Unimplemented();
-  }
-
-  private static StringBuilder printAccess(
-      StringBuilder builder, String indent, KeepFieldAccessPattern accessPattern) {
-    if (accessPattern.isAny()) {
-      // No text will match any access pattern.
-      // Don't print the indent in this case.
-      return builder;
-    }
-    throw new Unimplemented();
-  }
-
-  public static StringBuilder printClassName(
-      StringBuilder builder, KeepQualifiedClassNamePattern classNamePattern) {
-    if (classNamePattern.isAny()) {
-      return builder.append('*');
-    }
-    printPackagePrefix(builder, classNamePattern.getPackagePattern());
-    return printSimpleClassName(builder, classNamePattern.getNamePattern());
-  }
-
-  private static StringBuilder printPackagePrefix(
-      StringBuilder builder, KeepPackagePattern packagePattern) {
-    if (packagePattern.isAny()) {
-      return builder.append("**.");
-    }
-    if (packagePattern.isTop()) {
-      return builder;
-    }
-    assert packagePattern.isExact();
-    return builder.append(packagePattern.getExactPackageAsString()).append('.');
-  }
-
-  private static StringBuilder printSimpleClassName(
-      StringBuilder builder, KeepUnqualfiedClassNamePattern namePattern) {
-    if (namePattern.isAny()) {
-      return builder.append('*');
-    }
-    assert namePattern.isExact();
-    return builder.append(namePattern.asExact().getExactNameAsString());
-  }
-
-  private static String getOptionString(KeepOption option) {
-    switch (option) {
-      case SHRINKING:
-        return "shrinking";
-      case OPTIMIZING:
-        return "optimization";
-      case OBFUSCATING:
-        return "obfuscation";
-      case ACCESS_MODIFICATION:
-        return "accessmodification";
-      case ANNOTATION_REMOVAL:
-        return "annotationremoval";
-      default:
-        throw new Unimplemented();
-    }
-  }
-
-  private static String getTypePatternString(KeepTypePattern typePattern) {
-    if (typePattern.isAny()) {
-      return "***";
-    }
-    return descriptorToJavaType(typePattern.getDescriptor());
-  }
-
-  private static String descriptorToJavaType(String descriptor) {
-    if (descriptor.isEmpty()) {
-      throw new KeepEdgeException("Invalid empty type descriptor");
-    }
-    if (descriptor.length() == 1) {
-      return primitiveDescriptorToJavaType(descriptor.charAt(0));
-    }
-    if (descriptor.charAt(0) == '[') {
-      return arrayDescriptorToJavaType(descriptor);
-    }
-    return classDescriptorToJavaType(descriptor);
-  }
-
-  private static String primitiveDescriptorToJavaType(char descriptor) {
-    switch (descriptor) {
-      case 'Z':
-        return "boolean";
-      case 'B':
-        return "byte";
-      case 'S':
-        return "short";
-      case 'I':
-        return "int";
-      case 'J':
-        return "long";
-      case 'F':
-        return "float";
-      case 'D':
-        return "double";
-      default:
-        throw new KeepEdgeException("Invalid primitive descriptor: " + descriptor);
-    }
-  }
-
-  private static String classDescriptorToJavaType(String descriptor) {
-    int last = descriptor.length() - 1;
-    if (descriptor.charAt(0) != 'L' || descriptor.charAt(last) != ';') {
-      throw new KeepEdgeException("Invalid class descriptor: " + descriptor);
-    }
-    return descriptor.substring(1, last).replace('/', '.');
-  }
-
-  private static String arrayDescriptorToJavaType(String descriptor) {
-    for (int i = 0; i < descriptor.length(); i++) {
-      char c = descriptor.charAt(i);
-      if (c != '[') {
-        StringBuilder builder = new StringBuilder();
-        builder.append(descriptorToJavaType(descriptor.substring(i)));
-        for (int j = 0; j < i; j++) {
-          builder.append("[]");
+  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");
         }
-        return builder.toString();
+        classReference = reference;
       }
     }
-    throw new KeepEdgeException("Invalid array descriptor: " + descriptor);
+    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/PgRule.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
index 9a9ccc0..412a91e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
@@ -3,22 +3,46 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.keeprules;
 
+import static com.android.tools.r8.keepanno.keeprules.RulePrintingUtils.printClassHeader;
+import static com.android.tools.r8.keepanno.keeprules.RulePrintingUtils.printMemberClause;
+
 import com.android.tools.r8.keepanno.ast.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 com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor.Holder;
+import com.android.tools.r8.keepanno.keeprules.RulePrinter.BackReferencePrinter;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
 
 public abstract class PgRule {
+  public enum TargetKeepKind {
+    JUST_MEMBERS(RulePrintingUtils.KEEP_CLASS_MEMBERS),
+    CLASS_OR_MEMBERS(RulePrintingUtils.KEEP),
+    CLASS_AND_MEMBERS(RulePrintingUtils.KEEP_CLASSES_WITH_MEMBERS);
+
+    private final String ruleKind;
+
+    TargetKeepKind(String ruleKind) {
+      this.ruleKind = ruleKind;
+    }
+
+    String getKeepRuleKind() {
+      return ruleKind;
+    }
+  }
+
+  private static void printNonEmptyMembersPatternAsDefaultInitWorkaround(StringBuilder builder) {
+    // If no members is given, compat R8 and legacy full mode will implicitly keep <init>().
+    // Add a keep of finalize which is a library method that would be kept in any case.
+    builder.append(" { void finalize(); }");
+  }
+
   private final KeepEdgeMetaInfo metaInfo;
   private final KeepOptions options;
 
@@ -33,26 +57,27 @@
   // without a binding indirection).
   public static BiConsumer<StringBuilder, KeepClassReference> classReferencePrinter(
       KeepQualifiedClassNamePattern classNamePattern) {
-    return (StringBuilder builder, KeepClassReference classReference) -> {
+    return (builder, classReference) -> {
       assert classReference.isBindingReference()
           || classReference.asClassNamePattern().equals(classNamePattern);
-      KeepRuleExtractor.printClassName(builder, classNamePattern);
+      RulePrintingUtils.printClassName(
+          classNamePattern, RulePrinter.withoutBackReferences(builder));
     };
   }
 
   void printKeepOptions(StringBuilder builder) {
-    KeepRuleExtractor.printKeepOptions(builder, options);
+    RulePrintingUtils.printKeepOptions(builder, options);
   }
 
   public void printRule(StringBuilder builder) {
-    KeepRuleExtractor.printHeader(builder, metaInfo);
+    RulePrintingUtils.printHeader(builder, metaInfo);
     printCondition(builder);
     printConsequence(builder);
   }
 
   void printCondition(StringBuilder builder) {
     if (hasCondition()) {
-      builder.append("-if ");
+      builder.append(RulePrintingUtils.IF).append(' ');
       printConditionHolder(builder);
       List<String> members = getConditionMembers();
       if (!members.isEmpty()) {
@@ -86,7 +111,6 @@
   boolean hasCondition() {
     return false;
   }
-  ;
 
   List<String> getConditionMembers() {
     throw new KeepEdgeException("Unreachable");
@@ -109,64 +133,97 @@
   abstract void printTargetMember(StringBuilder builder, String member);
 
   /**
-   * Representation of an unconditional rule to keep a class.
+   * Representation of an unconditional rule to keep a class and methods.
    *
    * <pre>
-   *   -keep class <holder>
+   *   -keep[classeswithmembers] class <holder> [{ <members> }]
    * </pre>
    *
    * and with no dependencies / back-references.
    */
-  static class PgUnconditionalClassRule extends PgRule {
-    final KeepQualifiedClassNamePattern holderNamePattern;
-    final KeepItemPattern holderPattern;
+  static class PgUnconditionalRule extends PgRule {
+    private final KeepQualifiedClassNamePattern holderNamePattern;
+    private final KeepItemPattern holderPattern;
+    private final TargetKeepKind targetKeepKind;
+    private final List<String> targetMembers;
+    private final Map<String, KeepMemberPattern> memberPatterns;
 
-    public PgUnconditionalClassRule(KeepEdgeMetaInfo metaInfo, KeepOptions options, Holder holder) {
+    public PgUnconditionalRule(
+        KeepEdgeMetaInfo metaInfo,
+        Holder holder,
+        KeepOptions options,
+        Map<String, KeepMemberPattern> memberPatterns,
+        List<String> targetMembers,
+        TargetKeepKind targetKeepKind) {
       super(metaInfo, options);
+      assert !targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS);
       this.holderNamePattern = holder.namePattern;
       this.holderPattern = holder.itemPattern;
+      this.targetKeepKind = targetKeepKind;
+      this.memberPatterns = memberPatterns;
+      this.targetMembers = targetMembers;
     }
 
     @Override
     String getConsequenceKeepType() {
-      return "-keep";
+      return targetKeepKind.getKeepRuleKind();
     }
 
     @Override
     List<String> getTargetMembers() {
-      return Collections.emptyList();
+      return targetMembers;
     }
 
     @Override
     void printTargetHolder(StringBuilder builder) {
-      KeepRuleExtractor.printClassHeader(
-          builder, holderPattern, classReferencePrinter(holderNamePattern));
+      printClassHeader(builder, holderPattern, classReferencePrinter(holderNamePattern));
+      if (getTargetMembers().isEmpty()) {
+        printNonEmptyMembersPatternAsDefaultInitWorkaround(builder);
+      }
     }
 
     @Override
     void printTargetMember(StringBuilder builder, String memberReference) {
-      throw new KeepEdgeException("Unreachable");
+      KeepMemberPattern memberPattern = memberPatterns.get(memberReference);
+      printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
   }
 
-  abstract static class PgConditionalRuleBase extends PgRule {
+  /**
+   * Representation of conditional rules but without dependencies between condition and target.
+   *
+   * <pre>
+   *   -if class <class-condition> [{ <member-conditions> }]
+   *   -keepX class <class-target> [{ <member-targets> }]
+   * </pre>
+   *
+   * and with no dependencies / back-references.
+   */
+  static class PgConditionalRule extends PgRule {
+
     final KeepItemPattern classCondition;
     final KeepItemPattern classTarget;
     final Map<String, KeepMemberPattern> memberPatterns;
     final List<String> memberConditions;
+    private final List<String> memberTargets;
+    private final TargetKeepKind keepKind;
 
-    public PgConditionalRuleBase(
+    public PgConditionalRule(
         KeepEdgeMetaInfo metaInfo,
         KeepOptions options,
         Holder classCondition,
         Holder classTarget,
         Map<String, KeepMemberPattern> memberPatterns,
-        List<String> memberConditions) {
+        List<String> memberConditions,
+        List<String> memberTargets,
+        TargetKeepKind keepKind) {
       super(metaInfo, options);
       this.classCondition = classCondition.itemPattern;
       this.classTarget = classTarget.itemPattern;
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
+      this.memberTargets = memberTargets;
+      this.keepKind = keepKind;
     }
 
     @Override
@@ -181,92 +238,26 @@
 
     @Override
     void printConditionHolder(StringBuilder builder) {
-      KeepRuleExtractor.printClassHeader(builder, classCondition, this::printClassName);
+      printClassHeader(builder, classCondition, this::printClassName);
     }
 
     @Override
     void printConditionMember(StringBuilder builder, String member) {
       KeepMemberPattern memberPattern = memberPatterns.get(member);
-      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+      printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
 
     @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);
+      printClassHeader(builder, classTarget, this::printClassName);
+      if (getTargetMembers().isEmpty()) {
+        PgRule.printNonEmptyMembersPatternAsDefaultInitWorkaround(builder);
+      }
     }
 
     @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";
+      return keepKind.getKeepRuleKind();
     }
 
     @Override
@@ -277,36 +268,75 @@
     @Override
     void printTargetMember(StringBuilder builder, String member) {
       KeepMemberPattern memberPattern = memberPatterns.get(member);
-      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+      printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
+    }
+
+    private void printClassName(StringBuilder builder, KeepClassReference clazz) {
+      RulePrintingUtils.printClassName(
+          clazz.asClassNamePattern(), RulePrinter.withoutBackReferences(builder));
     }
   }
 
-  abstract static class PgDependentRuleBase extends PgRule {
+  /**
+   * Representation of a conditional rule that is match/instance dependent.
+   *
+   * <pre>
+   *   -if class <class-pattern> [{ <member-condition>* }]
+   *   -keepX class <class-backref> [{ <member-target | member-backref>* }]
+   * </pre>
+   *
+   * or if the only condition is the class itself and targeting members, just:
+   *
+   * <pre>
+   *   -keepclassmembers <class-pattern> { <member-target> }
+   * </pre>
+   */
+  static class PgDependentMembersRule extends PgRule {
 
-    final KeepQualifiedClassNamePattern holderNamePattern;
-    final KeepItemPattern holderPattern;
-    final Map<String, KeepMemberPattern> memberPatterns;
-    final List<String> memberConditions;
+    private final KeepQualifiedClassNamePattern holderNamePattern;
+    private final KeepItemPattern holderPattern;
+    private final Map<String, KeepMemberPattern> memberPatterns;
+    private final List<String> memberConditions;
+    private final List<String> memberTargets;
+    private final TargetKeepKind keepKind;
 
-    public PgDependentRuleBase(
+    private int nextBackReferenceNumber = 1;
+    private String holderBackReferencePattern = null;
+    private Map<String, String> membersBackReferencePatterns = new HashMap<>();
+
+    public PgDependentMembersRule(
         KeepEdgeMetaInfo metaInfo,
         Holder holder,
         KeepOptions options,
         Map<String, KeepMemberPattern> memberPatterns,
-        List<String> memberConditions) {
+        List<String> memberConditions,
+        List<String> memberTargets,
+        TargetKeepKind keepKind) {
       super(metaInfo, options);
       this.holderNamePattern = holder.namePattern;
       this.holderPattern = holder.itemPattern;
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
+      this.memberTargets = memberTargets;
+      this.keepKind = keepKind;
     }
 
-    int nextBackReferenceNumber = 1;
-    String holderBackReferencePattern;
-    // TODO(b/248408342): Support back-ref to members too.
+    private int getNextBackReferenceNumber() {
+      return nextBackReferenceNumber++;
+    }
 
-    private StringBuilder addBackRef(StringBuilder backReferenceBuilder) {
-      return backReferenceBuilder.append('<').append(nextBackReferenceNumber++).append('>');
+    @Override
+    boolean hasCondition() {
+      // We can avoid an if-rule if the condition is simply the class and the target is just
+      // members.
+      boolean canUseDependentRule =
+          memberConditions.isEmpty() && keepKind == TargetKeepKind.JUST_MEMBERS;
+      return !canUseDependentRule;
+    }
+
+    @Override
+    String getConsequenceKeepType() {
+      return keepKind.getKeepRuleKind();
     }
 
     @Override
@@ -315,174 +345,64 @@
     }
 
     @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 printConditionHolder(StringBuilder b) {
+      printClassHeader(
+          b,
+          holderPattern,
+          (builder, classReference) -> {
+            BackReferencePrinter printer =
+                RulePrinter.withBackReferences(b, this::getNextBackReferenceNumber);
+            RulePrintingUtils.printClassName(holderNamePattern, printer);
+            holderBackReferencePattern = printer.getBackReference();
+          });
+    }
+
+    @Override
+    void printConditionMember(StringBuilder builder, String member) {
+      KeepMemberPattern memberPattern = memberPatterns.get(member);
+      BackReferencePrinter printer =
+          RulePrinter.withBackReferences(builder, this::getNextBackReferenceNumber);
+      printMemberClause(memberPattern, printer);
+      membersBackReferencePatterns.put(member, printer.getBackReference());
+    }
+
+    @Override
     void printTargetHolder(StringBuilder builder) {
-      if (hasCondition()) {
-        super.printTargetHolder(builder);
-      } else {
-        KeepRuleExtractor.printClassHeader(
-            builder, holderPattern, classReferencePrinter(holderNamePattern));
+      printClassHeader(
+          builder,
+          holderPattern,
+          (b, reference) -> {
+            assert reference.isBindingReference()
+                || reference.asClassNamePattern().equals(holderNamePattern);
+            if (hasCondition()) {
+              b.append(holderBackReferencePattern);
+            } else {
+              assert holderBackReferencePattern == null;
+              RulePrintingUtils.printClassName(
+                  holderNamePattern, RulePrinter.withoutBackReferences(builder));
+            }
+          });
+      if (getTargetMembers().isEmpty()) {
+        PgRule.printNonEmptyMembersPatternAsDefaultInitWorkaround(builder);
       }
     }
 
     @Override
     void printTargetMember(StringBuilder builder, String member) {
-      // TODO(b/248408342): Support back-ref to member instances too.
+      if (hasCondition()) {
+        String backref = membersBackReferencePatterns.get(member);
+        if (backref != null) {
+          builder.append(backref);
+          return;
+        }
+      }
       KeepMemberPattern memberPattern = memberPatterns.get(member);
-      KeepRuleExtractor.printMemberClause(builder, memberPattern);
+      printMemberClause(memberPattern, RulePrinter.withoutBackReferences(builder));
     }
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
new file mode 100644
index 0000000..94694a2
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
@@ -0,0 +1,99 @@
+// 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 java.util.function.Supplier;
+
+public class RulePrinter {
+
+  public static RulePrinter withoutBackReferences(StringBuilder builder) {
+    return new RulePrinter(builder);
+  }
+
+  public static BackReferencePrinter withBackReferences(
+      StringBuilder builder, Supplier<Integer> nextReferenceGenerator) {
+    return new BackReferencePrinter(builder, nextReferenceGenerator);
+  }
+
+  private final StringBuilder builder;
+
+  private RulePrinter(StringBuilder builder) {
+    this.builder = builder;
+  }
+
+  public RulePrinter append(String str) {
+    assert !str.contains("*");
+    assert !str.contains("(...)");
+    return appendWithoutBackReferenceAssert(str);
+  }
+
+  public RulePrinter appendWithoutBackReferenceAssert(String str) {
+    builder.append(str);
+    return this;
+  }
+
+  public RulePrinter appendStar() {
+    return appendWithoutBackReferenceAssert("*");
+  }
+
+  public RulePrinter appendDoubleStar() {
+    return appendWithoutBackReferenceAssert("**");
+  }
+
+  public RulePrinter appendTripleStar() {
+    return appendWithoutBackReferenceAssert("***");
+  }
+
+  public RulePrinter appendAnyParameters() {
+    return appendWithoutBackReferenceAssert("(...)");
+  }
+
+  /** Printer with support for collecting the back-reference printing. */
+  public static class BackReferencePrinter extends RulePrinter {
+
+    final Supplier<Integer> nextNumberGenerator;
+    final StringBuilder backref = new StringBuilder();
+
+    private BackReferencePrinter(StringBuilder builder, Supplier<Integer> nextNumberGenerator) {
+      super(builder);
+      this.nextNumberGenerator = nextNumberGenerator;
+    }
+
+    public String getBackReference() {
+      return backref.toString();
+    }
+
+    private RulePrinter addBackRef(String wildcard) {
+      backref.append('<').append(nextNumberGenerator.get()).append('>');
+      return super.appendWithoutBackReferenceAssert(wildcard);
+    }
+
+    @Override
+    public RulePrinter appendWithoutBackReferenceAssert(String str) {
+      backref.append(str);
+      return super.appendWithoutBackReferenceAssert(str);
+    }
+
+    @Override
+    public RulePrinter appendStar() {
+      return addBackRef("*");
+    }
+
+    @Override
+    public RulePrinter appendDoubleStar() {
+      return addBackRef("**");
+    }
+
+    @Override
+    public RulePrinter appendTripleStar() {
+      return addBackRef("***");
+    }
+
+    @Override
+    public RulePrinter appendAnyParameters() {
+      // TODO(b/265892343): R8 does not yet support back reference to `...`.
+      return appendWithoutBackReferenceAssert("(...)");
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
new file mode 100644
index 0000000..6f08712
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -0,0 +1,307 @@
+// 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.KeepExtendsPattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepOptions;
+import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+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.List;
+import java.util.function.BiConsumer;
+
+public abstract class RulePrintingUtils {
+
+  public static final String IF = "-if";
+  public static final String KEEP = "-keep";
+  public static final String KEEP_CLASS_MEMBERS = "-keepclassmembers";
+  public static final String KEEP_CLASSES_WITH_MEMBERS = "-keepclasseswithmembers";
+
+  public static void printHeader(StringBuilder builder, KeepEdgeMetaInfo metaInfo) {
+    if (metaInfo.hasContext()) {
+      builder.append("# context: ").append(metaInfo.getContextDescriptorString()).append('\n');
+    }
+    if (metaInfo.hasDescription()) {
+      String escapedDescription = escapeLineBreaks(metaInfo.getDescriptionString());
+      builder.append("# description: ").append(escapedDescription).append('\n');
+    }
+  }
+
+  public static String escapeChar(char c) {
+    if (c == '\n') {
+      return "\\n";
+    }
+    if (c == '\r') {
+      return "\\r";
+    }
+    return null;
+  }
+
+  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.
+      if (escapeChar(charArray[i]) != null) {
+        StringBuilder builder = new StringBuilder(string.substring(0, i));
+        for (int j = i; j < charArray.length; j++) {
+          char c = charArray[j];
+          String escaped = escapeChar(c);
+          if (escaped != null) {
+            builder.append(escaped);
+          } else {
+            builder.append(c);
+          }
+        }
+        return builder.toString();
+      }
+    }
+    return string;
+  }
+
+  public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
+    for (KeepOption option : KeepOption.values()) {
+      if (options.isAllowed(option)) {
+        builder.append(",allow").append(getOptionString(option));
+      }
+    }
+  }
+
+  public static StringBuilder printClassHeader(
+      StringBuilder builder,
+      KeepItemPattern classPattern,
+      BiConsumer<StringBuilder, KeepClassReference> printClassReference) {
+    builder.append("class ");
+    printClassReference.accept(builder, classPattern.getClassReference());
+    KeepExtendsPattern extendsPattern = classPattern.getExtendsPattern();
+    if (!extendsPattern.isAny()) {
+      builder.append(" extends ");
+      printClassName(
+          extendsPattern.asClassNamePattern(), RulePrinter.withoutBackReferences(builder));
+    }
+    return builder;
+  }
+
+  public static RulePrinter printMemberClause(KeepMemberPattern member, RulePrinter printer) {
+    if (member.isAllMembers()) {
+      // Note: the rule language does not allow backref to a full member. A rule matching all
+      // members via a binding must be split in two up front: one for methods and one for fields.
+      return printer.appendWithoutBackReferenceAssert("*").append(";");
+    }
+    if (member.isMethod()) {
+      return printMethod(member.asMethod(), printer);
+    }
+    if (member.isField()) {
+      return printField(member.asField(), printer);
+    }
+    throw new Unimplemented();
+  }
+
+  private static RulePrinter printField(KeepFieldPattern fieldPattern, RulePrinter builder) {
+    printFieldAccess(builder, " ", fieldPattern.getAccessPattern());
+    printType(builder, fieldPattern.getTypePattern().asType());
+    builder.append(" ");
+    printFieldName(builder, fieldPattern.getNamePattern());
+    return builder.append(";");
+  }
+
+  private static RulePrinter printMethod(KeepMethodPattern methodPattern, RulePrinter builder) {
+    printMethodAccess(builder, " ", methodPattern.getAccessPattern());
+    printReturnType(builder, methodPattern.getReturnTypePattern());
+    builder.append(" ");
+    printMethodName(builder, methodPattern.getNamePattern());
+    printParameters(builder, methodPattern.getParametersPattern());
+    return builder.append(";");
+  }
+
+  private static RulePrinter printParameters(
+      RulePrinter builder, KeepMethodParametersPattern parametersPattern) {
+    if (parametersPattern.isAny()) {
+      return builder.appendAnyParameters();
+    }
+    builder.append("(");
+    List<KeepTypePattern> patterns = parametersPattern.asList();
+    for (int i = 0; i < patterns.size(); i++) {
+      if (i > 0) {
+        builder.append(", ");
+      }
+      printType(builder, patterns.get(i));
+    }
+    return builder.append(")");
+  }
+
+  private static RulePrinter printFieldName(RulePrinter builder, KeepFieldNamePattern namePattern) {
+    return namePattern.isAny()
+        ? builder.appendStar()
+        : builder.append(namePattern.asExact().getName());
+  }
+
+  private static RulePrinter printMethodName(
+      RulePrinter builder, KeepMethodNamePattern namePattern) {
+    return namePattern.isAny()
+        ? builder.appendStar()
+        : builder.append(namePattern.asExact().getName());
+  }
+
+  private static RulePrinter printReturnType(
+      RulePrinter builder, KeepMethodReturnTypePattern returnTypePattern) {
+    if (returnTypePattern.isVoid()) {
+      return builder.append("void");
+    }
+    return printType(builder, returnTypePattern.asType());
+  }
+
+  private static RulePrinter printType(RulePrinter builder, KeepTypePattern typePattern) {
+    if (typePattern.isAny()) {
+      return builder.appendTripleStar();
+    }
+    return builder.append(descriptorToJavaType(typePattern.getDescriptor()));
+  }
+
+  private static RulePrinter printMethodAccess(
+      RulePrinter builder, String indent, KeepMethodAccessPattern accessPattern) {
+    if (accessPattern.isAny()) {
+      // No text will match any access pattern.
+      // Don't print the indent in this case.
+      return builder;
+    }
+    throw new Unimplemented();
+  }
+
+  private static RulePrinter printFieldAccess(
+      RulePrinter builder, String indent, KeepFieldAccessPattern accessPattern) {
+    if (accessPattern.isAny()) {
+      // No text will match any access pattern.
+      // Don't print the indent in this case.
+      return builder;
+    }
+    throw new Unimplemented();
+  }
+
+  public static RulePrinter printClassName(
+      KeepQualifiedClassNamePattern classNamePattern, RulePrinter printer) {
+    if (classNamePattern.isAny()) {
+      return printer.appendStar();
+    }
+    printPackagePrefix(classNamePattern.getPackagePattern(), printer);
+    return printSimpleClassName(classNamePattern.getNamePattern(), printer);
+  }
+
+  private static RulePrinter printPackagePrefix(
+      KeepPackagePattern packagePattern, RulePrinter builder) {
+    if (packagePattern.isAny()) {
+      return builder.appendDoubleStar().append(".");
+    }
+    if (packagePattern.isTop()) {
+      return builder;
+    }
+    assert packagePattern.isExact();
+    return builder.append(packagePattern.getExactPackageAsString()).append(".");
+  }
+
+  private static RulePrinter printSimpleClassName(
+      KeepUnqualfiedClassNamePattern namePattern, RulePrinter builder) {
+    if (namePattern.isAny()) {
+      return builder.appendStar();
+    }
+    assert namePattern.isExact();
+    return builder.append(namePattern.asExact().getExactNameAsString());
+  }
+
+  private static String getOptionString(KeepOption option) {
+    switch (option) {
+      case SHRINKING:
+        return "shrinking";
+      case OPTIMIZING:
+        return "optimization";
+      case OBFUSCATING:
+        return "obfuscation";
+      case ACCESS_MODIFICATION:
+        return "accessmodification";
+      case ANNOTATION_REMOVAL:
+        return "annotationremoval";
+      default:
+        throw new Unimplemented();
+    }
+  }
+
+  private static String getTypePatternString(KeepTypePattern typePattern) {
+    if (typePattern.isAny()) {
+      return "***";
+    }
+    return descriptorToJavaType(typePattern.getDescriptor());
+  }
+
+  private static String descriptorToJavaType(String descriptor) {
+    if (descriptor.isEmpty()) {
+      throw new KeepEdgeException("Invalid empty type descriptor");
+    }
+    if (descriptor.length() == 1) {
+      return primitiveDescriptorToJavaType(descriptor.charAt(0));
+    }
+    if (descriptor.charAt(0) == '[') {
+      return arrayDescriptorToJavaType(descriptor);
+    }
+    return classDescriptorToJavaType(descriptor);
+  }
+
+  private static String primitiveDescriptorToJavaType(char descriptor) {
+    switch (descriptor) {
+      case 'Z':
+        return "boolean";
+      case 'B':
+        return "byte";
+      case 'S':
+        return "short";
+      case 'I':
+        return "int";
+      case 'J':
+        return "long";
+      case 'F':
+        return "float";
+      case 'D':
+        return "double";
+      default:
+        throw new KeepEdgeException("Invalid primitive descriptor: " + descriptor);
+    }
+  }
+
+  private static String classDescriptorToJavaType(String descriptor) {
+    int last = descriptor.length() - 1;
+    if (descriptor.charAt(0) != 'L' || descriptor.charAt(last) != ';') {
+      throw new KeepEdgeException("Invalid class descriptor: " + descriptor);
+    }
+    return descriptor.substring(1, last).replace('/', '.');
+  }
+
+  private static String arrayDescriptorToJavaType(String descriptor) {
+    for (int i = 0; i < descriptor.length(); i++) {
+      char c = descriptor.charAt(i);
+      if (c != '[') {
+        StringBuilder builder = new StringBuilder();
+        builder.append(descriptorToJavaType(descriptor.substring(i)));
+        for (int j = 0; j < i; j++) {
+          builder.append("[]");
+        }
+        return builder.toString();
+      }
+    }
+    throw new KeepEdgeException("Invalid array descriptor: " + descriptor);
+  }
+}
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index 1132b9a..51350bc 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -77,16 +77,6 @@
     {
       "api_level_below_or_equal": 32,
       "api_level_greater_or_equal": 24,
-      "emulate_interface": {
-        "java.util.Collection": "j$.util.Collection"
-      },
-      "dont_rewrite": [
-        "boolean java.util.Collection#removeIf(java.util.function.Predicate)",
-        "java.util.Spliterator java.util.Collection#spliterator()",
-        "java.util.stream.Stream java.util.Collection#parallelStream()",
-        "java.util.stream.Stream java.util.Collection#stream()",
-        "void java.util.Collection#forEach(java.util.function.Consumer)"
-      ],
       "rewrite_prefix": {
         "java.util.stream.DesugarCollectors": "j$.util.stream.DesugarCollectors"
       },
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index cd20a37..6cdf118 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -93,16 +93,6 @@
     {
       "api_level_below_or_equal": 32,
       "api_level_greater_or_equal": 24,
-      "emulate_interface": {
-        "java.util.Collection": "j$.util.Collection"
-      },
-      "dont_rewrite": [
-        "boolean java.util.Collection#removeIf(java.util.function.Predicate)",
-        "java.util.Spliterator java.util.Collection#spliterator()",
-        "java.util.stream.Stream java.util.Collection#parallelStream()",
-        "java.util.stream.Stream java.util.Collection#stream()",
-        "void java.util.Collection#forEach(java.util.function.Consumer)"
-      ],
       "rewrite_prefix": {
         "java.util.stream.DesugarCollectors": "j$.util.stream.DesugarCollectors"
       },
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 35d22bb..d7cbd04 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
 import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.InternalOptions.MappingComposeOptions;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -636,7 +637,8 @@
         !internal.debug && proguardMapConsumer != null
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
-
+    MappingComposeOptions mappingComposeOptions = internal.mappingComposeOptions();
+    mappingComposeOptions.enableExperimentalMappingComposition = true;
     // Assert and fixup defaults.
     assert !internal.isShrinking();
     assert !internal.isMinifying();
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 88f233f..70dc6f3 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -102,7 +102,6 @@
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
-import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.SelfRetraceTest;
@@ -409,24 +408,16 @@
           }
 
           TreePruner pruner = new TreePruner(appViewWithLiveness);
-          DirectMappedDexApplication prunedApp = pruner.run(executorService);
+          PrunedItems prunedItems = pruner.run(executorService, timing);
 
           // Recompute the subtyping information.
-          Set<DexType> removedClasses = pruner.getRemovedClasses();
-          appView.pruneItems(
-              PrunedItems.builder()
-                  .setPrunedApp(prunedApp)
-                  .addRemovedClasses(removedClasses)
-                  .addAdditionalPinnedItems(pruner.getMethodsToKeepForConfigurationDebugging())
-                  .build(),
-              executorService);
+          appView.pruneItems(prunedItems, executorService);
           new AbstractMethodRemover(
                   appViewWithLiveness, appViewWithLiveness.appInfo().computeSubtypingInfo())
               .run();
 
           AnnotationRemover annotationRemover =
-              annotationRemoverBuilder
-                  .build(appViewWithLiveness, removedClasses);
+              annotationRemoverBuilder.build(appViewWithLiveness, prunedItems.getRemovedClasses());
           annotationRemover.ensureValid().run(executorService);
           new GenericSignatureRewriter(appView, genericContextBuilder)
               .run(appView.appInfo().classes(), executorService);
@@ -608,21 +599,14 @@
                 GenericSignatureContextBuilder.create(appView);
 
             TreePruner pruner = new TreePruner(appViewWithLiveness, treePrunerConfiguration);
-            DirectMappedDexApplication application = pruner.run(executorService);
-            Set<DexType> removedClasses = pruner.getRemovedClasses();
+            PrunedItems prunedItems = pruner.run(executorService, timing, prunedTypes);
 
             if (options.usageInformationConsumer != null) {
               ExceptionUtils.withFinishedResourceHandler(
                   options.reporter, options.usageInformationConsumer);
             }
 
-            appView.pruneItems(
-                PrunedItems.builder()
-                    .setPrunedApp(application)
-                    .addRemovedClasses(CollectionUtils.mergeSets(prunedTypes, removedClasses))
-                    .addAdditionalPinnedItems(pruner.getMethodsToKeepForConfigurationDebugging())
-                    .build(),
-                executorService);
+            appView.pruneItems(prunedItems, executorService);
 
             new BridgeHoisting(appViewWithLiveness).run();
 
@@ -646,7 +630,7 @@
 
             // Remove annotations that refer to types that no longer exist.
             AnnotationRemover.builder(Mode.FINAL_TREE_SHAKING)
-                .build(appView.withLiveness(), removedClasses)
+                .build(appView.withLiveness(), prunedItems.getRemovedClasses())
                 .run(executorService);
             new GenericSignatureRewriter(appView, genericContextBuilder)
                 .run(appView.appInfo().classes(), executorService);
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java
index 826b8dc..77f653a 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.debuginfo.DebugRepresentation;
 import com.android.tools.r8.dex.FileWriter.ByteBufferResult;
 import com.android.tools.r8.dex.FileWriter.DexContainerSection;
+import com.android.tools.r8.dex.FileWriter.MapItem;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
@@ -176,31 +177,31 @@
       DexOutputBuffer dexOutputBuffer, List<DexContainerSection> sections) {
     // The last section has the shared string_ids table. Now it is written the final size and
     // offset is known and the remaining sections can be updated to point to the shared table.
-    // This updates both the size and offset in the header and in the map.
     DexContainerSection lastSection = ListUtils.last(sections);
     int stringIdsSize = lastSection.getFileWriter().getMixedSectionOffsets().getStringData().size();
     int stringIdsOffset = lastSection.getLayout().stringIdsOffset;
     for (DexContainerSection section : sections) {
       if (section != lastSection) {
+        // Update the string_ids size and offset in the header.
         dexOutputBuffer.moveTo(section.getLayout().headerOffset + Constants.STRING_IDS_SIZE_OFFSET);
         dexOutputBuffer.putInt(stringIdsSize);
         dexOutputBuffer.putInt(stringIdsOffset);
+        // Write the map. The map is sorted by offset, so write all entries after setting
+        // string_ids and sorting.
         dexOutputBuffer.moveTo(section.getLayout().getMapOffset());
-        // Skip size.
-        dexOutputBuffer.getInt();
-        while (dexOutputBuffer.position() < section.getLayout().getEndOfFile()) {
-          int sectionType = dexOutputBuffer.getShort();
-          dexOutputBuffer.getShort(); // Skip unused.
-          if (sectionType == Constants.TYPE_STRING_ID_ITEM) {
-            dexOutputBuffer.putInt(stringIdsSize);
-            dexOutputBuffer.putInt(stringIdsOffset);
-            break;
-          } else {
-            // Skip size and offset for this type.
-            dexOutputBuffer.getInt();
-            dexOutputBuffer.getInt();
-          }
+        List<MapItem> mapItems =
+            section
+                .getLayout()
+                .generateMapInfo(section.getFileWriter(), stringIdsSize, stringIdsOffset);
+        int originalSize = dexOutputBuffer.getInt();
+        int size = 0;
+        for (MapItem mapItem : mapItems) {
+          size += mapItem.write(dexOutputBuffer);
         }
+        assert originalSize == size;
+        // Calculate signature and checksum after the map is written.
+        section.getFileWriter().writeSignature(section.getLayout(), dexOutputBuffer);
+        section.getFileWriter().writeChecksum(section.getLayout(), dexOutputBuffer);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 9ddc2d2..fc9c037 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -66,6 +66,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -292,8 +293,7 @@
         this::writeAnnotationDirectory,
         4);
 
-    // Add the map at the end
-    layout.setMapOffset(dest.align(4));
+    // Add the map at the end.
     writeMap(layout);
     layout.setEndOfFile(dest.position());
 
@@ -315,8 +315,10 @@
 
     // Fill in the header information.
     writeHeader(layout);
-    writeSignature(layout);
-    writeChecksum(layout);
+    if (includeStringData) {
+      writeSignature(layout);
+      writeChecksum(layout);
+    }
 
     // Wrap backing buffer with actual length.
     return new DexContainerSection(this, dest, layout);
@@ -777,70 +779,15 @@
     }
   }
 
-  private int writeMapItem(int type, int offset, int length) {
-    if (length == 0) {
-      return 0;
-    }
-    if (Log.ENABLED) {
-      Log.debug(getClass(), "Map entry 0x%04x @ 0x%08x # %08d.", type, offset, length);
-    }
-    dest.putShort((short) type);
-    dest.putShort((short) 0);
-    dest.putInt(length);
-    dest.putInt(offset);
-    return 1;
-  }
-
-  private void writeMap(Layout layout) {
+  public void writeMap(Layout layout) {
     int startOfMap = dest.align(4);
+    layout.setMapOffset(startOfMap);
     dest.forward(4); // Leave space for size;
+    List<MapItem> mapItems = layout.generateMapInfo(this);
     int size = 0;
-    size += writeMapItem(Constants.TYPE_HEADER_ITEM, 0, 1);
-    size += writeMapItem(Constants.TYPE_STRING_ID_ITEM, layout.stringIdsOffset,
-        mapping.getStrings().size());
-    size += writeMapItem(Constants.TYPE_TYPE_ID_ITEM, layout.typeIdsOffset,
-        mapping.getTypes().size());
-    size += writeMapItem(Constants.TYPE_PROTO_ID_ITEM, layout.protoIdsOffset,
-        mapping.getProtos().size());
-    size += writeMapItem(Constants.TYPE_FIELD_ID_ITEM, layout.fieldIdsOffset,
-        mapping.getFields().size());
-    size += writeMapItem(Constants.TYPE_METHOD_ID_ITEM, layout.methodIdsOffset,
-        mapping.getMethods().size());
-    size += writeMapItem(Constants.TYPE_CLASS_DEF_ITEM, layout.classDefsOffset,
-        mapping.getClasses().length);
-    size += writeMapItem(Constants.TYPE_CALL_SITE_ID_ITEM, layout.callSiteIdsOffset,
-        mapping.getCallSites().size());
-    size += writeMapItem(Constants.TYPE_METHOD_HANDLE_ITEM, layout.methodHandleIdsOffset,
-        mapping.getMethodHandles().size());
-    size += writeMapItem(Constants.TYPE_CODE_ITEM, layout.getCodesOffset(),
-        mixedSectionOffsets.getCodes().size());
-    size += writeMapItem(Constants.TYPE_DEBUG_INFO_ITEM, layout.getDebugInfosOffset(),
-        mixedSectionOffsets.getDebugInfos().size());
-    size += writeMapItem(Constants.TYPE_TYPE_LIST, layout.getTypeListsOffset(),
-        mixedSectionOffsets.getTypeLists().size());
-    size +=
-        writeMapItem(
-            Constants.TYPE_STRING_DATA_ITEM,
-            layout.getStringDataOffsets(),
-            layout.getStringDataOffsets() == 0 ? 0 : mixedSectionOffsets.getStringData().size());
-    size += writeMapItem(Constants.TYPE_ANNOTATION_ITEM, layout.getAnnotationsOffset(),
-        mixedSectionOffsets.getAnnotations().size());
-    size += writeMapItem(Constants.TYPE_CLASS_DATA_ITEM, layout.getClassDataOffset(),
-        mixedSectionOffsets.getClassesWithData().size());
-    size +=
-        writeMapItem(
-            Constants.TYPE_ENCODED_ARRAY_ITEM,
-            layout.getEncodedArraysOffset(),
-            mixedSectionOffsets.getEncodedArrays().size());
-    size += writeMapItem(Constants.TYPE_ANNOTATION_SET_ITEM, layout.getAnnotationSetsOffset(),
-        mixedSectionOffsets.getAnnotationSets().size());
-    size += writeMapItem(Constants.TYPE_ANNOTATION_SET_REF_LIST,
-        layout.getAnnotationSetRefListsOffset(),
-        mixedSectionOffsets.getAnnotationSetRefLists().size());
-    size += writeMapItem(Constants.TYPE_ANNOTATIONS_DIRECTORY_ITEM,
-        layout.getAnnotationDirectoriesOffset(),
-        mixedSectionOffsets.getAnnotationDirectories().size());
-    size += writeMapItem(Constants.TYPE_MAP_LIST, layout.getMapOffset(), 1);
+    for (MapItem mapItem : mapItems) {
+      size += includeStringData ? mapItem.write(dest) : mapItem.size();
+    }
     dest.moveTo(startOfMap);
     dest.putInt(size);
     dest.forward(size * Constants.TYPE_MAP_LIST_ITEM_SIZE);
@@ -892,26 +839,34 @@
   }
 
   private void writeSignature(Layout layout) {
+    writeSignature(layout, dest);
+  }
+
+  public void writeSignature(Layout layout, DexOutputBuffer dexOutputBuffer) {
     try {
       MessageDigest md = MessageDigest.getInstance("SHA-1");
       md.update(
-          dest.asArray(),
+          dexOutputBuffer.asArray(),
           layout.headerOffset + Constants.FILE_SIZE_OFFSET,
           layout.getEndOfFile() - layout.headerOffset - Constants.FILE_SIZE_OFFSET);
-      md.digest(dest.asArray(), layout.headerOffset + Constants.SIGNATURE_OFFSET, 20);
+      md.digest(dexOutputBuffer.asArray(), layout.headerOffset + Constants.SIGNATURE_OFFSET, 20);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
   }
 
   private void writeChecksum(Layout layout) {
+    writeChecksum(layout, dest);
+  }
+
+  public void writeChecksum(Layout layout, DexOutputBuffer dexOutputBuffer) {
     Adler32 adler = new Adler32();
     adler.update(
-        dest.asArray(),
+        dexOutputBuffer.asArray(),
         layout.headerOffset + Constants.SIGNATURE_OFFSET,
         layout.getEndOfFile() - layout.headerOffset - Constants.SIGNATURE_OFFSET);
-    dest.moveTo(layout.headerOffset + Constants.CHECKSUM_OFFSET);
-    dest.putInt((int) adler.getValue());
+    dexOutputBuffer.moveTo(layout.headerOffset + Constants.CHECKSUM_OFFSET);
+    dexOutputBuffer.putInt((int) adler.getValue());
   }
 
   private static int alignSize(int bytes, int value) {
@@ -919,6 +874,48 @@
     return (value + mask) & ~mask;
   }
 
+  public static class MapItem {
+    final int type;
+    final int offset;
+    final int length;
+
+    public MapItem(int type, int offset, int size) {
+      this.type = type;
+      this.offset = offset;
+      this.length = size;
+    }
+
+    public int getType() {
+      return type;
+    }
+
+    public int getOffset() {
+      return offset;
+    }
+
+    public int getLength() {
+      return length;
+    }
+
+    public int write(DexOutputBuffer dest) {
+      if (length == 0) {
+        return 0;
+      }
+      if (Log.ENABLED) {
+        Log.debug(getClass(), "Map entry 0x%04x @ 0x%08x # %08d.", type, offset, length);
+      }
+      dest.putShort((short) type);
+      dest.putShort((short) 0);
+      dest.putInt(length);
+      dest.putInt(offset);
+      return 1;
+    }
+
+    public int size() {
+      return length == 0 ? 0 : 1;
+    }
+  }
+
   public static class Layout {
 
     private static final int NOT_SET = -1;
@@ -1124,6 +1121,104 @@
       this.mapOffset = mapOffset;
     }
 
+    public List<MapItem> generateMapInfo(FileWriter fileWriter) {
+      return generateMapInfo(
+          fileWriter, fileWriter.mixedSectionOffsets.getStringData().size(), stringIdsOffset);
+    }
+
+    public List<MapItem> generateMapInfo(
+        FileWriter fileWriter, int stringIdsSize, int stringIdsOffset) {
+      List<MapItem> mapItems = new ArrayList<>();
+      mapItems.add(new MapItem(Constants.TYPE_HEADER_ITEM, 0, 1));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_STRING_ID_ITEM,
+              stringIdsOffset,
+              fileWriter.mapping.getStrings().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_TYPE_ID_ITEM, typeIdsOffset, fileWriter.mapping.getTypes().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_PROTO_ID_ITEM, protoIdsOffset, fileWriter.mapping.getProtos().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_FIELD_ID_ITEM, fieldIdsOffset, fileWriter.mapping.getFields().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_METHOD_ID_ITEM,
+              methodIdsOffset,
+              fileWriter.mapping.getMethods().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_CLASS_DEF_ITEM,
+              classDefsOffset,
+              fileWriter.mapping.getClasses().length));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_CALL_SITE_ID_ITEM,
+              callSiteIdsOffset,
+              fileWriter.mapping.getCallSites().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_METHOD_HANDLE_ITEM,
+              methodHandleIdsOffset,
+              fileWriter.mapping.getMethodHandles().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_CODE_ITEM,
+              getCodesOffset(),
+              fileWriter.mixedSectionOffsets.getCodes().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_DEBUG_INFO_ITEM,
+              getDebugInfosOffset(),
+              fileWriter.mixedSectionOffsets.getDebugInfos().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_TYPE_LIST,
+              getTypeListsOffset(),
+              fileWriter.mixedSectionOffsets.getTypeLists().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_STRING_DATA_ITEM,
+              getStringDataOffsets(),
+              getStringDataOffsets() == 0 ? 0 : stringIdsSize));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_ANNOTATION_ITEM,
+              getAnnotationsOffset(),
+              fileWriter.mixedSectionOffsets.getAnnotations().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_CLASS_DATA_ITEM,
+              getClassDataOffset(),
+              fileWriter.mixedSectionOffsets.getClassesWithData().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_ENCODED_ARRAY_ITEM,
+              getEncodedArraysOffset(),
+              fileWriter.mixedSectionOffsets.getEncodedArrays().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_ANNOTATION_SET_ITEM,
+              getAnnotationSetsOffset(),
+              fileWriter.mixedSectionOffsets.getAnnotationSets().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_ANNOTATION_SET_REF_LIST,
+              getAnnotationSetRefListsOffset(),
+              fileWriter.mixedSectionOffsets.getAnnotationSetRefLists().size()));
+      mapItems.add(
+          new MapItem(
+              Constants.TYPE_ANNOTATIONS_DIRECTORY_ITEM,
+              getAnnotationDirectoriesOffset(),
+              fileWriter.mixedSectionOffsets.getAnnotationDirectories().size()));
+      mapItems.add(new MapItem(Constants.TYPE_MAP_LIST, getMapOffset(), 1));
+      mapItems.sort(Comparator.comparingInt(MapItem::getOffset));
+      return mapItems;
+    }
+
     public int getEndOfFile() {
       return endOfFile;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/PrunedItems.java b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
index 68c1dde..f49fcbd 100644
--- a/src/main/java/com/android/tools/r8/graph/PrunedItems.java
+++ b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
@@ -161,11 +161,21 @@
       return this;
     }
 
+    public Builder addRemovedFields(Collection<DexField> removedFields) {
+      this.removedFields.addAll(removedFields);
+      return this;
+    }
+
     public Builder addRemovedMethod(DexMethod removedMethod) {
       removedMethods.add(removedMethod);
       return this;
     }
 
+    public Builder addRemovedMethods(Collection<DexMethod> removedMethods) {
+      this.removedMethods.addAll(removedMethods);
+      return this;
+    }
+
     public Builder setRemovedClasses(Set<DexType> removedClasses) {
       this.removedClasses = removedClasses;
       return this;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateLintFiles.java
index 752964b..18ab413 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateLintFiles.java
@@ -459,6 +459,23 @@
       return this;
     }
 
+    StringBuilderWithIndent appendLineStart(String lineStart) {
+      builder.append(indent);
+      builder.append(lineStart);
+      return this;
+    }
+
+    StringBuilderWithIndent append(String string) {
+      builder.append(string);
+      return this;
+    }
+
+    StringBuilderWithIndent appendLineEnd(String lineEnd) {
+      builder.append(lineEnd);
+      builder.append(NL);
+      return this;
+    }
+
     StringBuilderWithIndent appendLine(String line) {
       builder.append(indent);
       builder.append(line);
@@ -668,8 +685,21 @@
       indent(indent);
     }
 
-    HTMLBuilder appendTdCode(String s) {
-      appendLine("<td><code>" + s + "</code></td>");
+    HTMLBuilder appendTdPackage(String s) {
+      appendLineStart("<td><code><em>" + s + "</em></code><br>");
+      if (s.startsWith("java.time")) {
+        append("<a href=\"#java-time-customizations\">See customizations</a><br");
+      } else if (s.startsWith("java.nio")) {
+        append("<a href=\"#java-nio-customizations\">See customizations</a><br");
+      }
+      return this;
+    }
+
+    HTMLBuilder appendTdClassName(String s) {
+      appendLineEnd(
+          "<code><br><br><div style=\"font-size:small;font-weight:bold;\">&nbsp;"
+              + s
+              + "</div></code><br><br></td>");
       return this;
     }
 
@@ -679,7 +709,7 @@
     }
 
     HTMLBuilder appendLiCode(String s) {
-      appendLine("<li><code>" + s + "</code></li>");
+      appendLine("<li class=\"java8_table\"><code>" + s + "</code></li>");
       return this;
     }
 
@@ -715,10 +745,14 @@
       HTMLBuilder builder = new HTMLBuilder();
       builder.start("tr");
       if (packageName.length() > 0) {
-        builder.appendTdCode(packageName);
+        builder.appendTdPackage(packageName);
       }
-      builder.appendTdCode(typeInPackage(className));
-      builder.start("td").start("ul");
+      builder.appendTdClassName(typeInPackage(className));
+      builder
+          .start("td")
+          .start(
+              "ul style=\"list-style-position:inside; list-style-type: none !important;"
+                  + " margin-left:0px;padding-left:0px !important;\"");
       if (!fields.isEmpty()) {
         assert newClass; // Currently no fields are added to existing classes.
         for (DexEncodedField field : fields) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/invokespecial/InvokeSpecialToSelfDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/invokespecial/InvokeSpecialToSelfDesugaring.java
index 428bdd0..310856f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/invokespecial/InvokeSpecialToSelfDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/invokespecial/InvokeSpecialToSelfDesugaring.java
@@ -28,7 +28,7 @@
 /** This class defines the desugaring of a single invoke-special instruction. */
 public class InvokeSpecialToSelfDesugaring implements CfInstructionDesugaring {
 
-  private static final String INVOKE_SPECIAL_BRIDGE_PREFIX = "$invoke$special$";
+  public static final String INVOKE_SPECIAL_BRIDGE_PREFIX = "$invoke$special$";
 
   private final DexItemFactory dexItemFactory;
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
index 91da4d9..42f22a6 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.desugar.itf;
 
-import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.InterfaceMethodDesugaringMode.ALL;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.InterfaceMethodDesugaringMode.EMULATED_INTERFACE_ONLY;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.InterfaceMethodDesugaringMode.NONE;
 
@@ -388,8 +387,6 @@
     this.dexItemFactory = appView.dexItemFactory();
     this.helper = new InterfaceDesugaringSyntheticHelper(appView);
     assert desugaringMode != NONE;
-    assert desugaringMode == ALL
-        || !appView.options().machineDesugaredLibrarySpecification.isEmpty();
     needsLibraryInfo = !appView.options().machineDesugaredLibrarySpecification.isEmpty();
     this.isLiveMethod = isLiveMethod;
     this.desugaringMode = desugaringMode;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index e91925d..4310355 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -215,6 +215,9 @@
    */
   @Override
   public void scan(ProgramMethod context, CfInstructionDesugaringEventConsumer eventConsumer) {
+    if (desugaringMode == EMULATED_INTERFACE_ONLY) {
+      return;
+    }
     if (isSyntheticMethodThatShouldNotBeDoubleProcessed(context)) {
       leavingStaticInvokeToInterface(context);
       return;
@@ -274,6 +277,9 @@
     if (invoke.isInvokeSpecial() && invoke.isInvokeConstructor(factory)) {
       return DesugarDescription.nothing();
     }
+    if (desugaringMode == EMULATED_INTERFACE_ONLY) {
+      return computeInvokeForEmulatedInterfaceOnly(invoke, context);
+    }
     // If the invoke is not an interface invoke, then there should generally not be any desugaring.
     // However, there are some cases where the insertion of forwarding methods can change behavior
     // so we need to identify them at the various call sites here.
@@ -302,9 +308,6 @@
     if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
       return computeInvokeVirtualDispatch(holder, invoke, context);
     }
-    if (desugaringMode == EMULATED_INTERFACE_ONLY) {
-      return DesugarDescription.nothing();
-    }
     if (invoke.isInvokeStatic()) {
       return computeInvokeStatic(holder, invoke, context);
     }
@@ -314,6 +317,51 @@
     return DesugarDescription.nothing();
   }
 
+  private DesugarDescription computeInvokeForEmulatedInterfaceOnly(
+      CfInvoke invoke, ProgramMethod context) {
+    if (!invoke.getMethod().getHolderType().isClassType()) {
+      return DesugarDescription.nothing();
+    }
+    DexClass holder = appView.definitionForHolder(invoke.getMethod(), context);
+    if (holder == null) {
+      return DesugarDescription.nothing();
+    }
+
+    if (!invoke.isInterface()) {
+      if (invoke.isInvokeSpecial()) {
+        return computeEmulatedInterfaceInvokeSpecial(holder, invoke.getMethod(), context);
+      }
+      if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
+        DesugarDescription description = computeEmulatedInterfaceVirtualDispatchOrNull(invoke);
+        return description == null ? DesugarDescription.nothing() : description;
+      }
+      return DesugarDescription.nothing();
+    }
+
+    if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
+      AppInfoWithClassHierarchy appInfoForDesugaring = appView.appInfoForDesugaring();
+      SingleResolutionResult<?> resolution =
+          appInfoForDesugaring
+              .resolveMethodLegacy(invoke.getMethod(), invoke.isInterface())
+              .asSingleResolution();
+      if (resolution != null
+          && resolution.getResolvedMethod().isPrivate()
+          && resolution.isAccessibleFrom(context, appInfoForDesugaring).isTrue()) {
+        return DesugarDescription.nothing();
+      }
+      if (resolution != null && resolution.getResolvedMethod().isStatic()) {
+        return DesugarDescription.nothing();
+      }
+      DesugarDescription description = computeEmulatedInterfaceVirtualDispatchOrNull(invoke);
+      return description != null ? description : DesugarDescription.nothing();
+    }
+
+    // Note: We do not rewrite invoke-static to emulated interfaces above api 24 and there is
+    // no way to encode it, but we could add the support here.
+
+    return DesugarDescription.nothing();
+  }
+
   private DesugarDescription computeNonInterfaceInvoke(CfInvoke invoke, ProgramMethod context) {
     assert !invoke.isInterface();
     // Emulated interface desugaring will rewrite non-interface invokes.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/nest/NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/nest/NestBasedAccessDesugaring.java
index 1b3e48e..ad59051 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/nest/NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/nest/NestBasedAccessDesugaring.java
@@ -56,14 +56,13 @@
 
   // Short names to avoid creating long strings
   public static final String NEST_ACCESS_NAME_PREFIX = "-$$Nest$";
-  private static final String NEST_ACCESS_METHOD_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "m";
-  private static final String NEST_ACCESS_STATIC_METHOD_NAME_PREFIX =
-      NEST_ACCESS_NAME_PREFIX + "sm";
-  private static final String NEST_ACCESS_FIELD_GET_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "fget";
-  private static final String NEST_ACCESS_STATIC_GET_FIELD_NAME_PREFIX =
+  public static final String NEST_ACCESS_METHOD_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "m";
+  public static final String NEST_ACCESS_STATIC_METHOD_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "sm";
+  public static final String NEST_ACCESS_FIELD_GET_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "fget";
+  public static final String NEST_ACCESS_STATIC_GET_FIELD_NAME_PREFIX =
       NEST_ACCESS_NAME_PREFIX + "sfget";
-  private static final String NEST_ACCESS_FIELD_PUT_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "fput";
-  private static final String NEST_ACCESS_STATIC_PUT_FIELD_NAME_PREFIX =
+  public static final String NEST_ACCESS_FIELD_PUT_NAME_PREFIX = NEST_ACCESS_NAME_PREFIX + "fput";
+  public static final String NEST_ACCESS_STATIC_PUT_FIELD_NAME_PREFIX =
       NEST_ACCESS_NAME_PREFIX + "sfput";
 
   protected final AppView<?> appView;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
index 652c185..78eb343 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
@@ -23,13 +23,6 @@
 // *) in vertical class merger,
 //   before moving a default interface method to its subtype, check if it does not collide with one
 //   in the given class hierarchy.
-// *) in uninstantiated type optimizer,
-//   to avoid signature collisions while discarding unused return type or parameters.
-// *) in unused argument removal,
-//   to avoid removing unused arguments from a virtual method if it is overriding another method or
-//   being overridden by a method in a subtype, and to check that a virtual method after unused
-//   argument removal does not collide with one in the existing class hierarchy.
-// TODO(b/66369976): to determine if a certain method can be made `final`.
 public class MethodPoolCollection extends MemberPoolCollection<DexMethod> {
 
   private final Predicate<DexEncodedMethod> methodTester;
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
index 810f2de..81d408f 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
@@ -4,9 +4,11 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getCompatibleKotlinInfo;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.toJvmFieldSignature;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.toJvmMethodSignature;
 import static com.android.tools.r8.utils.FunctionUtils.forEachApply;
+import static kotlinx.metadata.jvm.KotlinClassMetadata.Companion;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -17,6 +19,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
@@ -26,13 +29,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
+import kotlin.Metadata;
 import kotlinx.metadata.KmClass;
 import kotlinx.metadata.KmConstructor;
 import kotlinx.metadata.KmType;
 import kotlinx.metadata.jvm.JvmClassExtensionVisitor;
 import kotlinx.metadata.jvm.JvmExtensionsKt;
 import kotlinx.metadata.jvm.JvmMethodSignature;
-import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
 
 public class KotlinClassInfo implements KotlinClassLevelInfo {
@@ -57,6 +60,8 @@
   private final KotlinTypeInfo inlineClassUnderlyingType;
   private final int jvmFlags;
   private final String companionObjectName;
+  // Collection of context receiver types
+  private final List<KotlinTypeInfo> contextReceiverTypes;
 
   // List of tracked assignments of kotlin metadata.
   private final KotlinMetadataMembersTracker originalMembersWithKotlinInfo;
@@ -82,7 +87,8 @@
       KotlinTypeInfo inlineClassUnderlyingType,
       KotlinMetadataMembersTracker originalMembersWithKotlinInfo,
       int jvmFlags,
-      String companionObjectName) {
+      String companionObjectName,
+      List<KotlinTypeInfo> contextReceiverTypes) {
     this.flags = flags;
     this.name = name;
     this.nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin =
@@ -105,6 +111,7 @@
     this.originalMembersWithKotlinInfo = originalMembersWithKotlinInfo;
     this.jvmFlags = jvmFlags;
     this.companionObjectName = companionObjectName;
+    this.contextReceiverTypes = contextReceiverTypes;
   }
 
   public static KotlinClassInfo create(
@@ -188,7 +195,10 @@
         KotlinTypeInfo.create(kmClass.getInlineClassUnderlyingType(), factory, reporter),
         originalMembersWithKotlinInfo,
         JvmExtensionsKt.getJvmFlags(kmClass),
-        setCompanionObject(kmClass, hostClass, reporter));
+        setCompanionObject(kmClass, hostClass, reporter),
+        ListUtils.map(
+            kmClass.getContextReceiverTypes(),
+            contextRecieverType -> KotlinTypeInfo.create(contextRecieverType, factory, reporter)));
   }
 
   private static KotlinTypeReference getAnonymousObjectOrigin(
@@ -279,7 +289,7 @@
   }
 
   @Override
-  public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+  public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
     KmClass kmClass = new KmClass();
     // TODO(b/154348683): Set flags.
     kmClass.setFlags(flags);
@@ -414,6 +424,9 @@
       rewritten |=
           inlineClassUnderlyingType.rewrite(kmClass::visitInlineClassUnderlyingType, appView);
     }
+    for (KotlinTypeInfo contextReceiverType : contextReceiverTypes) {
+      rewritten |= contextReceiverType.rewrite(kmClass::visitContextReceiverType, appView);
+    }
     JvmClassExtensionVisitor extensionVisitor =
         (JvmClassExtensionVisitor) kmClass.visitExtensions(JvmClassExtensionVisitor.TYPE);
     extensionVisitor.visitJvmFlags(jvmFlags);
@@ -432,10 +445,8 @@
     rewritten |=
         localDelegatedProperties.rewrite(extensionVisitor::visitLocalDelegatedProperty, appView);
     extensionVisitor.visitEnd();
-    KotlinClassMetadata.Class.Writer writer = new KotlinClassMetadata.Class.Writer();
-    kmClass.accept(writer);
     return Pair.create(
-        writer.write().getHeader(),
+        Companion.writeClass(kmClass, getCompatibleKotlinInfo(), 0).getAnnotationData(),
         rewritten || !originalMembersWithKotlinInfo.isEqual(rewrittenReferences, appView));
   }
 
@@ -457,6 +468,7 @@
     forEachApply(superTypes, type -> type::trace, definitionSupplier);
     forEachApply(sealedSubClasses, sealedClass -> sealedClass::trace, definitionSupplier);
     forEachApply(nestedClasses, nested -> nested::trace, definitionSupplier);
+    forEachApply(contextReceiverTypes, nested -> nested::trace, definitionSupplier);
     localDelegatedProperties.trace(definitionSupplier);
     // TODO(b/154347404): trace enum entries.
     if (anonymousObjectOrigin != null) {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassLevelInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassLevelInfo.java
index e8f7fd0..d634e88 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassLevelInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassLevelInfo.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.shaking.EnqueuerMetadataTraceable;
 import com.android.tools.r8.utils.Pair;
-import kotlinx.metadata.jvm.KotlinClassHeader;
+import kotlin.Metadata;
 
 public interface KotlinClassLevelInfo extends EnqueuerMetadataTraceable {
 
@@ -56,7 +56,7 @@
     return null;
   }
 
-  Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView);
+  Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView);
 
   String getPackageName();
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
index eb07f4b..d02839a 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
@@ -21,6 +21,7 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.function.Consumer;
+import kotlin.Metadata;
 import kotlinx.metadata.InconsistentKotlinMetadataException;
 import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
@@ -142,8 +143,9 @@
       KotlinClassMetadata kMetadata,
       AppView<?> appView,
       Consumer<DexEncodedMethod> keepByteCode) {
-    String packageName = kMetadata.getHeader().getPackageName();
-    int[] metadataVersion = kMetadata.getHeader().getMetadataVersion();
+    Metadata annotationData = kMetadata.getAnnotationData();
+    String packageName = annotationData.pn();
+    int[] metadataVersion = annotationData.mv();
     if (kMetadata instanceof KotlinClassMetadata.Class) {
       return KotlinClassInfo.create(
           (KotlinClassMetadata.Class) kMetadata,
@@ -178,7 +180,7 @@
           kotlin,
           appView);
     } else {
-      throw new MetadataError("unsupported 'k' value: " + kMetadata.getHeader().getKind());
+      throw new MetadataError("unsupported 'k' value: " + annotationData.k());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinFileFacadeInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinFileFacadeInfo.java
index 3daa33d..59e482f 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinFileFacadeInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinFileFacadeInfo.java
@@ -4,15 +4,17 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getCompatibleKotlinInfo;
+import static kotlinx.metadata.jvm.KotlinClassMetadata.Companion;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.utils.Pair;
 import java.util.function.Consumer;
+import kotlin.Metadata;
 import kotlinx.metadata.KmPackage;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
 import kotlinx.metadata.jvm.KotlinClassMetadata.FileFacade;
 
 // Holds information about Metadata.FileFacade
@@ -57,12 +59,12 @@
   }
 
   @Override
-  public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+  public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
     KmPackage kmPackage = new KmPackage();
     boolean rewritten = packageInfo.rewrite(kmPackage, clazz, appView);
-    KotlinClassMetadata.FileFacade.Writer writer = new KotlinClassMetadata.FileFacade.Writer();
-    kmPackage.accept(writer);
-    return Pair.create(writer.write().getHeader(), rewritten);
+    return Pair.create(
+        Companion.writeFileFacade(kmPackage, getCompatibleKotlinInfo(), 0).getAnnotationData(),
+        rewritten);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinFunctionInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinFunctionInfo.java
index f5b40c2..69e4004 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinFunctionInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinFunctionInfo.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import java.util.List;
 import kotlinx.metadata.KmFunction;
@@ -41,6 +42,8 @@
   private final KotlinContractInfo contract;
   // A value describing if any of the parameters are crossinline.
   private final boolean crossInlineParameter;
+  // Collection of context receiver types
+  private final List<KotlinTypeInfo> contextReceiverTypes;
 
   private KotlinFunctionInfo(
       int flags,
@@ -53,7 +56,8 @@
       KotlinTypeReference lambdaClassOrigin,
       KotlinVersionRequirementInfo versionRequirements,
       KotlinContractInfo contract,
-      boolean crossInlineParameter) {
+      boolean crossInlineParameter,
+      List<KotlinTypeInfo> contextReceiverTypes) {
     this.flags = flags;
     this.name = name;
     this.returnType = returnType;
@@ -65,6 +69,7 @@
     this.versionRequirements = versionRequirements;
     this.contract = contract;
     this.crossInlineParameter = crossInlineParameter;
+    this.contextReceiverTypes = contextReceiverTypes;
   }
 
   public boolean hasCrossInlineParameter() {
@@ -98,7 +103,10 @@
         getlambdaClassOrigin(kmFunction, factory),
         KotlinVersionRequirementInfo.create(kmFunction.getVersionRequirements()),
         KotlinContractInfo.create(kmFunction.getContract(), factory, reporter),
-        isCrossInline);
+        isCrossInline,
+        ListUtils.map(
+            kmFunction.getContextReceiverTypes(),
+            contextRecieverType -> KotlinTypeInfo.create(contextRecieverType, factory, reporter)));
   }
 
   private static KotlinTypeReference getlambdaClassOrigin(
@@ -141,6 +149,9 @@
     for (KotlinTypeParameterInfo typeParameterInfo : typeParameters) {
       rewritten |= typeParameterInfo.rewrite(kmFunction::visitTypeParameter, appView);
     }
+    for (KotlinTypeInfo contextReceiverType : contextReceiverTypes) {
+      rewritten |= contextReceiverType.rewrite(kmFunction::visitContextReceiverType, appView);
+    }
     if (receiverParameterType != null) {
       rewritten |= receiverParameterType.rewrite(kmFunction::visitReceiverParameterType, appView);
     }
@@ -187,6 +198,7 @@
       receiverParameterType.trace(definitionSupplier);
     }
     forEachApply(typeParameters, param -> param::trace, definitionSupplier);
+    forEachApply(contextReceiverTypes, type -> type::trace, definitionSupplier);
     if (signature != null) {
       signature.trace(definitionSupplier);
     }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataAnnotationWrapper.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataAnnotationWrapper.java
new file mode 100644
index 0000000..a011e63
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataAnnotationWrapper.java
@@ -0,0 +1,41 @@
+// 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.kotlin;
+
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+/***
+ * This is a wrapper around kotlin.Metadata needed for tests to access the internal data. The need
+ * for the wrapper comes from R8 relocating kotlin.* to com.android.tools.r8.kotlin.* in R8lib but
+ * not in tests, so kotlin.Metadata cannot cross the boundary.
+ */
+public class KotlinMetadataAnnotationWrapper {
+
+  private final kotlin.Metadata metadata;
+
+  private KotlinMetadataAnnotationWrapper(kotlin.Metadata metadata) {
+    this.metadata = metadata;
+  }
+
+  public static KotlinMetadataAnnotationWrapper wrap(KotlinClassMetadata classMetadata) {
+    return new KotlinMetadataAnnotationWrapper(classMetadata.getAnnotationData());
+  }
+
+  public int kind() {
+    return metadata.k();
+  }
+
+  public String[] data1() {
+    return metadata.d1();
+  }
+
+  public String[] data2() {
+    return metadata.d2();
+  }
+
+  public String packageName() {
+    return metadata.pn();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
index c06aea8..e57fc63 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
@@ -27,7 +27,6 @@
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import kotlinx.metadata.jvm.KotlinClassHeader;
 
 public class KotlinMetadataRewriter {
 
@@ -155,17 +154,16 @@
       DexAnnotation oldMeta,
       WriteMetadataFieldInfo writeMetadataFieldInfo) {
     try {
-      Pair<KotlinClassHeader, Boolean> kotlinClassHeader = kotlinInfo.rewrite(clazz, appView);
+      Pair<kotlin.Metadata, Boolean> kotlinMetadata = kotlinInfo.rewrite(clazz, appView);
       // TODO(b/185756596): Remove when special handling is no longer needed.
-      if (!kotlinClassHeader.getSecond()
-          && appView.options().testing.keepMetadataInR8IfNotRewritten) {
+      if (!kotlinMetadata.getSecond() && appView.options().testing.keepMetadataInR8IfNotRewritten) {
         // No rewrite occurred and the data is the same as before.
         assert appView.checkForTesting(
             () ->
                 verifyRewrittenMetadataIsEquivalent(
                     clazz.annotations().getFirstMatching(factory.kotlinMetadataType),
                     createKotlinMetadataAnnotation(
-                        kotlinClassHeader.getFirst(),
+                        kotlinMetadata.getFirst(),
                         kotlinInfo.getPackageName(),
                         getMaxVersion(METADATA_VERSION_1_4, kotlinInfo.getMetadataVersion()),
                         writeMetadataFieldInfo)));
@@ -173,7 +171,7 @@
       }
       DexAnnotation newMeta =
           createKotlinMetadataAnnotation(
-              kotlinClassHeader.getFirst(),
+              kotlinMetadata.getFirst(),
               kotlinInfo.getPackageName(),
               getMaxVersion(METADATA_VERSION_1_4, kotlinInfo.getMetadataVersion()),
               writeMetadataFieldInfo);
@@ -222,7 +220,7 @@
   }
 
   private DexAnnotation createKotlinMetadataAnnotation(
-      KotlinClassHeader header,
+      kotlin.Metadata metadata,
       String packageName,
       int[] metadataVersion,
       WriteMetadataFieldInfo writeMetadataFieldInfo) {
@@ -234,31 +232,30 @@
     }
     if (writeMetadataFieldInfo.writeKind) {
       elements.add(
-          new DexAnnotationElement(kotlin.metadata.kind, DexValueInt.create(header.getKind())));
+          new DexAnnotationElement(kotlin.metadata.kind, DexValueInt.create(metadata.k())));
     }
     if (writeMetadataFieldInfo.writeData1) {
       elements.add(
-          new DexAnnotationElement(kotlin.metadata.data1, createStringArray(header.getData1())));
+          new DexAnnotationElement(kotlin.metadata.data1, createStringArray(metadata.d1())));
     }
     if (writeMetadataFieldInfo.writeData2) {
       elements.add(
-          new DexAnnotationElement(kotlin.metadata.data2, createStringArray(header.getData2())));
+          new DexAnnotationElement(kotlin.metadata.data2, createStringArray(metadata.d2())));
     }
     if (writeMetadataFieldInfo.writePackageName && packageName != null && !packageName.isEmpty()) {
       elements.add(
           new DexAnnotationElement(
               kotlin.metadata.packageName, new DexValueString(factory.createString(packageName))));
     }
-    if (writeMetadataFieldInfo.writeExtraString && !header.getExtraString().isEmpty()) {
+    if (writeMetadataFieldInfo.writeExtraString && !metadata.xs().isEmpty()) {
       elements.add(
           new DexAnnotationElement(
               kotlin.metadata.extraString,
-              new DexValueString(factory.createString(header.getExtraString()))));
+              new DexValueString(factory.createString(metadata.xs()))));
     }
-    if (writeMetadataFieldInfo.writeExtraInt && header.getExtraInt() != 0) {
+    if (writeMetadataFieldInfo.writeExtraInt && metadata.xi() != 0) {
       elements.add(
-          new DexAnnotationElement(
-              kotlin.metadata.extraInt, DexValueInt.create(header.getExtraInt())));
+          new DexAnnotationElement(kotlin.metadata.extraInt, DexValueInt.create(metadata.xi())));
     }
     DexEncodedAnnotation encodedAnnotation =
         new DexEncodedAnnotation(
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
index d5884f5..ccc4938 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.Pair;
 import com.google.common.base.Strings;
+import kotlin.Metadata;
 import kotlinx.metadata.KmExtensionType;
 import kotlinx.metadata.KmProperty;
 import kotlinx.metadata.KmPropertyExtensionVisitor;
@@ -28,7 +29,7 @@
 import kotlinx.metadata.jvm.JvmFieldSignature;
 import kotlinx.metadata.jvm.JvmMethodSignature;
 import kotlinx.metadata.jvm.JvmPropertyExtensionVisitor;
-import kotlinx.metadata.jvm.KotlinClassHeader;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
 
 public class KotlinMetadataUtils {
 
@@ -50,7 +51,7 @@
     }
 
     @Override
-    public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+    public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
       throw new Unreachable("Should never be called");
     }
 
@@ -232,4 +233,8 @@
     }
     return DescriptorUtils.descriptorToKotlinClassifier(descriptor);
   }
+
+  static int[] getCompatibleKotlinInfo() {
+    return KotlinClassMetadata.COMPATIBLE_METADATA_VERSION;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
index 1e4d633..dfb5d76 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
@@ -379,20 +379,31 @@
         indent,
         "constructors",
         sb,
-        newIndent -> {
-          appendKmList(
-              newIndent,
-              "KmConstructor",
-              sb,
-              kmClass.getConstructors().stream()
-                  .sorted(
-                      Comparator.comparing(
-                          kmConstructor -> JvmExtensionsKt.getSignature(kmConstructor).asString()))
-                  .collect(Collectors.toList()),
-              (nextIndent, constructor) -> {
-                appendKmConstructor(nextIndent, sb, constructor);
-              });
-        });
+        newIndent ->
+            appendKmList(
+                newIndent,
+                "KmConstructor",
+                sb,
+                kmClass.getConstructors().stream()
+                    .sorted(
+                        Comparator.comparing(
+                            kmConstructor ->
+                                JvmExtensionsKt.getSignature(kmConstructor).asString()))
+                    .collect(Collectors.toList()),
+                (nextIndent, constructor) -> {
+                  appendKmConstructor(nextIndent, sb, constructor);
+                }));
+    appendKeyValue(
+        indent,
+        "contextReceiverTypes",
+        sb,
+        newIndent ->
+            appendKmList(
+                newIndent,
+                "KmType",
+                sb,
+                kmClass.getContextReceiverTypes(),
+                (nextIndent, kmType) -> appendKmType(nextIndent, sb, kmType)));
     appendKmDeclarationContainer(indent, sb, kmClass);
   }
 
@@ -458,6 +469,17 @@
                   appendKmContract(nextIndent, sb, contract);
                 });
           }
+          appendKeyValue(
+              newIndent,
+              "contextReceiverTypes",
+              sb,
+              newNewIndent ->
+                  appendKmList(
+                      newNewIndent,
+                      "KmType",
+                      sb,
+                      function.getContextReceiverTypes(),
+                      (nextIndent, kmType) -> appendKmType(nextIndent, sb, kmType)));
           JvmMethodSignature signature = JvmExtensionsKt.getSignature(function);
           appendKeyValue(
               newIndent, "signature", sb, signature != null ? signature.asString() : "null");
@@ -500,6 +522,17 @@
               sb,
               nextIndent -> appendValueParameter(nextIndent, sb, kmProperty.getSetterParameter()));
           appendKmVersionRequirement(newIndent, sb, kmProperty.getVersionRequirements());
+          appendKeyValue(
+              newIndent,
+              "contextReceiverTypes",
+              sb,
+              newNewIndent ->
+                  appendKmList(
+                      newNewIndent,
+                      "KmType",
+                      sb,
+                      kmProperty.getContextReceiverTypes(),
+                      (nextIndent, kmType) -> appendKmType(nextIndent, sb, kmType)));
           appendKeyValue(newIndent, "jvmFlags", sb, JvmExtensionsKt.getJvmFlags(kmProperty) + "");
           JvmFieldSignature fieldSignature = JvmExtensionsKt.getFieldSignature(kmProperty);
           appendKeyValue(
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassFacadeInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassFacadeInfo.java
index 2a89998..7f0b67f 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassFacadeInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassFacadeInfo.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getCompatibleKotlinInfo;
 import static com.android.tools.r8.utils.FunctionUtils.forEachApply;
+import static kotlinx.metadata.jvm.KotlinClassMetadata.Companion;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -14,8 +16,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
+import kotlin.Metadata;
 import kotlinx.metadata.jvm.KotlinClassMetadata.MultiFileClassFacade;
 
 // Holds information about Metadata.MultiFileClassFace
@@ -55,7 +56,7 @@
   }
 
   @Override
-  public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+  public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
     List<String> partClassNameStrings = new ArrayList<>(partClassNames.size());
     boolean rewritten = false;
     for (KotlinTypeReference partClassName : partClassNames) {
@@ -69,9 +70,10 @@
               appView,
               null);
     }
-    KotlinClassMetadata.MultiFileClassFacade.Writer writer =
-        new KotlinClassMetadata.MultiFileClassFacade.Writer();
-    return Pair.create(writer.write(partClassNameStrings).getHeader(), rewritten);
+    return Pair.create(
+        Companion.writeMultiFileClassFacade(partClassNameStrings, getCompatibleKotlinInfo(), 0)
+            .getAnnotationData(),
+        rewritten);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassPartInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassPartInfo.java
index 62460ae..a51defc 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassPartInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMultiFileClassPartInfo.java
@@ -4,15 +4,17 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getCompatibleKotlinInfo;
+import static kotlinx.metadata.jvm.KotlinClassMetadata.Companion;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.utils.Pair;
 import java.util.function.Consumer;
+import kotlin.Metadata;
 import kotlinx.metadata.KmPackage;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
 import kotlinx.metadata.jvm.KotlinClassMetadata.MultiFileClassPart;
 
 // Holds information about Metadata.MultiFileClassPartInfo
@@ -63,13 +65,13 @@
   }
 
   @Override
-  public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+  public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
     KmPackage kmPackage = new KmPackage();
     boolean rewritten = packageInfo.rewrite(kmPackage, clazz, appView);
-    KotlinClassMetadata.MultiFileClassPart.Writer writer =
-        new KotlinClassMetadata.MultiFileClassPart.Writer();
-    kmPackage.accept(writer);
-    return Pair.create(writer.write(facadeClassName).getHeader(), rewritten);
+    return Pair.create(
+        Companion.writeMultiFileClassPart(kmPackage, facadeClassName, getCompatibleKotlinInfo(), 0)
+            .getAnnotationData(),
+        rewritten);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
index 6d10139..a11b9a9 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import java.util.List;
 import kotlinx.metadata.KmProperty;
@@ -58,6 +59,8 @@
   private final KotlinJvmMethodSignatureInfo syntheticMethodForAnnotations;
 
   private final KotlinJvmMethodSignatureInfo syntheticMethodForDelegate;
+  // Collection of context receiver types
+  private final List<KotlinTypeInfo> contextReceiverTypes;
 
   private KotlinPropertyInfo(
       int flags,
@@ -74,7 +77,8 @@
       KotlinJvmMethodSignatureInfo getterSignature,
       KotlinJvmMethodSignatureInfo setterSignature,
       KotlinJvmMethodSignatureInfo syntheticMethodForAnnotations,
-      KotlinJvmMethodSignatureInfo syntheticMethodForDelegate) {
+      KotlinJvmMethodSignatureInfo syntheticMethodForDelegate,
+      List<KotlinTypeInfo> contextReceiverTypes) {
     this.flags = flags;
     this.getterFlags = getterFlags;
     this.setterFlags = setterFlags;
@@ -90,6 +94,7 @@
     this.setterSignature = setterSignature;
     this.syntheticMethodForAnnotations = syntheticMethodForAnnotations;
     this.syntheticMethodForDelegate = syntheticMethodForDelegate;
+    this.contextReceiverTypes = contextReceiverTypes;
   }
 
   public static KotlinPropertyInfo create(
@@ -113,7 +118,10 @@
         KotlinJvmMethodSignatureInfo.create(
             JvmExtensionsKt.getSyntheticMethodForAnnotations(kmProperty), factory),
         KotlinJvmMethodSignatureInfo.create(
-            JvmExtensionsKt.getSyntheticMethodForDelegate(kmProperty), factory));
+            JvmExtensionsKt.getSyntheticMethodForDelegate(kmProperty), factory),
+        ListUtils.map(
+            kmProperty.getContextReceiverTypes(),
+            contextRecieverType -> KotlinTypeInfo.create(contextRecieverType, factory, reporter)));
   }
 
   @Override
@@ -160,6 +168,9 @@
     for (KotlinTypeParameterInfo typeParameter : typeParameters) {
       rewritten |= typeParameter.rewrite(kmProperty::visitTypeParameter, appView);
     }
+    for (KotlinTypeInfo contextReceiverType : contextReceiverTypes) {
+      rewritten |= contextReceiverType.rewrite(kmProperty::visitContextReceiverType, appView);
+    }
     rewritten |= versionRequirements.rewrite(kmProperty::visitVersionRequirement);
     JvmPropertyExtensionVisitor extensionVisitor =
         (JvmPropertyExtensionVisitor) kmProperty.visitExtensions(JvmPropertyExtensionVisitor.TYPE);
@@ -207,6 +218,7 @@
       setterParameter.trace(definitionSupplier);
     }
     forEachApply(typeParameters, param -> param::trace, definitionSupplier);
+    forEachApply(contextReceiverTypes, type -> type::trace, definitionSupplier);
     if (fieldSignature != null) {
       fieldSignature.trace(definitionSupplier);
     }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClassInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClassInfo.java
index fb9b55f..a0783ed 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClassInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClassInfo.java
@@ -4,14 +4,16 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getCompatibleKotlinInfo;
+import static kotlinx.metadata.jvm.KotlinClassMetadata.Companion;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.utils.Pair;
+import kotlin.Metadata;
 import kotlinx.metadata.KmLambda;
-import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata.SyntheticClass;
-import kotlinx.metadata.jvm.KotlinClassMetadata.SyntheticClass.Writer;
 
 // Holds information about a Metadata.SyntheticClass object.
 public class KotlinSyntheticClassInfo implements KotlinClassLevelInfo {
@@ -73,15 +75,16 @@
   }
 
   @Override
-  public Pair<KotlinClassHeader, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
-    Writer writer = new Writer();
-    boolean rewritten = false;
-    if (lambda != null) {
-      KmLambda kmLambda = new KmLambda();
-      rewritten = lambda.rewrite(() -> kmLambda, clazz, appView);
-      kmLambda.accept(writer);
+  public Pair<Metadata, Boolean> rewrite(DexClass clazz, AppView<?> appView) {
+    if (lambda == null) {
+      return Pair.create(
+          Companion.writeSyntheticClass(getCompatibleKotlinInfo(), 0).getAnnotationData(), false);
     }
-    return Pair.create(writer.write().getHeader(), rewritten);
+    KmLambda kmLambda = new KmLambda();
+    boolean rewritten = lambda.rewrite(() -> kmLambda, clazz, appView);
+    return Pair.create(
+        Companion.writeLambda(kmLambda, getCompatibleKotlinInfo(), 0).getAnnotationData(),
+        rewritten);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
index e83cff6..a20d86c 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.EnqueuerMetadataTraceable;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
@@ -86,7 +85,6 @@
   boolean rewrite(KmVisitorProviders.KmTypeVisitorProvider visitorProvider, AppView<?> appView) {
     // TODO(b/154348683): Check for correct flags
     KmTypeVisitor kmTypeVisitor = visitorProvider.get(flags);
-    NamingLens namingLens = appView.getNamingLens();
     boolean rewritten = classifier.rewrite(kmTypeVisitor, appView);
     if (abbreviatedType != null) {
       rewritten |= abbreviatedType.rewrite(kmTypeVisitor::visitAbbreviatedType, appView);
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index b9bc6bc..508c5db 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -142,14 +142,6 @@
         CharSource.wrap(contents).openBufferedStream(), diagnosticsHandler);
   }
 
-  // TODO(b/241763080): Remove when 2.2 is stable.
-  @Deprecated
-  public static ClassNameMapper mapperFromStringWithExperimental(String contents)
-      throws IOException {
-    return mapperFromBufferedReader(
-        CharSource.wrap(contents).openBufferedStream(), null, false, true, true);
-  }
-
   public static ClassNameMapper mapperFromString(
       String contents,
       DiagnosticsHandler diagnosticsHandler,
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/UnknownJsonMappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/UnknownJsonMappingInformation.java
index 98699b2..3e061db 100644
--- a/src/main/java/com/android/tools/r8/naming/mappinginformation/UnknownJsonMappingInformation.java
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/UnknownJsonMappingInformation.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.naming.mappinginformation;
 
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.MappingComposeException;
 import com.android.tools.r8.naming.mappinginformation.MappingInformation.PositionalMappingInformation;
 import com.google.gson.JsonObject;
@@ -31,7 +30,7 @@
 
   @Override
   public String serialize() {
-    throw new Unreachable("We should not at this point serialize unknown information");
+    return payload;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfile.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfile.java
index 11d080b..f3d4f1d 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfile.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfile.java
@@ -34,7 +34,6 @@
   private final List<ArtProfileRule> rules;
 
   ArtProfile(List<ArtProfileRule> rules) {
-    assert !rules.isEmpty();
     this.rules = rules;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index cb80e54..f7d7554 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -475,19 +475,8 @@
       futures.add(
           ThreadUtils.processAsynchronously(
               () -> {
-                Set<DexField> removedFields = prunedItems.getRemovedFields();
-                Set<DexMethod> removedMethods = prunedItems.getRemovedMethods();
-                if (map.size() <= removedFields.size() + removedMethods.size()) {
-                  map.keySet()
-                      .removeIf(
-                          member ->
-                              member.isDexField()
-                                  ? removedFields.contains(member.asDexField())
-                                  : removedMethods.contains(member.asDexMethod()));
-                } else {
-                  removedFields.forEach(map::remove);
-                  removedMethods.forEach(map::remove);
-                }
+                prunedItems.getRemovedFields().forEach(map::removeBoolean);
+                prunedItems.getRemovedMethods().forEach(map::removeBoolean);
               },
               executorService));
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 57726f6..4a4609f 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -19,12 +20,14 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
@@ -46,6 +49,8 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final TreePrunerConfiguration configuration;
   private final UnusedItemsPrinter unusedItemsPrinter;
+  private final Set<DexField> prunedFields = Sets.newIdentityHashSet();
+  private final Set<DexMethod> prunedMethods = Sets.newIdentityHashSet();
   private final Set<DexType> prunedTypes = Sets.newIdentityHashSet();
   private final Set<DexMethod> methodsToKeepForConfigurationDebugging = Sets.newIdentityHashSet();
 
@@ -66,21 +71,32 @@
             : UnusedItemsPrinter.DONT_PRINT;
   }
 
-  public DirectMappedDexApplication run(ExecutorService executorService) throws ExecutionException {
+  public PrunedItems run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    return run(executorService, timing, Collections.emptySet());
+  }
+
+  public PrunedItems run(ExecutorService executorService, Timing timing, Set<DexType> prunedTypes)
+      throws ExecutionException {
+    return timing.time("Pruning application...", () -> internalRun(executorService, prunedTypes));
+  }
+
+  private PrunedItems internalRun(
+      ExecutorService executorService, Set<DexType> previouslyPrunedTypes)
+      throws ExecutionException {
     DirectMappedDexApplication application = appView.appInfo().app().asDirect();
-    Timing timing = application.timing;
-    timing.begin("Pruning application...");
-    try {
       DirectMappedDexApplication.Builder builder = removeUnused(application);
       DirectMappedDexApplication newApplication =
           prunedTypes.isEmpty() && !appView.options().configurationDebugging
               ? application
               : builder.build();
       fixupOptimizationInfo(newApplication, executorService);
-      return newApplication;
-    } finally {
-      timing.end();
-    }
+    return PrunedItems.builder()
+        .setPrunedApp(newApplication)
+        .addRemovedClasses(CollectionUtils.mergeSets(previouslyPrunedTypes, prunedTypes))
+        .addRemovedFields(prunedFields)
+        .addRemovedMethods(prunedMethods)
+        .addAdditionalPinnedItems(methodsToKeepForConfigurationDebugging)
+        .build();
   }
 
   private DirectMappedDexApplication.Builder removeUnused(DirectMappedDexApplication application) {
@@ -342,6 +358,7 @@
           Log.debug(getClass(), "Removing method %s.", method.getReference());
         }
         unusedItemsPrinter.registerUnusedMethod(method);
+        prunedMethods.add(method.getReference());
       }
     }
     return reachableMethods.isEmpty()
@@ -367,7 +384,9 @@
     if (Log.ENABLED) {
       Log.debug(getClass(), "Removing field %s.", fields.get(firstUnreachable));
     }
-    unusedItemsPrinter.registerUnusedField(fields.get(firstUnreachable));
+    DexEncodedField firstUnreachableField = fields.get(firstUnreachable);
+    unusedItemsPrinter.registerUnusedField(firstUnreachableField);
+    prunedFields.add(firstUnreachableField.getReference());
     ArrayList<DexEncodedField> reachableOrReferencedFields = new ArrayList<>(fields.size());
     for (int i = 0; i < firstUnreachable; i++) {
       reachableOrReferencedFields.add(fields.get(i));
@@ -381,6 +400,7 @@
           Log.debug(getClass(), "Removing field %s.", field.getReference());
         }
         unusedItemsPrinter.registerUnusedField(field);
+        prunedFields.add(field.getReference());
       }
     }
     return reachableOrReferencedFields.isEmpty()
@@ -388,14 +408,6 @@
         : reachableOrReferencedFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
-  public Set<DexType> getRemovedClasses() {
-    return Collections.unmodifiableSet(prunedTypes);
-  }
-
-  public Collection<DexMethod> getMethodsToKeepForConfigurationDebugging() {
-    return Collections.unmodifiableCollection(methodsToKeepForConfigurationDebugging);
-  }
-
   private void fixupOptimizationInfo(
       DirectMappedDexApplication application, ExecutorService executorService)
       throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/utils/ClassReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/ClassReferenceUtils.java
index 072022d..0ee9a72 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassReferenceUtils.java
@@ -45,6 +45,10 @@
     }
   }
 
+  public static String toSmaliString(ClassReference classReference) {
+    return classReference.getDescriptor();
+  }
+
   public static DexType toDexType(ClassReference classReference, DexItemFactory dexItemFactory) {
     return dexItemFactory.createType(classReference.getDescriptor());
   }
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 b95ac53..d49f628 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -68,6 +68,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.nest.Nest;
 import com.android.tools.r8.ir.optimize.Inliner;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapVersion;
@@ -819,7 +820,7 @@
       new CallSiteOptimizationOptions();
   private final CfCodeAnalysisOptions cfCodeAnalysisOptions = new CfCodeAnalysisOptions();
   private final ClassInlinerOptions classInlinerOptions = new ClassInlinerOptions();
-  private final InlinerOptions inlinerOptions = new InlinerOptions();
+  private final InlinerOptions inlinerOptions = new InlinerOptions(this);
   private final HorizontalClassMergerOptions horizontalClassMergerOptions =
       new HorizontalClassMergerOptions();
   private final OpenClosedInterfacesOptions openClosedInterfacesOptions =
@@ -1528,7 +1529,7 @@
     boolean test(AppView<?> appView, ProgramMethod method, int inliningDepth);
   }
 
-  public class InlinerOptions {
+  public static class InlinerOptions {
 
     public boolean enableInlining =
         !parseSystemPropertyForDevelopmentOrDefault("com.android.tools.r8.disableinlining", false);
@@ -1559,17 +1560,31 @@
 
     public ApplyInliningToInlineePredicate applyInliningToInlineePredicateForTesting = null;
 
+    private final InternalOptions options;
+
+    public InlinerOptions(InternalOptions options) {
+      this.options = options;
+    }
+
+    public static void disableInlining(InternalOptions options) {
+      options.inlinerOptions().enableInlining = false;
+    }
+
+    public static void setOnlyForceInlining(InternalOptions options) {
+      options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+    }
+
     public int getSimpleInliningInstructionLimit() {
       // If a custom simple inlining instruction limit is set, then use that.
       if (simpleInliningInstructionLimit >= 0) {
         return simpleInliningInstructionLimit;
       }
       // Allow 3 instructions when generating to class files.
-      if (isGeneratingClassFiles()) {
+      if (options.isGeneratingClassFiles()) {
         return 3;
       }
       // Allow the size of the dex code to be up to 5 bytes.
-      assert isGeneratingDex();
+      assert options.isGeneratingDex();
       return 5;
     }
 
@@ -1578,7 +1593,7 @@
       if (applyInliningToInlineePredicateForTesting != null) {
         return applyInliningToInlineePredicateForTesting.test(appView, inlinee, inliningDepth);
       }
-      if (protoShrinking.shouldApplyInliningToInlinee(appView, inlinee, inliningDepth)) {
+      if (options.protoShrinking().shouldApplyInliningToInlinee(appView, inlinee, inliningDepth)) {
         return true;
       }
       return false;
@@ -1785,8 +1800,8 @@
   }
 
   public static class MappingComposeOptions {
-    // TODO(b/241763080): Remove when enabled.
     public boolean enableExperimentalMappingComposition = false;
+
     // TODO(b/247136434): Disable for internal builds.
     public boolean allowNonExistingOriginalRanges = true;
     public Consumer<ClassNameMapper> generatedClassNameMapperConsumer = null;
diff --git a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
index 11f01d2..b961f22 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
@@ -53,11 +53,12 @@
     if (appView.options().mappingComposeOptions().generatedClassNameMapperConsumer != null) {
       appView.options().mappingComposeOptions().generatedClassNameMapperConsumer.accept(mapper);
     }
-    if (appView.options().mappingComposeOptions().enableExperimentalMappingComposition) {
+    if (appView.options().mappingComposeOptions().enableExperimentalMappingComposition
+        && appView.appInfo().app().getProguardMap() != null) {
       timing.begin("Proguard map composition");
       try {
         mapper =
-            ClassNameMapper.mapperFromStringWithExperimental(
+            ClassNameMapper.mapperFromStringWithPreamble(
                 MappingComposer.compose(
                     appView.options(), appView.appInfo().app().getProguardMap(), mapper));
       } catch (IOException | MappingComposeException e) {
diff --git a/src/test/java/com/android/tools/r8/D8TestBuilder.java b/src/test/java/com/android/tools/r8/D8TestBuilder.java
index de0565f..c8c21fa 100644
--- a/src/test/java/com/android/tools/r8/D8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/D8TestBuilder.java
@@ -9,12 +9,16 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
@@ -32,6 +36,7 @@
 
   private StringBuilder proguardMapOutputBuilder = null;
   private boolean enableMissingLibraryApiModeling = true;
+  private List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
 
   @Override
   public boolean isD8TestBuilder() {
@@ -134,6 +139,16 @@
     return self();
   }
 
+  public D8TestBuilder addArtProfileForRewriting(ArtProfileProvider artProfileProvider) {
+    return addArtProfileForRewriting(
+        artProfileProvider,
+        ArtProfileTestingUtils.createResidualArtProfileConsumer(residualArtProfiles::add));
+  }
+
+  public D8TestBuilder addArtProfileForRewriting(ExternalArtProfile artProfile) {
+    return addArtProfileForRewriting(ArtProfileTestingUtils.createArtProfileProvider(artProfile));
+  }
+
   public D8TestBuilder addArtProfileForRewriting(
       ArtProfileProvider artProfileProvider, ArtProfileConsumer residualArtProfileConsumer) {
     builder.addArtProfileForRewriting(artProfileProvider, residualArtProfileConsumer);
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index 36f3b07..259f1a0 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MAX_SUPPORTED_VERSION;
 import static com.android.tools.r8.ToolHelper.isWindows;
+import static com.google.common.io.Files.getNameWithoutExtension;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -223,6 +224,10 @@
     return this;
   }
 
+  public KotlinCompilerTool enableExperimentalContextReceivers() {
+    return addArguments("-Xcontext-receivers");
+  }
+
   public KotlinCompilerTool addSourceFiles(Path... files) {
     return addSourceFiles(Arrays.asList(files));
   }
@@ -268,7 +273,8 @@
                   try {
                     // The Kotlin compiler does not require particular naming of files except for
                     // the extension, so just create a file called source.kt in a new directory.
-                    Path fileNamedKt = temp.newFolder().toPath().resolve("source.kt");
+                    String newFileName = getNameWithoutExtension(fileNotNamedKt.toString()) + ".kt";
+                    Path fileNamedKt = temp.newFolder().toPath().resolve(newFileName);
                     Files.copy(fileNotNamedKt, fileNamedKt);
                     return fileNamedKt;
                   } catch (IOException e) {
diff --git a/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java b/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
index bf1ad43..9fb85b3 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
@@ -59,6 +59,10 @@
     return result.stderr;
   }
 
+  public String getProguardMap() {
+    return proguardMap;
+  }
+
   @Override
   public CodeInspector inspector() throws IOException {
     return new CodeInspector(app, proguardMap);
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index a844c1f..34f8a0b 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -17,6 +17,8 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
 import com.android.tools.r8.shaking.CheckEnumUnboxedRule;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.shaking.KeepUnusedReturnValueRule;
@@ -68,6 +70,7 @@
   private boolean allowUnusedProguardConfigurationRules = false;
   private boolean enableMissingLibraryApiModeling = true;
   private CollectingGraphConsumer graphConsumer = null;
+  private List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
   private List<String> keepRules = new ArrayList<>();
   private List<Path> mainDexRulesFiles = new ArrayList<>();
   private List<String> applyMappingMaps = new ArrayList<>();
@@ -158,7 +161,8 @@
             createDefaultProguardMapConsumer ? proguardMapBuilder.toString() : null,
             graphConsumer,
             getMinApiLevel(),
-            features);
+            features,
+            residualArtProfiles);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -782,6 +786,16 @@
     return self();
   }
 
+  public T addArtProfileForRewriting(ArtProfileProvider artProfileProvider) {
+    return addArtProfileForRewriting(
+        artProfileProvider,
+        ArtProfileTestingUtils.createResidualArtProfileConsumer(residualArtProfiles::add));
+  }
+
+  public T addArtProfileForRewriting(ExternalArtProfile artProfile) {
+    return addArtProfileForRewriting(ArtProfileTestingUtils.createArtProfileProvider(artProfile));
+  }
+
   public T addArtProfileForRewriting(
       ArtProfileProvider artProfileProvider, ArtProfileConsumer residualArtProfileConsumer) {
     builder.addArtProfileForRewriting(artProfileProvider, residualArtProfileConsumer);
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 688ab43..b3b6e31 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -9,11 +9,14 @@
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -22,6 +25,7 @@
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 
 public class R8TestCompileResult extends TestCompileResult<R8TestCompileResult, R8TestRunResult> {
@@ -31,6 +35,7 @@
   private final String proguardMap;
   private final CollectingGraphConsumer graphConsumer;
   private final List<Path> features;
+  private final List<ExternalArtProfile> residualArtProfiles;
 
   R8TestCompileResult(
       TestState state,
@@ -42,13 +47,15 @@
       String proguardMap,
       CollectingGraphConsumer graphConsumer,
       int minApiLevel,
-      List<Path> features) {
+      List<Path> features,
+      List<ExternalArtProfile> residualArtProfiles) {
     super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
     this.proguardMap = proguardMap;
     this.graphConsumer = graphConsumer;
     this.features = features;
+    this.residualArtProfiles = residualArtProfiles;
   }
 
   @Override
@@ -124,6 +131,19 @@
     return self();
   }
 
+  public <E extends Throwable> R8TestCompileResult inspectResidualArtProfile(
+      ThrowingConsumer<ArtProfileInspector, E> consumer) throws E, IOException, ExecutionException {
+    return inspectResidualArtProfile(
+        (rewrittenArtProfile, inspector) -> consumer.accept(rewrittenArtProfile));
+  }
+
+  public <E extends Throwable> R8TestCompileResult inspectResidualArtProfile(
+      ThrowingBiConsumer<ArtProfileInspector, CodeInspector, E> consumer) throws E, IOException {
+    assertEquals(1, residualArtProfiles.size());
+    consumer.accept(new ArtProfileInspector(residualArtProfiles.iterator().next()), inspector());
+    return self();
+  }
+
   public GraphInspector graphInspector() throws IOException {
     assert graphConsumer != null;
     return new GraphInspector(graphConsumer, inspector());
diff --git a/src/test/java/com/android/tools/r8/SanityCheck.java b/src/test/java/com/android/tools/r8/SanityCheck.java
index b82995c..38fa873 100644
--- a/src/test/java/com/android/tools/r8/SanityCheck.java
+++ b/src/test/java/com/android/tools/r8/SanityCheck.java
@@ -29,7 +29,7 @@
 
   private static final String SRV_PREFIX = "META-INF/services/";
   private static final String METADATA_EXTENSION =
-      "com.android.tools.r8.jetbrains.kotlinx.metadata.impl.extensions.MetadataExtensions";
+      "com.android.tools.r8.jetbrains.kotlinx.metadata.internal.extensions.MetadataExtensions";
   private static final String EXT_IN_SRV = SRV_PREFIX + METADATA_EXTENSION;
 
     private void checkJarContent(
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index b487caf..8b2877f 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -205,7 +205,7 @@
     };
   }
 
-  static ApiModelingClassVerificationHelper verifyThat(
+  public static ApiModelingClassVerificationHelper verifyThat(
       CodeInspector inspector, TestParameters parameters, Class<?> clazz) {
     return new ApiModelingClassVerificationHelper(inspector, parameters, clazz);
   }
@@ -295,7 +295,7 @@
       hasNotOulinedInstructionWithClassReference(method, CodeMatchers::containsInstanceOf);
     }
 
-    void hasConstClassOutlinedFromUntil(Method method, AndroidApiLevel apiLevel) {
+    public void hasConstClassOutlinedFromUntil(Method method, AndroidApiLevel apiLevel) {
       if (parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(apiLevel)) {
         hasConstClassOutlinedFrom(method);
       } else {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LintFilesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LintFilesTest.java
index b357b2c..a33e2d3 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LintFilesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LintFilesTest.java
@@ -221,6 +221,9 @@
           directory2.toString()
         });
     List<String> html = Files.readAllLines(directory2.resolve("apis.html"));
-    assertTrue(html.contains("  <td><code>java.util.function</code></td>"));
+    // The doc has the same content than the lint data that is tested above, this is just a sanity
+    // check that the doc generation ran without error and looks sane.
+    assertEquals("<tr>", html.get(0));
+    assertEquals("</tr>", html.get(html.size() - 2));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
index 6f1b750..bd5554af 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/CollectionToArrayTest.java
@@ -18,6 +18,7 @@
 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;
@@ -53,6 +54,7 @@
     this.compilationSpecification = compilationSpecification;
   }
 
+  @Ignore("b/266401747")
   @Test
   public void test() throws Throwable {
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InterfacePartialDesugTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InterfacePartialDesugTest.java
new file mode 100644
index 0000000..a417783
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InterfacePartialDesugTest.java
@@ -0,0 +1,100 @@
+// 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.jdk11;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Supplier;
+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 InterfacePartialDesugTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+  private final CompilationSpecification compilationSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        ImmutableList.of(JDK11_PATH),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public InterfacePartialDesugTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+    this.compilationSpecification = compilationSpecification;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramClassFileData(getTransforms())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("42", "0");
+  }
+
+  private List<byte[]> getTransforms() throws IOException, NoSuchMethodException {
+    return ImmutableList.of(
+        transformer(Itf.class)
+            .setAccessFlags(
+                Itf.class.getDeclaredMethod("privateRun", Supplier.class),
+                flags -> {
+                  flags.unsetPublic();
+                  flags.setPrivate();
+                })
+            .setAccessFlags(
+                Itf.class.getDeclaredMethod("privateGet"),
+                flags -> {
+                  flags.unsetPublic();
+                  flags.setPrivate();
+                })
+            .transform());
+  }
+
+  public static class Main implements Itf {
+
+    public static void main(String[] args) {
+      System.out.println(new Main().get());
+      // We need to check a call to clone is correctly non desugared.
+      System.out.println((new Main[0]).clone().length);
+    }
+  }
+
+  public interface Itf {
+
+    default Object get() {
+      return (privateRun(this::privateGet));
+    }
+
+    // Method will be private at runtime.
+    default Object privateRun(Supplier<Object> getter) {
+      return getter.get();
+    }
+
+    // Method will be private at runtime.
+    default Object privateGet() {
+      return 42;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
index 4144aaa..ed81c32 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
@@ -27,6 +27,8 @@
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
 import com.android.tools.r8.profile.art.ArtProfileForRewriting;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
 import com.android.tools.r8.tracereferences.TraceReferences;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.FileUtils;
@@ -58,6 +60,7 @@
 
   private CustomLibrarySpecification customLibrarySpecification = null;
   private TestingKeepRuleConsumer keepRuleConsumer = null;
+  private List<ExternalArtProfile> l8ResidualArtProfiles = new ArrayList<>();
 
   public DesugaredLibraryTestBuilder(
       T test,
@@ -378,7 +381,8 @@
         libraryDesugaringSpecification,
         compilationSpecification,
         customLibCompile,
-        l8Compile);
+        l8Compile,
+        l8ResidualArtProfiles);
   }
 
   private D8TestCompileResult compileCustomLib() throws CompilationFailedException {
@@ -501,7 +505,18 @@
     return this;
   }
 
-  public DesugaredLibraryTestBuilder<?> addL8ArtProfileForRewriting(
+  public DesugaredLibraryTestBuilder<T> addL8ArtProfileForRewriting(
+      ArtProfileProvider artProfileProvider) {
+    return addL8ArtProfileForRewriting(
+        artProfileProvider,
+        ArtProfileTestingUtils.createResidualArtProfileConsumer(l8ResidualArtProfiles::add));
+  }
+
+  public DesugaredLibraryTestBuilder<T> addL8ArtProfileForRewriting(ExternalArtProfile artProfile) {
+    return addL8ArtProfileForRewriting(ArtProfileTestingUtils.createArtProfileProvider(artProfile));
+  }
+
+  public DesugaredLibraryTestBuilder<T> addL8ArtProfileForRewriting(
       ArtProfileProvider artProfileProvider, ArtProfileConsumer residualArtProfileConsumer) {
     l8ArtProfilesForRewriting.add(
         new ArtProfileForRewriting(artProfileProvider, residualArtProfileConsumer));
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestCompileResult.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestCompileResult.java
index 3172876..d49f2ef 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestCompileResult.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.test;
 
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.L8TestCompileResult;
@@ -13,6 +15,9 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
@@ -30,6 +35,7 @@
   private final CompilationSpecification compilationSpecification;
   private final D8TestCompileResult customLibCompile;
   private final L8TestCompileResult l8Compile;
+  private final List<ExternalArtProfile> l8ResidualArtProfiles;
   // In case of Cf2Cf desugaring the run on dex, the compileResult is the Cf desugaring result
   // while the runnableCompiledResult is the dexed compiledResult used to run on dex.
   private final TestCompileResult<?, ? extends SingleTestRunResult<?>> runnableCompiledResult;
@@ -41,7 +47,8 @@
       LibraryDesugaringSpecification libraryDesugaringSpecification,
       CompilationSpecification compilationSpecification,
       D8TestCompileResult customLibCompile,
-      L8TestCompileResult l8Compile)
+      L8TestCompileResult l8Compile,
+      List<ExternalArtProfile> l8ResidualArtProfiles)
       throws CompilationFailedException, IOException {
     this.test = test;
     this.compileResult = compileResult;
@@ -50,6 +57,7 @@
     this.compilationSpecification = compilationSpecification;
     this.customLibCompile = customLibCompile;
     this.l8Compile = l8Compile;
+    this.l8ResidualArtProfiles = l8ResidualArtProfiles;
     this.runnableCompiledResult = computeRunnableCompiledResult();
   }
 
@@ -66,6 +74,20 @@
     return this;
   }
 
+  public <E extends Throwable> DesugaredLibraryTestCompileResult<T> inspectL8ResidualArtProfile(
+      ThrowingConsumer<ArtProfileInspector, E> consumer) throws E, IOException, ExecutionException {
+    return inspectL8ResidualArtProfile(
+        (rewrittenArtProfile, inspector) -> consumer.accept(rewrittenArtProfile));
+  }
+
+  public <E extends Throwable> DesugaredLibraryTestCompileResult<T> inspectL8ResidualArtProfile(
+      ThrowingBiConsumer<ArtProfileInspector, CodeInspector, E> consumer) throws E, IOException {
+    assertEquals(1, l8ResidualArtProfiles.size());
+    consumer.accept(
+        new ArtProfileInspector(l8ResidualArtProfiles.iterator().next()), l8Inspector());
+    return this;
+  }
+
   public <E extends Throwable> DesugaredLibraryTestCompileResult<T> inspect(
       ThrowingConsumer<CodeInspector, E> consumer) throws IOException, E {
     compileResult.inspect(consumer);
diff --git a/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java b/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
index 6f68f53..d1f909d 100644
--- a/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
+++ b/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
@@ -3,13 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex.container;
 
+import static com.android.tools.r8.dex.Constants.CHECKSUM_OFFSET;
 import static com.android.tools.r8.dex.Constants.DATA_OFF_OFFSET;
 import static com.android.tools.r8.dex.Constants.DATA_SIZE_OFFSET;
+import static com.android.tools.r8.dex.Constants.FILE_SIZE_OFFSET;
 import static com.android.tools.r8.dex.Constants.MAP_OFF_OFFSET;
+import static com.android.tools.r8.dex.Constants.SIGNATURE_OFFSET;
 import static com.android.tools.r8.dex.Constants.STRING_IDS_OFF_OFFSET;
 import static com.android.tools.r8.dex.Constants.STRING_IDS_SIZE_OFFSET;
 import static com.android.tools.r8.dex.Constants.TYPE_STRING_ID_ITEM;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
@@ -31,8 +35,10 @@
 import java.io.IOException;
 import java.nio.ByteOrder;
 import java.nio.file.Path;
+import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.zip.Adler32;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -74,7 +80,7 @@
             .setMinApi(AndroidApiLevel.L)
             .compile()
             .writeToZip();
-    assertEquals(2, unzipContent(outputA).size());
+    validateDex(outputA, 2);
 
     Path outputB =
         testForD8(Backend.DEX)
@@ -82,7 +88,7 @@
             .setMinApi(AndroidApiLevel.L)
             .compile()
             .writeToZip();
-    assertEquals(2, unzipContent(outputB).size());
+    validateDex(outputB, 2);
 
     Path outputMerged =
         testForD8(Backend.DEX)
@@ -90,7 +96,7 @@
             .setMinApi(AndroidApiLevel.L)
             .compile()
             .writeToZip();
-    assertEquals(4, unzipContent(outputMerged).size());
+    validateDex(outputMerged, 4);
   }
 
   @Test
@@ -129,13 +135,21 @@
     validateSingleContainerDex(outputB);
   }
 
-  private void validateSingleContainerDex(Path output) throws IOException {
+  private void validateDex(Path output, int expectedDexes) throws Exception {
     List<byte[]> dexes = unzipContent(output);
-    assertEquals(1, dexes.size());
-    validateStringIdsSizeAndOffsets(dexes.get(0));
+    assertEquals(expectedDexes, dexes.size());
+    for (byte[] dex : dexes) {
+      validate(dex);
+    }
   }
 
-  private void validateStringIdsSizeAndOffsets(byte[] dex) {
+  private void validateSingleContainerDex(Path output) throws Exception {
+    List<byte[]> dexes = unzipContent(output);
+    assertEquals(1, dexes.size());
+    validate(dexes.get(0));
+  }
+
+  private void validate(byte[] dex) throws Exception {
     CompatByteBuffer buffer = CompatByteBuffer.wrap(dex);
     setByteOrder(buffer);
 
@@ -158,9 +172,53 @@
       assertEquals(stringIdsOffset, buffer.getInt(sectionOffset + STRING_IDS_OFF_OFFSET));
       assertEquals(stringIdsSize, getSizeFromMap(TYPE_STRING_ID_ITEM, buffer, sectionOffset));
       assertEquals(stringIdsOffset, getOffsetFromMap(TYPE_STRING_ID_ITEM, buffer, sectionOffset));
+      validateMap(buffer, sectionOffset);
+      validateSignature(buffer, sectionOffset);
+      validateChecksum(buffer, sectionOffset);
     }
   }
 
+  private void validateMap(CompatByteBuffer buffer, int offset) {
+    int mapOffset = buffer.getInt(offset + MAP_OFF_OFFSET);
+    buffer.position(mapOffset);
+    int mapSize = buffer.getInt();
+    int previousOffset = Integer.MAX_VALUE;
+    for (int i = 0; i < mapSize; i++) {
+      buffer.getShort(); // Skip section type.
+      buffer.getShort(); // Skip unused.
+      buffer.getInt(); // Skip section size.
+      int o = buffer.getInt();
+      if (i > 0) {
+        assertTrue("" + i + ": " + o + " " + previousOffset, o > previousOffset);
+      }
+      previousOffset = o;
+    }
+  }
+
+  private void validateSignature(CompatByteBuffer buffer, int offset) throws Exception {
+    int sectionSize = buffer.getInt(offset + FILE_SIZE_OFFSET);
+    MessageDigest md = MessageDigest.getInstance("SHA-1");
+    md.update(
+        buffer.asByteBuffer().array(),
+        offset + FILE_SIZE_OFFSET,
+        sectionSize - offset - FILE_SIZE_OFFSET);
+    byte[] expectedSignature = new byte[20];
+    md.digest(expectedSignature, 0, 20);
+    for (int i = 0; i < expectedSignature.length; i++) {
+      assertEquals(expectedSignature[i], buffer.get(offset + SIGNATURE_OFFSET + i));
+    }
+  }
+
+  private void validateChecksum(CompatByteBuffer buffer, int offset) {
+    int sectionSize = buffer.getInt(offset + FILE_SIZE_OFFSET);
+    Adler32 adler = new Adler32();
+    adler.update(
+        buffer.asByteBuffer().array(),
+        offset + SIGNATURE_OFFSET,
+        sectionSize - offset - SIGNATURE_OFFSET);
+    assertEquals((int) adler.getValue(), buffer.getInt(offset + CHECKSUM_OFFSET));
+  }
+
   private int getSizeFromMap(int type, CompatByteBuffer buffer, int offset) {
     int mapOffset = buffer.getInt(offset + MAP_OFF_OFFSET);
     buffer.position(mapOffset);
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java
index e43398a..922a3ce 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepFooIfBarSameClassTest.java
@@ -84,8 +84,7 @@
     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());
+    assertThat(inspector.clazz(B.class).uniqueInstanceInitializer(), isAbsent());
   }
 
   @KeepEdge(consequences = {@KeepTarget(classConstant = A.class)})
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepSameMethodTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepSameMethodTest.java
new file mode 100644
index 0000000..f4060f3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepSameMethodTest.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.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.KeepItemKind;
+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 KeepSameMethodTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("foo");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepSameMethodTest(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())
+        // The "all members" target will create an unused "all fields" rule.
+        .allowUnusedProguardConfigurationRules()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.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) throws Exception {
+    assertThat(inspector.clazz(A.class).method(A.class.getMethod("foo")), isPresent());
+    // TODO(b/265892343): The extracted rule will match all params so this is incorrectly kept.
+    assertThat(inspector.clazz(A.class).method(A.class.getMethod("foo", int.class)), isPresent());
+    // Bar is unused and thus removed.
+    assertThat(inspector.clazz(A.class).uniqueMethodWithOriginalName("bar"), isAbsent());
+  }
+
+  /**
+   * This conditional rule expresses that if any class in the program has a live member then that
+   * same member is to be kept (soft-pinned, e.g., no inlining, no renaming of the member, etc.).
+   */
+  @KeepEdge(
+      bindings = {
+        @KeepBinding(
+            bindingName = "AnyMemberOnA",
+            kind = KeepItemKind.ONLY_MEMBERS,
+            classConstant = A.class)
+      },
+      preconditions = {@KeepCondition(memberFromBinding = "AnyMemberOnA")},
+      consequences = {@KeepTarget(memberFromBinding = "AnyMemberOnA")})
+  static class A {
+
+    public void foo() throws Exception {
+      System.out.println(new Exception().getStackTrace()[0].getMethodName());
+    }
+
+    // TODO(b/265892343): There is no backref support for "any params", thus this method is hit by
+    //  the extracted rule.
+    public void foo(int unused) throws Exception {
+      System.out.println(new Exception().getStackTrace()[0].getMethodName());
+    }
+
+    public void bar() throws Exception {
+      System.out.println(new Exception().getStackTrace()[0].getMethodName());
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java
new file mode 100644
index 0000000..02e86ae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.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;
+
+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.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.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 KeepTargetClassAndMemberKindTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepTargetClassAndMemberKindTest(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();
+    for (String rule : rules) {
+      System.out.println(rule);
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .allowUnusedProguardConfigurationRules()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  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) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      // Ensure that the class A remains as we are assuming the contents of its name.
+      @KeepTarget(classConstant = A.class),
+      // Ensure that the class B remains as we are looking it up by reflected name.
+      // Ensure the method 'bar' remains as we are invoking it by reflected name.
+      // Explicitly using kind CLASS_AND_MEMBER ensures that both the type and the method are kept.
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_MEMBERS,
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameters = {},
+          methodReturnType = "void")
+    })
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+
+    // This annotation is not active as its implicit precondition "void A.foo(int)" is not used.
+    @UsesReflection({@KeepTarget(classConstant = C.class)})
+    public void foo(int unused) {
+      // Unused.
+    }
+  }
+
+  static class B {
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index b85a41b..dbe4642 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
@@ -47,8 +47,7 @@
                     .addTarget(KeepTarget.builder().setItemPattern(KeepItemPattern.any()).build())
                     .build())
             .build();
-    assertEquals(
-        StringUtils.unixLines("-keep class *", "-keepclassmembers class * { *; }"), extract(edge));
+    assertEquals(StringUtils.unixLines("-keep class * { *; }"), extract(edge));
   }
 
   @Test
@@ -70,11 +69,7 @@
     String allows = String.join(",allow", options);
     // The "any" item will be split in two rules, one for the targeted types and one for the
     // targeted members.
-    assertEquals(
-        StringUtils.unixLines(
-            "-keep,allow" + allows + " class *",
-            "-keepclassmembers,allow" + allows + " class * { *; }"),
-        extract(edge));
+    assertEquals(StringUtils.unixLines("-keep,allow" + allows + " class * { *; }"), extract(edge));
   }
 
   @Test
@@ -93,9 +88,7 @@
             .build();
     // Allow is just the ordered list of options.
     assertEquals(
-        StringUtils.unixLines(
-            "-keep,allowshrinking,allowobfuscation class *",
-            "-keepclassmembers,allowshrinking,allowobfuscation class * { *; }"),
+        StringUtils.unixLines("-keep,allowshrinking,allowobfuscation class * { *; }"),
         extract(edge));
   }
 
@@ -104,7 +97,8 @@
     KeepTarget target = target(classItem(CLASS));
     KeepConsequences consequences = KeepConsequences.builder().addTarget(target).build();
     KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
-    assertEquals(StringUtils.unixLines("-keep class " + CLASS), extract(edge));
+    assertEquals(
+        StringUtils.unixLines("-keep class " + CLASS + " { void finalize(); }"), extract(edge));
   }
 
   @Test
@@ -140,7 +134,9 @@
             .setConsequences(KeepConsequences.builder().addTarget(target(classItem(CLASS))).build())
             .build();
     assertEquals(
-        StringUtils.unixLines("-if class " + CLASS + " -keep class " + CLASS), extract(edge));
+        StringUtils.unixLines(
+            "-if class " + CLASS + " -keep class " + CLASS + " { void finalize(); }"),
+        extract(edge));
   }
 
   @Test
@@ -163,8 +159,7 @@
             .build();
     assertEquals(
         StringUtils.unixLines(
-            "-if class " + CLASS + " -keep class " + CLASS,
-            "-keepclassmembers class " + CLASS + " { void <init>(); }"),
+            "-if class " + CLASS + " -keep class " + CLASS + " { void <init>(); }"),
         extract(edge));
   }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java b/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
index 11ad201..4f334aa 100644
--- a/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
+++ b/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
@@ -94,7 +94,7 @@
             .compile()
             .inspect(
                 inspector ->
-                    assertEqualMetadata(
+                    assertEqualMetadataWithStringPoolValidation(
                         new CodeInspector(BASE_LIBRARY),
                         inspector,
                         (addedStrings, addedNonInitStrings) -> {}))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
index 55a048e..07539ed 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.kotlin.KotlinMetadataAnnotationWrapper;
 import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.IntBox;
@@ -31,8 +32,8 @@
 import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import junit.framework.TestCase;
-import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
+import org.junit.Assert;
 
 public abstract class KotlinMetadataTestBase extends KotlinTestBase {
 
@@ -54,7 +55,7 @@
   static final String KT_FUNCTION1 = "Lkotlin/Function1;";
   static final String KT_COMPARABLE = "Lkotlin/Comparable;";
 
-  public void assertEqualMetadata(
+  public void assertEqualMetadataWithStringPoolValidation(
       CodeInspector originalInspector,
       CodeInspector rewrittenInspector,
       BiConsumer<Integer, Integer> addedStringsInspector) {
@@ -73,9 +74,11 @@
         continue;
       }
       assertNotNull(rewrittenMetadata);
-      KotlinClassHeader originalHeader = originalMetadata.getHeader();
-      KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-      TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
+      KotlinMetadataAnnotationWrapper originalHeader =
+          KotlinMetadataAnnotationWrapper.wrap(originalMetadata);
+      KotlinMetadataAnnotationWrapper rewrittenHeader =
+          KotlinMetadataAnnotationWrapper.wrap(rewrittenMetadata);
+      TestCase.assertEquals(originalHeader.kind(), rewrittenHeader.kind());
 
       // We cannot assert equality of the data since it may be ordered differently. However, we
       // will check for the changes to the string pool and then validate the same parsing
@@ -87,8 +90,8 @@
                   .computeIfAbsent(
                       method.getFinalSignature().toDescriptor(), ignoreArgument(ArrayList::new))
                   .add(method.getFinalName()));
-      HashSet<String> originalStrings = new HashSet<>(Arrays.asList(originalHeader.getData2()));
-      HashSet<String> rewrittenStrings = new HashSet<>(Arrays.asList(rewrittenHeader.getData2()));
+      HashSet<String> originalStrings = new HashSet<>(Arrays.asList(originalHeader.data2()));
+      HashSet<String> rewrittenStrings = new HashSet<>(Arrays.asList(rewrittenHeader.data2()));
       rewrittenStrings.forEach(
           rewrittenString -> {
             if (originalStrings.contains(rewrittenString)) {
@@ -108,7 +111,7 @@
             }
             addedNonInitStrings.increment();
           });
-      assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
+      assertEquals(originalHeader.packageName(), rewrittenHeader.packageName());
 
       String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
       String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
@@ -117,6 +120,57 @@
     addedStringsInspector.accept(addedStrings.get(), addedNonInitStrings.get());
   }
 
+  public void assertEqualDeserializedMetadata(
+      CodeInspector inspector, CodeInspector otherInspector) {
+    for (FoundClassSubject clazzSubject : otherInspector.allClasses()) {
+      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
+      assertThat(r8Clazz, isPresent());
+      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
+      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
+      if (originalMetadata == null) {
+        assertNull(rewrittenMetadata);
+        continue;
+      }
+      assertNotNull(rewrittenMetadata);
+      KotlinMetadataAnnotationWrapper originalHeader =
+          KotlinMetadataAnnotationWrapper.wrap(originalMetadata);
+      KotlinMetadataAnnotationWrapper rewrittenHeader =
+          KotlinMetadataAnnotationWrapper.wrap(rewrittenMetadata);
+      TestCase.assertEquals(originalHeader.kind(), rewrittenHeader.kind());
+      TestCase.assertEquals(originalHeader.packageName(), rewrittenHeader.packageName());
+      // We cannot assert equality of the data since it may be ordered differently. We use the
+      // KotlinMetadataWriter to deserialize the metadata and assert those are equal.
+      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
+      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
+      TestCase.assertEquals(expected, actual);
+    }
+  }
+
+  public void assertEqualMetadata(CodeInspector inspector, CodeInspector otherInspector) {
+    for (FoundClassSubject clazzSubject : otherInspector.allClasses()) {
+      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
+      assertThat(r8Clazz, isPresent());
+      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
+      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
+      if (originalMetadata == null) {
+        assertNull(rewrittenMetadata);
+        continue;
+      }
+      TestCase.assertNotNull(rewrittenMetadata);
+      KotlinMetadataAnnotationWrapper originalHeader =
+          KotlinMetadataAnnotationWrapper.wrap(originalMetadata);
+      KotlinMetadataAnnotationWrapper rewrittenHeader =
+          KotlinMetadataAnnotationWrapper.wrap(rewrittenMetadata);
+      TestCase.assertEquals(originalHeader.kind(), rewrittenHeader.kind());
+      TestCase.assertEquals(originalHeader.packageName(), rewrittenHeader.packageName());
+      Assert.assertArrayEquals(originalHeader.data1(), rewrittenHeader.data1());
+      Assert.assertArrayEquals(originalHeader.data2(), rewrittenHeader.data2());
+      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
+      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
+      TestCase.assertEquals(expected, actual);
+    }
+  }
+
   public static void verifyExpectedWarningsFromKotlinReflectAndStdLib(
       TestCompileResult<?, ?> compileResult) {
     compileResult.assertAllWarningMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
index 5e5fc28..bcbc386 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
@@ -4,26 +4,13 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNotNull;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertNull;
-
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -97,7 +84,13 @@
                 ProguardKeepAttributes.INNER_CLASSES,
                 ProguardKeepAttributes.ENCLOSING_METHOD)
             .compile()
-            .inspect(this::inspect)
+            // Since this has a keep-all classes rule, we should just assert that the meta-data is
+            // equal to the original one.
+            .inspect(
+                inspector ->
+                    assertEqualDeserializedMetadata(
+                        inspector,
+                        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion))))
             .writeToZip();
     Path main =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
@@ -112,33 +105,6 @@
         .assertSuccessWithOutput(EXPECTED);
   }
 
-  private void inspect(CodeInspector inspector) throws IOException, ExecutionException {
-    // Since this has a keep-all classes rule, we should just assert that the meta-data is equal to
-    // the original one.
-    CodeInspector stdLibInspector =
-        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
-    for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
-      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
-      assertThat(r8Clazz, isPresent());
-      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
-      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
-      if (originalMetadata == null) {
-        assertNull(rewrittenMetadata);
-        continue;
-      }
-      assertNotNull(rewrittenMetadata);
-      KotlinClassHeader originalHeader = originalMetadata.getHeader();
-      KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-      assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
-      assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
-      // We cannot assert equality of the data since it may be ordered differently. Instead we use
-      // the KotlinMetadataWriter.
-      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
-      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
-      assertEquals(expected, actual);
-    }
-  }
-
   @Test
   public void testMetadataForReflect() throws Exception {
     Path libJar =
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteContextReceiverTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteContextReceiverTest.java
new file mode 100644
index 0000000..102a056
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteContextReceiverTest.java
@@ -0,0 +1,193 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata;
+
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_8_0;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinTargetVersion.JAVA_8;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewriteContextReceiverTest extends KotlinMetadataTestBase {
+
+  private static final String PKG_LIB = PKG + ".context_receiver_lib";
+  private static final String PKG_APP = PKG + ".context_receiver_app";
+  private static final Path LIB_FILE =
+      getFileInTest(PKG_PREFIX + "/context_receiver_lib", "lib.txt");
+  private static final Path MAIN_FILE =
+      getFileInTest(PKG_PREFIX + "/context_receiver_app", "main.txt");
+  private static final String MAIN = PKG_APP + ".MainKt";
+  private final TestParameters parameters;
+
+  private static final String EXPECTED =
+      StringUtils.lines(
+          "FooImpl::m1",
+          "BarImpl::m2",
+          "BazImpl::m3",
+          "BazImpl::m3",
+          "FooImpl::m1",
+          "BarImpl::m2",
+          "Hello World!");
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        getKotlinTestParameters()
+            .withCompilersStartingFromIncluding(KOTLINC_1_8_0)
+            .withOldCompilersStartingFrom(KOTLINC_1_8_0)
+            .withTargetVersion(JAVA_8)
+            .build());
+  }
+
+  public MetadataRewriteContextReceiverTest(
+      TestParameters parameters, KotlinTestParameters kotlinParameters) {
+    super(kotlinParameters);
+    this.parameters = parameters;
+  }
+
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer()
+          .configure(
+              kotlinc ->
+                  kotlinc
+                      .addSourceFilesWithNonKtExtension(getStaticTemp(), LIB_FILE)
+                      .enableExperimentalContextReceivers());
+
+  @Test
+  public void smokeTest() throws Exception {
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+            .addSourceFilesWithNonKtExtension(temp, MAIN_FILE)
+            .setOutputPath(temp.newFolder().toPath())
+            .enableExperimentalContextReceivers()
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(
+            kotlinc.getKotlinStdlibJar(),
+            kotlinc.getKotlinReflectJar(),
+            libJars.getForConfiguration(kotlinc, targetVersion))
+        .addClasspath(output)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testMetadataForCompilationWithKeepAll() throws Exception {
+    Path libJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(kotlinc.getKotlinAnnotationJar())
+            .addProgramFiles(
+                libJars.getForConfiguration(kotlinc, targetVersion), kotlinc.getKotlinStdlibJar())
+            .addKeepClassAndMembersRules(PKG_LIB + ".*")
+            .addKeepAttributes(
+                ProguardKeepAttributes.SIGNATURE,
+                ProguardKeepAttributes.INNER_CLASSES,
+                ProguardKeepAttributes.ENCLOSING_METHOD)
+            .addKeepKotlinMetadata()
+            .allowDiagnosticWarningMessages()
+            .addOptionsModification(
+                options -> options.testing.keepMetadataInR8IfNotRewritten = false)
+            .compile()
+            // Since this has a keep-all classes rule assert that the meta-data is equal to the
+            // original one.
+            .inspect(
+                inspector ->
+                    assertEqualDeserializedMetadata(
+                        inspector,
+                        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion))))
+            .writeToZip();
+    Path main =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFilesWithNonKtExtension(temp, MAIN_FILE)
+            .setOutputPath(temp.newFolder().toPath())
+            .enableExperimentalContextReceivers()
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
+        .addClasspath(main)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testMetadataInExtensionFunction_renamedKotlinSources() throws Exception {
+    R8TestCompileResult r8LibraryResult =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar())
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+            // Ensure that we do not rename members
+            .addKeepRules("-keepclassmembers class * { *; }")
+            // Keep the Foo class but rename it.
+            .addKeepRules("-keep,allowobfuscation class " + PKG_LIB + ".Foo")
+            // Keep the Bar class but rename it.
+            .addKeepRules("-keep,allowobfuscation class " + PKG_LIB + ".Bar")
+            // Keep the Baz class but rename it.
+            .addKeepRules("-keep,allowobfuscation class " + PKG_LIB + ".Baz")
+            // Keep all Printer fields.
+            .addKeepRules("-keep class " + PKG_LIB + ".Printer { *; }")
+            // Keep Super, but allow minification.
+            .addKeepRules("-keep class " + PKG_LIB + ".LibKt { <methods>; }")
+            .addKeepKotlinMetadata()
+            .compile();
+
+    // Rewrite the kotlin source to rewrite the classes from the mapping file
+    String kotlinSource = FileUtils.readTextFile(MAIN_FILE, StandardCharsets.UTF_8);
+
+    CodeInspector inspector = r8LibraryResult.inspector();
+    // Rewrite the source kotlin file that reference the renamed classes uses in the context
+    // receivers.
+    for (String className : new String[] {"Foo", "Bar", "Baz"}) {
+      String originalClassName = PKG_LIB + "." + className;
+      ClassSubject clazz = inspector.clazz(originalClassName);
+      assertThat(clazz, isPresentAndRenamed());
+      kotlinSource =
+          kotlinSource.replace("import " + originalClassName, "import " + clazz.getFinalName());
+      kotlinSource =
+          kotlinSource.replace(
+              ": " + className + " {",
+              ": "
+                  + DescriptorUtils.getSimpleClassNameFromDescriptor(clazz.getFinalDescriptor())
+                  + " {");
+    }
+
+    Path newSource = temp.newFolder().toPath().resolve("main.kt");
+    Files.write(newSource, kotlinSource.getBytes(StandardCharsets.UTF_8));
+
+    Path libJar = r8LibraryResult.writeToZip();
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(newSource)
+            .setOutputPath(temp.newFolder().toPath())
+            .enableExperimentalContextReceivers()
+            .compile();
+
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
+        .addClasspath(output)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java
index af22ae1..0704ecd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java
@@ -5,24 +5,15 @@
 package com.android.tools.r8.kotlin.metadata;
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_6_0;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import junit.framework.TestCase;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -84,7 +75,11 @@
                 "-keep class " + PKG + ".inline_class_lib.LibKt { *** login-*(java.lang.String); }")
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .compile()
-            .inspect(this::inspect)
+            .inspect(
+                inspector ->
+                    assertEqualMetadata(
+                        inspector,
+                        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion))))
             .writeToZip();
     Path main =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
@@ -98,24 +93,4 @@
         .run(parameters.getRuntime(), PKG + ".inline_class_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
   }
-
-  private void inspect(CodeInspector inspector) throws IOException {
-    CodeInspector stdLibInspector =
-        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
-    ClassSubject clazzSubject = stdLibInspector.clazz(passwordTypeName);
-    ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
-    assertThat(r8Clazz, isPresent());
-    KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
-    KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
-    TestCase.assertNotNull(rewrittenMetadata);
-    KotlinClassHeader originalHeader = originalMetadata.getHeader();
-    KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-    TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
-    TestCase.assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
-    Assert.assertArrayEquals(originalHeader.getData1(), rewrittenHeader.getData1());
-    Assert.assertArrayEquals(originalHeader.getData2(), rewrittenHeader.getData2());
-    String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
-    String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
-    TestCase.assertEquals(expected, actual);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
index c693a41..6f78827 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
@@ -4,24 +4,13 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertNull;
-
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import junit.framework.TestCase;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -78,7 +67,11 @@
                 ProguardKeepAttributes.INNER_CLASSES,
                 ProguardKeepAttributes.ENCLOSING_METHOD)
             .compile()
-            .inspect(this::inspect)
+            .inspect(
+                inspector ->
+                    assertEqualDeserializedMetadata(
+                        inspector,
+                        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion))))
             .writeToZip();
     Path main =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
@@ -92,29 +85,4 @@
         .run(parameters.getRuntime(), PKG + ".inline_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
   }
-
-  private void inspect(CodeInspector inspector) throws IOException {
-    CodeInspector stdLibInspector =
-        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
-    for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
-      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
-      assertThat(r8Clazz, isPresent());
-      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
-      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
-      if (originalMetadata == null) {
-        assertNull(rewrittenMetadata);
-        continue;
-      }
-      TestCase.assertNotNull(rewrittenMetadata);
-      KotlinClassHeader originalHeader = originalMetadata.getHeader();
-      KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-      TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
-      TestCase.assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
-      // We cannot assert equality of the data since it may be ordered differently. Instead we use
-      // the KotlinMetadataWriter.
-      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
-      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
-      TestCase.assertEquals(expected, actual);
-    }
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
index 24b3e58..ce9fba4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
@@ -10,10 +10,12 @@
 
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.kotlin.KotlinMetadataAnnotationWrapper;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.util.Collection;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -68,8 +70,9 @@
       if (clazz.getFinalName().startsWith("kotlin.io")
           || clazz.getFinalName().equals("kotlin.Metadata")
           || clazz.getFinalName().equals("kotlin.jvm.JvmName")) {
-        assertNotNull(clazz.getKotlinClassMetadata());
-        assertNotNull(clazz.getKotlinClassMetadata().getHeader().getData2());
+        KotlinClassMetadata kotlinClassMetadata = clazz.getKotlinClassMetadata();
+        assertNotNull(kotlinClassMetadata);
+        assertNotNull(KotlinMetadataAnnotationWrapper.wrap(kotlinClassMetadata).data2());
       } else {
         assertNull(clazz.getKotlinClassMetadata());
       }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java
index 2a7ee54..9fddbf0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java
@@ -70,7 +70,7 @@
             .compile()
             .inspect(
                 inspector ->
-                    assertEqualMetadata(
+                    assertEqualMetadataWithStringPoolValidation(
                         new CodeInspector(jars.getForConfiguration(kotlinc, targetVersion)),
                         inspector,
                         (addedStrings, addedNonInitStrings) -> {}))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
index ceb7d56..e06e1ba 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
@@ -50,14 +50,13 @@
         .addKeepAllClassesRule()
         .addKeepKotlinMetadata()
         .addKeepAttributes(
-            ProguardKeepAttributes.INNER_CLASSES,
-            ProguardKeepAttributes.ENCLOSING_METHOD)
+            ProguardKeepAttributes.INNER_CLASSES, ProguardKeepAttributes.ENCLOSING_METHOD)
         .allowDiagnosticWarningMessages()
         .compile()
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .inspect(
             inspector ->
-                assertEqualMetadata(
+                assertEqualMetadataWithStringPoolValidation(
                     new CodeInspector(kotlinc.getKotlinStdlibJar()),
                     inspector,
                     (addedStrings, addedNonInitStrings) -> {
@@ -75,7 +74,7 @@
         .compile()
         .inspect(
             inspector ->
-                assertEqualMetadata(
+                assertEqualMetadataWithStringPoolValidation(
                     new CodeInspector(kotlinc.getKotlinStdlibJar()),
                     inspector,
                     (addedStrings, addedNonInitStrings) -> {
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteRawTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteRawTest.java
index 7686ac3..6012fb0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteRawTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteRawTest.java
@@ -96,7 +96,7 @@
             .compile()
             .inspect(
                 inspector ->
-                    assertEqualMetadata(
+                    assertEqualMetadataWithStringPoolValidation(
                         new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion)),
                         inspector,
                         (addedStrings, addedNonInitStrings) -> {}))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteUnitPrimitiveTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteUnitPrimitiveTest.java
index 5d13395..4683d0b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteUnitPrimitiveTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteUnitPrimitiveTest.java
@@ -6,27 +6,16 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinTargetVersion.JAVA_8;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import junit.framework.TestCase;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -109,7 +98,12 @@
             .compile()
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
-            .inspect(this::inspect)
+            .inspect(
+                inspector ->
+                    assertEqualMetadata(
+                        inspector,
+                        new CodeInspector(
+                            kotlincLibJar.getForConfiguration(kotlinc, targetVersion))))
             .writeToZip();
     Path main =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
@@ -124,29 +118,4 @@
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
   }
-
-  private void inspect(CodeInspector inspector) throws IOException {
-    CodeInspector stdLibInspector =
-        new CodeInspector(kotlincLibJar.getForConfiguration(kotlinc, targetVersion));
-    for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
-      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
-      assertThat(r8Clazz, isPresent());
-      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
-      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
-      if (originalMetadata == null) {
-        assertNull(rewrittenMetadata);
-        continue;
-      }
-      TestCase.assertNotNull(rewrittenMetadata);
-      KotlinClassHeader originalHeader = originalMetadata.getHeader();
-      KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-      TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
-      TestCase.assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
-      Assert.assertArrayEquals(originalHeader.getData1(), rewrittenHeader.getData1());
-      Assert.assertArrayEquals(originalHeader.getData2(), rewrittenHeader.getData2());
-      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
-      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
-      TestCase.assertEquals(expected, actual);
-    }
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
index 6aecc85..b65643f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
@@ -9,7 +9,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.StringContains.containsString;
-import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
@@ -20,13 +19,9 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import junit.framework.TestCase;
-import kotlinx.metadata.jvm.KotlinClassHeader;
-import kotlinx.metadata.jvm.KotlinClassMetadata;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -112,31 +107,13 @@
   }
 
   private void inspect(CodeInspector inspector) throws IOException {
-    CodeInspector stdLibInspector =
-        new CodeInspector(kotlincLibJar.getForConfiguration(kotlinc, targetVersion));
-    for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
-      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
-      assertThat(r8Clazz, isPresent());
-      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
-      KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
-      if (originalMetadata == null) {
-        assertNull(rewrittenMetadata);
-        continue;
-      }
-      TestCase.assertNotNull(rewrittenMetadata);
-      KotlinClassHeader originalHeader = originalMetadata.getHeader();
-      KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
-      TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
-      TestCase.assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
-      // We cannot assert equality of the data since it may be ordered differently. Instead we use
-      // the KotlinMetadataWriter.
-      String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
-      String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
-      TestCase.assertEquals(expected, actual);
-      if (r8Clazz.getFinalName().equals(PKG_LIB + ".Name")) {
-        assertThat(actual, containsString("inlineClassUnderlyingPropertyName"));
-        assertThat(actual, containsString("inlineClassUnderlyingType"));
-      }
-    }
+    assertEqualDeserializedMetadata(
+        inspector, new CodeInspector(kotlincLibJar.getForConfiguration(kotlinc, targetVersion)));
+    ClassSubject r8Clazz = inspector.clazz(PKG_LIB + ".Name");
+    assertThat(r8Clazz, isPresent());
+    String actual =
+        KotlinMetadataWriter.kotlinMetadataToString("", r8Clazz.getKotlinClassMetadata());
+    assertThat(actual, containsString("inlineClassUnderlyingPropertyName"));
+    assertThat(actual, containsString("inlineClassUnderlyingType"));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_app/main.txt b/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_app/main.txt
new file mode 100644
index 0000000..9380c6f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_app/main.txt
@@ -0,0 +1,46 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.context_receiver_app
+
+import com.android.tools.r8.kotlin.metadata.context_receiver_lib.Bar
+import com.android.tools.r8.kotlin.metadata.context_receiver_lib.Foo
+import com.android.tools.r8.kotlin.metadata.context_receiver_lib.Baz
+import com.android.tools.r8.kotlin.metadata.context_receiver_lib.Printer
+import com.android.tools.r8.kotlin.metadata.context_receiver_lib.callFooBar
+
+class FooImpl : Foo {
+
+  override fun m1(): String {
+    println("FooImpl::m1")
+    return "Hello "
+  }
+}
+
+class BarImpl : Bar {
+  override fun m2(): String {
+    println("BarImpl::m2")
+    return "World!"
+  }
+}
+
+class BazImpl : Baz {
+  override fun m3(): String {
+    println("BazImpl::m3")
+    return "BazImpl::m3"
+  }
+}
+
+fun main() {
+  with (FooImpl()) {
+    with (BarImpl()) {
+      val printer = Printer()
+      printer.fooBar()
+      with (BazImpl()) {
+        println(printer.getValue)
+        println(callFooBar())
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_lib/lib.txt b/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_lib/lib.txt
new file mode 100644
index 0000000..0235848
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/context_receiver_lib/lib.txt
@@ -0,0 +1,35 @@
+// 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.kotlin.metadata.context_receiver_lib
+
+interface Foo {
+  fun m1(): String
+}
+
+interface Bar {
+  fun m2(): String
+}
+
+interface Baz {
+  fun m3(): String
+}
+
+context(Foo, Bar)
+class Printer {
+
+  fun fooBar() {
+    m1();
+    m2();
+  }
+
+  context(Baz)
+  val getValue: String
+    get() = if (System.currentTimeMillis() == 0L) "foo" else m3()
+}
+
+context(Foo, Bar)
+fun callFooBar() : String {
+  return m1() + m2();
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java
index a57ce5e..ea5c219 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java
@@ -31,26 +31,26 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.foo -> a:",
           "    void f1(int) -> f2",
           "    # { id: 'com.android.tools.r8.residualsignature', signature:'(Z)I' }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "a -> b:",
           "    int f2(boolean) -> f3");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.foo -> b:",
           "    void f1(int) -> f3",
           "    # {'id':'com.android.tools.r8.residualsignature','signature':'(Z)I'}");
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java
index 8647c07..a6aded1 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java
@@ -31,19 +31,19 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.foo -> a:",
           "    1:1:int f1(boolean) -> f2",
           "    2:2:void f1(int) -> f2",
           "    # { id: 'com.android.tools.r8.residualsignature', signature:'(Z)V' }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "a -> b:",
           "    8:8:void f2(boolean):2:2 -> f3");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.foo -> b:",
           "    1:1:int f1(boolean) -> f2",
           "    8:8:void f1(int) -> f3",
@@ -51,8 +51,8 @@
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingLineNumberTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingLineNumberTest.java
index f56d9af..1799999 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingLineNumberTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingLineNumberTest.java
@@ -31,34 +31,34 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.Class1 -> A:",
           "    1:1:com.Class1 m(com.Class2[][]):42:42 -> a",
           "com.Class2 -> B:");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "A -> B:",
           "    2:2:A a(B[][]):1:1 -> b",
           "B -> C:");
   private static final String mappingBaz =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "B -> C:",
           "    3:3:B b(C[][]):2:2 -> c",
           "C -> D:");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.Class1 -> C:",
           "    3:3:com.Class1 m(com.Class2[][]):42:42 -> c",
           "com.Class2 -> D:");
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
-    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithExperimental(mappingBaz);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
+    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithPreamble(mappingBaz);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar, mappingForBaz);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingTest.java
index 17794b3..4b7e9a9 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeNoResidualSignatureExistingNamingTest.java
@@ -31,28 +31,28 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.Class1 -> A:",
           "    com.Class1 f -> a",
           "    com.Class1 m(com.Class2[][]) -> a",
           "com.Class2 -> B:");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "A -> B:",
           "    A a -> b",
           "    A a(B[][]) -> b",
           "B -> C:");
   private static final String mappingBaz =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "B -> C:",
           "    B b -> c",
           "    B b(C[][]) -> c",
           "C -> D:");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.Class1 -> C:",
           "    com.Class1 f -> c",
           "    com.Class1 m(com.Class2[][]) -> c",
@@ -60,9 +60,9 @@
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
-    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithExperimental(mappingBaz);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
+    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithPreamble(mappingBaz);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar, mappingForBaz);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
index 587dc6a..657b551 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
@@ -31,7 +31,7 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: 'experimental' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
           "outline.Class -> a:",
           "    1:2:int some.inlinee():75:76 -> a",
           "    1:2:int outline():0 -> a",
@@ -44,11 +44,10 @@
           "    # { 'id':'com.android.tools.r8.outlineCallsite', 'positions': { '1': 4, '2': 5 },"
               + " 'outline':'La;a()I' }");
   private static final String mappingBar =
-      StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}", "a -> b:");
+      StringUtils.unixLines("# {'id':'com.android.tools.r8.mapping','version':'2.2'}", "a -> b:");
   private static final String mappingBaz =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "b -> c:",
           "    4:5:int a():1:2 -> m",
           "x -> y:",
@@ -56,7 +55,7 @@
           "    42:42:int s(int):27:27 -> o");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "outline.Callsite -> y:",
           "    8:8:int outlineCaller(int):23 -> o",
           "    9:9:int foo.bar.baz.outlineCaller(int):98:98 -> o",
@@ -71,9 +70,9 @@
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
-    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithExperimental(mappingBaz);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
+    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromStringWithPreamble(mappingBaz);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar, mappingForBaz);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
index 7b81cdc..93a9153 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
@@ -31,7 +31,7 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: 'experimental' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
           "my.CustomException -> a:",
           "foo.Bar -> x:",
           "    4:4:void other.Class.inlinee():23:23 -> a",
@@ -40,13 +40,13 @@
               + "conditions: ['throws(La;)'], actions: ['removeInnerFrames(1)'] }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: 'experimental' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
           "a -> b:",
           "x -> c:",
           "    8:8:void a(Other.Class):4:4 -> m");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "foo.Bar -> c:",
           "    8:8:void other.Class.inlinee():23:23 -> m",
           "    8:8:void caller(Other.Class):7 -> m",
@@ -56,8 +56,8 @@
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
index 53180d3..0954d3a 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
@@ -31,7 +31,7 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: 'experimental' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
           "com.foo -> a:",
           "# { id: 'com.android.tools.r8.synthesized' }",
           "    int f -> a",
@@ -40,7 +40,7 @@
           "    # { id: 'com.android.tools.r8.synthesized' }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: 'experimental' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
           "a -> b:",
           "    int a -> b",
           "com.bar -> c:",
@@ -49,7 +49,7 @@
           "    # { id: 'com.android.tools.r8.synthesized' }");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
           "com.bar -> c:",
           "# {'id':'com.android.tools.r8.synthesized'}",
           "    void bar() -> a",
@@ -57,14 +57,13 @@
           "com.foo -> b:",
           "# {'id':'com.android.tools.r8.synthesized'}",
           "    int f -> b",
-          // TODO(b/242673239): When fixed, we should emit synthetized info here as well.
           "    void m() -> b",
           "    # {'id':'com.android.tools.r8.synthesized'}");
 
   @Test
   public void testCompose() throws Exception {
-    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithExperimental(mappingFoo);
-    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithExperimental(mappingBar);
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
     String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
     assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeUnknownJsonTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeUnknownJsonTest.java
new file mode 100644
index 0000000..022f42f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeUnknownJsonTest.java
@@ -0,0 +1,60 @@
+// 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.mappingcompose;
+
+import static com.android.tools.r8.mappingcompose.ComposeTestHelpers.doubleToSingleQuote;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MappingComposer;
+import com.android.tools.r8.utils.StringUtils;
+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 ComposeUnknownJsonTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.unixLines(
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
+          "# { id: 'some.unknown.identifier', settings: 'message1' }",
+          "com.foo -> a:",
+          "# { id: 'some.other.unknown.identifier', foo: 'message2' }");
+  private static final String mappingBar =
+      StringUtils.unixLines(
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
+          "# { id: 'some.third.unknown.identifier', bar: 'message3' }",
+          "a -> b:",
+          "# { id: 'some.fourth.unknown.identifier', baz: 'message4' }");
+  private static final String mappingResult =
+      StringUtils.unixLines(
+          "# { id: 'some.unknown.identifier', settings: 'message1' }",
+          "# { id: 'some.third.unknown.identifier', bar: 'message3' }",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "com.foo -> b:",
+          "# {'id':'some.other.unknown.identifier','foo':'message2'}",
+          "# {'id':'some.fourth.unknown.identifier','baz':'message4'}");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromStringWithPreamble(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromStringWithPreamble(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
index 922cae7..1d08f27 100644
--- a/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -15,12 +14,11 @@
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.model.ExternalArtProfileClassRule;
 import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
-import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -56,21 +54,17 @@
 
   @Test
   public void test() throws Exception {
-    Box<ExternalArtProfile> residualArtProfile = new Box<>();
     testForR8(Backend.DEX)
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector ->
                 inspector.assertMergedInto(Foo.class, Bar.class).assertNoOtherClassesMerged())
-        .apply(
-            testBuilder ->
-                ArtProfileTestingUtils.addArtProfileForRewriting(
-                    getArtProfile(), residualArtProfile::set, testBuilder))
+        .addArtProfileForRewriting(getArtProfile())
         .enableInliningAnnotations()
         .setMinApi(AndroidApiLevel.LATEST)
         .compile()
-        .inspect(inspector -> inspect(inspector, residualArtProfile.get()));
+        .inspectResidualArtProfile(this::inspect);
   }
 
   public ExternalArtProfile getArtProfile() {
@@ -85,7 +79,7 @@
         .build();
   }
 
-  public ExternalArtProfile getExpectedResidualArtProfile(CodeInspector inspector) {
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
     ClassSubject barClassSubject = inspector.clazz(Bar.class);
     assertThat(barClassSubject, isPresentAndRenamed());
 
@@ -95,24 +89,13 @@
     MethodSubject worldMethodSubject = barClassSubject.uniqueMethodWithOriginalName("world");
     assertThat(worldMethodSubject, isPresentAndRenamed());
 
-    return ExternalArtProfile.builder()
-        .addRules(
-            ExternalArtProfileClassRule.builder().setClassReference(mainClassReference).build(),
-            ExternalArtProfileMethodRule.builder().setMethodReference(mainMethodReference).build(),
-            ExternalArtProfileClassRule.builder()
-                .setClassReference(barClassSubject.getFinalReference())
-                .build(),
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(helloMethodSubject.getFinalReference())
-                .build(),
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(worldMethodSubject.getFinalReference())
-                .build())
-        .build();
-  }
-
-  private void inspect(CodeInspector inspector, ExternalArtProfile residualArtProfile) {
-    assertEquals(getExpectedResidualArtProfile(inspector), residualArtProfile);
+    profileInspector
+        .assertContainsClassRules(mainClassReference, barClassSubject.getFinalReference())
+        .assertContainsMethodRules(
+            mainMethodReference,
+            helloMethodSubject.getFinalReference(),
+            worldMethodSubject.getFinalReference())
+        .assertContainsNoOtherRules();
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/profile/art/ArtProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/ArtProfileRewritingTest.java
index c5fb462..bbef17f 100644
--- a/src/test/java/com/android/tools/r8/profile/art/ArtProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/ArtProfileRewritingTest.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -15,12 +14,11 @@
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.model.ExternalArtProfileClassRule;
 import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
-import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -53,18 +51,14 @@
 
   @Test
   public void test() throws Exception {
-    Box<ExternalArtProfile> residualArtProfile = new Box<>();
     testForR8(Backend.DEX)
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .apply(
-            testBuilder ->
-                ArtProfileTestingUtils.addArtProfileForRewriting(
-                    getArtProfile(), residualArtProfile::set, testBuilder))
+        .addArtProfileForRewriting(getArtProfile())
         .enableInliningAnnotations()
         .setMinApi(AndroidApiLevel.LATEST)
         .compile()
-        .inspect(inspector -> inspect(inspector, residualArtProfile.get()));
+        .inspectResidualArtProfile(this::inspect);
   }
 
   private ExternalArtProfile getArtProfile() {
@@ -84,33 +78,22 @@
         .build();
   }
 
-  private ExternalArtProfile getExpectedResidualArtProfile(CodeInspector inspector) {
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
     ClassSubject greeterClassSubject = inspector.clazz(Greeter.class);
     assertThat(greeterClassSubject, isPresentAndRenamed());
 
     MethodSubject greetMethodSubject = greeterClassSubject.uniqueMethodWithOriginalName("greet");
     assertThat(greetMethodSubject, isPresentAndRenamed());
 
-    return ExternalArtProfile.builder()
-        .addRules(
-            ExternalArtProfileClassRule.builder().setClassReference(mainClassReference).build(),
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(mainMethodReference)
-                .setMethodRuleInfo(ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build())
-                .build(),
-            ExternalArtProfileClassRule.builder()
-                .setClassReference(greeterClassSubject.getFinalReference())
-                .build(),
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(greetMethodSubject.getFinalReference())
-                .setMethodRuleInfo(
-                    ArtProfileMethodRuleInfoImpl.builder().setIsHot().setIsPostStartup().build())
-                .build())
-        .build();
-  }
-
-  private void inspect(CodeInspector inspector, ExternalArtProfile residualArtProfile) {
-    assertEquals(getExpectedResidualArtProfile(inspector), residualArtProfile);
+    profileInspector
+        .assertContainsClassRules(mainClassReference, greeterClassSubject.getFinalReference())
+        .inspectMethodRule(
+            mainMethodReference,
+            ruleInspector -> ruleInspector.assertIsStartup().assertNotHot().assertNotPostStartup())
+        .inspectMethodRule(
+            greetMethodSubject.getFinalReference(),
+            ruleInspector -> ruleInspector.assertIsHot().assertIsPostStartup().assertNotStartup())
+        .assertContainsNoOtherRules();
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
index 52e0beb..3cd8e10 100644
--- a/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
@@ -18,10 +18,9 @@
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
-import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -59,16 +58,12 @@
   @Test
   public void test() throws Throwable {
     Assume.assumeTrue(libraryDesugaringSpecification.hasEmulatedInterfaceDesugaring(parameters));
-    Box<ExternalArtProfile> residualArtProfile = new Box<>();
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .apply(
-            testBuilder ->
-                ArtProfileTestingUtils.addArtProfileForRewriting(
-                    getArtProfile(), residualArtProfile::set, testBuilder))
+        .addL8ArtProfileForRewriting(getArtProfile())
         .compile()
-        .inspectL8(inspector -> inspect(inspector, residualArtProfile.get()))
+        .inspectL8ResidualArtProfile(this::inspect)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("0");
   }
@@ -91,16 +86,7 @@
         .build();
   }
 
-  private ExternalArtProfile getExpectedResidualArtProfile(MethodSubject forEachMethodSubject) {
-    return ExternalArtProfile.builder()
-        .addRule(
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(forEachMethodSubject.getFinalReference())
-                .build())
-        .build();
-  }
-
-  private void inspect(CodeInspector inspector, ExternalArtProfile residualArtProfile) {
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
     ClassSubject consumerClassSubject =
         inspector.clazz(
             libraryDesugaringSpecification.functionPrefix(parameters) + ".util.function.Consumer");
@@ -117,7 +103,7 @@
                 && libraryDesugaringSpecification == LibraryDesugaringSpecification.JDK8));
     assertEquals(consumerClassSubject.asTypeSubject(), forEachMethodSubject.getParameter(0));
 
-    assertEquals(getExpectedResidualArtProfile(forEachMethodSubject), residualArtProfile);
+    profileInspector.assertContainsMethodRule(forEachMethodSubject).assertContainsNoOtherRules();
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/profile/art/NoSuchClassAndMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/NoSuchClassAndMethodProfileRewritingTest.java
new file mode 100644
index 0000000..7b598a1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/NoSuchClassAndMethodProfileRewritingTest.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art;
+
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+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 NoSuchClassAndMethodProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .setMinApi(parameters.getApiLevel())
+        // TODO(b/266178791): Emit a warning for each discarded item.
+        .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertNoMessages)
+        .inspectResidualArtProfile(this::inspectResidualArtProfile)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    ClassReference missingClassReference = Reference.classFromDescriptor("Lfoo/Missing;");
+    return ExternalArtProfile.builder()
+        .addClassRule(missingClassReference)
+        .addMethodRule(Reference.methodFromDescriptor(missingClassReference, "m", "()V"))
+        .build();
+  }
+
+  private void inspectResidualArtProfile(ArtProfileInspector profileInspector) {
+    // None of the items in the profile exist in the input.
+    // TODO(b/266178791): Discard items from profile that is not in the input app.
+    profileInspector.assertNotEmpty();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/NonEmptyToEmptyProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/NonEmptyToEmptyProfileRewritingTest.java
new file mode 100644
index 0000000..60e14df
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/NonEmptyToEmptyProfileRewritingTest.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art;
+
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 NonEmptyToEmptyProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .inspectResidualArtProfile(this::inspectResidualArtProfile)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(Reference.methodFromMethod(Main.class.getDeclaredMethod("dead")))
+        .build();
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // Verify Main.dead() is removed.
+    ClassSubject mainClassSubject = inspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+    assertThat(mainClassSubject.uniqueMethodWithOriginalName("dead"), isAbsent());
+  }
+
+  private void inspectResidualArtProfile(ArtProfileInspector profileInspector) {
+    // After shaking of Main.dead() the ART profile should become empty.
+    profileInspector.assertEmpty();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+
+    static void dead() {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/ApiOutlineProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/ApiOutlineProfileRewritingTest.java
new file mode 100644
index 0000000..f5959e8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/ApiOutlineProfileRewritingTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import 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 ApiOutlineProfileRewritingTest extends TestBase {
+
+  private static final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public AndroidApiLevel getApiLevelForRuntime() {
+    return parameters.isCfRuntime()
+        ? AndroidApiLevel.B
+        : parameters.getRuntime().maxSupportedApiLevel();
+  }
+
+  public boolean isLibraryClassAlwaysPresent() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(classApiLevel);
+  }
+
+  public boolean isLibraryClassPresentInCurrentRuntime() {
+    return getApiLevelForRuntime().isGreaterThanOrEqualTo(classApiLevel);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getApiLevel() == AndroidApiLevel.B);
+    assertFalse(isLibraryClassPresentInCurrentRuntime());
+    testForJvm()
+        .addProgramClasses(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::inspectRunResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .apply(setMockApiLevelForClass(LibraryClass.class, classApiLevel))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .applyIf(
+            isLibraryClassPresentInCurrentRuntime(),
+            testBuilder -> testBuilder.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::inspectRunResult);
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector)
+      throws Exception {
+    // Verify that outlining happened.
+    verifyThat(inspector, parameters, LibraryClass.class)
+        .hasConstClassOutlinedFromUntil(
+            Main.class.getMethod("main", String[].class), classApiLevel);
+
+    // Check outline was added to program.
+    ClassSubject apiOutlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticApiOutlineClass(Main.class, 0));
+    assertThat(apiOutlineClassSubject, notIf(isPresent(), isLibraryClassAlwaysPresent()));
+
+    MethodSubject apiOutlineMethodSubject = apiOutlineClassSubject.uniqueMethod();
+    assertThat(apiOutlineMethodSubject, notIf(isPresent(), isLibraryClassAlwaysPresent()));
+
+    // TODO(b/265729283): When outlining the residual profile should include the outline method and
+    //  its holder.
+    profileInspector
+        .assertContainsMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .assertContainsNoOtherRules();
+  }
+
+  private void inspectRunResult(SingleTestRunResult<?> runResult) {
+    runResult.applyIf(
+        isLibraryClassPresentInCurrentRuntime(),
+        ignore -> runResult.assertSuccessWithOutputLines("class " + typeName(LibraryClass.class)),
+        ignore -> runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(LibraryClass.class);
+    }
+  }
+
+  // Only present from api 23.
+  public static class LibraryClass {}
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/BackportProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/BackportProfileRewritingTest.java
new file mode 100644
index 0000000..ba5013c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/BackportProfileRewritingTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Objects;
+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 BackportProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(InlinerOptions::disableInlining)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("true");
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .build();
+  }
+
+  private boolean isBackportingObjectsNonNull() {
+    return parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N);
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    ClassSubject backportClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticBackportClass(Main.class, 0));
+    assertThat(backportClassSubject, onlyIf(isBackportingObjectsNonNull(), isPresent()));
+
+    profileInspector.assertContainsMethodRules(MethodReferenceUtils.mainMethod(Main.class));
+
+    // TODO(b/265729283): Should include the backported method and its holder.
+    if (isBackportingObjectsNonNull()) {
+      MethodSubject backportMethodSubject = backportClassSubject.uniqueMethod();
+      assertThat(backportMethodSubject, isPresent());
+    }
+
+    profileInspector.assertContainsNoOtherRules();
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(Objects.nonNull(args));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/DefaultInterfaceMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/DefaultInterfaceMethodProfileRewritingTest.java
new file mode 100644
index 0000000..4e59b75
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/DefaultInterfaceMethodProfileRewritingTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 DefaultInterfaceMethodProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(InlinerOptions::disableInlining)
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(Reference.methodFromMethod(I.class.getDeclaredMethod("m")))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+      ClassSubject iClassSubject = inspector.clazz(I.class);
+      assertThat(iClassSubject, isPresent());
+
+      MethodSubject interfaceMethodSubject = iClassSubject.uniqueMethodWithOriginalName("m");
+      assertThat(interfaceMethodSubject, isPresent());
+
+      profileInspector.assertContainsMethodRule(interfaceMethodSubject);
+    } else {
+      ClassSubject iClassSubject = inspector.clazz(I.class);
+      assertThat(iClassSubject, isPresent());
+
+      ClassSubject aClassSubject = inspector.clazz(A.class);
+      assertThat(aClassSubject, isPresent());
+
+      ClassSubject companionClassSubject =
+          inspector.clazz(SyntheticItemsTestUtils.syntheticCompanionClass(I.class));
+      assertThat(companionClassSubject, isPresent());
+
+      MethodSubject interfaceMethodSubject = iClassSubject.uniqueMethodWithOriginalName("m");
+      assertThat(interfaceMethodSubject, isPresent());
+
+      MethodSubject implementationMethodSubject =
+          aClassSubject.method(interfaceMethodSubject.getFinalReference());
+      assertThat(implementationMethodSubject, isPresent());
+
+      MethodSubject movedMethodSubject = companionClassSubject.uniqueMethod();
+      assertThat(movedMethodSubject, isPresent());
+
+      // TODO(b/265729283): Should also include the two methods from desugaring.
+      profileInspector.assertContainsMethodRule(interfaceMethodSubject);
+    }
+
+    profileInspector.assertContainsNoOtherRules();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I i = System.currentTimeMillis() > 0 ? new A() : new B();
+      i.m();
+    }
+  }
+
+  interface I {
+
+    default void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class A implements I {}
+
+  @NoHorizontalClassMerging
+  static class B implements I {}
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/EnumUnboxingUtilityMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/EnumUnboxingUtilityMethodProfileRewritingTest.java
new file mode 100644
index 0000000..13a8c9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/EnumUnboxingUtilityMethodProfileRewritingTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import 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 EnumUnboxingUtilityMethodProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+        .addOptionsModification(InlinerOptions::disableInlining)
+        .noHorizontalClassMergingOfSynthetics()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .addMethodRule(Reference.methodFromMethod(MyEnum.class.getDeclaredMethod("greet")))
+        .addMethodRule(Reference.methodFromMethod(MyEnum.class.getDeclaredMethod("other")))
+        .addMethodRule(Reference.methodFromMethod(MyEnum.class.getDeclaredMethod("values")))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    ClassSubject mainClassSubject = inspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+    assertThat(mainClassSubject.mainMethod(), isPresent());
+
+    ClassSubject enumUnboxingLocalUtilityClassSubject =
+        inspector.clazz(
+            SyntheticItemsTestUtils.syntheticEnumUnboxingLocalUtilityClass(MyEnum.class));
+    assertThat(enumUnboxingLocalUtilityClassSubject, isPresent());
+
+    MethodSubject localGreetMethodSubject =
+        enumUnboxingLocalUtilityClassSubject.uniqueMethodWithOriginalName("greet");
+    assertThat(localGreetMethodSubject, isPresent());
+
+    MethodSubject localOtherMethodSubject =
+        enumUnboxingLocalUtilityClassSubject.uniqueMethodWithOriginalName("other");
+    assertThat(localOtherMethodSubject, isPresent());
+
+    MethodSubject localValuesMethodSubject =
+        enumUnboxingLocalUtilityClassSubject.uniqueMethodWithOriginalName("values");
+    assertThat(localValuesMethodSubject, isPresent());
+
+    ClassSubject enumUnboxingSharedUtilityClassSubject =
+        inspector.clazz(
+            SyntheticItemsTestUtils.syntheticEnumUnboxingSharedUtilityClass(MyEnum.class));
+    assertThat(enumUnboxingSharedUtilityClassSubject, isPresent());
+    assertThat(enumUnboxingSharedUtilityClassSubject.clinit(), isPresent());
+    assertThat(
+        enumUnboxingSharedUtilityClassSubject.uniqueMethodWithOriginalName("ordinal"), isPresent());
+    assertThat(
+        enumUnboxingSharedUtilityClassSubject.uniqueMethodWithOriginalName("values"), isPresent());
+
+    // TODO(b/265729283): Should also include the above methods from enum unboxing.
+    profileInspector
+        .assertContainsMethodRules(
+            mainClassSubject.mainMethod(),
+            localGreetMethodSubject,
+            localOtherMethodSubject,
+            localValuesMethodSubject)
+        .assertContainsNoOtherRules();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      MyEnum a = System.currentTimeMillis() > 0 ? MyEnum.A : MyEnum.B;
+      MyEnum b = a.other();
+      a.greet();
+      b.greet();
+    }
+  }
+
+  enum MyEnum {
+    A,
+    B;
+
+    void greet() {
+      switch (this) {
+        case A:
+          System.out.print("Hello");
+          break;
+        case B:
+          System.out.println(", world!");
+          break;
+      }
+    }
+
+    MyEnum other() {
+      return MyEnum.values()[1 - ordinal()];
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedConstructorMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedConstructorMethodProfileRewritingTest.java
new file mode 100644
index 0000000..985b723
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedConstructorMethodProfileRewritingTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class HorizontallyMergedConstructorMethodProfileRewritingTest extends TestBase {
+
+  private enum ArtProfileInputOutput {
+    A_CONSTRUCTOR,
+    B_CONSTRUCTOR;
+
+    public ExternalArtProfile getArtProfile() throws Exception {
+      switch (this) {
+        case A_CONSTRUCTOR:
+          return ExternalArtProfile.builder()
+              .addMethodRule(Reference.methodFromMethod(A.class.getDeclaredConstructor()))
+              .build();
+        case B_CONSTRUCTOR:
+          return ExternalArtProfile.builder()
+              .addMethodRule(Reference.methodFromMethod(B.class.getDeclaredConstructor()))
+              .build();
+        default:
+          throw new RuntimeException();
+      }
+    }
+
+    public void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+      ClassSubject aClassSubject = inspector.clazz(A.class);
+      assertThat(aClassSubject, isPresent());
+
+      MethodSubject syntheticConstructorSubject = aClassSubject.uniqueMethod();
+      assertThat(syntheticConstructorSubject, isPresent());
+
+      // TODO(b/265729283): Should contain the constructor.
+      profileInspector.assertEmpty();
+    }
+  }
+
+  @Parameter(0)
+  public ArtProfileInputOutput artProfileInputOutput;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ArtProfileInputOutput.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(artProfileInputOutput.getArtProfile())
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertMergedInto(B.class, A.class).assertNoOtherClassesMerged())
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(artProfileInputOutput::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A();
+      new B();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    public A() {
+      System.out.print("Hello");
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    public B() {
+      System.out.println(", world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedVirtualMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedVirtualMethodProfileRewritingTest.java
new file mode 100644
index 0000000..29e39f9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/HorizontallyMergedVirtualMethodProfileRewritingTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class HorizontallyMergedVirtualMethodProfileRewritingTest extends TestBase {
+
+  private enum ArtProfileInputOutput {
+    A_METHOD,
+    B_METHOD;
+
+    public ExternalArtProfile getArtProfile() throws Exception {
+      switch (this) {
+        case A_METHOD:
+          return ExternalArtProfile.builder()
+              .addMethodRule(Reference.methodFromMethod(A.class.getDeclaredMethod("m")))
+              .build();
+        case B_METHOD:
+          return ExternalArtProfile.builder()
+              .addMethodRule(Reference.methodFromMethod(B.class.getDeclaredMethod("m")))
+              .build();
+        default:
+          throw new RuntimeException();
+      }
+    }
+
+    public void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+      ClassSubject aClassSubject = inspector.clazz(A.class);
+      assertThat(aClassSubject, isPresent());
+
+      String stringConstInMovedMethod = this == A_METHOD ? "Hello" : ", world!";
+      MethodSubject movedMethodSubject =
+          aClassSubject.uniqueMethodThatMatches(
+              method ->
+                  method.isPrivate()
+                      && method
+                          .streamInstructions()
+                          .anyMatch(
+                              instruction -> instruction.isConstString(stringConstInMovedMethod)));
+      assertThat(movedMethodSubject, isPresent());
+
+      MethodSubject syntheticBridgeMethodSubject =
+          aClassSubject.uniqueMethodThatMatches(FoundMethodSubject::isVirtual);
+      assertThat(syntheticBridgeMethodSubject, isPresent());
+
+      // TODO(b/265729283): Should contain the synthetic bridge method from above.
+      profileInspector.assertContainsMethodRule(movedMethodSubject).assertContainsNoOtherRules();
+    }
+  }
+
+  @Parameter(0)
+  public ArtProfileInputOutput artProfileInputOutput;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ArtProfileInputOutput.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(artProfileInputOutput.getArtProfile())
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertMergedInto(B.class, A.class).assertNoOtherClassesMerged())
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
+        .addOptionsModification(
+            options -> options.callSiteOptimizationOptions().setEnableMethodStaticizing(false))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(artProfileInputOutput::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+    }
+  }
+
+  static class A {
+
+    public void m() {
+      System.out.print("Hello");
+    }
+  }
+
+  static class B {
+
+    public void m() {
+      System.out.println(", world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/InvokeSpecialToVirtualMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/InvokeSpecialToVirtualMethodProfileRewritingTest.java
new file mode 100644
index 0000000..75b9758
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/InvokeSpecialToVirtualMethodProfileRewritingTest.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+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.Opcodes;
+
+@RunWith(Parameterized.class)
+public class InvokeSpecialToVirtualMethodProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!", "Hello, world!");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedMain())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(InlinerOptions::disableInlining)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!", "Hello, world!");
+  }
+
+  private byte[] getTransformedMain() throws IOException {
+    BooleanBox anyRewritten = new BooleanBox();
+    return transformer(Main.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              if (opcode == Opcodes.INVOKEVIRTUAL) {
+                assertEquals("m", name);
+                if (anyRewritten.isFalse()) {
+                  visitor.visitMethodInsn(
+                      Opcodes.INVOKESPECIAL, owner, name, descriptor, isInterface);
+                  anyRewritten.set();
+                  return;
+                }
+              }
+              visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+            })
+        .transform();
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(Reference.methodFromMethod(Main.class.getDeclaredMethod("m")))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector)
+      throws Exception {
+    ClassSubject mainClassSubject = inspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+
+    MethodSubject mMethodSubject = mainClassSubject.uniqueMethodWithOriginalName("m");
+    assertThat(mMethodSubject, isPresent());
+
+    if (parameters.isDexRuntime()) {
+      MethodSubject mMovedMethodSubject =
+          mainClassSubject.method(
+              SyntheticItemsTestUtils.syntheticInvokeSpecialMethod(
+                  Main.class.getDeclaredMethod("m")));
+      assertThat(mMovedMethodSubject, isPresent());
+      assertNotEquals(
+          mMethodSubject.getProgramMethod().getName(),
+          mMovedMethodSubject.getProgramMethod().getName());
+    }
+
+    // TODO(b/265729283): Should also contain the synthetic method above when compiling to dex.
+    profileInspector.assertContainsMethodRule(mMethodSubject).assertContainsNoOtherRules();
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Main main = new Main();
+      main.m(); // transformed to invoke-special
+      main.m(); // remains an invoke-virtual (so that Main.m() survives shaking)
+    }
+
+    public void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/MovedStaticInterfaceMethodProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/MovedStaticInterfaceMethodProfileRewritingTest.java
new file mode 100644
index 0000000..e2f0c2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/MovedStaticInterfaceMethodProfileRewritingTest.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MovedStaticInterfaceMethodProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(Reference.methodFromMethod(I.class.getDeclaredMethod("m")))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+      ClassSubject iClassSubject = inspector.clazz(I.class);
+      assertThat(iClassSubject, isPresent());
+
+      MethodSubject mMethodSubject = iClassSubject.uniqueMethodWithOriginalName("m");
+      assertThat(mMethodSubject, isPresent());
+
+      profileInspector.assertContainsMethodRule(mMethodSubject).assertContainsNoOtherRules();
+    } else {
+      ClassSubject companionClassSubject =
+          inspector.clazz(SyntheticItemsTestUtils.syntheticCompanionClass(I.class));
+      assertThat(companionClassSubject, isPresent());
+
+      MethodSubject mMethodSubject = companionClassSubject.uniqueMethodWithOriginalName("m");
+      assertThat(mMethodSubject, isPresent());
+
+      // TODO(b/265729283): Should contain the companion method.
+      profileInspector.assertEmpty();
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I.m();
+    }
+  }
+
+  interface I {
+
+    @NeverInline
+    static void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/NestBasedAccessBridgesProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/NestBasedAccessBridgesProfileRewritingTest.java
new file mode 100644
index 0000000..e376dbc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/NestBasedAccessBridgesProfileRewritingTest.java
@@ -0,0 +1,195 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.profile.art.completeness.NestBasedAccessBridgesProfileRewritingTest.Main.NestMember;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NestBasedAccessBridgesProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
+    testForJvm()
+        .addProgramClassFileData(getProgramClassFileData())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("1", "2", "3", "4");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeFalse(parameters.isCfRuntime() && parameters.asCfRuntime().isOlderThan(CfVm.JDK11));
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(options -> options.callSiteOptimizationOptions().setEnabled(false))
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("1", "2", "3", "4");
+  }
+
+  private List<byte[]> getProgramClassFileData() throws Exception {
+    return ImmutableList.of(
+        transformer(Main.class).setNest(Main.class, NestMember.class).transform(),
+        transformer(NestMember.class)
+            .setNest(Main.class, NestMember.class)
+            .setPrivate(NestMember.class.getDeclaredConstructor())
+            .setPrivate(NestMember.class.getDeclaredField("instanceField"))
+            .setPrivate(NestMember.class.getDeclaredField("staticField"))
+            .setPrivate(NestMember.class.getDeclaredMethod("instanceMethod"))
+            .setPrivate(NestMember.class.getDeclaredMethod("staticMethod"))
+            .transform());
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector)
+      throws Exception {
+    ClassSubject nestMemberClassSubject = inspector.clazz(NestMember.class);
+    assertThat(nestMemberClassSubject, isPresent());
+
+    ClassSubject syntheticConstructorArgumentClassSubject =
+        inspector.clazz(
+            SyntheticItemsTestUtils.syntheticNestConstructorArgumentClass(
+                Reference.classFromClass(NestMember.class)));
+    assertThat(
+        syntheticConstructorArgumentClassSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestInstanceFieldGetterMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestInstanceFieldGetter(
+                    NestMember.class.getDeclaredField("instanceField"))
+                .getMethodName());
+    assertThat(
+        syntheticNestInstanceFieldGetterMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestInstanceFieldSetterMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestInstanceFieldSetter(
+                    NestMember.class.getDeclaredField("instanceField"))
+                .getMethodName());
+    assertThat(
+        syntheticNestInstanceFieldSetterMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestInstanceMethodAccessorMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestInstanceMethodAccessor(
+                    NestMember.class.getDeclaredMethod("instanceMethod"))
+                .getMethodName());
+    assertThat(
+        syntheticNestInstanceMethodAccessorMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestStaticFieldGetterMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestStaticFieldGetter(
+                    NestMember.class.getDeclaredField("staticField"))
+                .getMethodName());
+    assertThat(
+        syntheticNestStaticFieldGetterMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestStaticFieldSetterMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestStaticFieldSetter(
+                    NestMember.class.getDeclaredField("staticField"))
+                .getMethodName());
+    assertThat(
+        syntheticNestStaticFieldSetterMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    MethodSubject syntheticNestStaticMethodAccessorMethodSubject =
+        nestMemberClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestStaticMethodAccessor(
+                    NestMember.class.getDeclaredMethod("staticMethod"))
+                .getMethodName());
+    assertThat(
+        syntheticNestStaticMethodAccessorMethodSubject,
+        notIf(isPresent(), parameters.canUseNestBasedAccesses()));
+
+    // TODO(b/265729283): Should contain the nest bridge methods and the synthesized constructor
+    //  argument class.
+    profileInspector
+        .assertContainsMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .assertContainsNoOtherRules();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      NestMember nestMember = new NestMember();
+      nestMember.instanceField = System.currentTimeMillis() > 0 ? 1 : 0;
+      System.out.println(nestMember.instanceField);
+      System.out.println(nestMember.instanceMethod());
+      NestMember.staticField = System.currentTimeMillis() > 0 ? 3 : 0;
+      System.out.println(NestMember.staticField);
+      System.out.println(NestMember.staticMethod());
+    }
+
+    static class NestMember {
+
+      /*private*/ int instanceField;
+      /*private*/ static int staticField;
+
+      /*private*/ NestMember() {}
+
+      @NeverInline
+      /*private*/ int instanceMethod() {
+        return System.currentTimeMillis() > 0 ? 2 : 0;
+      }
+
+      @NeverInline
+      /*private*/ static int staticMethod() {
+        return System.currentTimeMillis() > 0 ? 4 : 0;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/OutlineOptimizationProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/OutlineOptimizationProfileRewritingTest.java
new file mode 100644
index 0000000..59e0636
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/OutlineOptimizationProfileRewritingTest.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import 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 OutlineOptimizationProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(InlinerOptions::disableInlining)
+        .addOptionsModification(
+            options -> {
+              options.outline.threshold = 2;
+              options.outline.minSize = 2;
+            })
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!", "Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    ClassSubject outlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticOutlineClass(Main.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+
+    MethodSubject outlineMethodSubject = outlineClassSubject.uniqueMethod();
+    assertThat(outlineMethodSubject, isPresent());
+
+    // TODO(b/265729283): Should contain the outline class and method.
+    profileInspector
+        .assertContainsMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+        .assertContainsNoOtherRules();
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      greet1();
+      greet2();
+    }
+
+    static void greet1() {
+      hello();
+      world();
+    }
+
+    static void greet2() {
+      hello();
+      world();
+    }
+
+    public static void hello() {
+      System.out.print("Hello");
+    }
+
+    public static void world() {
+      System.out.println(", world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/SyntheticLambdaClassProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/SyntheticLambdaClassProfileRewritingTest.java
index 3e9ef1c..704fe6a 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/SyntheticLambdaClassProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/SyntheticLambdaClassProfileRewritingTest.java
@@ -7,17 +7,19 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
-import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
-import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Collections;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -27,12 +29,115 @@
 @RunWith(Parameterized.class)
 public class SyntheticLambdaClassProfileRewritingTest extends TestBase {
 
+  private enum ArtProfileInputOutput {
+    MAIN_METHOD,
+    LAMBDA_BRIDGE_METHOD;
+
+    public ExternalArtProfile getArtProfile() {
+      switch (this) {
+        case MAIN_METHOD:
+          // Profile containing Main.main(). Should be rewritten to include the two lambda classes
+          // and their constructors.
+          return ExternalArtProfile.builder()
+              .addMethodRule(MethodReferenceUtils.mainMethod(Main.class))
+              .build();
+        case LAMBDA_BRIDGE_METHOD:
+          // Profile containing the two lambda implementation methods, Main.lambda$main${0,1}.
+          // Should be rewritten to include the two lambda accessibility bridge methods.
+          return ExternalArtProfile.builder()
+              .addMethodRule(
+                  Reference.method(
+                      Reference.classFromClass(Main.class),
+                      "lambda$main$0",
+                      Collections.emptyList(),
+                      null))
+              .addMethodRule(
+                  Reference.method(
+                      Reference.classFromClass(Main.class),
+                      "lambda$main$1",
+                      Collections.emptyList(),
+                      null))
+              .build();
+        default:
+          throw new RuntimeException();
+      }
+    }
+
+    public void inspect(
+        ArtProfileInspector profileInspector, CodeInspector inspector, TestParameters parameters) {
+      ClassSubject mainClassSubject = inspector.clazz(Main.class);
+      assertThat(mainClassSubject, isPresent());
+
+      MethodSubject mainMethodSubject = mainClassSubject.mainMethod();
+      assertThat(mainMethodSubject, isPresent());
+
+      MethodSubject lambdaImplementationMethod0 =
+          mainClassSubject.uniqueMethodWithOriginalName("lambda$main$0");
+      assertThat(lambdaImplementationMethod0, isPresent());
+
+      MethodSubject lambdaImplementationMethod1 =
+          mainClassSubject.uniqueMethodWithOriginalName("lambda$main$1");
+      assertThat(lambdaImplementationMethod1, isPresent());
+
+      // Verify that two lambdas were synthesized when compiling to dex.
+      assertThat(
+          inspector.clazz(SyntheticItemsTestUtils.syntheticLambdaClass(Main.class, 0)),
+          onlyIf(parameters.isDexRuntime(), isPresent()));
+      assertThat(
+          inspector.clazz(SyntheticItemsTestUtils.syntheticLambdaClass(Main.class, 1)),
+          onlyIf(parameters.isDexRuntime(), isPresent()));
+
+      if (parameters.isCfRuntime()) {
+        switch (this) {
+          case MAIN_METHOD:
+            profileInspector
+                .assertContainsMethodRule(mainMethodSubject)
+                .assertContainsNoOtherRules();
+            break;
+          case LAMBDA_BRIDGE_METHOD:
+            profileInspector
+                .assertContainsMethodRules(lambdaImplementationMethod0, lambdaImplementationMethod1)
+                .assertContainsNoOtherRules();
+            break;
+          default:
+            throw new RuntimeException();
+        }
+      } else {
+        assert parameters.isDexRuntime();
+        switch (this) {
+          case MAIN_METHOD:
+            // TODO(b/265729283): Since Main.main() is in the art profile, so should the two
+            //  synthetic lambdas be along with their initializers. Since Main.lambda$main$*() is
+            //  not in the art profile, the interface method implementation does not need to be
+            //  included in the profile.
+            profileInspector
+                .assertContainsMethodRule(mainMethodSubject)
+                .assertContainsNoOtherRules();
+            break;
+          case LAMBDA_BRIDGE_METHOD:
+            // TODO(b/265729283): Since Main.lambda$main$*() is in the art profile, so should the
+            //  two accessibility bridges be.
+            profileInspector
+                .assertContainsMethodRules(lambdaImplementationMethod0, lambdaImplementationMethod1)
+                .assertContainsNoOtherRules();
+            break;
+          default:
+            throw new RuntimeException();
+        }
+      }
+    }
+  }
+
   @Parameter(0)
+  public ArtProfileInputOutput artProfileInputOutput;
+
+  @Parameter(1)
   public TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameters(name = "{1}, {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ArtProfileInputOutput.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   @Test
@@ -40,48 +145,20 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .apply(
-            testBuilder ->
-                ArtProfileTestingUtils.addArtProfileForRewriting(
-                    getArtProfile(), this::inspectResidualArtProfile, testBuilder))
+        .addKeepRules(
+            "-neverinline class " + Main.class.getTypeName() + " { void lambda$main$*(); }")
+        .addArtProfileForRewriting(artProfileInputOutput.getArtProfile())
+        .enableProguardTestOptions()
         .noHorizontalClassMergingOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
-        .inspect(this::inspect)
+        .inspectResidualArtProfile(
+            (profileInspector, inspector) ->
+                artProfileInputOutput.inspect(profileInspector, inspector, parameters))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("Hello, world!");
   }
 
-  private ExternalArtProfile getArtProfile() {
-    return ExternalArtProfile.builder()
-        .addRule(
-            ExternalArtProfileMethodRule.builder()
-                .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
-                .build())
-        .build();
-  }
-
-  private void inspect(CodeInspector inspector) {
-    // Verify that two lambdas were synthesized when compiling to dex.
-    assertThat(
-        inspector.clazz(SyntheticItemsTestUtils.syntheticLambdaClass(Main.class, 0)),
-        onlyIf(parameters.isDexRuntime(), isPresent()));
-    assertThat(
-        inspector.clazz(SyntheticItemsTestUtils.syntheticLambdaClass(Main.class, 1)),
-        onlyIf(parameters.isDexRuntime(), isPresent()));
-  }
-
-  private void inspectResidualArtProfile(ExternalArtProfile residualArtProfile) {
-    if (parameters.isCfRuntime()) {
-      assertEquals(getArtProfile(), residualArtProfile);
-    } else {
-      assert parameters.isDexRuntime();
-      // TODO(b/265729283): Since Main.main() is in the art profile, so should the two synthetic
-      //  lambdas be.
-      assertEquals(getArtProfile(), residualArtProfile);
-    }
-  }
-
   static class Main {
 
     public static void main(String[] args) {
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
new file mode 100644
index 0000000..135a501
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.completeness;
+
+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.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingBridgeProfileRewritingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArtProfileForRewriting(getArtProfile())
+        .addOptionsModification(InlinerOptions::setOnlyForceInlining)
+        .addOptionsModification(
+            options -> options.callSiteOptimizationOptions().setEnableMethodStaticizing(false))
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectResidualArtProfile(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private ExternalArtProfile getArtProfile() throws Exception {
+    return ExternalArtProfile.builder()
+        .addMethodRule(Reference.methodFromMethod(A.class.getDeclaredMethod("m")))
+        .build();
+  }
+
+  private void inspect(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    ClassSubject bClassSubject = inspector.clazz(B.class);
+    assertThat(bClassSubject, isPresent());
+
+    MethodSubject movedMethodSubject =
+        bClassSubject.uniqueMethodThatMatches(FoundMethodSubject::isPrivate);
+    assertThat(movedMethodSubject, isPresent());
+
+    MethodSubject syntheticBridgeMethodSubject =
+        bClassSubject.uniqueMethodThatMatches(FoundMethodSubject::isVirtual);
+    assertThat(syntheticBridgeMethodSubject, isPresent());
+
+    // TODO(b/265729283): Should also contain the synthetic bridge method above.
+    profileInspector.assertContainsMethodRule(movedMethodSubject).assertContainsNoOtherRules();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new B().m();
+    }
+  }
+
+  static class A {
+
+    public void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  static class B extends A {}
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/diagnostic/HumanReadableArtProfileParserErrorDiagnosticFromArtProfileTest.java b/src/test/java/com/android/tools/r8/profile/art/diagnostic/HumanReadableArtProfileParserErrorDiagnosticFromArtProfileTest.java
index ba3a623..c22ad2a 100644
--- a/src/test/java/com/android/tools/r8/profile/art/diagnostic/HumanReadableArtProfileParserErrorDiagnosticFromArtProfileTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/diagnostic/HumanReadableArtProfileParserErrorDiagnosticFromArtProfileTest.java
@@ -10,20 +10,13 @@
 import static org.hamcrest.CoreMatchers.equalTo;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileBuilder;
-import com.android.tools.r8.profile.art.ArtProfileClassRuleInfo;
-import com.android.tools.r8.profile.art.ArtProfileConsumer;
-import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfo;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
-import com.android.tools.r8.profile.art.ArtProfileRuleConsumer;
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.UTF8TextInputStream;
@@ -49,7 +42,7 @@
   public void testD8() throws Exception {
     testForD8()
         .addProgramClasses(Main.class)
-        .addArtProfileForRewriting(createArtProfileProvider(), createArtProfileConsumer())
+        .addArtProfileForRewriting(createArtProfileProvider())
         .release()
         .setMinApi(AndroidApiLevel.LATEST)
         .compileWithExpectedDiagnostics(this::inspectDiagnostics);
@@ -60,7 +53,7 @@
     testForR8(Backend.DEX)
         .addProgramClasses(Main.class)
         .addKeepMainRule(Main.class)
-        .addArtProfileForRewriting(createArtProfileProvider(), createArtProfileConsumer())
+        .addArtProfileForRewriting(createArtProfileProvider())
         .release()
         .setMinApi(AndroidApiLevel.LATEST)
         .compileWithExpectedDiagnostics(this::inspectDiagnostics);
@@ -83,34 +76,6 @@
     };
   }
 
-  private ArtProfileConsumer createArtProfileConsumer() {
-    return new ArtProfileConsumer() {
-
-      @Override
-      public ArtProfileRuleConsumer getRuleConsumer() {
-        return new ArtProfileRuleConsumer() {
-
-          @Override
-          public void acceptClassRule(
-              ClassReference classReference, ArtProfileClassRuleInfo classRuleInfo) {
-            // Ignore.
-          }
-
-          @Override
-          public void acceptMethodRule(
-              MethodReference methodReference, ArtProfileMethodRuleInfo methodRuleInfo) {
-            // Ignore.
-          }
-        };
-      }
-
-      @Override
-      public void finished(DiagnosticsHandler handler) {
-        // Ignore.
-      }
-    };
-  }
-
   private void inspectDiagnostics(TestDiagnosticMessages diagnostics) {
     diagnostics.assertErrorsMatch(
         allOf(
diff --git a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
index 0082727..9951db3 100644
--- a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
+++ b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
@@ -4,9 +4,13 @@
 
 package com.android.tools.r8.profile.art.model;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfo;
+import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfoImpl;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.function.Consumer;
 
 /**
@@ -19,9 +23,49 @@
  */
 public class ExternalArtProfile {
 
-  private final List<ExternalArtProfileRule> rules;
+  private abstract static class ReferenceBox<R> {
 
-  ExternalArtProfile(List<ExternalArtProfileRule> rules) {
+    private final R reference;
+
+    ReferenceBox(R reference) {
+      this.reference = reference;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ReferenceBox<?> that = (ReferenceBox<?>) o;
+      return reference.equals(that.reference);
+    }
+
+    @Override
+    public int hashCode() {
+      return reference.hashCode();
+    }
+  }
+
+  private static class ClassReferenceBox extends ReferenceBox<ClassReference> {
+
+    ClassReferenceBox(ClassReference reference) {
+      super(reference);
+    }
+  }
+
+  private static class MethodReferenceBox extends ReferenceBox<MethodReference> {
+
+    MethodReferenceBox(MethodReference reference) {
+      super(reference);
+    }
+  }
+
+  private final Map<ReferenceBox<?>, ExternalArtProfileRule> rules;
+
+  ExternalArtProfile(Map<ReferenceBox<?>, ExternalArtProfileRule> rules) {
     this.rules = rules;
   }
 
@@ -29,14 +73,34 @@
     return new Builder();
   }
 
+  public boolean containsClassRule(ClassReference classReference) {
+    return rules.containsKey(new ClassReferenceBox(classReference));
+  }
+
+  public boolean containsMethodRule(MethodReference methodReference) {
+    return rules.containsKey(new MethodReferenceBox(methodReference));
+  }
+
   public void forEach(
       Consumer<ExternalArtProfileClassRule> classRuleConsumer,
       Consumer<ExternalArtProfileMethodRule> methodRuleConsumer) {
-    for (ExternalArtProfileRule rule : rules) {
+    for (ExternalArtProfileRule rule : rules.values()) {
       rule.accept(classRuleConsumer, methodRuleConsumer);
     }
   }
 
+  public ExternalArtProfileClassRule getClassRule(ClassReference classReference) {
+    return (ExternalArtProfileClassRule) rules.get(new ClassReferenceBox(classReference));
+  }
+
+  public ExternalArtProfileMethodRule getMethodRule(MethodReference methodReference) {
+    return (ExternalArtProfileMethodRule) rules.get(new MethodReferenceBox(methodReference));
+  }
+
+  public int size() {
+    return rules.size();
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
@@ -54,17 +118,46 @@
     return rules.hashCode();
   }
 
+  @Override
+  public String toString() {
+    return StringUtils.join(
+        System.lineSeparator(), rules.values(), ExternalArtProfileRule::toString);
+  }
+
   public static class Builder {
 
-    private final List<ExternalArtProfileRule> rules = new ArrayList<>();
+    private final Map<ReferenceBox<?>, ExternalArtProfileRule> rules = new LinkedHashMap<>();
+
+    public Builder addClassRule(ClassReference classReference) {
+      return addRule(
+          ExternalArtProfileClassRule.builder().setClassReference(classReference).build());
+    }
+
+    public Builder addMethodRule(MethodReference methodReference) {
+      return addMethodRule(methodReference, ArtProfileMethodRuleInfoImpl.empty());
+    }
+
+    public Builder addMethodRule(
+        MethodReference methodReference, ArtProfileMethodRuleInfo methodRuleInfo) {
+      return addRule(
+          ExternalArtProfileMethodRule.builder()
+              .setMethodReference(methodReference)
+              .setMethodRuleInfo(methodRuleInfo)
+              .build());
+    }
 
     public Builder addRule(ExternalArtProfileRule rule) {
-      rules.add(rule);
+      rule.accept(
+          classRule -> rules.put(new ClassReferenceBox(classRule.getClassReference()), classRule),
+          methodRule ->
+              rules.put(new MethodReferenceBox(methodRule.getMethodReference()), methodRule));
       return this;
     }
 
     public Builder addRules(ExternalArtProfileRule... rules) {
-      Collections.addAll(this.rules, rules);
+      for (ExternalArtProfileRule rule : rules) {
+        addRule(rule);
+      }
       return this;
     }
 
diff --git a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileClassRule.java b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileClassRule.java
index 84cc34b..956cd6e 100644
--- a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileClassRule.java
+++ b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileClassRule.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.references.ClassReference;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * Represents a class rule from an ART baseline profile, backed by {@link ClassReference}. Class
@@ -36,6 +37,13 @@
   }
 
   @Override
+  public boolean test(
+      Predicate<ExternalArtProfileClassRule> classRuleConsumer,
+      Predicate<ExternalArtProfileMethodRule> methodRuleConsumer) {
+    return classRuleConsumer.test(this);
+  }
+
+  @Override
   public boolean equals(Object obj) {
     if (this == obj) {
       return true;
@@ -52,6 +60,11 @@
     return classReference.hashCode();
   }
 
+  @Override
+  public String toString() {
+    return classReference.getDescriptor();
+  }
+
   public static class Builder {
 
     private ClassReference classReference;
diff --git a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileMethodRule.java b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileMethodRule.java
index 86894b9..4eadead 100644
--- a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileMethodRule.java
+++ b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileMethodRule.java
@@ -7,7 +7,9 @@
 import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfo;
 import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfoImpl;
 import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Represents a method rule from an ART baseline profile, backed by {@link MethodReference}. */
 public class ExternalArtProfileMethodRule extends ExternalArtProfileRule {
@@ -43,6 +45,13 @@
   }
 
   @Override
+  public boolean test(
+      Predicate<ExternalArtProfileClassRule> classRuleConsumer,
+      Predicate<ExternalArtProfileMethodRule> methodRuleConsumer) {
+    return methodRuleConsumer.test(this);
+  }
+
+  @Override
   public boolean equals(Object obj) {
     if (this == obj) {
       return true;
@@ -60,6 +69,11 @@
     return methodReference.hashCode();
   }
 
+  @Override
+  public String toString() {
+    return methodRuleInfo.toString() + MethodReferenceUtils.toSmaliString(methodReference);
+  }
+
   public static class Builder {
 
     private MethodReference methodReference;
diff --git a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileRule.java b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileRule.java
index bd3c99e..7138589 100644
--- a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileRule.java
+++ b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfileRule.java
@@ -5,10 +5,15 @@
 package com.android.tools.r8.profile.art.model;
 
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 public abstract class ExternalArtProfileRule {
 
   public abstract void accept(
       Consumer<ExternalArtProfileClassRule> classRuleConsumer,
       Consumer<ExternalArtProfileMethodRule> methodRuleConsumer);
+
+  public abstract boolean test(
+      Predicate<ExternalArtProfileClassRule> classRuleConsumer,
+      Predicate<ExternalArtProfileMethodRule> methodRuleConsumer);
 }
diff --git a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileClassRuleInspector.java b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileClassRuleInspector.java
new file mode 100644
index 0000000..eded89d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileClassRuleInspector.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.utils;
+
+import com.android.tools.r8.profile.art.model.ExternalArtProfileClassRule;
+
+public class ArtProfileClassRuleInspector {
+
+  private final ExternalArtProfileClassRule classRule;
+
+  ArtProfileClassRuleInspector(ExternalArtProfileClassRule classRule) {
+    this.classRule = classRule;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java
new file mode 100644
index 0000000..507dd67
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.utils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.model.ExternalArtProfileClassRule;
+import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ArtProfileInspector {
+
+  private final ExternalArtProfile artProfile;
+
+  private final Set<ClassReference> checkedClassReferences = new HashSet<>();
+  private final Set<MethodReference> checkedMethodReferences = new HashSet<>();
+
+  public ArtProfileInspector(ExternalArtProfile artProfile) {
+    this.artProfile = artProfile;
+  }
+
+  public ArtProfileInspector assertEmpty() {
+    assertEquals(0, artProfile.size());
+    return this;
+  }
+
+  public ArtProfileInspector assertNotEmpty() {
+    assertNotEquals(0, artProfile.size());
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsClassRule(ClassReference classReference) {
+    assertThat(artProfile, ArtProfileMatchers.containsClassRule(classReference));
+    checkedClassReferences.add(classReference);
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsClassRule(ClassSubject classSubject) {
+    return assertContainsClassRule(classSubject.getFinalReference());
+  }
+
+  public ArtProfileInspector assertContainsClassRules(ClassReference... classReferences) {
+    for (ClassReference classReference : classReferences) {
+      assertContainsClassRule(classReference);
+    }
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsMethodRule(MethodReference methodReference) {
+    assertThat(artProfile, ArtProfileMatchers.containsMethodRule(methodReference));
+    checkedMethodReferences.add(methodReference);
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsMethodRules(MethodReference... methodReferences) {
+    for (MethodReference methodReference : methodReferences) {
+      assertContainsMethodRule(methodReference);
+    }
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsMethodRule(MethodSubject methodSubject) {
+    return assertContainsMethodRule(methodSubject.getFinalReference());
+  }
+
+  public ArtProfileInspector assertContainsMethodRules(MethodSubject... methodSubjects) {
+    for (MethodSubject methodSubject : methodSubjects) {
+      assertContainsMethodRule(methodSubject);
+    }
+    return this;
+  }
+
+  public ArtProfileInspector assertContainsNoOtherRules() {
+    assertEquals(checkedClassReferences.size() + checkedMethodReferences.size(), artProfile.size());
+    return this;
+  }
+
+  public ArtProfileInspector inspectClassRule(
+      ClassReference classReference, Consumer<ArtProfileClassRuleInspector> inspector) {
+    assertContainsClassRule(classReference);
+    ExternalArtProfileClassRule classRule = artProfile.getClassRule(classReference);
+    inspector.accept(new ArtProfileClassRuleInspector(classRule));
+    return this;
+  }
+
+  public ArtProfileInspector inspectMethodRule(
+      MethodReference methodReference, Consumer<ArtProfileMethodRuleInspector> inspector) {
+    assertContainsMethodRule(methodReference);
+    ExternalArtProfileMethodRule methodRule = artProfile.getMethodRule(methodReference);
+    inspector.accept(new ArtProfileMethodRuleInspector(methodRule));
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMatchers.java b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMatchers.java
new file mode 100644
index 0000000..64355a9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMatchers.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.profile.art.utils;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.ClassReferenceUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class ArtProfileMatchers {
+
+  public static Matcher<ExternalArtProfile> containsClassRule(ClassReference classReference) {
+    return new TypeSafeMatcher<ExternalArtProfile>() {
+      @Override
+      protected boolean matchesSafely(ExternalArtProfile subject) {
+        return subject.containsClassRule(classReference);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(
+            "contains class rule " + ClassReferenceUtils.toSmaliString(classReference));
+      }
+
+      @Override
+      public void describeMismatchSafely(ExternalArtProfile subject, Description description) {
+        description.appendText("profile did not");
+      }
+    };
+  }
+
+  public static Matcher<ExternalArtProfile> containsClassRule(ClassSubject classSubject) {
+    assertThat(classSubject, isPresent());
+    return containsClassRule(classSubject.getFinalReference());
+  }
+
+  public static Matcher<ExternalArtProfile> containsMethodRule(MethodReference methodReference) {
+    return new TypeSafeMatcher<ExternalArtProfile>() {
+      @Override
+      protected boolean matchesSafely(ExternalArtProfile subject) {
+        return subject.containsMethodRule(methodReference);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(
+            "contains method rule " + MethodReferenceUtils.toSmaliString(methodReference));
+      }
+
+      @Override
+      public void describeMismatchSafely(ExternalArtProfile subject, Description description) {
+        description.appendText("profile did not");
+      }
+    };
+  }
+
+  public static Matcher<ExternalArtProfile> containsMethodRule(MethodSubject methodSubject) {
+    assertThat(methodSubject, isPresent());
+    return containsMethodRule(methodSubject.getFinalReference());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMethodRuleInspector.java b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMethodRuleInspector.java
new file mode 100644
index 0000000..fbca727
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileMethodRuleInspector.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.profile.art.utils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.profile.art.model.ExternalArtProfileMethodRule;
+
+public class ArtProfileMethodRuleInspector {
+
+  private final ExternalArtProfileMethodRule methodRule;
+
+  ArtProfileMethodRuleInspector(ExternalArtProfileMethodRule methodRule) {
+    this.methodRule = methodRule;
+  }
+
+  public ArtProfileMethodRuleInspector assertIsHot() {
+    assertTrue(methodRule.getMethodRuleInfo().isHot());
+    return this;
+  }
+
+  public ArtProfileMethodRuleInspector assertIsStartup() {
+    assertTrue(methodRule.getMethodRuleInfo().isStartup());
+    return this;
+  }
+
+  public ArtProfileMethodRuleInspector assertIsPostStartup() {
+    assertTrue(methodRule.getMethodRuleInfo().isPostStartup());
+    return this;
+  }
+
+  public ArtProfileMethodRuleInspector assertNotHot() {
+    assertFalse(methodRule.getMethodRuleInfo().isHot());
+    return this;
+  }
+
+  public ArtProfileMethodRuleInspector assertNotStartup() {
+    assertFalse(methodRule.getMethodRuleInfo().isStartup());
+    return this;
+  }
+
+  public ArtProfileMethodRuleInspector assertNotPostStartup() {
+    assertFalse(methodRule.getMethodRuleInfo().isPostStartup());
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileTestingUtils.java b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileTestingUtils.java
index 7443e49..c86ad4d 100644
--- a/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/profile/art/utils/ArtProfileTestingUtils.java
@@ -5,8 +5,6 @@
 package com.android.tools.r8.profile.art.utils;
 
 import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.R8TestBuilder;
-import com.android.tools.r8.desugar.desugaredlibrary.test.DesugaredLibraryTestBuilder;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileBuilder;
 import com.android.tools.r8.profile.art.ArtProfileClassRuleInfo;
@@ -23,30 +21,8 @@
 
 public class ArtProfileTestingUtils {
 
-  /**
-   * Adds the given {@param artProfile} as an ART profile for rewriting. The residual ART profile
-   * will be forwarded to the given test inspector, {@param residualArtProfileInspector}.
-   */
-  public static void addArtProfileForRewriting(
-      ExternalArtProfile artProfile,
-      Consumer<ExternalArtProfile> residualArtProfileInspector,
-      R8TestBuilder<?> testBuilder) {
-    testBuilder.addArtProfileForRewriting(
-        createArtProfileProvider(artProfile),
-        createResidualArtProfileConsumer(residualArtProfileInspector));
-  }
-
-  public static void addArtProfileForRewriting(
-      ExternalArtProfile artProfile,
-      Consumer<ExternalArtProfile> residualArtProfileInspector,
-      DesugaredLibraryTestBuilder<?> testBuilder) {
-    testBuilder.addL8ArtProfileForRewriting(
-        createArtProfileProvider(artProfile),
-        createResidualArtProfileConsumer(residualArtProfileInspector));
-  }
-
   // Creates an ArtProfileProvider for passing the given ART profile to a D8/L8/R8 compilation.
-  private static ArtProfileProvider createArtProfileProvider(ExternalArtProfile artProfile) {
+  public static ArtProfileProvider createArtProfileProvider(ExternalArtProfile artProfile) {
     return new ArtProfileProvider() {
 
       @Override
@@ -78,7 +54,7 @@
   }
 
   // Creates an ArtProfileConsumer for accepting the residual ART profile from the compilation.
-  private static ArtProfileConsumer createResidualArtProfileConsumer(
+  public static ArtProfileConsumer createResidualArtProfileConsumer(
       Consumer<ExternalArtProfile> residualArtProfileInspector) {
     return new ArtProfileConsumer() {
 
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java b/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java
index 367e1b6..861e990 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java
@@ -193,8 +193,6 @@
             .setMappingSupplier(
                 ProguardMappingSupplier.builder()
                     .setProguardMapProducer(ProguardMapProducer.fromPath(mappingFile))
-                    // TODO(b/241763080): Remove when stable.
-                    .setAllowExperimental(true)
                     .build())
             .build());
     return retracedLines;
@@ -246,8 +244,6 @@
         .addProgramFiles(r8Input)
         .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
         .addKeepRuleFiles(MAIN_KEEP)
-        // TODO(b/241763080): Remove when stable version is default.
-        .enableExperimentalMapFileVersion()
         .allowUnusedProguardConfigurationRules()
         .addDontObfuscate()
         .compile()
@@ -264,10 +260,10 @@
         .setMode(CompilationMode.RELEASE)
         .addProgramFiles(r8Input)
         .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
-        .enableExperimentalMapFileVersion()
-        // TODO(b/241763080): Enable CF PC test mapping for this compilation.
         .addOptionsModification(
-            options -> options.mappingComposeOptions().enableExperimentalMappingComposition = true)
+            options -> {
+              assertTrue(options.mappingComposeOptions().enableExperimentalMappingComposition);
+            })
         .apply(
             b ->
                 b.getBuilder()
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index 1b67cfc..cf968ea 100644
--- a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -3,18 +3,28 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.synthesis;
 
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_FIELD_GET_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_FIELD_PUT_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_METHOD_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_STATIC_GET_FIELD_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_STATIC_METHOD_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring.NEST_ACCESS_STATIC_PUT_FIELD_NAME_PREFIX;
 import static com.android.tools.r8.synthesis.SyntheticNaming.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
 import static org.hamcrest.CoreMatchers.containsString;
 
+import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
 import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringForTesting;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.synthesis.SyntheticNaming.Phase;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Collections;
 import org.hamcrest.Matcher;
 
 public class SyntheticItemsTestUtils {
@@ -53,6 +63,15 @@
         originalMethod.getMethodDescriptor());
   }
 
+  public static MethodReference syntheticInvokeSpecialMethod(Method method) {
+    MethodReference originalMethod = Reference.methodFromMethod(method);
+    return Reference.method(
+        originalMethod.getHolderClass(),
+        InvokeSpecialToSelfDesugaring.INVOKE_SPECIAL_BRIDGE_PREFIX + method.getName(),
+        originalMethod.getFormalTypes(),
+        originalMethod.getReturnType());
+  }
+
   public static MethodReference syntheticBackportWithForwardingMethod(
       ClassReference clazz, int id, MethodReference method) {
     // For backports with forwarding the backported method is not static, so the original method
@@ -127,6 +146,75 @@
         originalMethod.getMethodDescriptor());
   }
 
+  public static ClassReference syntheticNestConstructorArgumentClass(
+      ClassReference classReference) {
+    return Reference.classFromDescriptor(
+        SyntheticNaming.createDescriptor(
+            "", naming.INIT_TYPE_ARGUMENT, classReference.getBinaryName(), ""));
+  }
+
+  public static MethodReference syntheticNestInstanceFieldGetter(Field field) {
+    FieldReference fieldReference = Reference.fieldFromField(field);
+    return Reference.method(
+        fieldReference.getHolderClass(),
+        NEST_ACCESS_FIELD_GET_NAME_PREFIX + field.getName(),
+        Collections.emptyList(),
+        fieldReference.getFieldType());
+  }
+
+  public static MethodReference syntheticNestInstanceFieldSetter(Field field) {
+    FieldReference fieldReference = Reference.fieldFromField(field);
+    return Reference.method(
+        fieldReference.getHolderClass(),
+        NEST_ACCESS_FIELD_PUT_NAME_PREFIX + field.getName(),
+        ImmutableList.of(fieldReference.getFieldType()),
+        null);
+  }
+
+  public static MethodReference syntheticNestInstanceMethodAccessor(Method method) {
+    MethodReference originalMethod = Reference.methodFromMethod(method);
+    return Reference.methodFromDescriptor(
+        originalMethod.getHolderClass(),
+        NEST_ACCESS_METHOD_NAME_PREFIX + method.getName(),
+        originalMethod.getMethodDescriptor());
+  }
+
+  public static MethodReference syntheticNestStaticFieldGetter(Field field) {
+    FieldReference fieldReference = Reference.fieldFromField(field);
+    return Reference.method(
+        fieldReference.getHolderClass(),
+        NEST_ACCESS_STATIC_GET_FIELD_NAME_PREFIX + field.getName(),
+        Collections.emptyList(),
+        fieldReference.getFieldType());
+  }
+
+  public static MethodReference syntheticNestStaticFieldSetter(Field field) {
+    FieldReference fieldReference = Reference.fieldFromField(field);
+    return Reference.method(
+        fieldReference.getHolderClass(),
+        NEST_ACCESS_STATIC_PUT_FIELD_NAME_PREFIX + field.getName(),
+        ImmutableList.of(fieldReference.getFieldType()),
+        null);
+  }
+
+  public static MethodReference syntheticNestStaticMethodAccessor(Method method) {
+    MethodReference originalMethod = Reference.methodFromMethod(method);
+    return Reference.methodFromDescriptor(
+        originalMethod.getHolderClass(),
+        NEST_ACCESS_STATIC_METHOD_NAME_PREFIX + method.getName(),
+        originalMethod.getMethodDescriptor());
+  }
+
+  public static ClassReference syntheticEnumUnboxingLocalUtilityClass(Class<?> clazz) {
+    return Reference.classFromTypeName(
+        clazz.getTypeName() + naming.ENUM_UNBOXING_LOCAL_UTILITY_CLASS.getDescriptor());
+  }
+
+  public static ClassReference syntheticEnumUnboxingSharedUtilityClass(Class<?> clazz) {
+    return Reference.classFromTypeName(
+        clazz.getTypeName() + naming.ENUM_UNBOXING_SHARED_UTILITY_CLASS.getDescriptor());
+  }
+
   public static boolean isEnumUnboxingSharedUtilityClass(ClassReference reference) {
     return SyntheticNaming.isSynthetic(reference, null, naming.ENUM_UNBOXING_SHARED_UTILITY_CLASS);
   }
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 7742119..4adbc9a 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -502,14 +502,22 @@
     return setAccessFlags(method, MethodAccessFlags::setBridge);
   }
 
+  public ClassFileTransformer setPrivate(Constructor<?> constructor) {
+    return setAccessFlags(constructor, ClassFileTransformer::setPrivate);
+  }
+
   public ClassFileTransformer setPrivate(Field field) {
-    return setAccessFlags(
-        field,
-        accessFlags -> {
-          accessFlags.setPrivate();
-          accessFlags.unsetProtected();
-          accessFlags.unsetPublic();
-        });
+    return setAccessFlags(field, ClassFileTransformer::setPrivate);
+  }
+
+  public ClassFileTransformer setPrivate(Method method) {
+    return setAccessFlags(method, ClassFileTransformer::setPrivate);
+  }
+
+  private static void setPrivate(AccessFlags<?> accessFlags) {
+    accessFlags.unsetPublic();
+    accessFlags.unsetProtected();
+    accessFlags.setPrivate();
   }
 
   public ClassFileTransformer setPublic(Method method) {
@@ -522,16 +530,6 @@
         });
   }
 
-  public ClassFileTransformer setPrivate(Method method) {
-    return setAccessFlags(
-        method,
-        accessFlags -> {
-          accessFlags.unsetPublic();
-          accessFlags.unsetProtected();
-          accessFlags.setPrivate();
-        });
-  }
-
   public ClassFileTransformer setSynthetic(Method method) {
     return setAccessFlags(method, AccessFlags::setSynthetic);
   }