Merge commit '712a8cafb44ae7264f7f4a1614da452239e735f0' into dev-release

Change-Id: I1596cc8938eb7a85540dde68e8c1421c1b6e05bc
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index c41aa8b..c924bdcf 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -23,6 +23,7 @@
 - [Annotating code using reflection](#using-reflection)
   - [Invoking methods](#using-reflection-methods)
   - [Accessing fields](#using-reflection-fields)
+  - [Accessing annotations](#using-reflection-annotations)
 - [Annotating code used by reflection (or via JNI)](#used-by-reflection)
 - [Annotating APIs](#apis)
 - [Migrating rules to annotations](#migrating-rules)
@@ -151,6 +152,79 @@
 ```
 
 
+### Accessing annotations<a name="using-reflection-annotations"></a>
+
+If your program is reflectively inspecting annotations on classes, methods or fields, you
+will need to declare additional "annotation constraints" about what assumptions are made
+about the annotations.
+
+In the following example, we have defined an annotation that will record the printing name we
+would like to use for fields instead of printing the concrete field name. That may be useful
+so that the field can be renamed to follow coding conventions for example.
+
+We are only interested in matching objects that contain fields annotated by `MyNameAnnotation`,
+that is specified using [@KeepTarget.fieldAnnotatedByClassConstant](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#fieldAnnotatedByClassConstant()).
+
+At runtime we need to be able to find the annotation too, so we add a constraint on the
+annotation using [@KeepTarget.constrainAnnotations](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constrainAnnotations()).
+
+Finally, for the sake of example, we don't actually care about the name of the fields
+themselves, so we explicitly declare the smaller set of constraints to be
+[KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP) since we must find the fields via `Class.getDeclaredFields` as well as
+[KeepConstraint.VISIBILITY_RELAX](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#VISIBILITY_RELAX) and [KeepConstraint.FIELD_GET](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#FIELD_GET) in order to be able to get
+the actual field value without accessibility errors.
+
+The effect is that the default constraint [KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) is not specified which allows
+the shrinker to rename the fields at will.
+
+
+```
+public class MyAnnotationPrinter {
+
+  @Target(ElementType.FIELD)
+  @Retention(RetentionPolicy.RUNTIME)
+  public @interface MyNameAnnotation {
+    String value();
+  }
+
+  public static class MyClass {
+    @MyNameAnnotation("fieldOne")
+    public int mFieldOne = 1;
+
+    @MyNameAnnotation("fieldTwo")
+    public int mFieldTwo = 2;
+
+    public int mFieldThree = 3;
+  }
+
+  @UsesReflection(
+      @KeepTarget(
+          fieldAnnotatedByClassConstant = MyNameAnnotation.class,
+          constrainAnnotations = @AnnotationPattern(constant = MyNameAnnotation.class),
+          constraints = {
+            KeepConstraint.LOOKUP,
+            KeepConstraint.VISIBILITY_RELAX,
+            KeepConstraint.FIELD_GET
+          }))
+  public void printMyNameAnnotatedFields(Object obj) throws Exception {
+    for (Field field : obj.getClass().getDeclaredFields()) {
+      if (field.isAnnotationPresent(MyNameAnnotation.class)) {
+        System.out.println(
+            field.getAnnotation(MyNameAnnotation.class).value() + " = " + field.get(obj));
+      }
+    }
+  }
+}
+```
+
+
+If the annotations that need to be kept are not runtime
+visible annotations, then you must specify that by including the `RetentionPolicy.CLASS` value in the
+[@AnnotationPattern.retention](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/AnnotationPattern.html#retention()) property.
+An annotation is runtime visible if its definition is explicitly annotated with
+`Retention(RetentionPolicy.RUNTIME)`.
+
+
 
 ## Annotating code used by reflection (or via JNI)<a name="used-by-reflection"></a>
 
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 104bf02..54d910c 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -83,6 +83,19 @@
 
 [[[INCLUDE CODE:UsesReflectionFieldPrinter]]]
 
+### [Accessing annotations](using-reflection-annotations)
+
+[[[INCLUDE DOC:UsesReflectionOnAnnotations]]]
+
+[[[INCLUDE CODE:UsesReflectionOnAnnotations]]]
+
+If the annotations that need to be kept are not runtime
+visible annotations, then you must specify that by including the `RetentionPolicy.CLASS` value in the
+`@AnnotationPattern#retention` property.
+An annotation is runtime visible if its definition is explicitly annotated with
+`Retention(RetentionPolicy.RUNTIME)`.
+
+
 
 ## [Annotating code used by reflection (or via JNI)](used-by-reflection)
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/AnnotationPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/AnnotationPattern.java
new file mode 100644
index 0000000..cfd4302
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/AnnotationPattern.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A pattern structure for matching annotations.
+ *
+ * <p>If no properties are set, the default pattern matches any annotation with a runtime retention
+ * policy.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface AnnotationPattern {
+
+  /**
+   * Define the annotation-name pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining annotation-name:
+   *
+   * <ul>
+   *   <li>constant
+   *   <li>namePattern
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any annotation name.
+   *
+   * @return The qualified class name that defines the annotation.
+   */
+  String name() default "";
+
+  /**
+   * Define the annotation-name pattern by reference to a {@code Class} constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining annotation-name:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>namePattern
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any annotation name.
+   *
+   * @return The Class constant that defines the annotation.
+   */
+  Class<?> constant() default Object.class;
+
+  /**
+   * Define the annotation-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining annotation-name:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>constant
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any annotation name.
+   *
+   * @return The class-name pattern that defines the annotation.
+   */
+  ClassNamePattern namePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
+   * Specify which retention policies must be set for the annotations.
+   *
+   * <p>Matches annotations with matching retention policies
+   *
+   * @return Retention policies. By default {@code RetentionPolicy.RUNTIME}.
+   */
+  RetentionPolicy[] retention() default {RetentionPolicy.RUNTIME};
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
index 8cfa99d..bd6f7c5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
@@ -209,12 +209,4 @@
    * non-visible uses requires the same annotations to preserve as for reflective uses.
    */
   CLASS_OPEN_HIERARCHY,
-
-  /**
-   * Indicates that the annotations on the target item are being accessed reflectively.
-   *
-   * <p>If only a particular set of annotations is accessed, you should set the TBD property on the
-   * target item.
-   */
-  ANNOTATIONS,
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index bf9251b..20e5492 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
@@ -110,6 +110,22 @@
   KeepConstraint[] constraintAdditions() default {};
 
   /**
+   * Patterns for annotations that must remain on the item.
+   *
+   * <p>The annotations matching any of the patterns must remain on the item if the annotation types
+   * remain in the program.
+   *
+   * <p>Note that if the annotation types themselves are unused/removed, then their references on
+   * the item will be removed too. If the annotation types themselves are used reflectively then
+   * they too need a keep annotation or rule to ensure they remain in the program.
+   *
+   * <p>By default no annotation patterns are defined and no annotations are required to remain.
+   *
+   * @return Annotation patterns
+   */
+  AnnotationPattern[] constrainAnnotations() default {};
+
+  /**
    * Define the class pattern by reference to a binding.
    *
    * <p>Mutually exclusive with the following other properties defining class:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
index 6157cbd..076a93c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -132,6 +132,22 @@
   KeepConstraint[] constraintAdditions() default {};
 
   /**
+   * Patterns for annotations that must remain on the item.
+   *
+   * <p>The annotations matching any of the patterns must remain on the item if the annotation types
+   * remain in the program.
+   *
+   * <p>Note that if the annotation types themselves are unused/removed, then their references on
+   * the item will be removed too. If the annotation types themselves are used reflectively then
+   * they too need a keep annotation or rule to ensure they remain in the program.
+   *
+   * <p>By default no annotation patterns are defined and no annotations are required to remain.
+   *
+   * @return Annotation patterns
+   */
+  AnnotationPattern[] constrainAnnotations() default {};
+
+  /**
    * Define the member-annotated-by pattern by fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining member-annotated-by:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
index 17add2d..cf2225d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -132,6 +132,22 @@
   KeepConstraint[] constraintAdditions() default {};
 
   /**
+   * Patterns for annotations that must remain on the item.
+   *
+   * <p>The annotations matching any of the patterns must remain on the item if the annotation types
+   * remain in the program.
+   *
+   * <p>Note that if the annotation types themselves are unused/removed, then their references on
+   * the item will be removed too. If the annotation types themselves are used reflectively then
+   * they too need a keep annotation or rule to ensure they remain in the program.
+   *
+   * <p>By default no annotation patterns are defined and no annotations are required to remain.
+   *
+   * @return Annotation patterns
+   */
+  AnnotationPattern[] constrainAnnotations() default {};
+
+  /**
    * Define the member-annotated-by pattern by fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining member-annotated-by:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationPatternParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationPatternParser.java
new file mode 100644
index 0000000..f10f9cd
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationPatternParser.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.AnnotationPattern;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class AnnotationPatternParser
+    extends PropertyParserBase<KeepAnnotationPattern, AnnotationPatternParser.AnnotationProperty> {
+
+  public enum AnnotationProperty {
+    PATTERN
+  }
+
+  public AnnotationPatternParser(ParsingContext parsingContext) {
+    super(parsingContext);
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      AnnotationProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepAnnotationPattern> setValue) {
+    switch (property) {
+      case PATTERN:
+        AnnotationParsingContext parsingContext =
+            getParsingContext().property(name).annotation(descriptor);
+        AnnotationDeclarationParser parser = new AnnotationDeclarationParser(parsingContext);
+        return new ParserVisitor(
+            parsingContext,
+            parser,
+            () ->
+                setValue.accept(
+                    parser.isDeclared()
+                        ? parser.getValue()
+                        : KeepAnnotationPattern.anyWithRuntimeRetention()));
+      default:
+        return super.tryPropertyAnnotation(property, name, descriptor, setValue);
+    }
+  }
+
+  private enum RetentionProperty {
+    RETENTION
+  }
+
+  private static class RetentionParser
+      extends PropertyParserBase<RetentionPolicy, RetentionProperty> {
+
+    private static final String RETENTION_POLICY_DESC = "Ljava/lang/annotation/RetentionPolicy;";
+
+    public RetentionParser(ParsingContext parsingContext) {
+      super(parsingContext);
+    }
+
+    @Override
+    public boolean tryPropertyEnum(
+        RetentionProperty property,
+        String name,
+        String descriptor,
+        String value,
+        Consumer<RetentionPolicy> setValue) {
+      assert property == RetentionProperty.RETENTION;
+      if (RETENTION_POLICY_DESC.equals(descriptor)) {
+        setValue.accept(RetentionPolicy.valueOf(value));
+        return true;
+      }
+      return super.tryPropertyEnum(property, name, descriptor, value, setValue);
+    }
+  }
+
+  private static class AnnotationDeclarationParser
+      extends DeclarationParser<KeepAnnotationPattern> {
+
+    private final ClassNameParser nameParser;
+    private final ArrayPropertyParser<RetentionPolicy, RetentionProperty> retentionParser;
+    private final List<Parser<?>> parsers;
+
+    public AnnotationDeclarationParser(ParsingContext parsingContext) {
+      nameParser = new ClassNameParser(parsingContext);
+      nameParser.setProperty(AnnotationPattern.name, ClassNameProperty.NAME);
+      nameParser.setProperty(AnnotationPattern.constant, ClassNameProperty.CONSTANT);
+      nameParser.setProperty(AnnotationPattern.namePattern, ClassNameProperty.PATTERN);
+      retentionParser = new ArrayPropertyParser<>(parsingContext, RetentionParser::new);
+      retentionParser.setProperty(AnnotationPattern.retention, RetentionProperty.RETENTION);
+      retentionParser.setValueCheck(
+          (value, propertyContext) -> {
+            if (value.isEmpty()) {
+              throw propertyContext.error("Expected non-empty array of retention policies");
+            }
+          });
+      parsers = ImmutableList.of(nameParser, retentionParser);
+    }
+
+    @Override
+    List<Parser<?>> parsers() {
+      return parsers;
+    }
+
+    public KeepAnnotationPattern getValue() {
+      if (isDefault()) {
+        return null;
+      }
+      KeepAnnotationPattern.Builder builder = KeepAnnotationPattern.builder();
+      if (retentionParser.isDeclared()) {
+        List<RetentionPolicy> policies = retentionParser.getValue();
+        policies.forEach(builder::addRetentionPolicy);
+      } else {
+        builder.addRetentionPolicy(RetentionPolicy.RUNTIME);
+      }
+      return builder
+          .setNamePattern(nameParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
+          .build();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
index 3c6cd08..3ae1bd2 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
@@ -26,6 +26,7 @@
   AnnotationVisitor tryPropertyArray(P property, String name, Consumer<List<T>> setValue) {
     // The property name and type is forwarded to the element parser.
     values = new ArrayList<>();
+    // The context is explicitly *not* extended with the property name here as it is forwarded.
     ParsingContext parsingContext = getParsingContext();
     return new AnnotationVisitorBase(parsingContext) {
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
index 9137b3e..6bc17aa 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
@@ -89,7 +89,6 @@
           nameParser.setProperty(ClassNamePattern.simpleName, ClassSimpleNameProperty.NAME);
           return new ParserVisitor(
               parsingContext,
-              descriptor,
               ImmutableList.of(packageParser, nameParser),
               () ->
                   setValue.accept(
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintsParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintPropertiesParser.java
similarity index 81%
rename from src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintsParser.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintPropertiesParser.java
index e89279b..c652e91 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintsParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConstraintPropertiesParser.java
@@ -4,21 +4,22 @@
 
 package com.android.tools.r8.keepanno.asm;
 
-import com.android.tools.r8.keepanno.asm.ConstraintsParser.ConstraintsProperty;
+import com.android.tools.r8.keepanno.asm.ConstraintPropertiesParser.ConstraintsProperty;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.KeepConstraints;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
-public class ConstraintsParser extends PropertyParserBase<KeepConstraints, ConstraintsProperty> {
+public class ConstraintPropertiesParser
+    extends PropertyParserBase<KeepConstraints, ConstraintsProperty> {
 
   public enum ConstraintsProperty {
     CONSTRAINTS,
     ADDITIONS
   }
 
-  public ConstraintsParser(ParsingContext parsingContext) {
+  public ConstraintPropertiesParser(ParsingContext parsingContext) {
     super(parsingContext.group(Target.constraintsGroup));
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepConstraintsVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepConstraintsVisitor.java
index b0b58f8..33bf5be 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepConstraintsVisitor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepConstraintsVisitor.java
@@ -67,9 +67,6 @@
       case Constraints.CLASS_OPEN_HIERARCHY:
         builder.add(KeepConstraint.classOpenHierarchy());
         break;
-      case Constraints.ANNOTATIONS:
-        builder.add(KeepConstraint.annotationsAll());
-        break;
       default:
         super.visitEnum(ignore, descriptor, value);
     }
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 8c85b9a..1c6c71e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.keepanno.asm;
 
 import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
-import com.android.tools.r8.keepanno.asm.ConstraintsParser.ConstraintsProperty;
+import com.android.tools.r8.keepanno.asm.ConstraintPropertiesParser.ConstraintsProperty;
 import com.android.tools.r8.keepanno.asm.InstanceOfParser.InstanceOfProperties;
 import com.android.tools.r8.keepanno.asm.StringPatternParser.StringProperty;
 import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
@@ -21,6 +21,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
 import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
@@ -30,6 +31,7 @@
 import com.android.tools.r8.keepanno.ast.KeepClassItemReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepConstraint;
 import com.android.tools.r8.keepanno.ast.KeepConstraints;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
@@ -61,6 +63,7 @@
 import com.android.tools.r8.keepanno.ast.ParsingContext.ClassParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.FieldParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.MethodParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -521,16 +524,17 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
       if (name.equals(Edge.bindings)) {
-        return new KeepBindingsVisitor(parsingContext, bindingsHelper);
+        return new KeepBindingsVisitor(propertyParsingContext, bindingsHelper);
       }
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            parsingContext, builder::setPreconditions, bindingsHelper);
+            propertyParsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(Edge.consequences)) {
         return new KeepConsequencesVisitor(
-            parsingContext, builder::setConsequences, bindingsHelper);
+            propertyParsingContext, builder::setConsequences, bindingsHelper);
       }
       return super.visitArray(name);
     }
@@ -593,7 +597,7 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(ForApi.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            parsingContext,
+            parsingContext.property(name),
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -695,7 +699,7 @@
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(ForApi.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            parsingContext,
+            parsingContext.property(name),
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -715,6 +719,64 @@
     }
   }
 
+  public static class ConstraintDeclarationParser extends DeclarationParser<KeepConstraints> {
+    private final ConstraintPropertiesParser constraintsParser;
+    private final ArrayPropertyParser<
+            KeepAnnotationPattern, AnnotationPatternParser.AnnotationProperty>
+        annotationsParser;
+
+    public ConstraintDeclarationParser(ParsingContext parsingContext) {
+      constraintsParser = new ConstraintPropertiesParser(parsingContext);
+      constraintsParser.setProperty(Target.constraints, ConstraintsProperty.CONSTRAINTS);
+      constraintsParser.setProperty(Target.constraintAdditions, ConstraintsProperty.ADDITIONS);
+
+      annotationsParser = new ArrayPropertyParser<>(parsingContext, AnnotationPatternParser::new);
+      annotationsParser.setProperty(
+          Target.constrainAnnotations, AnnotationPatternParser.AnnotationProperty.PATTERN);
+      annotationsParser.setValueCheck(this::verifyAnnotationList);
+    }
+
+    private void verifyAnnotationList(
+        List<KeepAnnotationPattern> annotationList, ParsingContext parsingContext) {
+      if (annotationList.isEmpty()) {
+        throw parsingContext.error("Expected non-empty array of annotation patterns");
+      }
+    }
+
+    @Override
+    List<Parser<?>> parsers() {
+      return ImmutableList.of(constraintsParser, annotationsParser);
+    }
+
+    public KeepConstraints getValueOrDefault(KeepConstraints defaultValue) {
+      return isDeclared() ? getValue() : defaultValue;
+    }
+
+    public KeepConstraints getValue() {
+      if (isDefault()) {
+        return null;
+      }
+      // If only the constraints are set then those are the constraints as is.
+      if (annotationsParser.isDefault()) {
+        assert constraintsParser.isDeclared();
+        return constraintsParser.getValue();
+      }
+      KeepConstraints.Builder builder;
+      if (constraintsParser.isDeclared()) {
+        // If constraints are set use it as the initial set.
+        builder = KeepConstraints.builder().copyFrom(constraintsParser.getValue());
+        assert builder.verifyNoAnnotations();
+      } else {
+        // If only the annotations are set, add them as an extension of the defaults.
+        builder = KeepConstraints.builder().copyFrom(KeepConstraints.defaultConstraints());
+      }
+      annotationsParser
+          .getValue()
+          .forEach(pattern -> builder.add(KeepConstraint.annotation(pattern)));
+      return builder.build();
+    }
+  }
+
   /**
    * Parsing of @UsedByReflection or @UsedByNative on a class context.
    *
@@ -731,7 +793,7 @@
     private final KeepConsequences.Builder consequences = KeepConsequences.builder();
     private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
-    private final ConstraintsParser constraintsParser;
+    private final ConstraintDeclarationParser constraintsParser;
 
     UsedByReflectionClassVisitor(
         AnnotationParsingContext parsingContext,
@@ -743,11 +805,9 @@
       this.className = className;
       this.parent = parent;
       addContext.accept(metaInfoBuilder);
+      constraintsParser = new ConstraintDeclarationParser(parsingContext);
       // The class context/holder is the annotated class.
       visit(Item.className, className);
-      constraintsParser = new ConstraintsParser(parsingContext);
-      constraintsParser.setProperty(Target.constraints, ConstraintsProperty.CONSTRAINTS);
-      constraintsParser.setProperty(Target.constraintAdditions, ConstraintsProperty.ADDITIONS);
     }
 
     @Override
@@ -766,13 +826,14 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            parsingContext, builder::setPreconditions, bindingsHelper);
+            propertyParsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(UsedByReflection.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            parsingContext,
+            propertyParsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -842,7 +903,7 @@
     private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
     private final KeepConsequences.Builder consequences = KeepConsequences.builder();
     private ItemKind kind = KeepEdgeReader.ItemKind.ONLY_MEMBERS;
-    private final ConstraintsParser constraintsParser;
+    private final ConstraintDeclarationParser constraintsParser;
 
     UsedByReflectionMemberVisitor(
         AnnotationParsingContext parsingContext,
@@ -854,9 +915,7 @@
       this.parent = parent;
       this.context = context;
       addContext.accept(metaInfoBuilder);
-      constraintsParser = new ConstraintsParser(parsingContext);
-      constraintsParser.setProperty(Target.constraints, ConstraintsProperty.CONSTRAINTS);
-      constraintsParser.setProperty(Target.constraintAdditions, ConstraintsProperty.ADDITIONS);
+      constraintsParser = new ConstraintDeclarationParser(parsingContext);
     }
 
     @Override
@@ -883,13 +942,14 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
       if (name.equals(Edge.preconditions)) {
         return new KeepPreconditionsVisitor(
-            parsingContext, builder::setPreconditions, bindingsHelper);
+            propertyParsingContext, builder::setPreconditions, bindingsHelper);
       }
       if (name.equals(UsedByReflection.additionalTargets)) {
         return new KeepConsequencesVisitor(
-            parsingContext,
+            propertyParsingContext,
             additionalConsequences -> {
               additionalConsequences.forEachTarget(consequences::addTarget);
             },
@@ -973,13 +1033,14 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
       if (name.equals(AnnotationConstants.UsesReflection.value)) {
         return new KeepConsequencesVisitor(
-            parsingContext, builder::setConsequences, bindingsHelper);
+            propertyParsingContext, builder::setConsequences, bindingsHelper);
       }
       if (name.equals(AnnotationConstants.UsesReflection.additionalPreconditions)) {
         return new KeepPreconditionsVisitor(
-            parsingContext,
+            propertyParsingContext,
             additionalPreconditions -> {
               additionalPreconditions.forEach(preconditions::addCondition);
             },
@@ -1003,7 +1064,7 @@
     private final ParsingContext parsingContext;
     private final UserBindingsHelper helper;
 
-    public KeepBindingsVisitor(ParsingContext parsingContext, UserBindingsHelper helper) {
+    public KeepBindingsVisitor(PropertyParsingContext parsingContext, UserBindingsHelper helper) {
       super(parsingContext);
       this.parsingContext = parsingContext;
       this.helper = helper;
@@ -1011,6 +1072,7 @@
 
     @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      assert name == null;
       if (descriptor.equals(AnnotationConstants.Binding.DESCRIPTOR)) {
         return new KeepBindingVisitor(parsingContext.annotation(descriptor), helper);
       }
@@ -1025,7 +1087,7 @@
     private final UserBindingsHelper bindingsHelper;
 
     public KeepPreconditionsVisitor(
-        ParsingContext parsingContext,
+        PropertyParsingContext parsingContext,
         Parent<KeepPreconditions> parent,
         UserBindingsHelper bindingsHelper) {
       super(parsingContext);
@@ -1036,6 +1098,7 @@
 
     @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      assert name == null;
       if (descriptor.equals(Condition.DESCRIPTOR)) {
         return new KeepConditionVisitor(
             parsingContext.annotation(descriptor), builder::addCondition, bindingsHelper);
@@ -1056,7 +1119,7 @@
     private final UserBindingsHelper bindingsHelper;
 
     public KeepConsequencesVisitor(
-        ParsingContext parsingContext,
+        PropertyParsingContext parsingContext,
         Parent<KeepConsequences> parent,
         UserBindingsHelper bindingsHelper) {
       super(parsingContext);
@@ -1067,6 +1130,7 @@
 
     @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      assert name == null;
       if (descriptor.equals(Target.DESCRIPTOR)) {
         return KeepTargetVisitor.create(
             parsingContext.annotation(descriptor), builder::addTarget, bindingsHelper);
@@ -1786,7 +1850,7 @@
 
     private final Parent<KeepTarget> parent;
     private final UserBindingsHelper bindingsHelper;
-    private final ConstraintsParser optionsParser;
+    private final ConstraintDeclarationParser constraintsParser;
     private final KeepTarget.Builder builder = KeepTarget.builder();
 
     static KeepTargetVisitor create(
@@ -1803,9 +1867,7 @@
       super(parsingContext);
       this.parent = parent;
       this.bindingsHelper = bindingsHelper;
-      optionsParser = new ConstraintsParser(parsingContext);
-      optionsParser.setProperty(Target.constraints, ConstraintsProperty.CONSTRAINTS);
-      optionsParser.setProperty(Target.constraintAdditions, ConstraintsProperty.ADDITIONS);
+      constraintsParser = new ConstraintDeclarationParser(parsingContext);
     }
 
     @Override
@@ -1815,7 +1877,7 @@
 
     @Override
     public AnnotationVisitor visitArray(String name) {
-      AnnotationVisitor visitor = optionsParser.tryParseArray(name, unused -> {});
+      AnnotationVisitor visitor = constraintsParser.tryParseArray(name, unused -> {});
       if (visitor != null) {
         return visitor;
       }
@@ -1825,7 +1887,8 @@
     @Override
     public void visitEnd() {
       super.visitEnd();
-      builder.setConstraints(optionsParser.getValueOrDefault(KeepConstraints.defaultConstraints()));
+      builder.setConstraints(
+          constraintsParser.getValueOrDefault(KeepConstraints.defaultConstraints()));
       for (KeepItemReference item : getItemsWithBinding()) {
         parent.accept(builder.setItemReference(item).build());
       }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
index 5f1f69b..5acb59f 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
@@ -12,33 +12,26 @@
 /** Convert parser(s) into an annotation visitor. */
 public class ParserVisitor extends AnnotationVisitorBase {
 
-  private final List<PropertyParser<?, ?>> parsers;
+  private final List<Parser<?>> parsers;
   private final Runnable onVisitEnd;
 
   public ParserVisitor(
-      AnnotationParsingContext parsingContext,
-      String annotationDescriptor,
-      List<PropertyParser<?, ?>> parsers,
-      Runnable onVisitEnd) {
+      AnnotationParsingContext parsingContext, List<Parser<?>> parsers, Runnable onVisitEnd) {
     super(parsingContext);
     this.parsers = parsers;
     this.onVisitEnd = onVisitEnd;
-    assert annotationDescriptor.equals(parsingContext.getAnnotationDescriptor());
   }
 
   public ParserVisitor(
-      AnnotationParsingContext parsingContext,
-      String annotationDescriptor,
-      PropertyParser<?, ?> declaration,
-      Runnable onVisitEnd) {
-    this(parsingContext, annotationDescriptor, Collections.singletonList(declaration), onVisitEnd);
+      AnnotationParsingContext parsingContext, Parser<?> parser, Runnable onVisitEnd) {
+    this(parsingContext, Collections.singletonList(parser), onVisitEnd);
   }
 
   private <T> void ignore(T unused) {}
 
   @Override
   public void visit(String name, Object value) {
-    for (PropertyParser<?, ?> parser : parsers) {
+    for (Parser<?> parser : parsers) {
       if (parser.tryParse(name, value, this::ignore)) {
         return;
       }
@@ -48,7 +41,7 @@
 
   @Override
   public AnnotationVisitor visitArray(String name) {
-    for (PropertyParser<?, ?> parser : parsers) {
+    for (Parser<?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
       if (visitor != null) {
         return visitor;
@@ -59,7 +52,7 @@
 
   @Override
   public void visitEnum(String name, String descriptor, String value) {
-    for (PropertyParser<?, ?> parser : parsers) {
+    for (Parser<?> parser : parsers) {
       if (parser.tryParseEnum(name, descriptor, value, this::ignore)) {
         return;
       }
@@ -69,7 +62,7 @@
 
   @Override
   public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-    for (PropertyParser<?, ?> parser : parsers) {
+    for (Parser<?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
       if (visitor != null) {
         return visitor;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
index 0213ef3..f5b40c3 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
@@ -18,6 +19,7 @@
   private final Map<String, P> mapping = new HashMap<>();
   private String resultPropertyName = null;
   private T resultValue = null;
+  private BiConsumer<T, ParsingContext> check = null;
 
   protected PropertyParserBase(ParsingContext parsingContext) {
     this.parsingContext = parsingContext;
@@ -52,6 +54,9 @@
   private Consumer<T> wrap(String propertyName, Consumer<T> setValue) {
     return value -> {
       assert value != null;
+      if (check != null) {
+        check.accept(value, parsingContext.property(propertyName));
+      }
       if (resultPropertyName != null) {
         assert resultValue != null;
         error(propertyName);
@@ -144,4 +149,8 @@
     }
     return null;
   }
+
+  public void setValueCheck(BiConsumer<T, ParsingContext> check) {
+    this.check = check;
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java
index f866a90..2bb95df 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/StringPatternParser.java
@@ -57,7 +57,6 @@
           suffixParser.setProperty(StringPattern.endsWith, StringParser.Property.STRING);
           return new ParserVisitor(
               parsingContext,
-              descriptor,
               ImmutableList.of(exactParser, prefixParser, suffixParser),
               () -> {
                 if (exactParser.isDeclared()) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
index a40f450..a0c0afc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
@@ -58,7 +58,6 @@
           typeParser.setProperty(TypePattern.classNamePattern, TypeProperty.CLASS_NAME_PATTERN);
           return new ParserVisitor(
               context,
-              descriptor,
               typeParser,
               () -> setValue.accept(typeParser.getValueOrDefault(KeepTypePattern.any())));
         }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 9828d43..239dd0e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -139,6 +139,7 @@
     public static final String constraintsGroup = "constraints";
     public static final String constraints = "constraints";
     public static final String constraintAdditions = "constraintAdditions";
+    public static final String constrainAnnotations = "constrainAnnotations";
   }
 
   public static final class Kind {
@@ -169,7 +170,6 @@
     public static final String FIELD_REPLACE = "FIELD_REPLACE";
     public static final String NEVER_INLINE = "NEVER_INLINE";
     public static final String CLASS_OPEN_HIERARCHY = "CLASS_OPEN_HIERARCHY";
-    public static final String ANNOTATIONS = "ANNOTATIONS";
   }
 
   public static final class Option {
@@ -236,4 +236,14 @@
     public static final String simpleName = "simpleName";
     public static final String packageName = "packageName";
   }
+
+  public static final class AnnotationPattern {
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/AnnotationPattern;";
+    public static final String annotationNameGroup = "annotation-name";
+    public static final String name = "name";
+    public static final String constant = "constant";
+    public static final String namePattern = "namePattern";
+    public static final String retention = "retention";
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java
new file mode 100644
index 0000000..5c7cf96
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepAnnotationPattern.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+public class KeepAnnotationPattern {
+
+  private static final int RUNTIME_RETENTION_MASK = 0x1;
+  private static final int CLASS_RETENTION_MASK = 0x2;
+  private static final int ANY_RETENTION_MASK = 0x3;
+
+  private static final KeepAnnotationPattern ANY_WITH_ANY_RETENTION =
+      new KeepAnnotationPattern(KeepQualifiedClassNamePattern.any(), ANY_RETENTION_MASK);
+
+  private static final KeepAnnotationPattern ANY_WITH_RUNTIME_RETENTION =
+      new KeepAnnotationPattern(KeepQualifiedClassNamePattern.any(), RUNTIME_RETENTION_MASK);
+
+  private static final KeepAnnotationPattern ANY_WITH_CLASS_RETENTION =
+      new KeepAnnotationPattern(KeepQualifiedClassNamePattern.any(), CLASS_RETENTION_MASK);
+
+  public static KeepAnnotationPattern any() {
+    return KeepAnnotationPattern.ANY_WITH_ANY_RETENTION;
+  }
+
+  public static KeepAnnotationPattern anyWithRuntimeRetention() {
+    return KeepAnnotationPattern.ANY_WITH_RUNTIME_RETENTION;
+  }
+
+  public static KeepAnnotationPattern anyWithClassRetention() {
+    return KeepAnnotationPattern.ANY_WITH_CLASS_RETENTION;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private KeepQualifiedClassNamePattern namePattern = KeepQualifiedClassNamePattern.any();
+    private int retentionPolicies = 0x0;
+
+    private Builder() {}
+
+    public Builder setNamePattern(KeepQualifiedClassNamePattern namePattern) {
+      this.namePattern = namePattern;
+      return this;
+    }
+
+    public Builder addRetentionPolicy(RetentionPolicy policy) {
+      switch (policy) {
+        case RUNTIME:
+          retentionPolicies |= RUNTIME_RETENTION_MASK;
+          break;
+        case CLASS:
+          retentionPolicies |= CLASS_RETENTION_MASK;
+          break;
+        case SOURCE:
+          throw new KeepEdgeException("Retention policy SOURCE cannot be used in patterns");
+        default:
+          throw new KeepEdgeException("Invalid policy: " + policy);
+      }
+      return this;
+    }
+
+    public KeepAnnotationPattern build() {
+      if (retentionPolicies == 0x0) {
+        throw new KeepEdgeException("Invalid empty retention policy");
+      }
+      if (namePattern.isAny()) {
+        switch (retentionPolicies) {
+          case RUNTIME_RETENTION_MASK:
+            return ANY_WITH_RUNTIME_RETENTION;
+          case CLASS_RETENTION_MASK:
+            return ANY_WITH_CLASS_RETENTION;
+          case ANY_RETENTION_MASK:
+            return ANY_WITH_ANY_RETENTION;
+          default:
+            throw new KeepEdgeException("Invalid retention policy value: " + retentionPolicies);
+        }
+      }
+      return new KeepAnnotationPattern(namePattern, retentionPolicies);
+    }
+  }
+
+  private final KeepQualifiedClassNamePattern namePattern;
+  private final int retentionPolicies;
+
+  private KeepAnnotationPattern(KeepQualifiedClassNamePattern namePattern, int retentionPolicies) {
+    assert namePattern != null;
+    this.namePattern = namePattern;
+    this.retentionPolicies = retentionPolicies;
+  }
+
+  public boolean isAny() {
+    return this == ANY_WITH_ANY_RETENTION;
+  }
+
+  public boolean isAnyWithRuntimeRetention() {
+    return this == ANY_WITH_RUNTIME_RETENTION;
+  }
+
+  public boolean isAnyWithClassRetention() {
+    return this == ANY_WITH_CLASS_RETENTION;
+  }
+
+  public KeepQualifiedClassNamePattern getNamePattern() {
+    return namePattern;
+  }
+
+  public boolean includesRuntimeRetention() {
+    return (retentionPolicies & RUNTIME_RETENTION_MASK) > 0;
+  }
+
+  public boolean includesClassRetention() {
+    return (retentionPolicies & CLASS_RETENTION_MASK) > 0;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof KeepAnnotationPattern)) {
+      return false;
+    }
+    KeepAnnotationPattern that = (KeepAnnotationPattern) o;
+    return retentionPolicies == that.retentionPolicies && namePattern.equals(that.namePattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(namePattern, retentionPolicies);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
index 0f81deb..454f725 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
@@ -286,23 +286,42 @@
     return Annotation.ALL_INSTANCE;
   }
 
-  public static Annotation annotation(KeepQualifiedClassNamePattern pattern) {
+  public static Annotation annotationsAllWithRuntimeRetention() {
+    return Annotation.ALL_WITH_RUNTIME_RETENTION_INSTANCE;
+  }
+
+  public static Annotation annotationsAllWithClassRetention() {
+    return Annotation.ALL_WITH_CLASS_RETENTION_INSTANCE;
+  }
+
+  public static Annotation annotation(KeepAnnotationPattern pattern) {
     if (pattern.isAny()) {
       return annotationsAll();
     }
+    if (pattern.isAnyWithRuntimeRetention()) {
+      return annotationsAllWithRuntimeRetention();
+    }
+    if (pattern.isAnyWithClassRetention()) {
+      return annotationsAllWithClassRetention();
+    }
     return new Annotation(pattern);
   }
 
   public static final class Annotation extends KeepConstraint {
 
-    private static final Annotation ALL_INSTANCE =
-        new Annotation(KeepQualifiedClassNamePattern.any());
+    private static final Annotation ALL_INSTANCE = new Annotation(KeepAnnotationPattern.any());
 
-    private final KeepQualifiedClassNamePattern classNamePattern;
+    private static final Annotation ALL_WITH_RUNTIME_RETENTION_INSTANCE =
+        new Annotation(KeepAnnotationPattern.anyWithRuntimeRetention());
 
-    private Annotation(KeepQualifiedClassNamePattern classNamePattern) {
-      assert classNamePattern != null;
-      this.classNamePattern = classNamePattern;
+    private static final Annotation ALL_WITH_CLASS_RETENTION_INSTANCE =
+        new Annotation(KeepAnnotationPattern.anyWithClassRetention());
+
+    private final KeepAnnotationPattern annotationPattern;
+
+    private Annotation(KeepAnnotationPattern annotationPattern) {
+      assert annotationPattern != null;
+      this.annotationPattern = annotationPattern;
     }
 
     @Override
@@ -315,12 +334,16 @@
 
     @Override
     public void addRequiredKeepAttributes(Set<KeepAttribute> attributes) {
-      attributes.add(KeepAttribute.RUNTIME_VISIBLE_ANNOTATIONS);
-      attributes.add(KeepAttribute.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS);
-      attributes.add(KeepAttribute.RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
-      attributes.add(KeepAttribute.RUNTIME_INVISIBLE_ANNOTATIONS);
-      attributes.add(KeepAttribute.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS);
-      attributes.add(KeepAttribute.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS);
+      if (annotationPattern.includesRuntimeRetention()) {
+        attributes.add(KeepAttribute.RUNTIME_VISIBLE_ANNOTATIONS);
+        attributes.add(KeepAttribute.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS);
+        attributes.add(KeepAttribute.RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
+      }
+      if (annotationPattern.includesClassRetention()) {
+        attributes.add(KeepAttribute.RUNTIME_INVISIBLE_ANNOTATIONS);
+        attributes.add(KeepAttribute.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS);
+        attributes.add(KeepAttribute.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS);
+      }
     }
 
     @Override
@@ -332,12 +355,12 @@
         return false;
       }
       Annotation that = (Annotation) o;
-      return classNamePattern.equals(that.classNamePattern);
+      return annotationPattern.equals(that.annotationPattern);
     }
 
     @Override
     public int hashCode() {
-      return classNamePattern.hashCode();
+      return annotationPattern.hashCode();
     }
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
index c319a3c..eb5cc2e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.keepanno.ast;
 
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Annotation;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.google.common.collect.ImmutableSet;
 import java.util.Collections;
@@ -19,7 +20,12 @@
   }
 
   public static KeepConstraints defaultAdditions(KeepConstraints additionalConstraints) {
-    return new Additions(additionalConstraints);
+    if (additionalConstraints instanceof Constraints) {
+      return new Additions((Constraints) additionalConstraints);
+    }
+    // If no explicit constraints are set, this is just identity on the defaults/additions.
+    assert additionalConstraints instanceof Defaults || additionalConstraints instanceof Additions;
+    return additionalConstraints;
   }
 
   public static Builder builder() {
@@ -28,17 +34,39 @@
 
   public static class Builder {
 
+    private boolean defaultAdditions = false;
     private final Set<KeepConstraint> constraints = new HashSet<>();
 
     private Builder() {}
 
+    public Builder copyFrom(KeepConstraints fromConstraints) {
+      if (fromConstraints instanceof Defaults) {
+        // This builder is based on defaults so set the builder as an addition.
+        defaultAdditions = true;
+      } else if (fromConstraints instanceof Additions) {
+        // This builder is an addition, populate the additions into the constraint set.
+        defaultAdditions = true;
+        constraints.addAll(((Additions) fromConstraints).additions.constraints);
+      } else {
+        assert fromConstraints instanceof Constraints;
+        constraints.addAll(((Constraints) fromConstraints).constraints);
+      }
+      return this;
+    }
+
+    public boolean verifyNoAnnotations() {
+      assert constraints.stream().noneMatch(constraint -> constraint instanceof Annotation);
+      return true;
+    }
+
     public Builder add(KeepConstraint constraint) {
       constraints.add(constraint);
       return this;
     }
 
     public KeepConstraints build() {
-      return new Constraints(constraints);
+      Constraints constraintCollection = new Constraints(constraints);
+      return defaultAdditions ? new Additions(constraintCollection) : constraintCollection;
     }
   }
 
@@ -65,9 +93,9 @@
 
   private static class Additions extends KeepConstraints {
 
-    private final KeepConstraints additions;
+    private final Constraints additions;
 
-    public Additions(KeepConstraints additions) {
+    public Additions(Constraints additions) {
       this.additions = additions;
     }
 
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 b894eae..05ee81c 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
@@ -122,6 +122,12 @@
  *
  *   MEMBER_ACCESS_FLAG
  *     ::= public | protected | package-private | private | static | final | synthetic
+ *
+ *   ANNOTATION_PATTERN
+ *     ::= @QUALIFIED_CLASS_NAME_PATTERN retention(RETENTION_POLICY+)
+ *
+ *   RETENTION_POLICY
+ *     ::= RUNTIME | CLASS
  * </pre>
  */
 public final class KeepEdge extends KeepDeclaration {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
index 0d83a25..5f6bb55 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
@@ -55,6 +55,7 @@
   }
 
   public PropertyParsingContext property(String propertyName) {
+    assert propertyName != null;
     return new PropertyParsingContext(this, propertyName);
   }
 
@@ -273,5 +274,11 @@
     public String getContextFrameAsString() {
       return getPropertyName();
     }
+
+    @Override
+    public boolean isSynthetic() {
+      // The value property is the default and usually unnamed property, so we avoid printing it.
+      return "value".equals(propertyName);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 33b687b..766371d 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -455,11 +455,11 @@
         reporter.error(
             "Option --main-dex-list-output requires --main-dex-rules and/or --main-dex-list");
       }
-      if (getMinApiLevel() >= AndroidApiLevel.L.getLevel()) {
+      if (getMinApiLevel() >= AndroidApiLevel.L_MR1.getLevel()) {
         if (getMainDexListConsumer() != null || getAppBuilder().hasMainDexList()) {
           reporter.error(
               "D8 does not support main-dex inputs and outputs when compiling to API level "
-                  + AndroidApiLevel.L.getLevel()
+                  + AndroidApiLevel.L_MR1.getLevel()
                   + " and above");
         }
       }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 4507e1e..24f0c28 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.LirConverter;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
@@ -54,6 +55,7 @@
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingCfMethods;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
+import com.android.tools.r8.ir.optimize.info.OptimizationInfoRemover;
 import com.android.tools.r8.ir.optimize.templates.CfUtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.jar.CfApplicationWriter;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
@@ -508,7 +510,7 @@
               initialRuntimeTypeCheckInfoBuilder.build(appView.graphLens()));
 
       // TODO(b/225838009): Horizontal merging currently assumes pre-phase CF conversion.
-      appView.testing().enterLirSupportedPhase(appView, executorService);
+      LirConverter.enterLirSupportedPhase(appView, executorService);
 
       new ProtoNormalizer(appViewWithLiveness).run(executorService, timing);
 
@@ -532,6 +534,7 @@
 
       new PrimaryR8IRConverter(appViewWithLiveness, timing)
           .optimize(appViewWithLiveness, executorService);
+      assert LirConverter.verifyLirOnly(appView);
 
       assert ArtProfileCompletenessChecker.verify(
           appView, ALLOW_MISSING_ENUM_UNBOXING_UTILITY_METHODS);
@@ -715,14 +718,16 @@
         SyntheticFinalization.finalizeWithClassHierarchy(appView, executorService, timing);
       }
 
-      // TODO(b/225838009): Move further down.
-      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-
       // Read any -applymapping input to allow for repackaging to not relocate the classes.
       timing.begin("read -applymapping file");
       appView.loadApplyMappingSeedMapper();
       timing.end();
 
+      // Remove optimization info before remaining optimizations, since these optimization currently
+      // do not rewrite the optimization info, which is OK since the optimization info should
+      // already have been leveraged.
+      OptimizationInfoRemover.run(appView, executorService);
+
       // Perform repackaging.
       if (appView.hasLiveness()) {
         if (options.isRepackagingEnabled()) {
@@ -731,18 +736,21 @@
         assert Repackaging.verifyIdentityRepackaging(appView.withLiveness(), executorService);
       }
 
-      // Clear the reference type lattice element cache. This is required since class merging may
-      // need to build IR.
-      appView.dexItemFactory().clearTypeElementsCache();
-
-      GenericSignatureContextBuilder genericContextBuilderBeforeFinalMerging =
-          GenericSignatureContextBuilder.create(appView);
+      // Rewrite LIR with lens to allow building IR from LIR in class mergers.
+      LirConverter.rewriteLirWithLens(appView, timing, executorService);
 
       if (appView.hasLiveness()) {
         VerticalClassMerger.createForFinalClassMerging(appView.withLiveness())
             .runIfNecessary(executorService, timing);
       }
 
+      // TODO(b/225838009): Move further down.
+      LirConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+      assert appView.dexItemFactory().verifyNoCachedTypeElements();
+
+      GenericSignatureContextBuilder genericContextBuilderBeforeFinalMerging =
+          GenericSignatureContextBuilder.create(appView);
+
       // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
       // are always merged.
       HorizontalClassMerger.createForFinalClassMerging(appView)
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 47f7ed6..e3d2f78 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -229,7 +229,9 @@
     if (!options.isGeneratingDex()) {
       return true;
     }
-    AndroidApiLevel nativeMultiDex = AndroidApiLevel.L;
+    // Native multidex is supported from L, but the compiler supports compiling to L/21 using
+    // legacy multidex as there are some devices that have issues with it still.
+    AndroidApiLevel nativeMultiDex = AndroidApiLevel.L_MR1;
     if (options.getMinApiLevel().isLessThan(nativeMultiDex)) {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
index a30eb47..b8e779d 100644
--- a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
+++ b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
@@ -86,6 +86,8 @@
   abstract AbstractAccessContexts rewrittenWithLens(
       DexDefinitionSupplier definitions, GraphLens lens);
 
+  abstract AbstractAccessContexts withoutPrunedItems(PrunedItems prunedItems);
+
   public static EmptyAccessContexts empty() {
     return EmptyAccessContexts.getInstance();
   }
@@ -155,6 +157,11 @@
     public AbstractAccessContexts join(AbstractAccessContexts contexts) {
       return contexts;
     }
+
+    @Override
+    AbstractAccessContexts withoutPrunedItems(PrunedItems prunedItems) {
+      return this;
+    }
   }
 
   public static class ConcreteAccessContexts extends AbstractAccessContexts {
@@ -378,6 +385,14 @@
       contexts.asConcrete().accessesWithContexts.forEach(addAllMethods);
       return new ConcreteAccessContexts(newAccessesWithContexts);
     }
+
+    @Override
+    AbstractAccessContexts withoutPrunedItems(PrunedItems prunedItems) {
+      for (ProgramMethodSet methodSet : accessesWithContexts.values()) {
+        methodSet.removeIf(method -> prunedItems.isRemoved(method.getReference()));
+      }
+      return this;
+    }
   }
 
   public static class UnknownAccessContexts extends AbstractAccessContexts {
@@ -440,5 +455,10 @@
     public AbstractAccessContexts join(AbstractAccessContexts contexts) {
       return this;
     }
+
+    @Override
+    AbstractAccessContexts withoutPrunedItems(PrunedItems prunedItems) {
+      return this;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 691680c..c09b0fd 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -101,6 +101,7 @@
   private final WholeProgramOptimizations wholeProgramOptimizations;
   private GraphLens codeLens = GraphLens.getIdentityLens();
   private GraphLens graphLens = GraphLens.getIdentityLens();
+  private GraphLens genericSignaturesLens = GraphLens.getIdentityLens();
   private InitClassLens initClassLens;
   private GraphLens kotlinMetadataLens = GraphLens.getIdentityLens();
   private NamingLens namingLens = NamingLens.getIdentityLens();
@@ -678,6 +679,14 @@
     return false;
   }
 
+  public GraphLens getGenericSignaturesLens() {
+    return genericSignaturesLens;
+  }
+
+  public void setGenericSignaturesLens(GraphLens genericSignaturesLens) {
+    this.genericSignaturesLens = genericSignaturesLens;
+  }
+
   private boolean disallowFurtherInitClassUses = false;
 
   public void dissallowFurtherInitClassUses() {
@@ -871,7 +880,6 @@
       testing().verticallyMergedClassesConsumer.accept(dexItemFactory(), verticallyMergedClasses);
     } else {
       assert this.verticallyMergedClasses != null;
-      assert verticallyMergedClasses.isEmpty();
     }
   }
 
@@ -1088,7 +1096,10 @@
                 public void run(Timing timing) {
                   if (appView.hasLiveness()) {
                     result =
-                        appView.appInfoWithLiveness().rewrittenWithLens(application, lens, timing);
+                        appView
+                            .appInfoWithLiveness()
+                            .rewrittenWithLens(
+                                application, lens, appliedLensInModifiedLens, timing);
                   } else {
                     assert appView.hasClassHierarchy();
                     AppView<AppInfoWithClassHierarchy> appViewWithClassHierarchy =
@@ -1246,6 +1257,23 @@
                 public boolean shouldRun() {
                   return !appView.getStartupProfile().isEmpty();
                 }
+              },
+              new ThreadTask() {
+                @Override
+                public void run(Timing timing) {
+                  ImmutableSet.Builder<DexMethod> cfByteCodePassThroughBuilder =
+                      ImmutableSet.builder();
+                  for (DexMethod method : appView.cfByteCodePassThrough) {
+                    cfByteCodePassThroughBuilder.add(
+                        lens.getRenamedMethodSignature(method, appliedLensInModifiedLens));
+                  }
+                  appView.cfByteCodePassThrough = cfByteCodePassThroughBuilder.build();
+                }
+
+                @Override
+                public boolean shouldRun() {
+                  return !appView.cfByteCodePassThrough.isEmpty();
+                }
               });
         });
 
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
index 665a472..f43f02e 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -19,17 +19,24 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.SyntheticStraightLineSourceCode;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirEncodingStrategy;
+import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -85,7 +92,12 @@
       AppView<?> appView, ProgramMethod method, DexType superType) {
     DexEncodedMethod definition = method.getDefinition();
     assert definition.getCode().isDefaultInstanceInitializerCode();
-    method.setCode(get().toCfCode(method, appView.dexItemFactory(), superType), appView);
+    if (appView.testing().isSupportedLirPhase()) {
+      method.setCode(get().toLirCode(appView, method), appView);
+    } else {
+      assert appView.testing().isPreLirPhase();
+      method.setCode(get().toCfCode(method, appView.dexItemFactory(), superType), appView);
+    }
   }
 
   @Override
@@ -370,6 +382,27 @@
     return new CfCode(method.getHolderType(), getMaxStack(), getMaxLocals(method), instructions);
   }
 
+  public LirCode<?> toLirCode(AppView<?> appView, ProgramMethod method) {
+    TypeElement receiverType =
+        method.getHolder().getType().toTypeElement(appView, Nullability.definitelyNotNull());
+    Value receiver = new Value(0, receiverType, null);
+    DexMethod invokedMethod =
+        appView.dexItemFactory().createInstanceInitializer(method.getHolder().getSuperType());
+    LirEncodingStrategy<Value, Integer> strategy =
+        LirStrategy.getDefaultStrategy().getEncodingStrategy();
+    strategy.defineValue(receiver, 0);
+    return LirCode.builder(
+            method.getReference(),
+            method.getDefinition().isD8R8Synthesized(),
+            strategy,
+            appView.options())
+        .setMetadata(IRMetadata.unknown())
+        .addArgument(0, false)
+        .addInvokeDirect(invokedMethod, ImmutableList.of(receiver), false)
+        .addReturnVoid()
+        .build();
+  }
+
   @Override
   public void writeCf(
       ProgramMethod method,
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 557bb44..af6a62f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -48,6 +48,10 @@
   // Mapping from register to local for currently open/visible locals.
   private Int2ReferenceMap<DebugLocalInfo> pendingLocals = null;
 
+  // The method's preamble position. This is used in release mode to preserve the preamble position
+  // for methods without throwing instructions that have been moved (i.e., have a caller position).
+  private Position preamblePosition;
+
   // Conservative pending-state of locals to avoid some equality checks on locals.
   // pendingLocalChanges == true ==> localsEqual(emittedLocals, pendingLocals).
   private boolean pendingLocalChanges = false;
@@ -81,6 +85,9 @@
     // Initialize locals state on block entry.
     if (isBlockEntry) {
       updateBlockEntry(instruction);
+      if (preamblePosition == null) {
+        preamblePosition = instruction.getPosition();
+      }
     }
     assert pendingLocals != null;
 
@@ -116,8 +123,17 @@
   public DexDebugInfo build() {
     assert pendingLocals == null;
     assert !pendingLocalChanges;
+    assert preamblePosition != null;
     if (startLine == NO_LINE_INFO) {
-      return null;
+      if (!preamblePosition.hasCallerPosition()) {
+        return null;
+      }
+      return new EventBasedDebugInfo(
+          preamblePosition.getLine(),
+          new DexString[method.getReference().getArity()],
+          new DexDebugEvent[] {
+            factory.createPositionFrame(preamblePosition), factory.zeroChangeDefaultEvent
+          });
     }
     DexString[] params = new DexString[method.getReference().getArity()];
     if (arguments != null) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 7465b0a..3c3533a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -10,6 +10,7 @@
 import static com.android.tools.r8.graph.DexEncodedMethod.CompilationState.PROCESSED_INLINING_CANDIDATE_SUBCLASS;
 import static com.android.tools.r8.graph.DexEncodedMethod.CompilationState.PROCESSED_NOT_INLINING_CANDIDATE;
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getNoKotlinInfo;
 import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
 import static java.util.Objects.requireNonNull;
@@ -30,18 +31,15 @@
 import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.cf.code.CfThrow;
 import com.android.tools.r8.dex.MixedSectionCollection;
-import com.android.tools.r8.dex.code.DexConstString;
-import com.android.tools.r8.dex.code.DexInstruction;
-import com.android.tools.r8.dex.code.DexInvokeDirect;
-import com.android.tools.r8.dex.code.DexInvokeStatic;
-import com.android.tools.r8.dex.code.DexNewInstance;
-import com.android.tools.r8.dex.code.DexThrow;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation.AnnotatedKind;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.NestUtils;
@@ -52,6 +50,10 @@
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.kotlin.KotlinMethodLevelInfo;
+import com.android.tools.r8.lightir.LirBuilder;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirEncodingStrategy;
+import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.NamingLens;
@@ -911,28 +913,6 @@
     return getReference().toSourceString();
   }
 
-  /** Generates a {@link DexCode} object for the given instructions. */
-  private DexCode generateCodeFromTemplate(
-      int numberOfRegisters, int outRegisters, DexInstruction... instructions) {
-    int offset = 0;
-    for (DexInstruction instruction : instructions) {
-      instruction.setOffset(offset);
-      offset += instruction.getSize();
-    }
-    int requiredArgRegisters = accessFlags.isStatic() ? 0 : 1;
-    for (DexType type : getReference().proto.parameters.values) {
-      requiredArgRegisters += ValueType.fromDexType(type).requiredRegisters();
-    }
-    return new DexCode(
-        Math.max(numberOfRegisters, requiredArgRegisters),
-        requiredArgRegisters,
-        outRegisters,
-        instructions,
-        new DexCode.Try[0],
-        new DexCode.TryHandler[0],
-        null);
-  }
-
   public CfCode buildInstanceOfCfCode(DexType type, boolean negate) {
     CfInstruction[] instructions = new CfInstruction[3 + BooleanUtils.intValue(negate) * 2];
     int i = 0;
@@ -950,13 +930,15 @@
         Arrays.asList(instructions));
   }
 
-  public DexEncodedMethod toMethodThatLogsError(AppView<?> appView) {
+  public DexEncodedMethod toMethodThatLogsError(
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    assert appView.testing().isPreLirPhase() || appView.testing().isSupportedLirPhase();
     Builder builder =
         builder(this)
             .setCode(
-                appView.options().isGeneratingClassFiles()
+                appView.testing().isPreLirPhase()
                     ? toCfCodeThatLogsError(appView.dexItemFactory())
-                    : toDexCodeThatLogsError(appView.dexItemFactory()))
+                    : toLirCodeThatLogsError(appView))
             .setIsLibraryMethodOverrideIf(
                 belongsToVirtualPool() && !isLibraryMethodOverride().isUnknown(),
                 isLibraryMethodOverride())
@@ -979,50 +961,86 @@
     }
   }
 
-  public static void setDebugInfoWithExtraParameters(
-      Code code, int arity, int extraParameters, AppView<?> appView) {
-    if (code.isDexCode()) {
-      DexCode dexCode = code.asDexCode();
-      DexDebugInfo newDebugInfo =
-          dexCode.debugInfoWithExtraParameters(appView.dexItemFactory(), extraParameters);
-      assert (newDebugInfo == null) || (arity == newDebugInfo.getParameterCount());
-      dexCode.setDebugInfo(newDebugInfo);
-    } else {
-      assert code.isCfCode();
-      // We don't have anything to do for Cf.
-    }
-  }
-
-  private DexCode toDexCodeThatLogsError(DexItemFactory itemFactory) {
+  private LirCode<?> toLirCodeThatLogsError(AppView<? extends AppInfoWithClassHierarchy> appView) {
     checkIfObsolete();
+    DexItemFactory factory = appView.dexItemFactory();
     Signature signature = MethodSignature.fromDexMethod(getReference());
     DexString message =
-        itemFactory.createString(
+        factory.createString(
             CONFIGURATION_DEBUGGING_PREFIX
                 + getReference().holder.toSourceString()
                 + ": "
                 + signature);
-    DexString tag = itemFactory.createString("[R8]");
-    DexType[] args = {itemFactory.stringType, itemFactory.stringType};
-    DexProto proto = itemFactory.createProto(itemFactory.intType, args);
-    DexMethod logMethod =
-        itemFactory.createMethod(
-            itemFactory.androidUtilLogType, proto, itemFactory.createString("e"));
-    DexType exceptionType = itemFactory.runtimeExceptionType;
+    DexString tag = factory.createString("[R8]");
+    DexType logger = factory.javaUtilLoggingLoggerType;
+    DexMethod getLogger =
+        factory.createMethod(
+            logger,
+            factory.createProto(logger, factory.stringType),
+            factory.createString("getLogger"));
+    DexMethod severe =
+        factory.createMethod(
+            logger,
+            factory.createProto(factory.voidType, factory.stringType),
+            factory.createString("severe"));
+    DexType exceptionType = factory.runtimeExceptionType;
     DexMethod exceptionInitMethod =
-        itemFactory.createMethod(
+        factory.createMethod(
             exceptionType,
-            itemFactory.createProto(itemFactory.voidType, itemFactory.stringType),
-            itemFactory.constructorMethodName);
-    return generateCodeFromTemplate(
-        2,
-        2,
-        new DexConstString(0, tag),
-        new DexConstString(1, message),
-        new DexInvokeStatic(2, logMethod, 0, 1, 0, 0, 0),
-        new DexNewInstance(0, exceptionType),
-        new DexInvokeDirect(2, exceptionInitMethod, 0, 1, 0, 0, 0),
-        new DexThrow(0));
+            factory.createProto(factory.voidType, factory.stringType),
+            factory.constructorMethodName);
+
+    TypeElement stringValueType = TypeElement.stringClassType(appView, definitelyNotNull());
+    TypeElement loggerValueType = logger.toTypeElement(appView);
+    TypeElement exceptionValueType = exceptionType.toTypeElement(appView, definitelyNotNull());
+
+    LirEncodingStrategy<Value, Integer> strategy =
+        LirStrategy.getDefaultStrategy().getEncodingStrategy();
+    LirBuilder<Value, Integer> lirBuilder =
+        LirCode.builder(getReference(), isD8R8Synthesized(), strategy, appView.options())
+            .setMetadata(IRMetadata.unknown());
+    int instructionIndex = 0;
+    for (; instructionIndex < getNumberOfArguments(); instructionIndex++) {
+      DexType argumentType = getArgumentType(instructionIndex);
+      lirBuilder.addArgument(instructionIndex, argumentType.isBooleanType());
+    }
+
+    // Load tag.
+    Value tagValue = new Value(instructionIndex, stringValueType, null);
+    strategy.defineValue(tagValue, tagValue.getNumber());
+    lirBuilder.addConstString(tag);
+    instructionIndex++;
+
+    // Get logger.
+    Value loggerValue = new Value(instructionIndex, loggerValueType, null);
+    strategy.defineValue(loggerValue, loggerValue.getNumber());
+    lirBuilder.addInvokeStatic(getLogger, ImmutableList.of(tagValue), false);
+    instructionIndex++;
+
+    // Load message.
+    Value messageValue = new Value(instructionIndex, stringValueType, null);
+    strategy.defineValue(messageValue, messageValue.getNumber());
+    lirBuilder.addConstString(message);
+    instructionIndex++;
+
+    // Call logger.
+    lirBuilder.addInvokeVirtual(severe, ImmutableList.of(loggerValue, messageValue));
+    instructionIndex++;
+
+    // Instantiate exception.
+    Value exceptionValue = new Value(instructionIndex, exceptionValueType, null);
+    strategy.defineValue(exceptionValue, exceptionValue.getNumber());
+    lirBuilder
+        .addNewInstance(exceptionType)
+        .addInvokeDirect(
+            exceptionInitMethod, ImmutableList.of(exceptionValue, messageValue), false);
+    instructionIndex += 2;
+
+    // Throw exception.
+    lirBuilder.addThrow(exceptionValue);
+    instructionIndex++;
+
+    return lirBuilder.build();
   }
 
   private CfCode toCfCodeThatLogsError(DexItemFactory itemFactory) {
@@ -1292,6 +1310,11 @@
     optimizationInfo = info;
   }
 
+  public void unsetOptimizationInfo() {
+    checkIfObsolete();
+    optimizationInfo = DefaultMethodOptimizationInfo.getInstance();
+  }
+
   public void copyMetadata(AppView<?> appView, DexEncodedMethod from) {
     checkIfObsolete();
     if (from.hasClassFileVersion()) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
index ac780e9..3250adb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
@@ -60,4 +60,31 @@
     // Always just return the object type since this is safe for all api versions.
     return factory.objectType;
   }
+
+  public static boolean isTypeAccessibleInMethodContext(
+      AppView<?> appView, DexType type, ProgramMethod context) {
+    if (type.isPrimitiveType()) {
+      return true;
+    }
+    if (type.isIdenticalTo(context.getHolderType())
+        || (context.getHolder().hasSuperType()
+            && type.isIdenticalTo(context.getHolder().getSuperType()))
+        || context.getHolder().getInterfaces().contains(type)) {
+      // In principle we don't know if the supertypes are guaranteed to be accessible in the current
+      // context. However, if they aren't, the current class will never be successfully loaded
+      // anyway.
+      return true;
+    }
+    DexClass clazz = appView.definitionFor(type, context);
+    if (clazz == null) {
+      return false;
+    }
+    if (clazz.isLibraryClass()) {
+      return AndroidApiLevelUtils.isApiSafeForReference(clazz.asLibraryClass(), appView);
+    }
+    if (appView.hasClassHierarchy()) {
+      return AccessControl.isClassAccessible(clazz, context, appView.withClassHierarchy()).isTrue();
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
index 6b2bad4..29cd5a4 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
@@ -9,7 +9,9 @@
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.Timing;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -102,4 +104,17 @@
     assert infos.values().size() == SetUtils.newIdentityHashSet(infos.values()).size();
     return true;
   }
+
+  public FieldAccessInfoCollectionImpl withoutPrunedItems(PrunedItems prunedItems) {
+    Iterator<Entry<DexField, FieldAccessInfoImpl>> iterator = infos.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<DexField, FieldAccessInfoImpl> entry = iterator.next();
+      if (prunedItems.isRemoved(entry.getKey())) {
+        iterator.remove();
+      } else {
+        entry.setValue(entry.getValue().withoutPrunedItems(prunedItems));
+      }
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
index 0dae257..39aa866 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
@@ -406,4 +406,10 @@
     merged.writesWithContexts = writesWithContexts.join(impl.writesWithContexts);
     return merged;
   }
+
+  public FieldAccessInfoImpl withoutPrunedItems(PrunedItems prunedItems) {
+    readsWithContexts = readsWithContexts.withoutPrunedItems(prunedItems);
+    writesWithContexts = writesWithContexts.withoutPrunedItems(prunedItems);
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
index d519324..916a8e2 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
@@ -38,7 +38,7 @@
         appView.appInfo().hasLiveness()
             ? appView.appInfo().withLiveness()::wasPruned
             : alwaysFalse(),
-        appView.graphLens()::lookupType,
+        type -> appView.graphLens().lookupType(type, appView.getGenericSignaturesLens()),
         context,
         hasGenericTypeVariables);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
index 3b8681b..b541bfc 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
@@ -17,7 +17,9 @@
 import com.android.tools.r8.utils.collections.ThrowingMap;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
@@ -178,18 +180,18 @@
     }
   }
 
-  public MethodAccessInfoCollection withoutPrunedItems(PrunedItems prunedItems) {
+  public MethodAccessInfoCollection withoutPrunedContexts(PrunedItems prunedItems) {
     if (!fullyDestroyed) {
-      pruneItems(prunedItems, directInvokes);
-      pruneItems(prunedItems, interfaceInvokes);
-      pruneItems(prunedItems, staticInvokes);
-      pruneItems(prunedItems, superInvokes);
-      pruneItems(prunedItems, virtualInvokes);
+      pruneContexts(prunedItems, directInvokes);
+      pruneContexts(prunedItems, interfaceInvokes);
+      pruneContexts(prunedItems, staticInvokes);
+      pruneContexts(prunedItems, superInvokes);
+      pruneContexts(prunedItems, virtualInvokes);
     }
     return this;
   }
 
-  private static void pruneItems(
+  private static void pruneContexts(
       PrunedItems prunedItems, Map<DexMethod, ProgramMethodSet> invokes) {
     if (isThrowingMap(invokes)) {
       return;
@@ -212,6 +214,45 @@
             });
   }
 
+  public MethodAccessInfoCollection withoutPrunedItems(PrunedItems prunedItems) {
+    if (!fullyDestroyed) {
+      pruneItems(prunedItems, directInvokes);
+      pruneItems(prunedItems, interfaceInvokes);
+      pruneItems(prunedItems, staticInvokes);
+      pruneItems(prunedItems, superInvokes);
+      pruneItems(prunedItems, virtualInvokes);
+    }
+    return this;
+  }
+
+  private static void pruneItems(
+      PrunedItems prunedItems, Map<DexMethod, ProgramMethodSet> invokes) {
+    if (isThrowingMap(invokes)) {
+      return;
+    }
+    Iterator<Entry<DexMethod, ProgramMethodSet>> iterator = invokes.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<DexMethod, ProgramMethodSet> entry = iterator.next();
+      if (prunedItems.isRemoved(entry.getKey())) {
+        iterator.remove();
+      } else {
+        ProgramMethodSet contexts = entry.getValue();
+        contexts.removeIf(
+            context -> {
+              if (prunedItems.isRemoved(context.getReference())) {
+                return true;
+              }
+              assert prunedItems.getPrunedApp().definitionFor(context.getReference()) != null
+                  : "Expected method to be present: " + context.getReference().toSourceString();
+              return false;
+            });
+        if (contexts.isEmpty()) {
+          iterator.remove();
+        }
+      }
+    }
+  }
+
   public boolean verify(AppView<AppInfoWithLiveness> appView) {
     assert verifyNoNonResolving(appView);
     return true;
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollection.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollection.java
index 70d1666..b55811b 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollection.java
@@ -43,5 +43,5 @@
       AppInfo appInfo);
 
   ObjectAllocationInfoCollection rewrittenWithLens(
-      DexDefinitionSupplier definitions, GraphLens lens, Timing timing);
+      DexDefinitionSupplier definitions, GraphLens lens, GraphLens appliedLens, Timing timing);
 }
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 6050c9d..2ffe096 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.shaking.GraphReporter;
 import com.android.tools.r8.shaking.InstantiationReason;
@@ -144,14 +145,17 @@
 
   @Override
   public ObjectAllocationInfoCollectionImpl rewrittenWithLens(
-      DexDefinitionSupplier definitions, GraphLens lens, Timing timing) {
+      DexDefinitionSupplier definitions, GraphLens lens, GraphLens appliedLens, Timing timing) {
     return timing.time(
-        "Rewrite ObjectAllocationInfoCollectionImpl", () -> rewrittenWithLens(definitions, lens));
+        "Rewrite ObjectAllocationInfoCollectionImpl",
+        () -> rewrittenWithLens(definitions, lens, appliedLens));
   }
 
   private ObjectAllocationInfoCollectionImpl rewrittenWithLens(
-      DexDefinitionSupplier definitions, GraphLens lens) {
-    return builder(true, null).rewrittenWithLens(this, definitions, lens).build(definitions);
+      DexDefinitionSupplier definitions, GraphLens lens, GraphLens appliedLens) {
+    return builder(true, null)
+        .rewrittenWithLens(this, definitions, lens, appliedLens)
+        .build(definitions);
   }
 
   public ObjectAllocationInfoCollectionImpl withoutPrunedItems(PrunedItems prunedItems) {
@@ -474,11 +478,12 @@
     Builder rewrittenWithLens(
         ObjectAllocationInfoCollectionImpl objectAllocationInfos,
         DexDefinitionSupplier definitions,
-        GraphLens lens) {
+        GraphLens lens,
+        GraphLens appliedLens) {
       instantiatedHierarchy = null;
       objectAllocationInfos.classesWithoutAllocationSiteTracking.forEach(
           clazz -> {
-            DexType type = lens.lookupType(clazz.type);
+            DexType type = lens.lookupType(clazz.type, appliedLens);
             if (type.isPrimitiveType()) {
               return;
             }
@@ -488,7 +493,7 @@
           });
       objectAllocationInfos.classesWithAllocationSiteTracking.forEach(
           (clazz, allocationSitesForClass) -> {
-            DexType type = lens.lookupType(clazz.type);
+            DexType type = lens.lookupType(clazz.type, appliedLens);
             if (type.isPrimitiveType()) {
               return;
             }
@@ -507,7 +512,7 @@
           });
       for (DexProgramClass abstractType :
           objectAllocationInfos.interfacesWithUnknownSubtypeHierarchy) {
-        DexType type = lens.lookupType(abstractType.type);
+        DexType type = lens.lookupType(abstractType.type, appliedLens);
         if (type.isPrimitiveType()) {
           assert false;
           continue;
@@ -517,15 +522,19 @@
         assert !interfacesWithUnknownSubtypeHierarchy.contains(rewrittenClass);
         interfacesWithUnknownSubtypeHierarchy.add(rewrittenClass);
       }
+      LensCodeRewriterUtils rewriter = new LensCodeRewriterUtils(definitions, lens, appliedLens);
       objectAllocationInfos.instantiatedLambdas.forEach(
           (iface, lambdas) -> {
-            DexType type = lens.lookupType(iface);
+            DexType type = lens.lookupType(iface, appliedLens);
             if (type.isPrimitiveType()) {
               assert false;
               return;
             }
-            // TODO(b/150277553): Rewrite lambda descriptor.
-            instantiatedLambdas.computeIfAbsent(type, ignoreKey(ArrayList::new)).addAll(lambdas);
+            List<LambdaDescriptor> newLambdas =
+                instantiatedLambdas.computeIfAbsent(type, ignoreKey(ArrayList::new));
+            for (LambdaDescriptor lambda : lambdas) {
+              newLambdas.add(lambda.rewrittenWithLens(lens, appliedLens, rewriter));
+            }
           });
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
index 9b4fa96..a86dbc8 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
@@ -204,18 +204,16 @@
     return appView.options().debug || getOrComputeReachabilitySensitive(appView);
   }
 
-  @SuppressWarnings("ReferenceEquality")
   public ProgramMethod rewrittenWithLens(
       GraphLens lens, GraphLens appliedLens, DexDefinitionSupplier definitions) {
     DexMethod newMethod = lens.getRenamedMethodSignature(getReference(), appliedLens);
-    if (newMethod == getReference() && !getDefinition().isObsolete()) {
+    if (newMethod.isIdenticalTo(getReference()) && !getDefinition().isObsolete()) {
       assert verifyIsConsistentWithLookup(definitions);
       return this;
     }
     return asProgramMethodOrNull(definitions.definitionFor(newMethod));
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean verifyIsConsistentWithLookup(DexDefinitionSupplier definitions) {
     DexClassAndMethod lookupMethod = definitions.definitionFor(getReference());
     assert getDefinition() == lookupMethod.getDefinition();
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index d0f23ae..96948b9 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -26,10 +26,7 @@
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingLens;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.CollectionUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.Timing;
@@ -43,7 +40,6 @@
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.ArrayDeque;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Deque;
 import java.util.IdentityHashMap;
@@ -472,35 +468,6 @@
     return true;
   }
 
-  public <T extends DexReference> boolean assertPinnedNotModified(
-      AppView<AppInfoWithLiveness> appView) {
-    List<DexReference> pinnedItems = new ArrayList<>();
-    KeepInfoCollection keepInfo = appView.getKeepInfo();
-    InternalOptions options = appView.options();
-    keepInfo.forEachPinnedType(pinnedItems::add, options);
-    keepInfo.forEachPinnedMethod(pinnedItems::add, options);
-    keepInfo.forEachPinnedField(pinnedItems::add, options);
-    return assertReferencesNotModified(pinnedItems);
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  public <T extends DexReference> boolean assertReferencesNotModified(Iterable<T> references) {
-    for (DexReference reference : references) {
-      if (reference.isDexField()) {
-        DexField field = reference.asDexField();
-        assert getRenamedFieldSignature(field) == field;
-      } else if (reference.isDexMethod()) {
-        DexMethod method = reference.asDexMethod();
-        assert getRenamedMethodSignature(method) == method;
-      } else {
-        assert reference.isDexType();
-        DexType type = reference.asDexType();
-        assert lookupType(type) == type;
-      }
-    }
-    return true;
-  }
-
   public Map<DexCallSite, ProgramMethodSet> rewriteCallSites(
       Map<DexCallSite, ProgramMethodSet> callSites,
       DexDefinitionSupplier definitions,
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
index 987bfe3..e657932 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
@@ -12,8 +12,6 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.utils.ThrowingAction;
 import com.google.common.collect.Streams;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Predicate;
 
 public abstract class NonIdentityGraphLens extends GraphLens {
@@ -22,8 +20,6 @@
   private final DexItemFactory dexItemFactory;
   private GraphLens previousLens;
 
-  private final Map<DexType, DexType> arrayTypeCache = new ConcurrentHashMap<>();
-
   public NonIdentityGraphLens(AppView<?> appView) {
     this(appView, appView.graphLens());
   }
@@ -104,20 +100,14 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public final DexType lookupType(DexType type, GraphLens appliedLens) {
     if (type.isClassType()) {
       return lookupClassType(type, appliedLens);
     }
     if (type.isArrayType()) {
-      DexType result = arrayTypeCache.get(type);
-      if (result == null) {
-        DexType baseType = type.toBaseType(dexItemFactory);
-        DexType newType = lookupType(baseType, appliedLens);
-        result = baseType == newType ? type : type.replaceBaseType(newType, dexItemFactory);
-        arrayTypeCache.put(type, result);
-      }
-      return result;
+      DexType baseType = type.toBaseType(dexItemFactory);
+      DexType newType = lookupType(baseType, appliedLens);
+      return baseType.isIdenticalTo(newType) ? type : type.replaceBaseType(newType, dexItemFactory);
     }
     assert type.isNullValueType() || type.isPrimitiveType() || type.isVoidType();
     return type;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index 38b433a..85794e3 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.StringSwitch;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
@@ -53,7 +54,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 3570828..d784fa3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -438,6 +438,17 @@
     }
   }
 
+  public void removeAllExceptionalSuccessors() {
+    assert hasCatchHandlers();
+    IntList successorsToRemove = new IntArrayList();
+    int numberOfExceptionalSuccessors = numberOfExceptionalSuccessors();
+    for (int i = 0; i < numberOfExceptionalSuccessors; i++) {
+      successorsToRemove.add(i);
+      successors.get(i).getMutablePredecessors().remove(this);
+    }
+    removeSuccessorsByIndex(successorsToRemove);
+  }
+
   public void swapSuccessors(BasicBlock a, BasicBlock b) {
     assert a != b;
     int aIndex = successors.indexOf(a);
@@ -1153,6 +1164,36 @@
     return true;
   }
 
+  private boolean isExceptionTrampoline() {
+    boolean ret = instructions.size() == 2 && entry().isMoveException() && exit().isGoto();
+    assert !ret || !hasCatchHandlers() : "Trampoline should not have catch handlers";
+    return ret;
+  }
+
+  /** Returns whether the given blocks are in the same try block. */
+  public boolean hasEquivalentCatchHandlers(BasicBlock other) {
+    if (this == other) {
+      return true;
+    }
+    List<Integer> targets1 = catchHandlers.getAllTargets();
+    List<Integer> targets2 = other.catchHandlers.getAllTargets();
+    int numHandlers = targets1.size();
+    if (numHandlers != targets2.size()) {
+      return false;
+    }
+    // If all catch handlers are trampolines to the same block, then they are from the same try.
+    for (int i = 0; i < numHandlers; ++i) {
+      BasicBlock catchBlock1 = successors.get(targets1.get(i));
+      BasicBlock catchBlock2 = other.successors.get(targets2.get(i));
+      if (!catchBlock1.isExceptionTrampoline()
+          || !catchBlock2.isExceptionTrampoline()
+          || catchBlock1.getUniqueSuccessor() != catchBlock2.getUniqueSuccessor()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public void clearCurrentDefinitions() {
     currentDefinitions = null;
     for (Phi phi : getPhis()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
index 1717cb3..7da900e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
@@ -5,7 +5,9 @@
 
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
@@ -110,6 +112,12 @@
     return new CatchHandlers<>(newGuards, newTargets);
   }
 
+  public CatchHandlers<T> rewriteWithLens(GraphLens graphLens, GraphLens codeLens) {
+    List<DexType> newGuards =
+        ListUtils.mapOrElse(guards, guard -> graphLens.lookupType(guard, codeLens), null);
+    return newGuards != null ? new CatchHandlers<>(newGuards, targets) : this;
+  }
+
   public void forEach(BiConsumer<DexType, T> consumer) {
     for (int i = 0; i < size(); ++i) {
       consumer.accept(guards.get(i), targets.get(i));
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
index af8c834..1bd40d4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -86,10 +87,17 @@
       ProgramMethod context,
       AbstractValueSupplier abstractValueSupplier,
       SideEffectAssumption assumption) {
-    return !(size().definition != null
-        && size().definition.isConstNumber()
-        && size().definition.asConstNumber().getRawValue() >= 0
-        && size().definition.asConstNumber().getRawValue() < Integer.MAX_VALUE);
+    assert type.isArrayType();
+    return isArrayTypeInaccessible(appView, context) || isArraySizeMaybeNegative();
+  }
+
+  private boolean isArrayTypeInaccessible(AppView<?> appView, ProgramMethod context) {
+    DexType baseType = type.toBaseType(appView.dexItemFactory());
+    return !DexTypeUtils.isTypeAccessibleInMethodContext(appView, baseType, context);
+  }
+
+  private boolean isArraySizeMaybeNegative() {
+    return sizeIfConst() < 0;
   }
 
   @Override
@@ -111,12 +119,7 @@
     if (instructionInstanceCanThrow(appView, code.context())) {
       return DeadInstructionResult.notDead();
     }
-    // This would belong better in instructionInstanceCanThrow, but that is not passed an appInfo.
-    DexType baseType = type.toBaseType(appView.dexItemFactory());
-    if (baseType.isPrimitiveType() || appView.definitionFor(baseType) != null) {
-      return DeadInstructionResult.deadIfOutValueIsDead();
-    }
-    return DeadInstructionResult.notDead();
+    return DeadInstructionResult.deadIfOutValueIsDead();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index ba73a12..365708f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -406,6 +406,14 @@
     return uniquePhiUsers = ImmutableSet.copyOf(phiUsers);
   }
 
+  public Set<BasicBlock> uniquePhiUserBlocks() {
+    Set<BasicBlock> ret = Sets.newIdentityHashSet();
+    for (Phi phi : phiUsers) {
+      ret.add(phi.getBlock());
+    }
+    return ret;
+  }
+
   public Set<Instruction> debugUsers() {
     return debugData == null ? null : Collections.unmodifiableSet(debugData.users);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 9f48b43..c03f772 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -57,6 +57,7 @@
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
 import com.android.tools.r8.ir.optimize.PhiOptimizations;
 import com.android.tools.r8.ir.optimize.peepholes.BasicBlockMuncher;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
@@ -90,6 +91,7 @@
   private CfRegisterAllocator registerAllocator;
 
   private Position currentPosition = Position.none();
+  private Position preamblePosition = null;
 
   private final Int2ReferenceMap<DebugLocalInfo> emittedLocals = new Int2ReferenceOpenHashMap<>();
   private Int2ReferenceMap<DebugLocalInfo> pendingLocals = null;
@@ -413,6 +415,7 @@
     if (method.getDefinition().getCode().isCfCode()) {
       diagnosticPosition = method.getDefinition().getCode().asCfCode().getDiagnosticPosition();
     }
+    materializePreamblePosition();
     return new CfCode(
         method.getHolderType(),
         stackHeightTracker.maxHeight,
@@ -424,6 +427,25 @@
         bytecodeMetadataBuilder.build());
   }
 
+  private void materializePreamblePosition() {
+    if (!currentPosition.isNone()
+        || preamblePosition == null
+        || !preamblePosition.hasCallerPosition()) {
+      return;
+    }
+    CfLabel existingLabel = ListUtils.first(instructions).asLabel();
+    int instructionIncrement = existingLabel != null ? 1 : 2;
+    List<CfInstruction> newInstructions =
+        new ArrayList<>(instructions.size() + instructionIncrement);
+    CfLabel label = existingLabel != null ? existingLabel : new CfLabel();
+    newInstructions.add(label);
+    newInstructions.add(new CfPosition(label, preamblePosition));
+    for (int i = existingLabel == null ? 0 : 1; i < instructions.size(); i++) {
+      newInstructions.add(instructions.get(i));
+    }
+    instructions = newInstructions;
+  }
+
   private static boolean isNopInstruction(Instruction instruction, BasicBlock nextBlock) {
     // From DexBuilder
     return instruction.isArgument()
@@ -559,6 +581,9 @@
   @SuppressWarnings("ReferenceEquality")
   private void updatePositionAndLocals(Instruction instruction) {
     Position position = instruction.getPosition();
+    if (preamblePosition == null) {
+      preamblePosition = position;
+    }
     boolean didLocalsChange = localsChanged();
     boolean didPositionChange =
         position.isSome()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
index 6b13747..4fc003f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
@@ -67,6 +67,11 @@
   }
 
   @Override
+  public boolean isD8MethodProcessor() {
+    return true;
+  }
+
+  @Override
   public boolean isProcessedConcurrently(ProgramMethod method) {
     // In D8 all methods are considered independently compiled.
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 9a7b1e0..2dc1a3f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -85,7 +85,6 @@
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.shaking.LibraryMethodOverrideAnalysis;
 import com.android.tools.r8.utils.Action;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -129,7 +128,6 @@
   protected ServiceLoaderRewriter serviceLoaderRewriter;
   protected final EnumUnboxer enumUnboxer;
   protected final NumberUnboxer numberUnboxer;
-  protected InstanceInitializerOutliner instanceInitializerOutliner;
   protected final RemoveVerificationErrorForUnknownReturnedValues
       removeVerificationErrorForUnknownReturnedValues;
 
@@ -218,7 +216,6 @@
       this.enumUnboxer = EnumUnboxer.empty();
       this.numberUnboxer = NumberUnboxer.empty();
       this.assumeInserter = null;
-      this.instanceInitializerOutliner = null;
       this.removeVerificationErrorForUnknownReturnedValues = null;
       return;
     }
@@ -230,13 +227,6 @@
         options.processCovariantReturnTypeAnnotations
             ? new CovariantReturnTypeAnnotationTransformer(appView, this)
             : null;
-    if (appView.options().desugarState.isOn()
-        && appView.options().apiModelingOptions().enableOutliningOfMethods
-        && appView.options().getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L)) {
-      this.instanceInitializerOutliner = new InstanceInitializerOutliner(appView);
-    } else {
-      this.instanceInitializerOutliner = null;
-    }
     removeVerificationErrorForUnknownReturnedValues =
         (appView.options().apiModelingOptions().enableLibraryApiModeling
                 && appView.options().canHaveVerifyErrorForUnknownUnusedReturnValue())
@@ -652,12 +642,9 @@
     timing.end();
     previous = printMethod(code, "IR after enum-switch optimization (SSA)", previous);
 
-    if (instanceInitializerOutliner != null) {
-      instanceInitializerOutliner.rewriteInstanceInitializers(
-          code, context, methodProcessor, methodProcessingContext);
-      assert code.verifyTypes(appView);
-      previous = printMethod(code, "IR after instance initializer outlining (SSA)", previous);
-    }
+    new InstanceInitializerOutliner(appView)
+        .run(code, methodProcessor, methodProcessingContext, timing);
+    previous = printMethod(code, "IR after instance initializer outlining (SSA)", previous);
 
     // Update the IR code if collected call site optimization info has something useful.
     // While aggregation of parameter information at call sites would be more precise than static
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
index 013bfde..0e31e93 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
@@ -118,15 +118,19 @@
         .getName();
   }
 
-  @SuppressWarnings("ReferenceEquality")
   public DexMethodHandle rewriteDexMethodHandle(
       DexMethodHandle methodHandle, MethodHandleUse use, ProgramMethod context) {
+    return rewriteDexMethodHandle(methodHandle, use, context.getReference());
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  public DexMethodHandle rewriteDexMethodHandle(
+      DexMethodHandle methodHandle, MethodHandleUse use, DexMethod context) {
     if (methodHandle.isMethodHandle()) {
       DexMethod invokedMethod = methodHandle.asMethod();
       MethodHandleType oldType = methodHandle.type;
       MethodLookupResult lensLookup =
-          graphLens.lookupMethod(
-              invokedMethod, context.getReference(), oldType.toInvokeType(), codeLens);
+          graphLens.lookupMethod(invokedMethod, context, oldType.toInvokeType(), codeLens);
       DexMethod rewrittenTarget = lensLookup.getReference();
       DexMethod actualTarget;
       MethodHandleType newType;
@@ -161,7 +165,7 @@
         }
       }
       if (newType != oldType || actualTarget != invokedMethod || rewrittenTarget != actualTarget) {
-        DexClass holder = definitions.definitionFor(actualTarget.holder, context);
+        DexClass holder = definitions.definitionFor(actualTarget.holder);
         boolean isInterface = holder != null ? holder.isInterface() : methodHandle.isInterface;
         return definitions
             .dexItemFactory()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
new file mode 100644
index 0000000..19bb6dc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
@@ -0,0 +1,284 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.conversion;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.passes.FilledNewArrayRewriter;
+import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.lightir.IR2LirConverter;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirStrategy;
+import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class LirConverter {
+
+  public static void enterLirSupportedPhase(
+      AppView<AppInfoWithClassHierarchy> appView, ExecutorService executorService)
+      throws ExecutionException {
+    assert appView.testing().canUseLir(appView);
+    assert appView.testing().isPreLirPhase();
+    appView.testing().enterLirSupportedPhase();
+    // Convert code objects to LIR.
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz -> {
+          // TODO(b/225838009): Also convert instance initializers to LIR, by adding support for
+          //  computing the inlining constraint for LIR and using that in the class mergers, and
+          //  class initializers, by updating the concatenation of clinits in horizontal class
+          //  merging.
+          clazz.forEachProgramMethodMatching(
+              method ->
+                  method.hasCode()
+                      && !method.isInitializer()
+                      && !appView.isCfByteCodePassThrough(method),
+              method -> {
+                IRCode code = method.buildIR(appView, MethodConversionOptions.forLirPhase(appView));
+                LirCode<Integer> lirCode =
+                    IR2LirConverter.translate(
+                        code,
+                        BytecodeMetadataProvider.empty(),
+                        LirStrategy.getDefaultStrategy().getEncodingStrategy(),
+                        appView.options());
+                // TODO(b/312890994): Setting a custom code lens is only needed until we convert
+                //  code objects to LIR before we create the first code object with a custom code
+                //  lens (horizontal class merging).
+                GraphLens codeLens = method.getDefinition().getCode().getCodeLens(appView);
+                if (codeLens != appView.codeLens()) {
+                  lirCode =
+                      new LirCode<>(lirCode) {
+                        @Override
+                        public GraphLens getCodeLens(AppView<?> appView) {
+                          return codeLens;
+                        }
+                      };
+                }
+                method.setCode(lirCode, appView);
+              });
+        },
+        appView.options().getThreadingModule(),
+        executorService);
+    // Conversion to LIR via IR will allocate type elements.
+    // They are not needed after construction so remove them again.
+    appView.dexItemFactory().clearTypeElementsCache();
+  }
+
+  public static void rewriteLirWithLens(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Timing timing,
+      ExecutorService executorService)
+      throws ExecutionException {
+    assert appView.testing().canUseLir(appView);
+    assert appView.testing().isSupportedLirPhase();
+    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
+    assert verifyLirOnly(appView);
+
+    GraphLens graphLens = appView.graphLens();
+    assert graphLens.isNonIdentityLens();
+    assert appView.codeLens().isAppliedLens();
+
+    MemberRebindingIdentityLens memberRebindingIdentityLens =
+        graphLens.asNonIdentityLens().find(GraphLens::isMemberRebindingIdentityLens);
+    assert memberRebindingIdentityLens != null;
+    if (graphLens == memberRebindingIdentityLens
+        && memberRebindingIdentityLens.getPrevious().isAppliedLens()) {
+      // Nothing to rewrite.
+      return;
+    }
+
+    timing.begin("LIR->LIR@" + graphLens.getClass().getTypeName());
+    rewriteLirWithUnappliedLens(appView, executorService);
+    timing.end();
+
+    // At this point all code has been mapped according to the graph lens.
+    updateCodeLens(appView);
+  }
+
+  private static void rewriteLirWithUnappliedLens(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService)
+      throws ExecutionException {
+    LensCodeRewriterUtils rewriterUtils = new LensCodeRewriterUtils(appView, true);
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz ->
+            clazz.forEachProgramMethodMatching(
+                m ->
+                    m.hasCode()
+                        && !m.getCode().isSharedCodeObject()
+                        && !appView.isCfByteCodePassThrough(m),
+                m -> rewriteLirMethodWithLens(m, appView, rewriterUtils)),
+        appView.options().getThreadingModule(),
+        executorService);
+
+    // Clear the reference type cache after conversion to reduce memory pressure.
+    appView.dexItemFactory().clearTypeElementsCache();
+  }
+
+  private static void rewriteLirMethodWithLens(
+      ProgramMethod method,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      LensCodeRewriterUtils rewriterUtils) {
+    Code code = method.getDefinition().getCode();
+    if (!code.isLirCode()) {
+      assert false;
+      return;
+    }
+    LirCode<Integer> lirCode = code.asLirCode();
+    LirCode<Integer> rewrittenLirCode =
+        lirCode.rewriteWithSimpleLens(method, appView, rewriterUtils);
+    if (ObjectUtils.notIdentical(lirCode, rewrittenLirCode)) {
+      method.setCode(rewrittenLirCode, appView);
+    }
+  }
+
+  public static void finalizeLirToOutputFormat(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Timing timing,
+      ExecutorService executorService)
+      throws ExecutionException {
+    assert appView.testing().canUseLir(appView);
+    assert appView.testing().isSupportedLirPhase();
+    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
+    assert verifyLirOnly(appView);
+    appView.testing().exitLirSupportedPhase();
+    LensCodeRewriterUtils rewriterUtils = new LensCodeRewriterUtils(appView, true);
+    DeadCodeRemover deadCodeRemover = new DeadCodeRemover(appView);
+    String output = appView.options().isGeneratingClassFiles() ? "CF" : "DEX";
+    timing.begin("LIR->IR->" + output);
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz ->
+            clazz.forEachProgramMethod(
+                m -> finalizeLirMethodToOutputFormat(m, deadCodeRemover, appView, rewriterUtils)),
+        appView.options().getThreadingModule(),
+        executorService);
+    timing.end();
+    // Clear the reference type cache after conversion to reduce memory pressure.
+    appView.dexItemFactory().clearTypeElementsCache();
+    // At this point all code has been mapped according to the graph lens.
+    updateCodeLens(appView);
+  }
+
+  private static void updateCodeLens(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    final NonIdentityGraphLens lens = appView.graphLens().asNonIdentityLens();
+    if (lens == null) {
+      assert false;
+      return;
+    }
+
+    // If the current graph lens is the member rebinding identity lens then code lens is simply
+    // the previous lens. This is the same structure as the more complicated case below but where
+    // there is no need to rewrite any previous pointers.
+    if (lens.isMemberRebindingIdentityLens()) {
+      appView.setCodeLens(lens.getPrevious());
+      return;
+    }
+
+    // Otherwise search out where the lens pointing to the member rebinding identity lens.
+    NonIdentityGraphLens lensAfterMemberRebindingIdentityLens =
+        lens.find(p -> p.getPrevious().isMemberRebindingIdentityLens());
+    if (lensAfterMemberRebindingIdentityLens == null) {
+      // With the current compiler structure we expect to always find the lens.
+      assert false;
+      appView.setCodeLens(lens);
+      return;
+    }
+
+    GraphLens codeLens = appView.codeLens();
+    MemberRebindingIdentityLens memberRebindingIdentityLens =
+        lensAfterMemberRebindingIdentityLens.getPrevious().asMemberRebindingIdentityLens();
+
+    // We are assuming that the member rebinding identity lens is always installed after the current
+    // applied lens/code lens and also that there should not be a rebinding lens from the compilers
+    // first phase (this subroutine is only used after IR conversion for now).
+    assert memberRebindingIdentityLens
+        == lens.findPrevious(
+            p -> p == memberRebindingIdentityLens || p == codeLens || p.isMemberRebindingLens());
+
+    // Rewrite the graph lens effects from 'lens' and up to the member rebinding identity lens.
+    MemberRebindingIdentityLens rewrittenMemberRebindingLens =
+        memberRebindingIdentityLens.toRewrittenMemberRebindingIdentityLens(
+            appView, lens, memberRebindingIdentityLens, lens);
+
+    // The current previous pointers for the graph lenses are:
+    //   lens -> ... -> lensAfterMemberRebindingIdentityLens -> memberRebindingIdentityLens -> g
+    // we rewrite them now to:
+    //   rewrittenMemberRebindingLens -> lens -> ... -> lensAfterMemberRebindingIdentityLens -> g
+
+    // The above will construct the new member rebinding lens such that it points to the new
+    // code-lens point already.
+    assert rewrittenMemberRebindingLens.getPrevious() == lens;
+
+    // Update the previous pointer on the new code lens to jump over the old member rebinding
+    // identity lens.
+    lensAfterMemberRebindingIdentityLens.setPrevious(memberRebindingIdentityLens.getPrevious());
+
+    // The applied lens can now be updated and the rewritten member rebinding lens installed as
+    // the current "unapplied lens".
+    appView.setCodeLens(lens);
+    appView.setGraphLens(rewrittenMemberRebindingLens);
+  }
+
+  private static void finalizeLirMethodToOutputFormat(
+      ProgramMethod method,
+      DeadCodeRemover deadCodeRemover,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      LensCodeRewriterUtils rewriterUtils) {
+    Code code = method.getDefinition().getCode();
+    if (!(code instanceof LirCode)) {
+      return;
+    }
+    Timing onThreadTiming = Timing.empty();
+    LirCode<Integer> lirCode = code.asLirCode();
+    LirCode<Integer> rewrittenLirCode =
+        lirCode.rewriteWithSimpleLens(method, appView, rewriterUtils);
+    if (ObjectUtils.notIdentical(lirCode, rewrittenLirCode)) {
+      method.setCode(rewrittenLirCode, appView);
+    }
+    IRCode irCode = method.buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
+    FilledNewArrayRewriter filledNewArrayRewriter = new FilledNewArrayRewriter(appView);
+    boolean changed = filledNewArrayRewriter.run(irCode, onThreadTiming).hasChanged().toBoolean();
+    if (appView.options().isGeneratingDex() && changed) {
+      ConstantCanonicalizer constantCanonicalizer =
+          new ConstantCanonicalizer(appView, method, irCode);
+      constantCanonicalizer.canonicalize();
+    }
+    // Processing is done and no further uses of the meta-data should arise.
+    BytecodeMetadataProvider noMetadata = BytecodeMetadataProvider.empty();
+    // During processing optimization info may cause previously live code to become dead.
+    // E.g., we may now have knowledge that an invoke does not have side effects.
+    // Thus, we re-run the dead-code remover now as it is assumed complete by CF/DEX finalization.
+    deadCodeRemover.run(irCode, onThreadTiming);
+    MethodConversionOptions conversionOptions = irCode.getConversionOptions();
+    assert !conversionOptions.isGeneratingLir();
+    IRFinalizer<?> finalizer = conversionOptions.getFinalizer(deadCodeRemover, appView);
+    method.setCode(finalizer.finalizeCode(irCode, noMetadata, onThreadTiming), appView);
+  }
+
+  public static boolean verifyLirOnly(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      for (DexEncodedMethod method : clazz.methods(DexEncodedMethod::hasCode)) {
+        assert method.getCode().isLirCode()
+            || method.getCode().isSharedCodeObject()
+            || appView.isCfByteCodePassThrough(method)
+            || appView.options().skipIR;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
index a80055a..829c985 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -17,6 +17,10 @@
     return null;
   }
 
+  public boolean isD8MethodProcessor() {
+    return false;
+  }
+
   public boolean isPrimaryMethodProcessor() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
index e1a4c6f..625a337 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
@@ -71,10 +71,6 @@
     reportNestDesugarDependencies();
     clearNestAttributes();
 
-    if (instanceInitializerOutliner != null) {
-      processSimpleSynthesizeMethods(
-          instanceInitializerOutliner.getSynthesizedMethods(), executorService);
-    }
     if (assertionErrorTwoArgsConstructorRewriter != null) {
       processSimpleSynthesizeMethods(
           assertionErrorTwoArgsConstructorRewriter.getSynthesizedMethods(), executorService);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index e87faba..3dd62c9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -6,28 +6,17 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.conversion.passes.FilledNewArrayRewriter;
-import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
-import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.info.MethodResolutionOptimizationInfoAnalysis;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
-import com.android.tools.r8.lightir.LirCode;
-import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
 import com.android.tools.r8.optimize.compose.ComposableOptimizationPass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.io.IOException;
@@ -239,139 +228,6 @@
     return appView.appInfo().app();
   }
 
-  public static void finalizeLirToOutputFormat(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      Timing timing,
-      ExecutorService executorService)
-      throws ExecutionException {
-    appView.testing().exitLirSupportedPhase();
-    if (!appView.testing().canUseLir(appView)) {
-      return;
-    }
-    LensCodeRewriterUtils rewriterUtils = new LensCodeRewriterUtils(appView, true);
-    DeadCodeRemover deadCodeRemover = new DeadCodeRemover(appView);
-    String output = appView.options().isGeneratingClassFiles() ? "CF" : "DEX";
-    timing.begin("LIR->IR->" + output);
-    ThreadUtils.processItems(
-        appView.appInfo().classes(),
-        clazz ->
-            clazz.forEachProgramMethod(
-                m -> finalizeLirMethodToOutputFormat(m, deadCodeRemover, appView, rewriterUtils)),
-        appView.options().getThreadingModule(),
-        executorService);
-    appView
-        .getSyntheticItems()
-        .getPendingSyntheticClasses()
-        .forEach(
-            clazz ->
-                clazz.forEachProgramMethod(
-                    m ->
-                        finalizeLirMethodToOutputFormat(
-                            m, deadCodeRemover, appView, rewriterUtils)));
-    timing.end();
-    // Clear the reference type cache after conversion to reduce memory pressure.
-    appView.dexItemFactory().clearTypeElementsCache();
-    // At this point all code has been mapped according to the graph lens.
-    updateCodeLens(appView);
-  }
-
-  private static void updateCodeLens(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    final NonIdentityGraphLens lens = appView.graphLens().asNonIdentityLens();
-    if (lens == null) {
-      assert false;
-      return;
-    }
-
-    // If the current graph lens is the member rebinding identity lens then code lens is simply
-    // the previous lens. This is the same structure as the more complicated case below but where
-    // there is no need to rewrite any previous pointers.
-    if (lens.isMemberRebindingIdentityLens()) {
-      appView.setCodeLens(lens.getPrevious());
-      return;
-    }
-
-    // Otherwise search out where the lens pointing to the member rebinding identity lens.
-    NonIdentityGraphLens lensAfterMemberRebindingIdentityLens =
-        lens.find(p -> p.getPrevious().isMemberRebindingIdentityLens());
-    if (lensAfterMemberRebindingIdentityLens == null) {
-      // With the current compiler structure we expect to always find the lens.
-      assert false;
-      appView.setCodeLens(lens);
-      return;
-    }
-
-    GraphLens codeLens = appView.codeLens();
-    MemberRebindingIdentityLens memberRebindingIdentityLens =
-        lensAfterMemberRebindingIdentityLens.getPrevious().asMemberRebindingIdentityLens();
-
-    // We are assuming that the member rebinding identity lens is always installed after the current
-    // applied lens/code lens and also that there should not be a rebinding lens from the compilers
-    // first phase (this subroutine is only used after IR conversion for now).
-    assert memberRebindingIdentityLens
-        == lens.findPrevious(
-            p -> p == memberRebindingIdentityLens || p == codeLens || p.isMemberRebindingLens());
-
-    // Rewrite the graph lens effects from 'lens' and up to the member rebinding identity lens.
-    MemberRebindingIdentityLens rewrittenMemberRebindingLens =
-        memberRebindingIdentityLens.toRewrittenMemberRebindingIdentityLens(
-            appView, lens, memberRebindingIdentityLens, lens);
-
-    // The current previous pointers for the graph lenses are:
-    //   lens -> ... -> lensAfterMemberRebindingIdentityLens -> memberRebindingIdentityLens -> g
-    // we rewrite them now to:
-    //   rewrittenMemberRebindingLens -> lens -> ... -> lensAfterMemberRebindingIdentityLens -> g
-
-    // The above will construct the new member rebinding lens such that it points to the new
-    // code-lens point already.
-    assert rewrittenMemberRebindingLens.getPrevious() == lens;
-
-    // Update the previous pointer on the new code lens to jump over the old member rebinding
-    // identity lens.
-    lensAfterMemberRebindingIdentityLens.setPrevious(memberRebindingIdentityLens.getPrevious());
-
-    // The applied lens can now be updated and the rewritten member rebinding lens installed as
-    // the current "unapplied lens".
-    appView.setCodeLens(lens);
-    appView.setGraphLens(rewrittenMemberRebindingLens);
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private static void finalizeLirMethodToOutputFormat(
-      ProgramMethod method,
-      DeadCodeRemover deadCodeRemover,
-      AppView<?> appView,
-      LensCodeRewriterUtils rewriterUtils) {
-    Code code = method.getDefinition().getCode();
-    if (!(code instanceof LirCode)) {
-      return;
-    }
-    Timing onThreadTiming = Timing.empty();
-    LirCode<Integer> lirCode = code.asLirCode();
-    LirCode<Integer> rewrittenLirCode =
-        lirCode.rewriteWithSimpleLens(method, appView, rewriterUtils);
-    if (lirCode != rewrittenLirCode) {
-      method.setCode(rewrittenLirCode, appView);
-    }
-    IRCode irCode = method.buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
-    FilledNewArrayRewriter filledNewArrayRewriter = new FilledNewArrayRewriter(appView);
-    boolean changed = filledNewArrayRewriter.run(irCode, onThreadTiming).hasChanged().toBoolean();
-    if (appView.options().isGeneratingDex() && changed) {
-      ConstantCanonicalizer constantCanonicalizer =
-          new ConstantCanonicalizer(appView, method, irCode);
-      constantCanonicalizer.canonicalize();
-    }
-    // Processing is done and no further uses of the meta-data should arise.
-    BytecodeMetadataProvider noMetadata = BytecodeMetadataProvider.empty();
-    // During processing optimization info may cause previously live code to become dead.
-    // E.g., we may now have knowledge that an invoke does not have side effects.
-    // Thus, we re-run the dead-code remover now as it is assumed complete by CF/DEX finalization.
-    deadCodeRemover.run(irCode, onThreadTiming);
-    MethodConversionOptions conversionOptions = irCode.getConversionOptions();
-    assert !conversionOptions.isGeneratingLir();
-    IRFinalizer<?> finalizer = conversionOptions.getFinalizer(deadCodeRemover, appView);
-    method.setCode(finalizer.finalizeCode(irCode, noMetadata, onThreadTiming), appView);
-  }
-
   private void clearDexMethodCompilationState() {
     appView.appInfo().classes().forEach(this::clearDexMethodCompilationState);
   }
@@ -430,10 +286,6 @@
     if (inliner != null) {
       inliner.onLastWaveDone(postMethodProcessorBuilder, executorService, timing);
     }
-    if (instanceInitializerOutliner != null) {
-      instanceInitializerOutliner.onLastWaveDone(postMethodProcessorBuilder);
-      instanceInitializerOutliner = null;
-    }
     if (serviceLoaderRewriter != null) {
       serviceLoaderRewriter.onLastWaveDone(postMethodProcessorBuilder);
       serviceLoaderRewriter = null;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index df8d38a..41379ad 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -7,24 +7,28 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.LinearFlowInstructionListIterator;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewArrayFilled;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
-import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.DominatorChecker;
 import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions;
-import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.ValueUtils;
+import com.android.tools.r8.utils.ValueUtils.ArrayValues;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -79,8 +83,11 @@
  */
 public class ArrayConstructionSimplifier extends CodeRewriterPass<AppInfo> {
 
+  private final RewriteArrayOptions rewriteArrayOptions;
+
   public ArrayConstructionSimplifier(AppView<?> appView) {
     super(appView);
+    rewriteArrayOptions = options.rewriteArrayOptions();
   }
 
   @Override
@@ -90,228 +97,271 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    boolean hasChanged = false;
-    WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks);
-    while (worklist.hasNext()) {
-      BasicBlock block = worklist.next();
-      hasChanged |= simplifyArrayConstructionBlock(block, worklist, code, appView.options());
+    ArrayList<ArrayValues> candidates = findOptimizableArrays(code);
+
+    if (candidates.isEmpty()) {
+      return CodeRewriterResult.NO_CHANGE;
     }
-    return CodeRewriterResult.hasChanged(hasChanged);
+    applyChanges(code, candidates);
+    return CodeRewriterResult.HAS_CHANGED;
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return code.metadata().mayHaveNewArrayEmpty();
   }
 
-  private boolean simplifyArrayConstructionBlock(
-      BasicBlock block, WorkList<BasicBlock> worklist, IRCode code, InternalOptions options) {
-    boolean hasChanged = false;
-    RewriteArrayOptions rewriteOptions = options.rewriteArrayOptions();
-    InstructionListIterator it = block.listIterator(code);
-    while (it.hasNext()) {
-      FilledArrayCandidate candidate = computeFilledArrayCandidate(it.next(), rewriteOptions);
-      if (candidate == null) {
-        continue;
-      }
-      FilledArrayConversionInfo info =
-          computeConversionInfo(
-              code, candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex()));
-      if (info == null) {
-        continue;
-      }
-
-      Instruction instructionAfterCandidate = it.peekNext();
-      NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty;
-      DexType arrayType = newArrayEmpty.type;
-      int size = candidate.size;
-      Set<Instruction> instructionsToRemove = SetUtils.newIdentityHashSet(size + 1);
-      assert newArrayEmpty.getLocalInfo() == null;
-      Instruction lastArrayPut = info.lastArrayPutIterator.peekPrevious();
-      Value invokeValue = code.createValue(newArrayEmpty.getOutType(), null);
-      NewArrayFilled invoke =
-          new NewArrayFilled(arrayType, invokeValue, Arrays.asList(info.values));
-      invoke.setPosition(lastArrayPut.getPosition());
-      for (Value value : newArrayEmpty.inValues()) {
-        value.removeUser(newArrayEmpty);
-      }
-      newArrayEmpty.outValue().replaceUsers(invokeValue);
-      instructionsToRemove.add(newArrayEmpty);
-
-      boolean originalAllocationPointHasHandlers = block.hasCatchHandlers();
-      boolean insertionPointHasHandlers = lastArrayPut.getBlock().hasCatchHandlers();
-
-      if (!insertionPointHasHandlers && !originalAllocationPointHasHandlers) {
-        info.lastArrayPutIterator.add(invoke);
-      } else {
-        BasicBlock insertionBlock = info.lastArrayPutIterator.split(code);
-        if (originalAllocationPointHasHandlers) {
-          if (!insertionBlock.isTrivialGoto()) {
-            BasicBlock blockAfterInsertion = insertionBlock.listIterator(code).split(code);
-            assert insertionBlock.isTrivialGoto();
-            worklist.addIfNotSeen(blockAfterInsertion);
-          }
-          insertionBlock.moveCatchHandlers(block);
-        } else {
-          worklist.addIfNotSeen(insertionBlock);
-        }
-        insertionBlock.listIterator(code).add(invoke);
-      }
-
-      instructionsToRemove.addAll(info.arrayPutsToRemove);
-      Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet();
-      for (Instruction instruction : instructionsToRemove) {
-        BasicBlock ownerBlock = instruction.getBlock();
-        // If owner block is null, then the instruction has been removed already. We can't rely on
-        // just having the block pointer nulled, so the visited blocks guards reprocessing.
-        if (ownerBlock != null && visitedBlocks.add(ownerBlock)) {
-          InstructionListIterator removeIt = ownerBlock.listIterator(code);
-          while (removeIt.hasNext()) {
-            if (instructionsToRemove.contains(removeIt.next())) {
-              removeIt.removeOrReplaceByDebugLocalRead();
-            }
-          }
+  private ArrayList<ArrayValues> findOptimizableArrays(IRCode code) {
+    ArrayList<ArrayValues> candidates = new ArrayList<>();
+    for (Instruction instruction : code.instructions()) {
+      NewArrayEmpty newArrayEmpty = instruction.asNewArrayEmpty();
+      if (newArrayEmpty != null) {
+        ArrayValues arrayValues = analyzeCandidate(newArrayEmpty, code);
+        if (arrayValues != null) {
+          candidates.add(arrayValues);
         }
       }
-
-      // The above has invalidated the block iterator so reset it and continue.
-      it = block.listIterator(code, instructionAfterCandidate);
-      hasChanged = true;
     }
-    if (hasChanged) {
-      code.removeRedundantBlocks();
-    }
-
-    return hasChanged;
+    return candidates;
   }
 
-  private static class FilledArrayConversionInfo {
-
-    Value[] values;
-    List<ArrayPut> arrayPutsToRemove;
-    LinearFlowInstructionListIterator lastArrayPutIterator;
-
-    public FilledArrayConversionInfo(int size) {
-      values = new Value[size];
-      arrayPutsToRemove = new ArrayList<>(size);
+  private ArrayValues analyzeCandidate(NewArrayEmpty newArrayEmpty, IRCode code) {
+    if (newArrayEmpty.getLocalInfo() != null) {
+      return null;
     }
+    if (!rewriteArrayOptions.isPotentialSize(newArrayEmpty.sizeIfConst())) {
+      return null;
+    }
+
+    ArrayValues arrayValues = ValueUtils.computeInitialArrayValues(newArrayEmpty);
+    // Holes (default-initialized entries) could be supported, but they are rare and would
+    // complicate the logic.
+    if (arrayValues == null || arrayValues.containsHoles()) {
+      return null;
+    }
+
+    // See if all instructions are in the same try/catch.
+    ArrayPut lastArrayPut = ArrayUtils.last(arrayValues.getArrayPutsByIndex());
+    if (!newArrayEmpty.getBlock().hasEquivalentCatchHandlers(lastArrayPut.getBlock())) {
+      // Possible improvements:
+      // 1) Ignore catch handlers that do not catch OutOfMemoryError / NoClassDefFoundError.
+      // 2) Use the catch handlers from the new-array-empty if all exception blocks exit without
+      //    side effects (e.g. no method calls & no monitor instructions).
+      return null;
+    }
+
+    if (!checkTypesAreCompatible(arrayValues, code)) {
+      return null;
+    }
+    if (!checkDominance(arrayValues)) {
+      return null;
+    }
+    return arrayValues;
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  private FilledArrayConversionInfo computeConversionInfo(
-      IRCode code, FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) {
-    NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty;
-    assert it.peekPrevious() == newArrayEmpty;
-    Value arrayValue = newArrayEmpty.outValue();
-    int size = candidate.size;
-
+  private boolean checkTypesAreCompatible(ArrayValues arrayValues, IRCode code) {
     // aput-object allows any object for arrays of interfaces, but new-filled-array fails to verify
     // if types require a cast.
     // TODO(b/246971330): Check if adding a checked-cast would have the same observable result. E.g.
     //   if aput-object throws a ClassCastException if given an object that does not implement the
     //   desired interface, then we could add check-cast instructions for arguments we're not sure
     //   about.
+    NewArrayEmpty newArrayEmpty = arrayValues.getDefinition().asNewArrayEmpty();
     DexType elementType = newArrayEmpty.type.toArrayElementType(dexItemFactory);
     boolean needsTypeCheck =
-        !elementType.isPrimitiveType() && elementType != dexItemFactory.objectType;
+        !elementType.isPrimitiveType() && elementType.isNotIdenticalTo(dexItemFactory.objectType);
+    if (!needsTypeCheck) {
+      return true;
+    }
 
-    FilledArrayConversionInfo info = new FilledArrayConversionInfo(size);
-    Value[] values = info.values;
-    int remaining = size;
-    Set<Instruction> users = newArrayEmpty.outValue().uniqueUsers();
-    while (it.hasNext()) {
-      Instruction instruction = it.next();
-      BasicBlock block = instruction.getBlock();
-      // If we encounter an instruction that can throw an exception we need to bail out of the
-      // optimization so that we do not transform half-initialized arrays into fully initialized
-      // arrays on exceptional edges. If the block has no handlers it is not observable so
-      // we perform the rewriting.
-      if (block.hasCatchHandlers()
-          && instruction.instructionInstanceCanThrow(appView, code.context())) {
-        return null;
-      }
-      if (!users.contains(instruction)) {
-        // If any instruction can transfer control between the new-array and the last array put
-        // then it is not safe to move the new array to the point of the last put.
-        if (block.hasCatchHandlers() && instruction.instructionTypeCanThrow()) {
-          return null;
-        }
+    // Not safe to move allocation if NoClassDefError is possible.
+    // TODO(b/246971330): Make this work for D8 where it ~always returns false by checking that
+    // all instructions between new-array-empty and the last array-put report
+    // !instruction.instructionMayHaveSideEffects(). Alternatively, we could replace the
+    // new-array-empty with a const-class instruction in this case.
+    if (!DexTypeUtils.isTypeAccessibleInMethodContext(
+        appView, elementType.toBaseType(dexItemFactory), code.context())) {
+      return false;
+    }
+
+    for (ArrayPut arrayPut : arrayValues.getArrayPutsByIndex()) {
+      Value value = arrayPut.value();
+      if (value.isAlwaysNull(appView)) {
         continue;
       }
-      ArrayPut arrayPut = instruction.asArrayPut();
-      // If the initialization sequence is broken by another use we cannot use a fill-array-data
-      // instruction.
+      DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory);
+      if (elementType.isArrayType()) {
+        if (elementType.isNotIdenticalTo(valueDexType)) {
+          return false;
+        }
+      } else if (valueDexType.isArrayType()) {
+        // isSubtype asserts for this case.
+        return false;
+      } else if (valueDexType.isNullValueType()) {
+        // Assume instructions can cause value.isAlwaysNull() == false while the DexType is
+        // null.
+        // TODO(b/246971330): Figure out how to write a test in SimplifyArrayConstructionTest
+        //   that hits this case.
+      } else {
+        // TODO(b/246971330): When in d8 mode, we might still be able to see if this is true for
+        //   library types (which this helper does not do).
+        if (appView.isSubtype(valueDexType, elementType).isPossiblyFalse()) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  private static boolean checkDominance(ArrayValues arrayValues) {
+    Value arrayValue = arrayValues.getArrayValue();
+
+    Set<BasicBlock> usageBlocks = Sets.newIdentityHashSet();
+    Set<Instruction> uniqueUsers = arrayValue.uniqueUsers();
+    for (Instruction user : uniqueUsers) {
+      ArrayPut arrayPut = user.asArrayPut();
       if (arrayPut == null || arrayPut.array() != arrayValue) {
-        return null;
+        usageBlocks.add(user.getBlock());
       }
-      int index = arrayPut.indexIfConstAndInBounds(values.length);
-      if (index < 0 || values[index] != null) {
-        return null;
+    }
+
+    // Ensure all blocks for users of the array are dominated by the last array-put's block.
+    ArrayPut lastArrayPut = ArrayUtils.last(arrayValues.getArrayPutsByIndex());
+    BasicBlock lastArrayPutBlock = lastArrayPut.getBlock();
+    BasicBlock subgraphEntryBlock = arrayValue.definition.getBlock();
+    for (BasicBlock usageBlock : usageBlocks) {
+      if (!DominatorChecker.check(subgraphEntryBlock, usageBlock, lastArrayPutBlock)) {
+        return false;
       }
-      if (arrayPut.instructionInstanceCanThrow(appView, code.context())) {
-        return null;
+    }
+
+    // Ensure all array users in the same block appear after the last array-put
+    for (Instruction inst : lastArrayPutBlock.getInstructions()) {
+      if (inst == lastArrayPut) {
+        break;
       }
-      Value value = arrayPut.value();
-      if (needsTypeCheck && !value.isAlwaysNull(appView)) {
-        DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory);
-        if (elementType.isArrayType()) {
-          if (elementType != valueDexType) {
-            return null;
-          }
-        } else if (valueDexType.isArrayType()) {
-          // isSubtype asserts for this case.
-          return null;
-        } else if (valueDexType.isNullValueType()) {
-          // Assume instructions can cause value.isAlwaysNull() == false while the DexType is null.
-          // TODO(b/246971330): Figure out how to write a test in SimplifyArrayConstructionTest
-          //   that hits this case.
-        } else {
-          // TODO(b/246971330): When in d8 mode, we might still be able to see if this is true for
-          //   library types (which this helper does not do).
-          if (appView.isSubtype(valueDexType, elementType).isPossiblyFalse()) {
-            return null;
+      if (uniqueUsers.contains(inst)) {
+        ArrayPut arrayPut = inst.asArrayPut();
+        if (arrayPut == null || arrayPut.array() != arrayValue) {
+          return false;
+        }
+      }
+    }
+
+    // It will not be the case that the newArrayEmpty dominates the phi user (or else it would
+    // just be a normal user). It is safe to optimize if all paths from the new-array-empty to the
+    // phi user include the last array-put (where the filled-new-array will end up).
+    if (anyPhiUsersReachableWhenOptimized(arrayValues)) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Determines if there are any paths from the new-array-empty to any of its phi users that do not
+   * go through the last array-put, and that do not go through exceptional edges for new-array-empty
+   * / array-puts that will be removed.
+   */
+  private static boolean anyPhiUsersReachableWhenOptimized(ArrayValues arrayValues) {
+    Value arrayValue = arrayValues.getArrayValue();
+    if (!arrayValue.hasPhiUsers()) {
+      return false;
+    }
+    Set<BasicBlock> phiUserBlocks = arrayValue.uniquePhiUserBlocks();
+    WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
+    // Mark the last array-put as seen in order to find paths that do not contain it.
+    workList.markAsSeen(ArrayUtils.last(arrayValues.getArrayPutsByIndex()).getBlock());
+    // Start with normal successors since if optimized, the new-array-empty block will have no
+    // throwing instructions.
+    workList.addIfNotSeen(arrayValue.definition.getBlock().getNormalSuccessors());
+    while (workList.hasNext()) {
+      BasicBlock current = workList.removeLast();
+      if (phiUserBlocks.contains(current)) {
+        return true;
+      }
+      if (current.hasCatchHandlers()) {
+        Instruction throwingInstruction = current.exceptionalExit();
+        if (throwingInstruction != null) {
+          ArrayPut arrayPut = throwingInstruction.asArrayPut();
+          // Ignore exceptional edges that will be remove if optimized.
+          if (arrayPut != null && arrayPut.array() == arrayValue) {
+            workList.addIfNotSeen(current.getNormalSuccessors());
+            continue;
           }
         }
       }
-      info.arrayPutsToRemove.add(arrayPut);
-      values[index] = value;
-      --remaining;
-      if (remaining == 0) {
-        info.lastArrayPutIterator = it;
-        return info;
+      workList.addIfNotSeen(current.getSuccessors());
+    }
+    return false;
+  }
+
+  private void applyChanges(IRCode code, List<ArrayValues> candidates) {
+    Set<BasicBlock> relevantBlocks = Sets.newIdentityHashSet();
+    // All keys instructionsToChange are removed, and also maps lastArrayPut -> newArrayFilled.
+    Map<Instruction, Instruction> instructionsToChange = new IdentityHashMap<>();
+    boolean needToRemoveUnreachableBlocks = false;
+
+    for (ArrayValues arrayValues : candidates) {
+      NewArrayEmpty newArrayEmpty = arrayValues.getDefinition().asNewArrayEmpty();
+      instructionsToChange.put(newArrayEmpty, newArrayEmpty);
+      BasicBlock allocationBlock = newArrayEmpty.getBlock();
+      relevantBlocks.add(allocationBlock);
+
+      ArrayPut[] arrayPutsByIndex = arrayValues.getArrayPutsByIndex();
+      int lastArrayPutIndex = arrayPutsByIndex.length - 1;
+      for (int i = 0; i < lastArrayPutIndex; ++i) {
+        ArrayPut arrayPut = arrayPutsByIndex[i];
+        instructionsToChange.put(arrayPut, arrayPut);
+        relevantBlocks.add(arrayPut.getBlock());
+      }
+      ArrayPut lastArrayPut = arrayPutsByIndex[lastArrayPutIndex];
+      BasicBlock lastArrayPutBlock = lastArrayPut.getBlock();
+      relevantBlocks.add(lastArrayPutBlock);
+
+      // newArrayEmpty's outValue must be cleared before trying to remove newArrayEmpty. Rather than
+      // store the outValue for later, create and store newArrayFilled.
+      Value arrayValue = newArrayEmpty.clearOutValue();
+      NewArrayFilled newArrayFilled =
+          new NewArrayFilled(newArrayEmpty.type, arrayValue, arrayValues.getElementValues());
+      newArrayFilled.setPosition(lastArrayPut.getPosition());
+      instructionsToChange.put(lastArrayPut, newArrayFilled);
+
+      if (arrayValue.hasPhiUsers() && allocationBlock.hasCatchHandlers()) {
+        // When phi users exist, the phis belong to the exceptional successors of the allocation
+        // block. In order to preserve them, move them to the new allocation block.
+        // This is safe because we've already checked hasEquivalentCatchHandlers().
+        lastArrayPutBlock.removeAllExceptionalSuccessors();
+        lastArrayPutBlock.moveCatchHandlers(allocationBlock);
+        needToRemoveUnreachableBlocks = true;
       }
     }
-    return null;
-  }
 
-  private static class FilledArrayCandidate {
+    for (BasicBlock block : relevantBlocks) {
+      boolean hasCatchHandlers = block.hasCatchHandlers();
+      InstructionListIterator it = block.listIterator(code);
+      while (it.hasNext()) {
+        Instruction possiblyNewArray = instructionsToChange.get(it.next());
+        if (possiblyNewArray != null) {
+          if (possiblyNewArray.isNewArrayFilled()) {
+            // Change the last array-put to the new-array-filled.
+            it.replaceCurrentInstruction(possiblyNewArray);
+          } else {
+            it.removeOrReplaceByDebugLocalRead();
+            if (hasCatchHandlers) {
+              // Removing these catch handlers shrinks their ranges to be only that where the
+              // allocation occurs.
+              needToRemoveUnreachableBlocks = true;
+              assert !block.canThrow();
+              block.removeAllExceptionalSuccessors();
+            }
+          }
+        }
+      }
+    }
 
-    final NewArrayEmpty newArrayEmpty;
-    final int size;
-
-    public FilledArrayCandidate(NewArrayEmpty newArrayEmpty, int size) {
-      assert size > 0;
-      this.newArrayEmpty = newArrayEmpty;
-      this.size = size;
+    if (needToRemoveUnreachableBlocks) {
+      code.removeUnreachableBlocks();
     }
-  }
-
-  private FilledArrayCandidate computeFilledArrayCandidate(
-      Instruction instruction, RewriteArrayOptions options) {
-    NewArrayEmpty newArrayEmpty = instruction.asNewArrayEmpty();
-    if (newArrayEmpty == null) {
-      return null;
-    }
-    if (instruction.getLocalInfo() != null) {
-      return null;
-    }
-    if (!newArrayEmpty.size().isConstant()) {
-      return null;
-    }
-    int size = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue();
-    if (!options.isPotentialSize(size)) {
-      return null;
-    }
-    return new FilledArrayCandidate(newArrayEmpty, size);
+    code.removeRedundantBlocks();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
index e8672b3..160335c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.ir.code.Ushr;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.Xor;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.ImmutableMap;
@@ -244,7 +245,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return options.testing.enableBinopOptimization
         && !isDebugMode(code.context())
         && code.metadata().mayHaveArithmeticOrLogicalBinop();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index c663705..978a99f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -78,7 +78,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return code.metadata().mayHaveIf() || code.metadata().mayHaveSwitch();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
index 73afe6a..98a92e8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
@@ -50,7 +50,7 @@
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    if (shouldRewriteCode(code)) {
+    if (shouldRewriteCode(code, methodProcessor)) {
       assert verifyConsistentCode(code, isAcceptingSSA(), "before");
       CodeRewriterResult result = rewriteCode(code, methodProcessor, methodProcessingContext);
       assert result.hasChanged().isFalse() || verifyConsistentCode(code, isProducingSSA(), "after");
@@ -100,5 +100,5 @@
     return rewriteCode(code);
   }
 
-  protected abstract boolean shouldRewriteCode(IRCode code);
+  protected abstract boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
index 4089a9b..3e51706 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.base.Equivalence;
@@ -35,7 +36,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index 89b6f2d..9584e8a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
 import com.android.tools.r8.utils.LazyBox;
@@ -71,7 +72,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index 28a40a5..7c476ff 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions;
@@ -126,7 +127,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return code.metadata().mayHaveNewArrayFilled();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
index a9084b1..c8df326 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import java.util.Set;
 
@@ -28,7 +29,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return code.metadata().mayHaveArrayLength();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
index 42c9c91..a7277a8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
@@ -37,7 +38,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return options.isGeneratingDex() && code.metadata().mayHaveInvokeMethod();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index eaa0edb..3096671 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Sub;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.WorkList;
@@ -59,7 +60,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     // This is relevant only if a loop may be present, which implies at least 4 blocks.
     return appView.options().enableLoopUnrolling
         && code.metadata().mayHaveIf()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
index 28b76ae..43d249f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.utils.CollectionUtils;
@@ -139,7 +140,7 @@
 
   /** Only run this when the rewriting may actually enable more constructor inlining. */
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     if (!appView.hasClassHierarchy()) {
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
index 298a742..f031b5c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.LazyBox;
 import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
@@ -51,7 +52,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     if (appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()
         && !appView.options().testing.forceRedundantConstNumberRemoval) {
       // See also b/124152497.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 7ebed43..c8455fc 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.ListUtils;
@@ -38,7 +39,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     // This is relevant only if there is a diamond followed by an if which is a minimum of 6 blocks.
     return code.metadata().mayHaveIf() && code.getBlocks().size() >= 6;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
index b9b34ad..3935555 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
index 60ff58e..73fd010 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
@@ -46,7 +46,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return appView.enableWholeProgramOptimizations()
         && appView.options().testing.enableCheckCastAndInstanceOfRemoval
         && (code.metadata().mayHaveCheckCast() || code.metadata().mayHaveInstanceOf());
@@ -231,7 +231,8 @@
     // type.
     if (castType.isClassType()
         && castType.isAlwaysNull(appViewWithLiveness)
-        && !outValue.hasDebugUsers()) {
+        && !outValue.hasDebugUsers()
+        && !appView.getSyntheticItems().isFinalized()) {
       // Replace all usages of the out-value by null.
       it.previous();
       Value nullValue = it.insertConstNullInstruction(code, options);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
index 3ecb8be..c0fb1af 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -87,7 +88,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 46988a7..e204f8e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -178,9 +178,7 @@
   // Synthesize virtual methods.
   private void synthesizeVirtualMethods(
       SyntheticProgramClassBuilder builder, DesugarInvoke desugarInvoke) {
-    DexMethod mainMethod =
-        appView.dexItemFactory().createMethod(type, descriptor.erasedProto, descriptor.name);
-
+    DexMethod mainMethod = descriptor.getMainMethod().withHolder(type, appView.dexItemFactory());
     List<DexEncodedMethod> methods = new ArrayList<>(1 + descriptor.bridges.size());
 
     // Synthesize main method.
@@ -198,7 +196,7 @@
     // Synthesize bridge methods.
     for (DexProto bridgeProto : descriptor.bridges) {
       DexMethod bridgeMethod =
-          appView.dexItemFactory().createMethod(type, bridgeProto, descriptor.name);
+          appView.dexItemFactory().createMethod(type, bridgeProto, descriptor.getName());
       methods.add(
           DexEncodedMethod.syntheticBuilder()
               .setMethod(bridgeMethod)
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
index 0c356a2..e3cbc07 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
@@ -20,6 +20,10 @@
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry.MethodHandleUse;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.List;
@@ -39,13 +43,11 @@
 
   final String uniqueId;
   final DexMethod mainMethod;
-  public final DexString name;
-  final DexProto erasedProto;
   final DexProto enforcedProto;
   public final DexMethodHandle implHandle;
 
-  public final List<DexType> interfaces = new ArrayList<>();
-  public final Set<DexProto> bridges = Sets.newIdentityHashSet();
+  public final List<DexType> interfaces;
+  public final Set<DexProto> bridges;
   public final DexTypeList captures;
 
   // Used for accessibility analysis and few assertions only.
@@ -53,21 +55,21 @@
   private final DexType targetHolder;
 
   private LambdaDescriptor() {
-    uniqueId = null;
-    name = null;
-    erasedProto = null;
-    enforcedProto = null;
-    implHandle = null;
-    captures = null;
-    targetAccessFlags = null;
-    targetHolder = null;
-    mainMethod = null;
+    this(null, null, null, null, null, null, null, null, null);
+  }
+
+  public DexProto getErasedProto() {
+    return mainMethod.getProto();
   }
 
   public DexMethod getMainMethod() {
     return mainMethod;
   }
 
+  public DexString getName() {
+    return mainMethod.getName();
+  }
+
   private LambdaDescriptor(
       AppView<?> appView,
       AppInfoWithClassHierarchy appInfo,
@@ -89,12 +91,11 @@
     assert captures != null;
     this.mainMethod = appInfo.dexItemFactory().createMethod(mainInterface, erasedProto, name);
     this.uniqueId = callSite.getHash();
-    this.name = name;
-    this.erasedProto = erasedProto;
     this.enforcedProto = enforcedProto;
     this.implHandle = implHandle;
     this.captures = captures;
-
+    this.bridges = Sets.newIdentityHashSet();
+    this.interfaces = new ArrayList<>();
     this.interfaces.add(mainInterface);
     DexClassAndMethod targetMethod =
         context == null ? null : lookupTargetMethod(appView, appInfo, context);
@@ -107,6 +108,27 @@
     }
   }
 
+  private LambdaDescriptor(
+      String uniqueId,
+      DexMethod mainMethod,
+      DexProto enforcedProto,
+      DexMethodHandle implHandle,
+      List<DexType> interfaces,
+      Set<DexProto> bridges,
+      DexTypeList captures,
+      MethodAccessFlags targetAccessFlags,
+      DexType targetHolder) {
+    this.uniqueId = uniqueId;
+    this.mainMethod = mainMethod;
+    this.enforcedProto = enforcedProto;
+    this.implHandle = implHandle;
+    this.interfaces = interfaces;
+    this.bridges = bridges;
+    this.captures = captures;
+    this.targetAccessFlags = targetAccessFlags;
+    this.targetHolder = targetHolder;
+  }
+
   final DexType getImplReceiverType() {
     // The receiver of instance impl-method is captured as the first captured
     // value or should be the first argument of the enforced method signature.
@@ -179,7 +201,7 @@
     return method.getDefinition().isPublicized() && isInstanceMethod(method);
   }
 
-  public final boolean verifyTargetFoundInClass(DexType type) {
+  public boolean verifyTargetFoundInClass(DexType type) {
     return targetHolder.isIdenticalTo(type);
   }
 
@@ -189,14 +211,15 @@
   }
 
   public void forEachErasedAndEnforcedTypes(BiConsumer<DexType, DexType> consumer) {
-    consumer.accept(erasedProto.returnType, enforcedProto.returnType);
+    DexProto erasedProto = getErasedProto();
+    consumer.accept(erasedProto.getReturnType(), enforcedProto.getReturnType());
     for (int i = 0; i < enforcedProto.getArity(); i++) {
       consumer.accept(erasedProto.getParameter(i), enforcedProto.getParameter(i));
     }
   }
 
   /** Is a stateless lambda, i.e. lambda does not capture any values */
-  final boolean isStateless() {
+  boolean isStateless() {
     return captures.isEmpty();
   }
 
@@ -495,4 +518,32 @@
 
     return false;
   }
+
+  public LambdaDescriptor rewrittenWithLens(
+      GraphLens lens, GraphLens appliedLens, LensCodeRewriterUtils rewriter) {
+    String newUniqueId = uniqueId;
+    DexMethod newMainMethod = lens.getRenamedMethodSignature(mainMethod, appliedLens);
+    DexProto newEnforcedProto = rewriter.rewriteProto(enforcedProto);
+    DexMethodHandle newImplHandle =
+        rewriter.rewriteDexMethodHandle(
+            implHandle, MethodHandleUse.ARGUMENT_TO_LAMBDA_METAFACTORY, mainMethod);
+    List<DexType> newInterfaces =
+        new ArrayList<>(
+            SetUtils.mapLinkedHashSet(interfaces, itf -> lens.lookupType(itf, appliedLens)));
+    Set<DexProto> newBridges = SetUtils.mapIdentityHashSet(bridges, rewriter::rewriteProto);
+    DexTypeList newCaptures = captures.map(capture -> lens.lookupType(capture, appliedLens));
+    MethodAccessFlags newTargetAccessFlags = targetAccessFlags;
+    DexType newTargetHolder =
+        targetHolder != null ? lens.lookupType(targetHolder, appliedLens) : null;
+    return new LambdaDescriptor(
+        newUniqueId,
+        newMainMethod,
+        newEnforcedProto,
+        newImplHandle,
+        newInterfaces,
+        newBridges,
+        newCaptures,
+        newTargetAccessFlags,
+        newTargetHolder);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index df5b8b4..44f0307 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.ValueType;
@@ -181,8 +182,8 @@
 
     DexMethod methodToCall = target.callTarget;
     DexType[] capturedTypes = lambda.descriptor.captures.values;
-    DexType[] erasedParams = lambda.descriptor.erasedProto.parameters.values;
-    DexType erasedReturnType = lambda.descriptor.erasedProto.returnType;
+    DexTypeList erasedParams = lambda.descriptor.getErasedProto().getParameters();
+    DexType erasedReturnType = lambda.descriptor.getErasedProto().getReturnType();
     DexType[] enforcedParams = lambda.descriptor.enforcedProto.parameters.values;
     DexType enforcedReturnType = lambda.descriptor.enforcedProto.returnType;
     if (enforcedReturnType.isPrimitiveType() && mainMethod.getReturnType().isReferenceType()) {
@@ -244,14 +245,14 @@
 
     // Prepare arguments.
     int maxLocals = 1; // Local 0 is the lambda/receiver.
-    for (int i = 0; i < erasedParams.length; i++) {
+    for (int i = 0; i < erasedParams.size(); i++) {
       ValueType valueType = ValueType.fromDexType(mainMethod.getParameters().values[i]);
       instructions.add(new CfLoad(valueType, maxLocals));
       maxLocals += valueType.requiredRegisters();
       DexType expectedParamType = implReceiverAndArgs.get(i + capturedValues);
       maxStack +=
           prepareParameterValue(
-              erasedParams[i], enforcedParams[i], expectedParamType, instructions, factory);
+              erasedParams.get(i), enforcedParams[i], expectedParamType, instructions, factory);
     }
 
     CfInvoke invoke =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index 064c4b2..1a6c122 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.RedundantFieldLoadAndStoreElimination.RedundantFieldLoadAndStoreEliminationOnCode.ExistingValue;
@@ -83,7 +84,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return appView.options().enableRedundantFieldLoadElimination
         && (code.metadata().mayHaveArrayGet()
             || code.metadata().mayHaveFieldInstruction()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
index 2a90c2e..a9dfc03 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -16,7 +17,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
@@ -29,13 +29,14 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.PostMethodProcessor;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.ir.synthetic.NewInstanceSourceCode;
 import com.android.tools.r8.shaking.ComputeApiLevelUseRegistry;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.Sets;
-import java.util.ArrayList;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,32 +47,21 @@
  * Unlike the ApiInvokeOutlinerDesugaring that works on CF, this works on IR to properly replace the
  * users of the NewInstance call.
  */
-public class InstanceInitializerOutliner {
+public class InstanceInitializerOutliner extends CodeRewriterPass<AppInfo> {
 
-  private final AppView<?> appView;
   private final DexItemFactory factory;
 
-  private final List<ProgramMethod> synthesizedMethods = new ArrayList<>();
-
   public InstanceInitializerOutliner(AppView<?> appView) {
-    this.appView = appView;
+    super(appView);
     this.factory = appView.dexItemFactory();
   }
 
-  public List<ProgramMethod> getSynthesizedMethods() {
-    return synthesizedMethods;
-  }
-
-  public void rewriteInstanceInitializers(
+  @Override
+  protected CodeRewriterResult rewriteCode(
       IRCode code,
-      ProgramMethod context,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     assert !methodProcessor.isPostMethodProcessor();
-    // Do not outline from already synthesized methods.
-    if (context.getDefinition().isD8R8Synthesized()) {
-      return;
-    }
     Map<NewInstance, Value> rewrittenNewInstances = new IdentityHashMap<>();
     ComputedApiLevel minApiLevel = appView.computedMinApiLevel();
     InstructionListIterator iterator = code.instructionListIterator();
@@ -104,7 +94,7 @@
         continue;
       }
       // Check if this is already outlined.
-      if (isOutlinedAtSameOrLowerLevel(context.getHolder(), apiReferenceLevel)) {
+      if (isOutlinedAtSameOrLowerLevel(code.context().getHolder(), apiReferenceLevel)) {
         continue;
       }
       DexEncodedMethod synthesizedInstanceInitializer =
@@ -125,7 +115,7 @@
       rewrittenNewInstances.put(newInstance, outlinedMethodInvoke.outValue());
     }
     if (rewrittenNewInstances.isEmpty()) {
-      return;
+      return CodeRewriterResult.NO_CHANGE;
     }
     // Scan over NewInstance calls that needs to be outlined. We insert a call to a synthetic method
     // with a NewInstance to preserve class-init semantics.
@@ -173,14 +163,10 @@
     // the outline again in R8 - but allow inlining of other calls to min api level methods, we have
     // to recompute the api level.
     if (appView.enableWholeProgramOptimizations()) {
-      recomputeApiLevel(context, code);
+      recomputeApiLevel(code.context(), code);
     }
 
-    assert code.isConsistentSSA(appView);
-  }
-
-  public void onLastWaveDone(PostMethodProcessor.Builder postMethodProcessorBuilder) {
-    postMethodProcessorBuilder.addAll(synthesizedMethods, appView.graphLens());
+    return CodeRewriterResult.HAS_CHANGED;
   }
 
   private boolean canSkipClInit(
@@ -249,9 +235,7 @@
     methodProcessor
         .getEventConsumer()
         .acceptInstanceInitializerOutline(method, methodProcessingContext.getMethodContext());
-    synchronized (synthesizedMethods) {
-      synthesizedMethods.add(method);
-    }
+    methodProcessor.scheduleDesugaredMethodForProcessing(method);
     return method.getDefinition();
   }
 
@@ -271,31 +255,56 @@
                 kinds -> kinds.API_MODEL_OUTLINE,
                 methodProcessingContext.createUniqueContext(),
                 appView,
-                builder ->
-                    builder
-                        .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
-                        .setProto(proto)
-                        .setApiLevelForDefinition(appView.computedMinApiLevel())
-                        .setApiLevelForCode(computedApiLevel)
-                        .setCode(
-                            m ->
-                                ForwardMethodBuilder.builder(appView.dexItemFactory())
-                                    .setConstructorTargetWithNewInstance(targetMethod)
-                                    .setStaticSource(m)
-                                    .build()));
+                builder -> {
+                  DynamicType exactDynamicReturnType =
+                      DynamicType.createExact(
+                          targetMethod
+                              .getHolderType()
+                              .toTypeElement(appView, Nullability.definitelyNotNull())
+                              .asClassType());
+                  builder
+                      .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                      .setProto(proto)
+                      .setApiLevelForDefinition(appView.computedMinApiLevel())
+                      .setApiLevelForCode(computedApiLevel)
+                      .setCode(
+                          m ->
+                              ForwardMethodBuilder.builder(appView.dexItemFactory())
+                                  .setConstructorTargetWithNewInstance(targetMethod)
+                                  .setStaticSource(m)
+                                  .build())
+                      .setOptimizationInfo(
+                          DefaultMethodOptimizationInfo.getInstance()
+                              .toMutableOptimizationInfo()
+                              .setDynamicType(exactDynamicReturnType));
+                });
     methodProcessor
         .getEventConsumer()
         .acceptInstanceInitializerOutline(method, methodProcessingContext.getMethodContext());
-    synchronized (synthesizedMethods) {
-      synthesizedMethods.add(method);
-      ClassTypeElement exactType =
-          targetMethod
-              .getHolderType()
-              .toTypeElement(appView, Nullability.definitelyNotNull())
-              .asClassType();
-      OptimizationFeedback.getSimpleFeedback()
-          .setDynamicReturnType(method, appView, DynamicType.createExact(exactType));
-    }
+    methodProcessor.scheduleDesugaredMethodForProcessing(method);
     return method.getDefinition();
   }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    if (!appView.options().desugarState.isOn()
+        || !appView.options().apiModelingOptions().enableOutliningOfMethods
+        || !appView.options().getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L)) {
+      return false;
+    }
+    // Only outline in primary optimization pass.
+    if (!methodProcessor.isD8MethodProcessor() && !methodProcessor.isPrimaryMethodProcessor()) {
+      return false;
+    }
+    // Do not outline from already synthesized methods.
+    if (code.context().getDefinition().isD8R8Synthesized()) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  protected String getRewriterId() {
+    return "InstanceInitializerOutliner";
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index 814bb26..9f3a782 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
@@ -186,7 +187,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     if (!options.enableEnumValueOptimization || !appView.hasLiveness()) {
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index ea5f65d..36c1bce 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -122,7 +122,6 @@
 
   @Override
   public int getReturnedArgument() {
-    assert returnsArgument();
     return UNKNOWN_RETURNED_ARGUMENT;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 47f2e03..9b9b529 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -919,6 +919,7 @@
         feedback.classInitializerMayBePostponed(method);
       } else {
         assert options.debug
+                || appView.getSyntheticItems().isFinalized()
                 || appView
                     .getSyntheticItems()
                     .verifySyntheticLambdaProperty(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
index 9617c85..6c7a777 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -84,6 +84,10 @@
         abstractValue.rewrittenWithLens(appView, field.getType(), lens, codeLens), field);
   }
 
+  public void unsetAbstractValue() {
+    abstractValue = AbstractValue.unknown();
+  }
+
   @Override
   public int getReadBits() {
     return readBits;
@@ -111,6 +115,10 @@
     this.dynamicType = dynamicType;
   }
 
+  public void unsetDynamicType() {
+    setDynamicType(DynamicType.unknown());
+  }
+
   @Override
   public boolean isDead() {
     return (flags & FLAGS_IS_DEAD) != 0;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 602b555..300c8c0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -33,7 +33,6 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import java.util.BitSet;
-import java.util.Collections;
 import java.util.Set;
 
 public class MutableMethodOptimizationInfo extends MethodOptimizationInfo
@@ -297,6 +296,10 @@
     return this;
   }
 
+  public void unsetArgumentInfos() {
+    argumentInfos = CallSiteOptimizationInfo.top();
+  }
+
   @Override
   public ClassInlinerMethodConstraint getClassInlinerMethodConstraint() {
     return classInlinerConstraint;
@@ -630,11 +633,16 @@
   }
 
   void markInitializesClassesOnNormalExit(Set<DexType> initializedClassesOnNormalExit) {
-    this.initializedClassesOnNormalExit = initializedClassesOnNormalExit;
+    if (initializedClassesOnNormalExit.isEmpty()) {
+      unsetInitializedClassesOnNormalExit();
+    } else {
+      this.initializedClassesOnNormalExit = initializedClassesOnNormalExit;
+    }
   }
 
   void unsetInitializedClassesOnNormalExit() {
-    initializedClassesOnNormalExit = Collections.emptySet();
+    initializedClassesOnNormalExit =
+        DefaultMethodOptimizationInfo.getInstance().getInitializedClassesOnNormalExit();
   }
 
   void markReturnsArgument(int returnedArgumentIndex) {
@@ -710,7 +718,7 @@
     setDynamicType(newDynamicType);
   }
 
-  private MutableMethodOptimizationInfo setDynamicType(DynamicType dynamicType) {
+  public MutableMethodOptimizationInfo setDynamicType(DynamicType dynamicType) {
     assert !dynamicType.hasDynamicUpperBoundType()
         || !dynamicType.asDynamicTypeWithUpperBound().getDynamicUpperBoundType().isPrimitiveType();
     this.dynamicType = dynamicType;
@@ -767,6 +775,30 @@
     return isFlagSet(RETURN_VALUE_HAS_BEEN_PROPAGATED_FLAG);
   }
 
+  @SuppressWarnings("ReferenceEquality")
+  public boolean isEffectivelyDefault() {
+    DefaultMethodOptimizationInfo top = DefaultMethodOptimizationInfo.getInstance();
+    return argumentInfos == top.getArgumentInfos()
+        && initializedClassesOnNormalExit == top.getInitializedClassesOnNormalExit()
+        && returnedArgument == top.getReturnedArgument()
+        && abstractReturnValue == top.getAbstractReturnValue()
+        && classInlinerConstraint == top.getClassInlinerMethodConstraint()
+        && convertCheckNotNull == top.isConvertCheckNotNull()
+        && enumUnboxerMethodClassification == top.getEnumUnboxerMethodClassification()
+        && dynamicType == top.getDynamicType()
+        && inlining == InlinePreference.Default
+        && isReturnValueUsed == top.isReturnValueUsed()
+        && bridgeInfo == top.getBridgeInfo()
+        && instanceInitializerInfoCollection.isEmpty()
+        && nonNullParamOrThrow == top.getNonNullParamOrThrow()
+        && nonNullParamOnNormalExits == top.getNonNullParamOnNormalExits()
+        && simpleInliningConstraint == top.getSimpleInliningConstraint()
+        && maxRemovedAndroidLogLevel == top.getMaxRemovedAndroidLogLevel()
+        && parametersWithBitwiseOperations == top.getParametersWithBitwiseOperations()
+        && unusedArguments == top.getUnusedArguments()
+        && flags == DEFAULT_FLAGS;
+  }
+
   @Override
   public boolean isMutableOptimizationInfo() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationInfoRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationInfoRemover.java
new file mode 100644
index 0000000..a336a3d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationInfoRemover.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.info;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Clears any optimization info on fields and methods that need lens code rewriting. This avoids the
+ * need to lens code rewrite such optimization info in repackaging and other optimizations where the
+ * optimization info is mostly unused, since no more optimizations passes will be run.
+ */
+public class OptimizationInfoRemover {
+
+  public static void run(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        OptimizationInfoRemover::processClass,
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private static void processClass(DexProgramClass clazz) {
+    for (DexEncodedField field : clazz.fields()) {
+      processField(field);
+    }
+    for (DexEncodedMethod method : clazz.methods()) {
+      processMethod(method);
+    }
+  }
+
+  private static void processField(DexEncodedField field) {
+    MutableFieldOptimizationInfo optimizationInfo =
+        field.getOptimizationInfo().asMutableFieldOptimizationInfo();
+    if (optimizationInfo == null) {
+      return;
+    }
+    optimizationInfo.unsetAbstractValue();
+    optimizationInfo.unsetDynamicType();
+  }
+
+  private static void processMethod(DexEncodedMethod method) {
+    MutableMethodOptimizationInfo optimizationInfo =
+        method.getOptimizationInfo().asMutableMethodOptimizationInfo();
+    if (optimizationInfo == null) {
+      return;
+    }
+    optimizationInfo.unsetAbstractReturnValue();
+    optimizationInfo.unsetArgumentInfos();
+    optimizationInfo.unsetDynamicType();
+    optimizationInfo.unsetInitializedClassesOnNormalExit();
+    optimizationInfo.unsetInstanceInitializerInfoCollection();
+    if (optimizationInfo.isEffectivelyDefault()) {
+      method.unsetOptimizationInfo();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index bc16337..eb0aa0d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
@@ -95,7 +96,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return options.enableStringConcatenationOptimization
         && !isDebugMode(code.context())
         && (code.metadata().mayHaveNewInstance()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 3eeaae6..ad4c24f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
@@ -52,7 +53,7 @@
   }
 
   @Override
-  protected boolean shouldRewriteCode(IRCode code) {
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
     return !isDebugMode(code.context());
   }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index 056b7fa..29c7ca6 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -36,6 +36,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.FastMapUtils;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
@@ -200,6 +201,15 @@
       return TryCatchTable::specify;
     }
 
+    public TryCatchTable rewriteWithLens(GraphLens graphLens, GraphLens codeLens) {
+      Int2ReferenceMap<CatchHandlers<Integer>> newTryCatchHandlers =
+          FastMapUtils.mapInt2ReferenceOpenHashMapOrElse(
+              tryCatchHandlers,
+              (block, blockHandlers) -> blockHandlers.rewriteWithLens(graphLens, codeLens),
+              null);
+      return newTryCatchHandlers != null ? new TryCatchTable(newTryCatchHandlers) : this;
+    }
+
     private static void specify(StructuralSpecification<TryCatchTable, ?> spec) {
       spec.withInt2CustomItemMap(
           s -> s.tryCatchHandlers,
@@ -774,6 +784,24 @@
         metadataMap);
   }
 
+  public LirCode<EV> newCodeWithRewrittenTryCatchTable(TryCatchTable rewrittenTryCatchTable) {
+    if (rewrittenTryCatchTable == tryCatchTable) {
+      return this;
+    }
+    return new LirCode<>(
+        irMetadata,
+        constants,
+        positionTable,
+        argumentCount,
+        instructions,
+        instructionCount,
+        rewrittenTryCatchTable,
+        debugLocalInfoTable,
+        strategyInfo,
+        useDexEstimationStrategy,
+        metadataMap);
+  }
+
   public LirCode<EV> rewriteWithSimpleLens(
       ProgramMethod context, AppView<?> appView, LensCodeRewriterUtils rewriterUtils) {
     GraphLens graphLens = appView.graphLens();
diff --git a/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java b/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java
index d093823..1a4d065 100644
--- a/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java
+++ b/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.lightir;
 
+import static com.android.tools.r8.graph.UseRegistry.MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -17,6 +20,8 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.code.Opcodes;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.lightir.LirBuilder.RecordFieldValuesPayload;
+import com.android.tools.r8.lightir.LirCode.TryCatchTable;
 import com.android.tools.r8.utils.ArrayUtils;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
@@ -68,6 +73,12 @@
     addRewrittenMapping(callSite, helper.rewriteCallSite(callSite, context));
   }
 
+  public void onMethodHandleReference(DexMethodHandle methodHandle) {
+    addRewrittenMapping(
+        methodHandle,
+        helper.rewriteDexMethodHandle(methodHandle, NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context));
+  }
+
   public void onProtoReference(DexProto proto) {
     addRewrittenMapping(proto, helper.rewriteProto(proto));
   }
@@ -143,7 +154,8 @@
 
   public LirCode<EV> rewrite() {
     LirCode<EV> rewritten = rewriteConstantPoolAndScanForTypeChanges(getCode());
-    return rewriteInstructionsWithInvokeTypeChanges(rewritten);
+    rewritten = rewriteInstructionsWithInvokeTypeChanges(rewritten);
+    return rewriteTryCatchTable(rewritten);
   }
 
   private LirCode<EV> rewriteConstantPoolAndScanForTypeChanges(LirCode<EV> code) {
@@ -152,12 +164,16 @@
     // fields/methods that need to be examined.
     boolean hasPotentialRewrittenMethod = false;
     for (LirConstant constant : code.getConstantPool()) {
+      // RecordFieldValuesPayload is lowered to NewArrayEmpty before lens code rewriting any LIR.
+      assert !(constant instanceof RecordFieldValuesPayload);
       if (constant instanceof DexType) {
         onTypeReference((DexType) constant);
       } else if (constant instanceof DexField) {
         onFieldReference((DexField) constant);
       } else if (constant instanceof DexCallSite) {
         onCallSiteReference((DexCallSite) constant);
+      } else if (constant instanceof DexMethodHandle) {
+        onMethodHandleReference((DexMethodHandle) constant);
       } else if (constant instanceof DexProto) {
         onProtoReference((DexProto) constant);
       } else if (!hasPotentialRewrittenMethod && constant instanceof DexMethod) {
@@ -265,4 +281,13 @@
             byteWriter.toByteArray());
     return newCode;
   }
+
+  private LirCode<EV> rewriteTryCatchTable(LirCode<EV> code) {
+    TryCatchTable tryCatchTable = code.getTryCatchTable();
+    if (tryCatchTable == null) {
+      return code;
+    }
+    TryCatchTable newTryCatchTable = tryCatchTable.rewriteWithLens(graphLens, codeLens);
+    return code.newCodeWithRewrittenTryCatchTable(newTryCatchTable);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
index 7e91d18..9e966d0 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
@@ -116,5 +116,6 @@
         },
         appView.options().getThreadingModule(),
         executorService);
+    appView.setGenericSignaturesLens(appView.graphLens());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
index 3d6fc32..5272500 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.graph.fixup.TreeFixerBase;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.naming.Minifier.MinificationPackageNamingStrategy;
+import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.repackaging.RepackagingLens.Builder;
 import com.android.tools.r8.shaking.AnnotationFixer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -78,6 +79,7 @@
     if (lens != null) {
       appView.rewriteWithLensAndApplication(lens, appBuilder.build(), executorService, timing);
       appView.testing().repackagingLensConsumer.accept(appView.dexItemFactory(), lens);
+      new GenericSignatureRewriter(appView).run(appView.appInfo().classes(), executorService);
     }
     appView.notifyOptimizationFinishedForTesting();
     timing.end();
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 93ac4f4..e634f97 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Definition;
+import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndField;
@@ -316,8 +317,8 @@
         pruneMethods(previous.bootstrapMethods, prunedItems, tasks),
         pruneMethods(previous.virtualMethodsTargetedByInvokeDirect, prunedItems, tasks),
         pruneMethods(previous.liveMethods, prunedItems, tasks),
-        previous.fieldAccessInfoCollection,
-        previous.methodAccessInfoCollection.withoutPrunedItems(prunedItems),
+        previous.fieldAccessInfoCollection.withoutPrunedItems(prunedItems),
+        previous.methodAccessInfoCollection.withoutPrunedContexts(prunedItems),
         previous.objectAllocationInfoCollection.withoutPrunedItems(prunedItems),
         pruneCallSites(previous.callSites, prunedItems),
         extendPinnedItems(previous, prunedItems.getAdditionalPinnedItems()),
@@ -1090,12 +1091,19 @@
     return appInfoWithLiveness;
   }
 
+  public AppInfoWithLiveness rebuildWithLiveness(DexApplication application) {
+    return rebuildWithLiveness(getSyntheticItems().commit(application));
+  }
+
   public AppInfoWithLiveness rebuildWithLiveness(CommittedItems committedItems) {
     return new AppInfoWithLiveness(this, committedItems);
   }
 
   public AppInfoWithLiveness rewrittenWithLens(
-      DirectMappedDexApplication application, NonIdentityGraphLens lens, Timing timing) {
+      DirectMappedDexApplication application,
+      NonIdentityGraphLens lens,
+      GraphLens appliedLens,
+      Timing timing) {
     assert checkIfObsolete();
 
     // Switchmap classes should never be affected by renaming.
@@ -1126,7 +1134,8 @@
         lens.rewriteReferences(liveMethods),
         fieldAccessInfoCollection.rewrittenWithLens(definitionSupplier, lens, timing),
         methodAccessInfoCollection.rewrittenWithLens(definitionSupplier, lens, timing),
-        objectAllocationInfoCollection.rewrittenWithLens(definitionSupplier, lens, timing),
+        objectAllocationInfoCollection.rewrittenWithLens(
+            definitionSupplier, lens, appliedLens, timing),
         lens.rewriteCallSites(callSites, definitionSupplier, timing),
         keepInfo.rewrite(definitionSupplier, lens, application.options, timing),
         // Take any rule in case of collisions.
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index f0080f3..dc0a4ac 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -84,9 +84,11 @@
       return hasher.hash();
     }
     if (intermediate) {
-      // If in intermediate mode, include the context type as sharing is restricted to within a
-      // single context.
-      getContext().getSynthesizingContextType().hashWithTypeEquivalence(hasher, map);
+      // If in intermediate mode, include the *input* context type to restrict sharing.
+      // This restricts sharing to only allow sharing the synthetics with the same *input* context.
+      // If the synthetic is itself an input from a previous compilation it is restricted to share
+      // within its own context only. The input context should not be mapped to an equivalence type.
+      getContext().getSynthesizingInputContext(intermediate).hash(hasher);
     }
     hasher.putInt(context.getFeatureSplit().hashCode());
     internalComputeHash(hasher, map);
@@ -103,7 +105,6 @@
     return compareTo(other, includeContext, graphLens, classToFeatureSplitMap) == 0;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   int compareTo(
       D other,
       boolean includeContext,
@@ -138,10 +139,12 @@
     if (graphLens.isNonIdentityLens()) {
       DexType thisOrigType = graphLens.getOriginalType(thisType);
       DexType otherOrigType = graphLens.getOriginalType(otherType);
-      if (thisType != thisOrigType || otherType != otherOrigType) {
+      if (thisType.isNotIdenticalTo(thisOrigType) || otherType.isNotIdenticalTo(otherOrigType)) {
         map =
             t -> {
-              if (t == otherType || t == thisOrigType || t == otherOrigType) {
+              if (DexType.identical(t, otherType)
+                  || DexType.identical(t, thisOrigType)
+                  || DexType.identical(t, otherOrigType)) {
                 return thisType;
               }
               return t;
@@ -149,7 +152,7 @@
       }
     }
     if (map == null) {
-      map = t -> t == otherType ? thisType : t;
+      map = t -> otherType.isIdenticalTo(t) ? thisType : t;
     }
     return internalCompareTo(other, map);
   }
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 8b3a049..1c83db6 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -512,7 +512,7 @@
                     }
                     for (DexProto bridgeProto : descriptor.bridges) {
                       DexEncodedMethod bridgeMethod =
-                          interfaceDefinition.lookupMethod(bridgeProto, descriptor.name);
+                          interfaceDefinition.lookupMethod(bridgeProto, descriptor.getName());
                       if (bridgeMethod != null) {
                         registerInvokeInterface(bridgeMethod.getReference());
                       }
diff --git a/src/main/java/com/android/tools/r8/utils/DominatorChecker.java b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
index c2fd047..d1dc853 100644
--- a/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
+++ b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.google.common.collect.Sets;
-import java.util.ArrayDeque;
 import java.util.Collections;
 import java.util.Set;
 
@@ -32,20 +31,20 @@
   }
 
   class TraversingDominatorChecker implements DominatorChecker {
-    private final BasicBlock sourceBlock;
-    private final BasicBlock destBlock;
+    private final BasicBlock subgraphEntryBlock;
+    private final BasicBlock subgraphExitBlock;
     private final Set<BasicBlock> knownDominators;
-    private final ArrayDeque<BasicBlock> workQueue = new ArrayDeque<>();
-    private final Set<BasicBlock> visited;
+    private final WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
     private BasicBlock prevTargetBlock;
 
     private TraversingDominatorChecker(
-        BasicBlock sourceBlock, BasicBlock destBlock, Set<BasicBlock> knownDominators) {
-      this.sourceBlock = sourceBlock;
-      this.destBlock = destBlock;
+        BasicBlock subgraphEntryBlock,
+        BasicBlock subgraphExitBlock,
+        Set<BasicBlock> knownDominators) {
+      this.subgraphEntryBlock = subgraphEntryBlock;
+      this.subgraphExitBlock = subgraphExitBlock;
       this.knownDominators = knownDominators;
-      this.visited = Sets.newIdentityHashSet();
-      prevTargetBlock = destBlock;
+      prevTargetBlock = subgraphExitBlock;
     }
 
     @Override
@@ -60,7 +59,7 @@
       if (firstSplittingBlock.hasUniqueSuccessor()) {
         do {
           // knownDominators prevents firstSplittingBlock from being destBlock.
-          assert firstSplittingBlock != destBlock;
+          assert firstSplittingBlock != subgraphExitBlock;
           firstSplittingBlock = firstSplittingBlock.getUniqueSuccessor();
         } while (firstSplittingBlock.hasUniqueSuccessor());
 
@@ -73,13 +72,12 @@
       boolean ret;
       // Since we know the previously checked block is a dominator, narrow the check by using it for
       // either sourceBlock or destBlock.
-      if (visited.contains(targetBlock)) {
-        visited.clear();
-        ret =
-            checkWithTraversal(prevTargetBlock, destBlock, firstSplittingBlock, visited, workQueue);
+      if (workList.isSeen(targetBlock)) {
+        workList.clearSeen();
+        ret = checkWithTraversal(prevTargetBlock, subgraphExitBlock, firstSplittingBlock, workList);
         prevTargetBlock = firstSplittingBlock;
       } else {
-        ret = checkWithTraversal(sourceBlock, prevTargetBlock, targetBlock, visited, workQueue);
+        ret = checkWithTraversal(subgraphEntryBlock, prevTargetBlock, targetBlock, workList);
         prevTargetBlock = targetBlock;
       }
       if (ret) {
@@ -93,70 +91,71 @@
       return ret;
     }
 
+    /**
+     * Within the subgraph defined by the given entry/exit blocks, returns whether targetBlock
+     * dominates the exit block.
+     */
     private static boolean checkWithTraversal(
-        BasicBlock sourceBlock,
-        BasicBlock destBlock,
+        BasicBlock subgraphEntryBlock,
+        BasicBlock subgraphExitBlock,
         BasicBlock targetBlock,
-        Set<BasicBlock> visited,
-        ArrayDeque<BasicBlock> workQueue) {
-      assert workQueue.isEmpty();
+        WorkList<BasicBlock> workList) {
+      assert workList.isEmpty();
 
-      visited.add(targetBlock);
-
-      workQueue.addAll(destBlock.getPredecessors());
-      do {
-        BasicBlock curBlock = workQueue.removeLast();
-        if (!visited.add(curBlock)) {
-          continue;
-        }
-        if (curBlock == sourceBlock) {
-          // There is a path from sourceBlock -> destBlock that does not go through block.
+      workList.markAsSeen(targetBlock);
+      workList.addIfNotSeen(subgraphExitBlock.getPredecessors());
+      while (!workList.isEmpty()) {
+        BasicBlock curBlock = workList.removeLast();
+        if (curBlock == subgraphEntryBlock) {
+          // There is a path from subgraphExitBlock -> subgraphEntryBlock that does not go through
+          // targetBlock.
           return false;
         }
-        assert !curBlock.isEntry() : "sourceBlock did not dominate destBlock";
-        workQueue.addAll(curBlock.getPredecessors());
-      } while (!workQueue.isEmpty());
+        assert !curBlock.isEntry() : "subgraphEntryBlock did not dominate subgraphExitBlock";
+        workList.addIfNotSeen(curBlock.getPredecessors());
+      }
 
       return true;
     }
   }
 
-  static DominatorChecker create(BasicBlock sourceBlock, BasicBlock destBlock) {
+  static DominatorChecker create(BasicBlock subgraphEntryBlock, BasicBlock subgraphExitBlock) {
     // Fast-path: blocks are the same.
     // As of Nov 2023: in Chrome for String.format() optimization, this covers 77% of cases.
-    if (sourceBlock == destBlock) {
-      return new PrecomputedDominatorChecker(Collections.singleton(sourceBlock));
+    if (subgraphEntryBlock == subgraphExitBlock) {
+      return new PrecomputedDominatorChecker(Collections.singleton(subgraphEntryBlock));
     }
 
-    // Shrink the subgraph by moving sourceBlock forward to the first block with multiple
+    // Shrink the subgraph by moving subgraphEntryBlock forward to the first block with multiple
     // successors.
     Set<BasicBlock> headAndTailDominators = Sets.newIdentityHashSet();
-    headAndTailDominators.add(sourceBlock);
-    while (sourceBlock.hasUniqueSuccessor()) {
-      sourceBlock = sourceBlock.getUniqueSuccessor();
-      if (!headAndTailDominators.add(sourceBlock)) {
+    headAndTailDominators.add(subgraphEntryBlock);
+    while (subgraphEntryBlock.hasUniqueSuccessor()) {
+      subgraphEntryBlock = subgraphEntryBlock.getUniqueSuccessor();
+      if (!headAndTailDominators.add(subgraphEntryBlock)) {
         // Hit an infinite loop. Code would not verify in this case.
         assert false;
         return FALSE_CHECKER;
       }
-      if (sourceBlock == destBlock) {
+      if (subgraphEntryBlock == subgraphExitBlock) {
         // As of Nov 2023: in Chrome for String.format() optimization, a linear path from
         // source->dest was 14% of cases.
         return new PrecomputedDominatorChecker(headAndTailDominators);
       }
     }
-    if (sourceBlock.getSuccessors().isEmpty()) {
+    if (subgraphEntryBlock.getSuccessors().isEmpty()) {
       return FALSE_CHECKER;
     }
 
-    // Shrink the subgraph by moving destBlock back to the first block with multiple predecessors.
-    headAndTailDominators.add(destBlock);
-    while (destBlock.hasUniquePredecessor()) {
-      destBlock = destBlock.getUniquePredecessor();
-      if (!headAndTailDominators.add(destBlock)) {
-        if (sourceBlock == destBlock) {
-          // This normally happens when moving sourceBlock forwards, but when moving destBlock
-          // backwards when sourceBlock has multiple successors.
+    // Shrink the subgraph by moving subgraphExitBlock back to the first block with multiple
+    // predecessors.
+    headAndTailDominators.add(subgraphExitBlock);
+    while (subgraphExitBlock.hasUniquePredecessor()) {
+      subgraphExitBlock = subgraphExitBlock.getUniquePredecessor();
+      if (!headAndTailDominators.add(subgraphExitBlock)) {
+        if (subgraphEntryBlock == subgraphExitBlock) {
+          // This normally happens when moving subgraphEntryBlock forwards, but can also occur when
+          // moving subgraphExitBlock backwards when subgraphEntryBlock has multiple successors.
           return new PrecomputedDominatorChecker(headAndTailDominators);
         }
         // Hit an infinite loop. Code would not verify in this case.
@@ -165,10 +164,28 @@
       }
     }
 
-    if (destBlock.isEntry()) {
+    if (subgraphExitBlock.isEntry()) {
       return FALSE_CHECKER;
     }
 
-    return new TraversingDominatorChecker(sourceBlock, destBlock, headAndTailDominators);
+    return new TraversingDominatorChecker(
+        subgraphEntryBlock, subgraphExitBlock, headAndTailDominators);
+  }
+
+  /**
+   * Returns whether targetBlock dominates subgraphExitBlock by performing a depth-first traversal
+   * from subgraphExitBlock to subgraphEntryBlock with targetBlock removed from the graph.
+   */
+  @SuppressWarnings("InconsistentOverloads")
+  static boolean check(
+      BasicBlock subgraphEntryBlock, BasicBlock subgraphExitBlock, BasicBlock targetBlock) {
+    if (targetBlock == subgraphExitBlock) {
+      return true;
+    }
+    if (subgraphEntryBlock == subgraphExitBlock) {
+      return false;
+    }
+    return TraversingDominatorChecker.checkWithTraversal(
+        subgraphEntryBlock, subgraphExitBlock, targetBlock, WorkList.newIdentityWorkList());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/FastMapUtils.java b/src/main/java/com/android/tools/r8/utils/FastMapUtils.java
new file mode 100644
index 0000000..e64f202
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/FastMapUtils.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import java.util.Iterator;
+import java.util.function.Function;
+
+public class FastMapUtils {
+
+  public static <V> Int2ReferenceMap<V> destructiveMapValues(
+      Int2ReferenceMap<V> map, Function<V, V> valueMapper) {
+    Iterator<Int2ReferenceMap.Entry<V>> iterator = map.int2ReferenceEntrySet().iterator();
+    while (iterator.hasNext()) {
+      Int2ReferenceMap.Entry<V> entry = iterator.next();
+      V newValue = valueMapper.apply(entry.getValue());
+      if (newValue != null) {
+        entry.setValue(newValue);
+      } else {
+        iterator.remove();
+      }
+    }
+    return map;
+  }
+
+  public static <V> Int2ReferenceMap<V> mapInt2ReferenceOpenHashMapOrElse(
+      Int2ReferenceMap<V> map,
+      IntObjToObjFunction<V, V> valueMapper,
+      Int2ReferenceMap<V> defaultValue) {
+    Int2ReferenceMap<V> newMap = null;
+    Iterator<Int2ReferenceMap.Entry<V>> iterator = map.int2ReferenceEntrySet().iterator();
+    while (iterator.hasNext()) {
+      Int2ReferenceMap.Entry<V> entry = iterator.next();
+      int key = entry.getIntKey();
+      V value = entry.getValue();
+      V newValue = valueMapper.apply(key, value);
+      if (newMap == null) {
+        if (newValue == value) {
+          continue;
+        }
+        // This is the first entry where the value has been changed. Create the new map and copy
+        // over previous entries that did not change.
+        Int2ReferenceMap<V> newFinalMap = new Int2ReferenceOpenHashMap<>(map.size());
+        CollectionUtils.forEachUntilExclusive(
+            map.int2ReferenceEntrySet(),
+            previousEntry -> newFinalMap.put(previousEntry.getIntKey(), previousEntry.getValue()),
+            entry);
+        newMap = newFinalMap;
+      }
+      if (newValue != null) {
+        newMap.put(key, newValue);
+      } else {
+        iterator.remove();
+      }
+    }
+    return newMap != null ? newMap : defaultValue;
+  }
+}
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 011fbfb..0dc92d1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -65,14 +65,11 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis;
-import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
-import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.desugar.TypeRewriter;
 import com.android.tools.r8.ir.desugar.TypeRewriter.MachineTypeRewriter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
@@ -80,9 +77,6 @@
 import com.android.tools.r8.ir.desugar.nest.Nest;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
-import com.android.tools.r8.lightir.IR2LirConverter;
-import com.android.tools.r8.lightir.LirCode;
-import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
@@ -133,8 +127,6 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
@@ -2200,56 +2192,9 @@
 
     private LirPhase currentPhase = LirPhase.PRE;
 
-    public void enterLirSupportedPhase(AppView<?> appView, ExecutorService executorService)
-        throws ExecutionException {
+    public void enterLirSupportedPhase() {
       assert isPreLirPhase();
       currentPhase = LirPhase.SUPPORTED;
-      if (!canUseLir(appView)) {
-        return;
-      }
-      // Convert code objects to LIR.
-      ThreadUtils.processItems(
-          appView.appInfo().classes(),
-          clazz -> {
-            // TODO(b/225838009): Also convert instance initializers to LIR, by adding support for
-            //  computing the inlining constraint for LIR and using that in the class mergers, and
-            //  class initializers, by updating the concatenation of clinits in horizontal class
-            //  merging.
-            clazz.forEachProgramMethodMatching(
-                method ->
-                    method.hasCode()
-                        && !method.isInitializer()
-                        && !appView.isCfByteCodePassThrough(method),
-                method -> {
-                  IRCode code =
-                      method.buildIR(appView, MethodConversionOptions.forLirPhase(appView));
-                  LirCode<Integer> lirCode =
-                      IR2LirConverter.translate(
-                          code,
-                          BytecodeMetadataProvider.empty(),
-                          LirStrategy.getDefaultStrategy().getEncodingStrategy(),
-                          appView.options());
-                  // TODO(b/312890994): Setting a custom code lens is only needed until we convert
-                  //  code objects to LIR before we create the first code object with a custom code
-                  //  lens (horizontal class merging).
-                  GraphLens codeLens = method.getDefinition().getCode().getCodeLens(appView);
-                  if (codeLens != appView.codeLens()) {
-                    lirCode =
-                        new LirCode<>(lirCode) {
-                          @Override
-                          public GraphLens getCodeLens(AppView<?> appView) {
-                            return codeLens;
-                          }
-                        };
-                  }
-                  method.setCode(lirCode, appView);
-                });
-          },
-          appView.options().getThreadingModule(),
-          executorService);
-      // Conversion to LIR via IR will allocate type elements.
-      // They are not needed after construction so remove them again.
-      appView.dexItemFactory().clearTypeElementsCache();
     }
 
     public void exitLirSupportedPhase() {
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index be05c5d..a7c3366 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -6,9 +6,11 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
@@ -115,7 +117,7 @@
     return builder.build();
   }
 
-  public static <T, S> Set<T> mapIdentityHashSet(Set<S> set, Function<S, T> fn) {
+  public static <T, S> Set<T> mapIdentityHashSet(Collection<S> set, Function<S, T> fn) {
     Set<T> out = newIdentityHashSet(set.size());
     for (S element : set) {
       out.add(fn.apply(element));
@@ -123,6 +125,14 @@
     return out;
   }
 
+  public static <T, S> Set<T> mapLinkedHashSet(Collection<S> set, Function<S, T> fn) {
+    Set<T> out = new LinkedHashSet<>(set.size());
+    for (S element : set) {
+      out.add(fn.apply(element));
+    }
+    return out;
+  }
+
   public static <T> T removeFirst(Set<T> set) {
     T element = set.iterator().next();
     set.remove(element);
diff --git a/src/main/java/com/android/tools/r8/utils/ValueUtils.java b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
index 68d3364..9082be5 100644
--- a/src/main/java/com/android/tools/r8/utils/ValueUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
@@ -57,12 +57,15 @@
   public static final class ArrayValues {
     private List<Value> elementValues;
     private ArrayPut[] arrayPutsByIndex;
+    private final Value arrayValue;
 
-    private ArrayValues(List<Value> elementValues) {
+    private ArrayValues(Value arrayValue, List<Value> elementValues) {
+      this.arrayValue = arrayValue;
       this.elementValues = elementValues;
     }
 
-    private ArrayValues(ArrayPut[] arrayPutsByIndex) {
+    private ArrayValues(Value arrayValue, ArrayPut[] arrayPutsByIndex) {
+      this.arrayValue = arrayValue;
       this.arrayPutsByIndex = arrayPutsByIndex;
     }
 
@@ -83,6 +86,28 @@
     public int size() {
       return elementValues != null ? elementValues.size() : arrayPutsByIndex.length;
     }
+
+    public boolean containsHoles() {
+      for (ArrayPut arrayPut : arrayPutsByIndex) {
+        if (arrayPut == null) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public ArrayPut[] getArrayPutsByIndex() {
+      assert arrayPutsByIndex != null;
+      return arrayPutsByIndex;
+    }
+
+    public Value getArrayValue() {
+      return arrayValue;
+    }
+
+    public Instruction getDefinition() {
+      return arrayValue.definition;
+    }
   }
 
   /**
@@ -92,12 +117,8 @@
    * 1) The Array has a single users (other than array-puts)
    *   * This constraint is to ensure other users do not modify the array.
    *   * When users are in different blocks, their order is hard to know.
-   * 2) The array size is a constant.
+   * 2) The array size is a constant and non-negative.
    * 3) All array-put instructions have constant and unique indices.
-   *   * With the exception of array-puts that are in the same block as singleUser, in which case
-   *     non-unique puts are allowed.
-   *   * Indices must be unique in other blocks because order is hard to know when multiple blocks
-   *     are concerned (and reassignment is rare anyways).
    * 4) The array-put instructions are guaranteed to be executed before singleUser.
    * </pre>
    *
@@ -106,7 +127,6 @@
    * @return The computed array values, or null if they could not be determined.
    */
   public static ArrayValues computeSingleUseArrayValues(Value arrayValue, Instruction singleUser) {
-    assert singleUser == null || arrayValue.uniqueUsers().contains(singleUser);
     TypeElement arrayType = arrayValue.getType();
     if (!arrayType.isArrayType() || arrayValue.hasDebugUsers() || arrayValue.isPhi()) {
       return null;
@@ -121,7 +141,7 @@
       if (!arrayValue.hasSingleUniqueUser() || arrayValue.hasPhiUsers()) {
         return null;
       }
-      return new ArrayValues(newArrayFilled.inValues());
+      return new ArrayValues(arrayValue, newArrayFilled.inValues());
     } else if (newArrayEmpty == null) {
       return null;
     }
@@ -132,28 +152,83 @@
       return null;
     }
 
-    if (singleUser == null) {
-      for (Instruction user : arrayValue.uniqueUsers()) {
-        ArrayPut arrayPut = user.asArrayPut();
-        if (arrayPut == null || arrayPut.array() != arrayValue || arrayPut.value() == arrayValue) {
-          if (singleUser == null) {
-            singleUser = user;
-          } else {
-            return null;
-          }
+    return computeArrayValuesInternal(newArrayEmpty, arraySize, singleUser, false);
+  }
+
+  /**
+   * Determines the values for the given array at the point the last element is assigned.
+   *
+   * <pre>
+   * Returns null under the following conditions:
+   *  * The array has a non-const, negative, or abnormally large size.
+   *  * An array-put with non-constant index exists.
+   *  * An array-put with an out-of-bounds index exists.
+   *  * An array-put for the last index is not found.
+   *  * An array-put is found after the last-index array-put.
+   *  * An array-put is found where the array and value are the same: arr[index] = arr;
+   *  * There are multiple array-put instructions for the same index.
+   * </pre>
+   */
+  public static ArrayValues computeInitialArrayValues(NewArrayEmpty newArrayEmpty) {
+    int arraySize = newArrayEmpty.sizeIfConst();
+    if (arraySize < 0 || arraySize > MAX_ARRAY_SIZE) {
+      // Array is non-const size.
+      return null;
+    }
+
+    Value arrayValue = newArrayEmpty.outValue();
+    // Find array-put for the last element, as well as blocks for other array users.
+    ArrayPut lastArrayPut = null;
+    int lastIndex = arraySize - 1;
+    for (Instruction user : arrayValue.uniqueUsers()) {
+      ArrayPut arrayPut = user.asArrayPut();
+      if (arrayPut != null && arrayPut.array() == arrayValue) {
+        int index = arrayPut.indexOrDefault(-1);
+        if (index == lastIndex) {
+          lastArrayPut = arrayPut;
+          break;
         }
       }
     }
+    if (lastArrayPut == null) {
+      return null;
+    }
 
+    // Find all array-puts up until the last one.
+    // Also checks that no array-puts appear after the last one.
+    ArrayValues ret = computeArrayValuesInternal(newArrayEmpty, arraySize, lastArrayPut, true);
+    if (ret == null) {
+      return null;
+    }
+    // Since the last array-put is used as firstUser, it will not already be in arrayPutsByIndex.
+    if (ret.arrayPutsByIndex[lastIndex] != null) {
+      return null;
+    }
+    ret.arrayPutsByIndex[lastIndex] = lastArrayPut;
+    return ret;
+  }
+
+  private static ArrayValues computeArrayValuesInternal(
+      NewArrayEmpty newArrayEmpty, int arraySize, Instruction firstUser, boolean allowOtherUsers) {
     ArrayPut[] arrayPutsByIndex = new ArrayPut[arraySize];
-    BasicBlock usageBlock = singleUser.getBlock();
+    Value arrayValue = newArrayEmpty.outValue();
+    BasicBlock usageBlock = firstUser.getBlock();
+
+    // Collect array-puts from non-usage blocks, and (optionally) check for multiple users.
     for (Instruction user : arrayValue.uniqueUsers()) {
       ArrayPut arrayPut = user.asArrayPut();
-      if (arrayPut == null || arrayPut.array() != arrayValue || arrayPut.value() == arrayValue) {
-        if (user == singleUser) {
+      if (arrayPut == null || arrayPut.array() != arrayValue) {
+        if (user == firstUser) {
           continue;
         }
         // Found a second non-array-put user.
+        if (allowOtherUsers) {
+          continue;
+        }
+        return null;
+      } else if (arrayPut.value() == arrayValue) {
+        // An array that contains itself is uncommon and hard to reason about.
+        // e.g.: arr[0] = arr;
         return null;
       }
       // Process same-block instructions later.
@@ -168,8 +243,10 @@
       arrayPutsByIndex[index] = arrayPut;
     }
 
-    // Ensure that all paths from new-array-empty to |usage| contain all array-put instructions.
-    DominatorChecker dominatorChecker = DominatorChecker.create(definition.getBlock(), usageBlock);
+    // Ensure that all paths from new-array-empty's block to |usage|'s block contain all array-put
+    // instructions.
+    DominatorChecker dominatorChecker =
+        DominatorChecker.create(newArrayEmpty.getBlock(), usageBlock);
     // Visit in reverse order because array-puts generally appear in order, and DominatorChecker's
     // cache is more effective when visiting in reverse order.
     for (int i = arraySize - 1; i >= 0; --i) {
@@ -179,17 +256,18 @@
       }
     }
 
-    boolean seenSingleUser = false;
+    // Collect array-puts from the usage block, and ensure no array-puts come after the first user.
+    boolean seenFirstUser = false;
     for (Instruction inst : usageBlock.getInstructions()) {
-      if (inst == singleUser) {
-        seenSingleUser = true;
+      if (inst == firstUser) {
+        seenFirstUser = true;
         continue;
       }
       ArrayPut arrayPut = inst.asArrayPut();
       if (arrayPut == null || arrayPut.array() != arrayValue) {
         continue;
       }
-      if (seenSingleUser) {
+      if (seenFirstUser) {
         // Found an array-put after the array was used. This is too uncommon of a thing to support.
         return null;
       }
@@ -197,10 +275,14 @@
       if (index < 0) {
         return null;
       }
-      // We can allow reassignment at this point since we are visiting in order.
+      // Do not allow re-assignment so that we can use arrayPutsByIndex to find all array-put
+      // instructions.
+      if (arrayPutsByIndex[index] != null) {
+        return null;
+      }
       arrayPutsByIndex[index] = arrayPut;
     }
 
-    return new ArrayValues(arrayPutsByIndex);
+    return new ArrayValues(arrayValue, arrayPutsByIndex);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index cdfb34c..ed63185 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -145,8 +145,8 @@
     return seen.contains(item);
   }
 
-  public void markAsSeen(T item) {
-    seen.add(item);
+  public boolean markAsSeen(T item) {
+    return seen.add(item);
   }
 
   public void markAsSeen(Iterable<T> items) {
@@ -154,16 +154,23 @@
   }
 
   public T next() {
-    assert hasNext();
     return workingList.removeFirst();
   }
 
+  public T removeLast() {
+    return workingList.removeLast();
+  }
+
   public T removeSeen() {
     T next = next();
     seen.remove(next);
     return next;
   }
 
+  public void clearSeen() {
+    seen.clear();
+  }
+
   public Set<T> getSeenSet() {
     return SetUtils.unmodifiableForTesting(seen);
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
index cfbc759..5bb0780 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/IllegalAccessDetector.java
@@ -81,7 +81,7 @@
     DexType baseType =
         rewrittenMethod.getHolderType().toBaseType(appViewWithClassHierarchy.dexItemFactory());
     if (baseType.isClassType() && baseType.isSamePackage(getContext().getHolderType())) {
-      if (checkTypeReference(rewrittenMethod.getHolderType())) {
+      if (checkRewrittenTypeReference(rewrittenMethod.getHolderType())) {
         return checkFoundPackagePrivateAccess();
       }
       MethodResolutionResult resolutionResult =
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index 3987216..994795d 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -8,11 +8,13 @@
 
 import com.android.tools.r8.classmerging.ClassMergerGraphLens;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
@@ -22,6 +24,9 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
@@ -283,7 +288,10 @@
     }
     if (type.isInterface()
         && mergedClasses.hasInterfaceBeenMergedIntoClass(previousMethod.getHolderType())) {
-      return InvokeType.VIRTUAL;
+      DexClass newMethodHolder = appView.definitionForHolder(newMethod);
+      if (newMethodHolder != null && !newMethodHolder.isInterface()) {
+        return InvokeType.VIRTUAL;
+      }
     }
     return type;
   }
@@ -309,6 +317,29 @@
     return true;
   }
 
+  public boolean assertPinnedNotModified(AppView<AppInfoWithLiveness> appView) {
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    InternalOptions options = appView.options();
+    keepInfo.forEachPinnedType(this::assertReferenceNotModified, options);
+    keepInfo.forEachPinnedMethod(this::assertReferenceNotModified, options);
+    keepInfo.forEachPinnedField(this::assertReferenceNotModified, options);
+    return true;
+  }
+
+  private void assertReferenceNotModified(DexReference reference) {
+    if (reference.isDexField()) {
+      DexField field = reference.asDexField();
+      assert getNextFieldSignature(field).isIdenticalTo(field);
+    } else if (reference.isDexMethod()) {
+      DexMethod method = reference.asDexMethod();
+      assert getNextMethodSignature(method).isIdenticalTo(method);
+    } else {
+      assert reference.isDexType();
+      DexType type = reference.asDexType();
+      assert getNextClassType(type).isIdenticalTo(type);
+    }
+  }
+
   public static class Builder
       extends BuilderBase<VerticalClassMergerGraphLens, VerticallyMergedClasses> {
 
@@ -469,13 +500,6 @@
           staticizedMethods);
     }
 
-    // TODO: should be removed.
-    public boolean hasMappingForSignatureInContext(DexProgramClass context, DexMethod signature) {
-      return contextualSuperToImplementationInContexts
-          .getOrDefault(context.getType(), Collections.emptyMap())
-          .containsKey(signature);
-    }
-
     public void markMethodAsMerged(DexEncodedMethod method) {
       mergedMethods.add(method.getReference());
     }
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index 842b6a8..aaee1b0 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -329,15 +329,25 @@
     assertTrue(ToolHelper.getApp(command).hasMainDexListResources());
   }
 
+  @Test
+  public void mainDexListNonLegacyMinApiL() throws Throwable {
+    Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
+    D8Command command =
+        parse(
+            "--min-api", Integer.toString(AndroidApiLevel.L.getLevel()),
+            "--main-dex-list", mainDexList.toString());
+    assertTrue(ToolHelper.getApp(command).hasMainDexListResources());
+  }
+
   @Test(expected = CompilationFailedException.class)
-  public void mainDexListWithNonLegacyMinApi() throws Throwable {
+  public void mainDexListWithNonLegacyMinApiAboveL() throws Throwable {
     Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
     DiagnosticsChecker.checkErrorsContains(
         "does not support main-dex",
         (handler) ->
             D8Command.builder(handler)
                 .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
-                .setMinApiLevel(AndroidApiLevel.L.getLevel())
+                .setMinApiLevel(AndroidApiLevel.L_MR1.getLevel())
                 .addMainDexListFiles(mainDexList)
                 .build());
   }
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeVirtualToInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeVirtualToInterfaceTest.java
new file mode 100644
index 0000000..2a8fa07
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInvokeVirtualToInterfaceTest.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoParameterTypeStrengthening;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import 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 VerticalClassMergerInvokeVirtualToInterfaceTest 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)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(J.class).assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoParameterTypeStrengtheningAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!", "Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      callOnI(new B());
+      callOnJ(new B());
+    }
+
+    @NeverInline
+    @NoParameterTypeStrengthening
+    static void callOnI(I i) {
+      i.m();
+    }
+
+    @NeverInline
+    @NoParameterTypeStrengthening
+    static void callOnJ(J j) {
+      j.m();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+
+    void m();
+  }
+
+  @NoUnusedInterfaceRemoval
+  interface J {
+
+    void m();
+  }
+
+  @NoVerticalClassMerging
+  abstract static class A implements I, J {}
+
+  @NeverClassInline
+  static class B extends A {
+
+    @NeverInline
+    public void m() {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index 128178a..ba3e8e6 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -16,6 +16,8 @@
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.SyntheticInfoConsumer;
+import com.android.tools.r8.SyntheticInfoConsumerData;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -23,7 +25,9 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.desugar.backports.AbstractBackportTest.MiniAssert;
+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.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -37,8 +41,10 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.junit.Test;
@@ -342,6 +348,115 @@
     assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
   }
 
+  private static class ContextCollector implements SyntheticInfoConsumer {
+
+    Map<ClassReference, Set<ClassReference>> contextToSynthetics = new HashMap<>();
+
+    @Override
+    public synchronized void acceptSyntheticInfo(SyntheticInfoConsumerData data) {
+      contextToSynthetics
+          .computeIfAbsent(data.getSynthesizingContextClass(), k -> new HashSet<>())
+          .add(data.getSyntheticClass());
+    }
+
+    @Override
+    public void finished() {}
+  }
+
+  private static class PerFileCollector extends DexFilePerClassFileConsumer.ForwardingConsumer {
+
+    Map<ClassReference, byte[]> data = new HashMap<>();
+    ContextCollector contexts = new ContextCollector();
+
+    public PerFileCollector() {
+      super(null);
+    }
+
+    @Override
+    public boolean combineSyntheticClassesWithPrimaryClass() {
+      return false;
+    }
+
+    @Override
+    public synchronized void accept(
+        String primaryClassDescriptor,
+        ByteDataView data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      super.accept(primaryClassDescriptor, data, descriptors, handler);
+      this.data.put(Reference.classFromDescriptor(primaryClassDescriptor), data.copyByteData());
+    }
+  }
+
+  @Test
+  public void testDoubleCompileSyntheticInputsD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    // This is a regression test for the pathological case of recompiling intermediates in
+    // intermediate mode, but where the second round of compilation can share more than what was
+    // originally shared. Such a case should never be hit by any reasonable compilation pipeline,
+    // but it could by chance happen if a bytecode transformation was between the two intermediate
+    // steps that ended up making two previously distinct synthetics equivalent. To simulate such
+    // a case the sharing of synthetics is internally disabled so that we know the second round
+    // would see equivalent synthetics.
+
+    // Compile part 1 of the input with sharing completely disabled.
+    PerFileCollector out1 = new PerFileCollector();
+    testForD8(parameters.getBackend())
+        .addOptionsModification(o -> o.testing.enableSyntheticSharing = false)
+        .addProgramClasses(User1.class)
+        .addClasspathClasses(CLASSES)
+        .setMinApi(parameters)
+        .setIntermediate(true)
+        .setProgramConsumer(out1)
+        .apply(b -> b.getBuilder().setSyntheticInfoConsumer(out1.contexts))
+        .compile();
+    // The total number of outputs is 8 of which 7 are synthetics in User1.
+    ClassReference user1 = Reference.classFromClass(User1.class);
+    assertEquals(8, out1.data.size());
+    assertEquals(1, out1.contexts.contextToSynthetics.size());
+    assertEquals(7, out1.contexts.contextToSynthetics.get(user1).size());
+
+    // Recompile a "shard" containing all the synthetics, but not the context.
+    PerFileCollector out2 = new PerFileCollector();
+    testForD8(parameters.getBackend())
+        .apply(
+            b ->
+                out1.contexts
+                    .contextToSynthetics
+                    .get(user1)
+                    .forEach(synthetic -> b.addProgramDexFileData(out1.data.get(synthetic))))
+        .addClasspathClasses(CLASSES)
+        .setMinApi(parameters)
+        .setIntermediate(true)
+        .setProgramConsumer(out2)
+        .apply(b -> b.getBuilder().setSyntheticInfoConsumer(out2.contexts))
+        .compile();
+    // Again the total number of synthetics should remain 7 with no sharing taking place.
+    assertEquals(7, out2.data.size());
+
+    // Compile the remaining program inputs not compiled in the above.
+    // Note: the order of final synthetics depends on compiling to intermediates before merge.
+    Path out3 =
+        testForD8(parameters.getBackend())
+            .addProgramClasses(MiniAssert.class, TestClass.class, User2.class)
+            .setMinApi(parameters)
+            .setIntermediate(true)
+            .compile()
+            .writeToZip();
+
+    // Merge all into a final build.
+    testForD8(parameters.getBackend())
+        .addProgramDexFileData(out1.data.get(user1))
+        .addProgramDexFileData(out2.data.values())
+        .addProgramFiles(out3)
+        .setMinApi(parameters)
+        .setIntermediate(false)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(this::checkExpectedSynthetics);
+  }
+
   static class User1 {
 
     private static void testBooleanCompare() {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
index f7c850a..c23da1d 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMappingTest.java
@@ -18,22 +18,20 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class EnumUnboxingMappingTest extends EnumUnboxingTestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withDexRuntimes().withAllApiLevels().build();
   }
 
-  public EnumUnboxingMappingTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testEnumUnboxing() throws Exception {
     testForR8(parameters.getBackend())
@@ -64,14 +62,14 @@
     assertEquals("int", debugInfoMethod.getFinalSignature().asMethodSignature().parameters[0]);
     assertEquals("int", noDebugInfoMethod.getFinalSignature().asMethodSignature().parameters[0]);
 
-    assertEquals(MyEnum.class.getName(), debugInfoMethod.getOriginalSignature().parameters[0]);
-    // TODO(b/314076309): The original parameter should be MyEnum.class but is int.
-    assertEquals("int", noDebugInfoMethod.getOriginalSignature().parameters[0]);
+    assertEquals(MyEnum.class.getTypeName(), debugInfoMethod.getOriginalSignature().parameters[0]);
+    assertEquals(
+        MyEnum.class.getTypeName(), noDebugInfoMethod.getOriginalSignature().parameters[0]);
 
     ClassSubject indirection = codeInspector.clazz(Indirection.class);
     MethodSubject abstractMethod = indirection.uniqueMethodWithOriginalName("intermediate");
     assertTrue(abstractMethod.isAbstract());
-    assertEquals(MyEnum.class.getName(), abstractMethod.getOriginalSignature().parameters[0]);
+    assertEquals(MyEnum.class.getTypeName(), abstractMethod.getOriginalSignature().parameters[0]);
   }
 
   @NeverClassInline
diff --git a/src/test/java/com/android/tools/r8/examples/filledarray/FilledArray.java b/src/test/java/com/android/tools/r8/examples/filledarray/FilledArray.java
index f25b76b..6551be6 100644
--- a/src/test/java/com/android/tools/r8/examples/filledarray/FilledArray.java
+++ b/src/test/java/com/android/tools/r8/examples/filledarray/FilledArray.java
@@ -115,8 +115,7 @@
     }
 
     try {
-      // Array creation that cannot be turned into fill-array-data because an exception would
-      // cause the initialization sequence to be interrupted.
+      // Exception does not prevent fill-array-data since only usage is within the try.
       int[] ints = new int[5];
       ints[0] = 0;
       ints[1] = 1;
diff --git a/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternAnyRetentionTest.java b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternAnyRetentionTest.java
new file mode 100644
index 0000000..4b11ba6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternAnyRetentionTest.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class AnnotationPatternAnyRetentionTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1: A1");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public AnnotationPatternAnyRetentionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, Reflector.class, A1.class, A2.class, C1.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    // The class retention annotation is used and kept. It can be renamed as nothing prohibits that.
+    assertThat(inspector.clazz(A1.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(A2.class), isPresentAndRenamed());
+    // The class is retained by the keep-annotation.
+    ClassSubject clazz = inspector.clazz(C1.class);
+    assertThat(clazz, isPresentAndNotRenamed());
+    assertThat(clazz.annotation(A1.class), isPresent());
+    assertThat(clazz.annotation(A2.class), isPresent());
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.CLASS)
+  @interface A2 {}
+
+  static class Reflector {
+
+    @UsesReflection({
+      @KeepTarget(
+          classAnnotatedByClassConstant = A2.class,
+          constrainAnnotations =
+              @AnnotationPattern(retention = {RetentionPolicy.CLASS, RetentionPolicy.RUNTIME})),
+    })
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        String typeName = clazz.getTypeName();
+        System.out.print(typeName.substring(typeName.lastIndexOf('$') + 1) + ":");
+        if (clazz.isAnnotationPresent(A1.class)) {
+          System.out.print(" A1");
+        }
+        // The below code will not trigger as A2 is not visible at runtime, but it will ensure the
+        // annotation is used.
+        if (clazz.isAnnotationPresent(A2.class)) {
+          System.out.print(" A2");
+        }
+        System.out.println();
+      }
+    }
+  }
+
+  @A1
+  @A2
+  static class C1 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternClassRetentionTest.java b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternClassRetentionTest.java
new file mode 100644
index 0000000..efc77cf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternClassRetentionTest.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class AnnotationPatternClassRetentionTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1:");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public AnnotationPatternClassRetentionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, Reflector.class, A1.class, A2.class, C1.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    // Nothing is keeping A1 and no references exist to it.
+    // Having the constraint on annotations with class retention should not prohibit removal.
+    assertThat(inspector.clazz(A1.class), isAbsent());
+    // The class retention annotation is used and kept. It can be renamed as nothing prohibits that.
+    assertThat(inspector.clazz(A2.class), isPresentAndRenamed());
+    // The class is retained by the keep-annotation.
+    ClassSubject clazz = inspector.clazz(C1.class);
+    assertThat(clazz, isPresentAndNotRenamed());
+    assertThat(clazz.annotation(A1.class), isAbsent());
+    assertThat(clazz.annotation(A2.class), isPresent());
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.CLASS)
+  @interface A2 {}
+
+  static class Reflector {
+
+    @UsesReflection({
+      @KeepTarget(
+          classAnnotatedByClassConstant = A2.class,
+          constrainAnnotations = @AnnotationPattern(retention = RetentionPolicy.CLASS)),
+    })
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        String typeName = clazz.getTypeName();
+        System.out.print(typeName.substring(typeName.lastIndexOf('$') + 1) + ":");
+        // Ignoring A1 as we explicitly have no keep-annotation to preserve it.
+        // The below code will not trigger as A2 is not visible at runtime, but it will ensure the
+        // annotation is used.
+        if (clazz.isAnnotationPresent(A2.class)) {
+          System.out.print(" A2");
+        }
+        System.out.println();
+      }
+    }
+  }
+
+  @A1
+  @A2
+  static class C1 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternMultipleTest.java b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternMultipleTest.java
new file mode 100644
index 0000000..33a4f6d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/AnnotationPatternMultipleTest.java
@@ -0,0 +1,146 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class AnnotationPatternMultipleTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1: A1", "C2:");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public AnnotationPatternMultipleTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(
+        TestClass.class, Reflector.class, A1.class, A2.class, A3.class, C1.class, C2.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A1.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(A2.class), isPresentAndRenamed());
+    // No use of A3 so R8 will remove it.
+    assertThat(inspector.clazz(A3.class), isAbsent());
+    // The class is retained by the keep-annotation.
+    ClassSubject c1 = inspector.clazz(C1.class);
+    assertThat(c1, isPresentAndNotRenamed());
+    assertThat(c1.annotation(A1.class), isPresent());
+    assertThat(c1.annotation(A3.class), isAbsent());
+
+    ClassSubject c2 = inspector.clazz(C2.class);
+    assertThat(c2, isPresentAndNotRenamed());
+    assertThat(c2.annotation(A2.class), isPresent());
+    assertThat(c2.annotation(A3.class), isAbsent());
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.CLASS)
+  @interface A2 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A3 {}
+
+  static class Reflector {
+
+    @UsesReflection(
+        @KeepTarget(
+            classAnnotatedByClassNamePattern =
+                @ClassNamePattern(packageName = "com.android.tools.r8.keepanno"),
+            constrainAnnotations = {
+              @AnnotationPattern(constant = A1.class, retention = RetentionPolicy.RUNTIME),
+              @AnnotationPattern(constant = A2.class, retention = RetentionPolicy.CLASS)
+            }))
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        String typeName = clazz.getTypeName();
+        System.out.print(typeName.substring(typeName.lastIndexOf('$') + 1) + ":");
+        if (clazz.isAnnotationPresent(A1.class)) {
+          System.out.print(" A1");
+        }
+        // The below code will not trigger as A2 is not visible at runtime, but it will ensure the
+        // annotation is used.
+        if (clazz.isAnnotationPresent(A2.class)) {
+          System.out.print(" A2");
+        }
+        System.out.println();
+      }
+    }
+  }
+
+  @A1
+  @A3
+  static class C1 {}
+
+  @A2
+  @A3
+  static class C2 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class, C2.class);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoConstraintPatternTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoConstraintPatternTest.java
new file mode 100644
index 0000000..0a33131
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoConstraintPatternTest.java
@@ -0,0 +1,153 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ClassAnnotatedByAnyAnnoConstraintPatternTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1: A1", "C2: A2");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ClassAnnotatedByAnyAnnoConstraintPatternTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(
+        TestClass.class,
+        Reflector.class,
+        A1.class,
+        A2.class,
+        A3.class,
+        C1.class,
+        C2.class,
+        C3.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    // The class constant use will ensure the annotations remains.
+    // They are not renamed as the keep-annotation will match them (they are themselves annotated).
+    assertThat(inspector.clazz(A1.class), isPresentAndNotRenamed());
+    assertThat(inspector.clazz(A2.class), isPresentAndNotRenamed());
+    assertThat(inspector.clazz(A3.class), isPresentAndNotRenamed());
+    // The first two classes are annotated so the keep-annotation applies and retains their name.
+    assertThat(inspector.clazz(C1.class), isPresentAndNotRenamed());
+    assertThat(inspector.clazz(C2.class), isPresentAndNotRenamed());
+    // The last class will remain due to the class constant, but it is optimized/renamed.
+    assertThat(inspector.clazz(C3.class), isPresentAndRenamed());
+    // The class-retention annotation should not be present on the class as the pattern defaults
+    // to only match runtime retention classes.
+    assertThat(inspector.clazz(C2.class).annotation(A3.class), isAbsent());
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A2 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.CLASS)
+  @interface A3 {}
+
+  static class Reflector {
+
+    @UsesReflection({
+      @KeepTarget(
+          classAnnotatedByClassNamePattern = @ClassNamePattern,
+          constraints = {KeepConstraint.NAME},
+          constrainAnnotations = @AnnotationPattern),
+    })
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        if (clazz.getAnnotations().length > 0) {
+          String typeName = clazz.getTypeName();
+          System.out.print(typeName.substring(typeName.lastIndexOf('$') + 1) + ":");
+          if (clazz.isAnnotationPresent(A1.class)) {
+            System.out.print(" A1");
+          }
+          if (clazz.isAnnotationPresent(A2.class)) {
+            System.out.print(" A2");
+          }
+          if (clazz.isAnnotationPresent(A3.class)) {
+            System.out.print(" A3");
+          }
+          System.out.println();
+        }
+      }
+    }
+  }
+
+  @A1
+  static class C1 {}
+
+  @A2
+  @A3
+  static class C2 {}
+
+  static class C3 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class, C2.class, C3.class);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java
index 684a4e2..6e8ea51 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
 import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
@@ -91,11 +92,14 @@
 
   static class Reflector {
 
-    @UsesReflection({
-      @KeepTarget(
-          classAnnotatedByClassNamePattern = @ClassNamePattern,
-          constraints = {KeepConstraint.ANNOTATIONS, KeepConstraint.NAME}),
-    })
+    @UsesReflection(
+        @KeepTarget(
+            classAnnotatedByClassNamePattern = @ClassNamePattern,
+            constraints = KeepConstraint.NAME,
+            constrainAnnotations = {
+              @AnnotationPattern(constant = A1.class),
+              @AnnotationPattern(constant = A2.class)
+            }))
     public void foo(Class<?>... classes) throws Exception {
       for (Class<?> clazz : classes) {
         if (clazz.getAnnotations().length > 0) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java
index 6d7e6c8..d828d7f 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
@@ -95,7 +96,8 @@
     @UsesReflection(
         @KeepTarget(
             classAnnotatedByClassConstant = A1.class,
-            constraints = {KeepConstraint.ANNOTATIONS, KeepConstraint.NAME}))
+            constraints = KeepConstraint.NAME,
+            constrainAnnotations = @AnnotationPattern(constant = A1.class)))
     public void foo(Class<?>... classes) throws Exception {
       for (Class<?> clazz : classes) {
         if (clazz.isAnnotationPresent(A1.class)) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java
index dda99e1..6b4bfac 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -106,7 +106,8 @@
     @UsesReflection({
       @KeepTarget(
           instanceOfClassConstantExclusive = Base.class,
-          constraints = {KeepConstraint.ANNOTATIONS})
+          constraints = {},
+          constrainAnnotations = @AnnotationPattern(constant = Anno.class))
     })
     public Base() {
       Anno annotation = getClass().getAnnotation(Anno.class);
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
index 0eace48..d8d5941 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInvalidTargetTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.StringPattern;
@@ -186,6 +187,48 @@
     }
   }
 
+  @Test
+  public void testEmptyAnnotationConstraints() {
+    assertThrowsWith(
+        () -> extractRuleForClass(EmptyAnnotationConstraints.class),
+        allOf(
+            containsString("Expected non-empty array"),
+            containsString("at property: constrainAnnotations")));
+  }
+
+  static class EmptyAnnotationConstraints {
+
+    @UsesReflection(
+        @KeepTarget(
+            classConstant = A.class,
+            constrainAnnotations = {}))
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+
+  @Test
+  public void testEmptyAnnotationPatternRetention() {
+    assertThrowsWith(
+        () -> extractRuleForClass(EmptyAnnotationPatternRetention.class),
+        allOf(
+            containsString("Expected non-empty array"),
+            containsString("at property: retention"),
+            containsString("at annotation: @AnnotationPattern"),
+            containsString("at property: constrainAnnotations")));
+  }
+
+  static class EmptyAnnotationPatternRetention {
+
+    @UsesReflection(
+        @KeepTarget(
+            classConstant = A.class,
+            constrainAnnotations = @AnnotationPattern(retention = {})))
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+
   static class A {
     // just a target.
   }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
index 19e2290..7c57ea6 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
@@ -10,8 +10,8 @@
 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.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.KeepCondition;
-import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -108,7 +108,7 @@
         preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
         // Both the class and method are reflectively accessed.
         kind = KeepItemKind.CLASS_AND_MEMBERS,
-        constraintAdditions = {KeepConstraint.ANNOTATIONS})
+        constrainAnnotations = @AnnotationPattern(constant = Anno.class))
     @Anno("anno-on-bar")
     public static void bar() {
       System.out.println("Hello, world");
diff --git a/src/test/java/com/android/tools/r8/keepanno/MembersAnnotatedByPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/MembersAnnotatedByPatternsTest.java
index 642e269..0f2aadf 100644
--- a/src/test/java/com/android/tools/r8/keepanno/MembersAnnotatedByPatternsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/MembersAnnotatedByPatternsTest.java
@@ -11,8 +11,8 @@
 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.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
-import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
@@ -29,6 +29,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.List;
+import org.checkerframework.checker.units.qual.A;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -122,19 +123,23 @@
           classConstant = OnMembers.class,
           kind = KeepItemKind.CLASS_AND_MEMBERS,
           memberAnnotatedByClassConstant = A.class,
-          constraintAdditions = {KeepConstraint.ANNOTATIONS}),
+          constrainAnnotations = @AnnotationPattern(constant = A.class)),
       @KeepTarget(
           classConstant = OnFields.class,
           kind = KeepItemKind.CLASS_AND_FIELDS,
           fieldAnnotatedByClassName =
               "com.android.tools.r8.keepanno.MembersAnnotatedByPatternsTest$B",
-          constraintAdditions = {KeepConstraint.ANNOTATIONS}),
+          constrainAnnotations =
+              @AnnotationPattern(
+                  name = "com.android.tools.r8.keepanno.MembersAnnotatedByPatternsTest$B")),
       @KeepTarget(
           classConstant = OnMethods.class,
           kind = KeepItemKind.CLASS_AND_METHODS,
           methodAnnotatedByClassNamePattern =
               @ClassNamePattern(simpleName = "MembersAnnotatedByPatternsTest$C"),
-          constraintAdditions = {KeepConstraint.ANNOTATIONS})
+          constrainAnnotations =
+              @AnnotationPattern(
+                  namePattern = @ClassNamePattern(simpleName = "MembersAnnotatedByPatternsTest$C")))
     })
     public void foo(Class<?> clazz) throws Exception {
       for (Field field : clazz.getDeclaredFields()) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java
new file mode 100644
index 0000000..a694a82
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java
@@ -0,0 +1,159 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.doctests;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest.Example1.MyAnnotationPrinter.MyClass;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UsesReflectionAnnotationsDocumentationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("fieldOne = 1", "fieldTwo = 2");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public UsesReflectionAnnotationsDocumentationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = inspector.clazz(MyClass.class);
+              assertThat(clazz.uniqueFieldWithOriginalName("mFieldOne"), isPresentAndRenamed());
+              assertThat(clazz.uniqueFieldWithOriginalName("mFieldTwo"), isPresentAndRenamed());
+              assertThat(clazz.uniqueFieldWithOriginalName("mFieldThree"), isAbsent());
+            });
+  }
+
+  public List<Class<?>> getExampleClasses() {
+    return ImmutableList.of(Example1.class);
+  }
+
+  static class Example1 {
+
+    /* INCLUDE DOC: UsesReflectionOnAnnotations
+    If your program is reflectively inspecting annotations on classes, methods or fields, you
+    will need to declare additional "annotation constraints" about what assumptions are made
+    about the annotations.
+
+    In the following example, we have defined an annotation that will record the printing name we
+    would like to use for fields instead of printing the concrete field name. That may be useful
+    so that the field can be renamed to follow coding conventions for example.
+
+    We are only interested in matching objects that contain fields annotated by `MyNameAnnotation`,
+    that is specified using `@KeepTarget#fieldAnnotatedByClassConstant`.
+
+    At runtime we need to be able to find the annotation too, so we add a constraint on the
+    annotation using `@KeepTarget#constrainAnnotations`.
+
+    Finally, for the sake of example, we don't actually care about the name of the fields
+    themselves, so we explicitly declare the smaller set of constraints to be
+    `@KeepConstraint#LOOKUP` since we must find the fields via `Class.getDeclaredFields` as well as
+    `@KeepConstraint#VISIBILITY_RELAX` and `@KeepConstraint#FIELD_GET` in order to be able to get
+    the actual field value without accessibility errors.
+
+    The effect is that the default constraint `@KeepConstraint#NAME` is not specified which allows
+    the shrinker to rename the fields at will.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsesReflectionOnAnnotations
+    public class MyAnnotationPrinter {
+
+      @Target(ElementType.FIELD)
+      @Retention(RetentionPolicy.RUNTIME)
+      public @interface MyNameAnnotation {
+        String value();
+      }
+
+      public static class MyClass {
+        @MyNameAnnotation("fieldOne")
+        public int mFieldOne = 1;
+
+        @MyNameAnnotation("fieldTwo")
+        public int mFieldTwo = 2;
+
+        public int mFieldThree = 3;
+      }
+
+      @UsesReflection(
+          @KeepTarget(
+              fieldAnnotatedByClassConstant = MyNameAnnotation.class,
+              constrainAnnotations = @AnnotationPattern(constant = MyNameAnnotation.class),
+              constraints = {
+                KeepConstraint.LOOKUP,
+                KeepConstraint.VISIBILITY_RELAX,
+                KeepConstraint.FIELD_GET
+              }))
+      public void printMyNameAnnotatedFields(Object obj) throws Exception {
+        for (Field field : obj.getClass().getDeclaredFields()) {
+          if (field.isAnnotationPresent(MyNameAnnotation.class)) {
+            System.out.println(
+                field.getAnnotation(MyNameAnnotation.class).value() + " = " + field.get(obj));
+          }
+        }
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      new MyAnnotationPrinter().printMyNameAnnotatedFields(new MyAnnotationPrinter.MyClass());
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      Example1.run();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index 9376fa3..c2dec45 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
 import com.android.tools.r8.keepanno.annotations.KeepBinding;
 import com.android.tools.r8.keepanno.annotations.KeepCondition;
@@ -17,11 +18,14 @@
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
 import com.android.tools.r8.keepanno.annotations.MethodAccessFlags;
+import com.android.tools.r8.keepanno.annotations.StringPattern;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
 import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
+import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
 import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
 import com.android.tools.r8.utils.FileUtils;
@@ -86,6 +90,9 @@
             UsedByReflection.class,
             UsedByNative.class,
             KeepForApi.class,
+            StringPattern.class,
+            TypePattern.class,
+            AnnotationPattern.class,
             // Enums.
             KeepConstraint.class,
             KeepItemKind.class,
@@ -94,6 +101,7 @@
             FieldAccessFlags.class);
     populateCodeAndDocReplacements(
         UsesReflectionDocumentationTest.class,
+        UsesReflectionAnnotationsDocumentationTest.class,
         ForApiDocumentationTest.class,
         MainMethodsDocumentationTest.class);
   }
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index 0852bde..475813b 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.cfmethodgeneration.CodeGenerationBase;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.CheckOptimizedOut;
 import com.android.tools.r8.keepanno.annotations.CheckRemoved;
 import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
@@ -301,6 +302,7 @@
     private static String MEMBER_ANNOTATED_BY_GROUP = "member-annotated-by";
     private static String METHOD_ANNOTATED_BY_GROUP = "method-annotated-by";
     private static String FIELD_ANNOTATED_BY_GROUP = "field-annotated-by";
+    private static String ANNOTATION_NAME_GROUP = "annotation-name";
 
     private Group createDescriptionGroup() {
       return new Group("description")
@@ -487,12 +489,13 @@
               docLink(KeepItemKind.ONLY_MEMBERS) + " otherwise.");
     }
 
-    private Group getKeepConstraintsGroup() {
-      return new Group(CONSTRAINTS_GROUP).addMember(constraints()).addMember(constraintAdditions());
+    private void forEachKeepConstraintGroups(Consumer<Group> fn) {
+      fn.accept(getKeepConstraintsGroup());
+      fn.accept(new Group("constrain-annotations").addMember(constrainAnnotations()));
     }
 
-    private static String docLinkList(Enum<?>... values) {
-      return StringUtils.join(", ", values, v -> docLink(v), BraceType.TUBORG);
+    private Group getKeepConstraintsGroup() {
+      return new Group(CONSTRAINTS_GROUP).addMember(constraints()).addMember(constraintAdditions());
     }
 
     private static GroupMember constraints() {
@@ -527,6 +530,69 @@
           .defaultArrayEmpty(KeepConstraint.class);
     }
 
+    private static GroupMember constrainAnnotations() {
+      return new GroupMember("constrainAnnotations")
+          .setDocTitle("Patterns for annotations that must remain on the item.")
+          .addParagraph(
+              "The annotations matching any of the patterns must remain on the item",
+              "if the annotation types remain in the program.")
+          .addParagraph(
+              "Note that if the annotation types themselves are unused/removed,",
+              "then their references on the item will be removed too.",
+              "If the annotation types themselves are used reflectively then they too need a",
+              "keep annotation or rule to ensure they remain in the program.")
+          .addParagraph(
+              "By default no annotation patterns are defined and no annotations are required to",
+              "remain.")
+          .setDocReturn("Annotation patterns")
+          .defaultArrayEmpty(AnnotationPattern.class);
+    }
+
+    private Group annotationNameGroup() {
+      return new Group(ANNOTATION_NAME_GROUP)
+          .addMember(annotationName())
+          .addMember(annotationConstant())
+          .addMember(annotationNamePattern())
+          .addDocFooterParagraph(
+              "If none are specified the default is to match any annotation name.");
+    }
+
+    private GroupMember annotationName() {
+      return new GroupMember("name")
+          .setDocTitle(
+              "Define the " + ANNOTATION_NAME_GROUP + " pattern by fully qualified class name.")
+          .setDocReturn("The qualified class name that defines the annotation.")
+          .defaultEmptyString();
+    }
+
+    private GroupMember annotationConstant() {
+      return new GroupMember("constant")
+          .setDocTitle(
+              "Define the "
+                  + ANNOTATION_NAME_GROUP
+                  + " pattern by reference to a {@code Class} constant.")
+          .setDocReturn("The Class constant that defines the annotation.")
+          .defaultObjectClass();
+    }
+
+    private GroupMember annotationNamePattern() {
+      return new GroupMember("namePattern")
+          .setDocTitle(
+              "Define the "
+                  + ANNOTATION_NAME_GROUP
+                  + " pattern by reference to a class-name pattern.")
+          .setDocReturn("The class-name pattern that defines the annotation.")
+          .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN);
+    }
+
+    private static GroupMember annotationRetention() {
+      return new GroupMember("retention")
+          .setDocTitle("Specify which retention policies must be set for the annotations.")
+          .addParagraph("Matches annotations with matching retention policies")
+          .setDocReturn("Retention policies. By default {@code RetentionPolicy.RUNTIME}.")
+          .defaultArrayValue(RetentionPolicy.class, "RetentionPolicy.RUNTIME");
+    }
+
     private GroupMember bindingName() {
       return new GroupMember("bindingName")
           .setDocTitle(
@@ -1024,6 +1090,30 @@
       println("}");
     }
 
+    private void generateAnnotationPattern() {
+      printCopyRight(2024);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A pattern structure for matching annotations.")
+          .addParagraph(
+              "If no properties are set, the default pattern matches any annotation",
+              "with a runtime retention policy.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + simpleName(AnnotationPattern.class) + " {");
+      println();
+      withIndent(
+          () -> {
+            annotationNameGroup().generate(this);
+            println();
+            annotationRetention().generate(this);
+          });
+      println();
+      println("}");
+    }
+
     private void generateKeepBinding() {
       printCopyRight(2022);
       printPackage("annotations");
@@ -1073,8 +1163,11 @@
           () -> {
             getKindGroup().generate(this);
             println();
-            getKeepConstraintsGroup().generate(this);
-            println();
+            forEachKeepConstraintGroups(
+                g -> {
+                  g.generate(this);
+                  println();
+                });
             generateClassAndMemberPropertiesWithClassAndMemberBinding();
           });
       println();
@@ -1294,8 +1387,11 @@
                         + " if annotating a member.")
                 .generate(this);
             println();
-            getKeepConstraintsGroup().generate(this);
-            println();
+            forEachKeepConstraintGroups(
+                g -> {
+                  g.generate(this);
+                  println();
+                });
             generateMemberPropertiesNoBinding();
           });
       println();
@@ -1318,6 +1414,10 @@
       return "{@link " + simpleName(kind.getClass()) + "#" + kind.name() + "}";
     }
 
+    private static String docLinkList(Enum<?>... values) {
+      return StringUtils.join(", ", values, v -> docLink(v), BraceType.TUBORG);
+    }
+
     private void generateConstants() {
       printCopyRight(2023);
       printPackage("ast");
@@ -1356,6 +1456,7 @@
             generateStringPatternConstants();
             generateTypePatternConstants();
             generateClassNamePatternConstants();
+            generateAnnotationPatternConstants();
           });
       println("}");
     }
@@ -1509,7 +1610,7 @@
           () -> {
             generateAnnotationConstants(KeepTarget.class);
             getKindGroup().generateConstants(this);
-            getKeepConstraintsGroup().generateConstants(this);
+            forEachKeepConstraintGroups(g -> g.generateConstants(this));
           });
       println("}");
       println();
@@ -1663,6 +1764,18 @@
       println();
     }
 
+    private void generateAnnotationPatternConstants() {
+      println("public static final class AnnotationPattern {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(AnnotationPattern.class);
+            annotationNameGroup().generateConstants(this);
+            annotationRetention().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private static void writeFile(Path file, Consumer<Generator> fn) throws IOException {
       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
       PrintStream printStream = new PrintStream(byteStream);
@@ -1691,6 +1804,7 @@
       writeFile(source(annoPkg, StringPattern.class), Generator::generateStringPattern);
       writeFile(source(annoPkg, TypePattern.class), Generator::generateTypePattern);
       writeFile(source(annoPkg, ClassNamePattern.class), Generator::generateClassNamePattern);
+      writeFile(source(annoPkg, AnnotationPattern.class), Generator::generateAnnotationPattern);
       writeFile(source(annoPkg, KeepBinding.class), Generator::generateKeepBinding);
       writeFile(source(annoPkg, KeepTarget.class), Generator::generateKeepTarget);
       writeFile(source(annoPkg, KeepCondition.class), Generator::generateKeepCondition);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexApi21Test.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexApi21Test.java
new file mode 100644
index 0000000..b9c3c87
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexApi21Test.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.maindexlist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+// Test for allowing main-dex support for API 21 / L (See b//320893283).
+@RunWith(Parameterized.class)
+public class MainDexApi21Test extends TestBase {
+
+  static class TestClassA {}
+
+  static class TestClassB {}
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public MainDexApi21Test(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  static class TestConsumer extends DexIndexedConsumer.ForwardingConsumer {
+
+    Map<Integer, byte[]> data = new HashMap<>();
+    Map<Integer, Set<String>> descriptors = new HashMap<>();
+
+    public TestConsumer() {
+      super(null);
+    }
+
+    @Override
+    public synchronized void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      assertNull(this.data.put(fileIndex, data.copyByteData()));
+      assertNull(this.descriptors.put(fileIndex, descriptors));
+    }
+  }
+
+  @Test
+  public void testWithoutMainDexInputs() throws Exception {
+    // Test to ensure that running at API 21 / L without any main-dex content works.
+    // Since L does support native multdex the compiler should not require any main-dex inputs.
+    TestConsumer programConsumer = new TestConsumer();
+    testForD8()
+        .setMinApi(AndroidApiLevel.L)
+        .addProgramClasses(ImmutableList.of(TestClassA.class, TestClassB.class))
+        .setProgramConsumer(programConsumer)
+        .compile();
+    assertEquals(1, programConsumer.data.size());
+    assertEquals(1, programConsumer.descriptors.size());
+    assertEquals(
+        ImmutableSet.of(descriptor(TestClassA.class), descriptor(TestClassB.class)),
+        programConsumer.descriptors.get(0));
+  }
+
+  @Test
+  public void testWithMainDexRules() throws Exception {
+    // Test to ensure that running at API 21 / L with main-dex rules will actually produce a
+    // main-dex file. Debug mode will compile to a minimal main-dex, so we will observe two files.
+    TestConsumer programConsumer = new TestConsumer();
+    testForD8()
+        .setMinApi(AndroidApiLevel.L)
+        .addProgramClasses(ImmutableList.of(TestClassA.class, TestClassB.class))
+        .addMainDexKeepClassAndMemberRules(TestClassB.class)
+        .setProgramConsumer(programConsumer)
+        .compile();
+    assertEquals(2, programConsumer.data.size());
+    assertEquals(2, programConsumer.descriptors.size());
+    assertEquals(ImmutableSet.of(descriptor(TestClassB.class)), programConsumer.descriptors.get(0));
+    assertEquals(ImmutableSet.of(descriptor(TestClassA.class)), programConsumer.descriptors.get(1));
+  }
+
+  @Test
+  public void testWithMainDexList() throws Exception {
+    // Test to ensure that running at API 21 / L with main-dex list will actually produce a
+    // main-dex file. Debug mode will compile to a minimal main-dex, so we will observe two files.
+    TestConsumer programConsumer = new TestConsumer();
+    testForD8()
+        .setMinApi(AndroidApiLevel.L)
+        .addProgramClasses(ImmutableList.of(TestClassA.class, TestClassB.class))
+        .addMainDexListClasses(TestClassB.class)
+        .setProgramConsumer(programConsumer)
+        .compile();
+    assertEquals(2, programConsumer.data.size());
+    assertEquals(2, programConsumer.descriptors.size());
+    assertEquals(ImmutableSet.of(descriptor(TestClassB.class)), programConsumer.descriptors.get(0));
+    assertEquals(ImmutableSet.of(descriptor(TestClassA.class)), programConsumer.descriptors.get(1));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
index 82ae38f..26a4674 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
@@ -63,13 +63,16 @@
   }
 
   private void inspect(CodeInspector inspector) {
+    int canUseStrings = canUseFilledNewArrayOfStringObjects(parameters) ? 1 : 0;
+    int canUseObjects = canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0;
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
-        2
-            + (canUseFilledNewArrayOfStringObjects(parameters) ? 2 : 0)
-            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 2 : 0),
+        2 + 2 * canUseStrings + 2 * canUseObjects,
         2);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 2, 2);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+        2 + 2 * canUseStrings + 1 * canUseObjects,
+        compilationMode.isDebug() ? 10 : 11);
   }
 
   @Test
@@ -129,32 +132,32 @@
 
     @NeverInline
     public static void m2() {
+      Object[] array = null;
       try {
-        Object[] array = {
-          new byte[] {(byte) 1},
-          new short[] {(short) 1},
-          new int[] {1},
-          new long[] {1L},
-          new char[] {(char) 1},
-          new float[] {1.0f},
-          new double[] {1.0d},
-          new String[] {"one"},
-          new Object[] {
-            new byte[] {(byte) 2},
-            new short[] {(short) 2},
-            new int[] {2},
-            new long[] {2L},
-            new char[] {(char) 2},
-            new float[] {2.0f},
-            new double[] {2.0d},
-            new String[] {"two"},
-          }
-        };
-        printArray(array);
-        System.out.println();
+        array = new Object[9];
+        array[0] = new byte[] {(byte) 1};
+        array[1] = new short[] {(short) 1};
+        array[2] = new int[] {1};
+        array[3] = new long[] {1L};
+        array[4] = new char[] {(char) 1};
+        array[5] = new float[] {1.0f};
+        array[6] = new double[] {1.0d};
+        array[7] = new String[] {"one"};
+        array[8] =
+            new Object[] {
+              new byte[] {(byte) 2},
+              new short[] {(short) 2},
+              new int[] {2},
+              new long[] {2L},
+              new char[] {(char) 2},
+              new float[] {2.0f},
+              new double[] {2.0d},
+              new String[] {"two"},
+            };
       } catch (Exception e) {
-        throw new RuntimeException();
       }
+      printArray(array);
+      System.out.println();
     }
 
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
index 63fec32..b37c8a2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
@@ -62,10 +62,7 @@
 
   private void inspect(CodeInspector inspector) {
     inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, 1);
-    inspect(
-        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
-        0,
-        compilationMode.isDebug() ? 1 : 2);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, 4);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
index edc54de..c429641 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
@@ -59,15 +59,15 @@
 
   private void inspect(CodeInspector inspector) {
     // This test use smaller int arrays, where filled-new-array is preferred over filled-array-data.
+    int canUseObjects = canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0;
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
-        4 + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        4 + 1 * canUseObjects,
         1);
-    // With catch handler the int[][] creation is not converted to filled-new-array.
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
-        4,
-        compilationMode.isDebug() ? 1 : 2);
+        4 + 1 * canUseObjects,
+        4);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
index 1934f24..d238b1a 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
@@ -63,28 +63,27 @@
   }
 
   private void inspect(CodeInspector inspector) {
+    int canUseStrings = canUseFilledNewArrayOfStringObjects(parameters) ? 1 : 0;
+    int canUseObjects = canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0;
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
-        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
-            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        4 * canUseStrings + 1 * canUseObjects,
         1);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
-        0,
-        compilationMode.isDebug() ? 2 : 1);
+        4 * canUseStrings + 1 * canUseObjects,
+        4);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"),
-        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
-            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 5 : 0),
+        4 * canUseStrings + 5 * canUseObjects,
         5);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m4"),
-        0,
-        compilationMode.isDebug() ? 2 : 1);
+        4 * canUseStrings + 5 * canUseObjects,
+        5);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m5"),
-        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
-            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        4 * canUseStrings + 1 * canUseObjects,
         compilationMode.isDebug() ? 6 : 4);
   }
 
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
index 4b6e64c..c27a171 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -46,9 +45,8 @@
     EXPECTING_APUTOBJECT
   }
 
-  private void inspect(MethodSubject method, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method) {
+    boolean expectingFilledNewArray = canUseFilledNewArrayOfNonStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : 5,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -78,8 +76,8 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"));
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
index 3a364dd..7d8bba7 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
@@ -46,10 +46,8 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
 
-  private void inspect(
-      MethodSubject method, int constClasses, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method, int constClasses, int puts) {
+    boolean expectingFilledNewArray = canUseFilledNewArrayOfNonStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -68,12 +66,11 @@
   }
 
   private void inspectD8(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
         maxMaterializingConstants == 2 ? 98 : 26,
-        104,
-        false);
+        104);
   }
 
   @Test
@@ -89,12 +86,11 @@
   }
 
   private void inspectR8(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
         maxMaterializingConstants == 2 ? 32 : 26,
-        104,
-        false);
+        104);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
index c7f847b..3c7976f 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
@@ -52,9 +52,8 @@
     EXPECTING_APUTOBJECT
   }
 
-  private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method, int puts) {
+    boolean expectingFilledNewArray = canUseFilledNewArrayOfNonStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -86,9 +85,9 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, true);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/FilledArrayDataWithCatchHandlerTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/FilledArrayDataWithCatchHandlerTest.java
deleted file mode 100644
index e28a4fc..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/FilledArrayDataWithCatchHandlerTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.rewrite.arrays;
-
-import static com.android.tools.r8.cf.methodhandles.fields.ClassFieldMethodHandleTest.Main.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexFillArrayData;
-import com.android.tools.r8.dex.code.DexNewArray;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionOffsetSubject;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class FilledArrayDataWithCatchHandlerTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("1");
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
-  }
-
-  public FilledArrayDataWithCatchHandlerTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForRuntime(parameters)
-        .addInnerClasses(FilledArrayDataWithCatchHandlerTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED);
-  }
-
-  @Test
-  public void testReleaseD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8(parameters.getBackend())
-        .release()
-        .setMinApi(parameters)
-        .addInnerClasses(FilledArrayDataWithCatchHandlerTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkInstructions);
-  }
-
-  private void checkInstructions(CodeInspector inspector) {
-    MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithFinalName("foo");
-    List<InstructionSubject> newArrays =
-        foo.streamInstructions()
-            .filter(i -> i.asDexInstruction().getInstruction() instanceof DexNewArray)
-            .collect(Collectors.toList());
-    assertEquals(1, newArrays.size());
-
-    List<InstructionSubject> fillArrays =
-        foo.streamInstructions()
-            .filter(i -> i.asDexInstruction().getInstruction() instanceof DexFillArrayData)
-            .collect(Collectors.toList());
-    assertEquals(1, fillArrays.size());
-
-    InstructionOffsetSubject offsetNew = newArrays.get(0).getOffset(foo);
-    InstructionOffsetSubject offsetFill = newArrays.get(0).getOffset(foo);
-    assertTrue(
-        foo.streamTryCatches()
-            .allMatch(r -> r.getRange().includes(offsetNew) && r.getRange().includes(offsetFill)));
-  }
-
-  static class TestClass {
-
-    public static int foo() {
-      int value = 1;
-      int[] array = null;
-      try {
-        array = new int[6];
-      } catch (RuntimeException e) {
-        return array[0];
-      }
-      array[0] = value;
-      array[1] = value;
-      array[2] = value;
-      array[3] = value;
-      array[4] = value;
-      array[5] = value;
-      return array[5];
-    }
-
-    public static void main(String[] args) {
-      System.out.println(foo());
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInCatchRangeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInCatchRangeTest.java
deleted file mode 100644
index f9e3ffe..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInCatchRangeTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.rewrite.arrays;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionOffsetSubject;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class NewArrayInCatchRangeTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("1");
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
-  }
-
-  public NewArrayInCatchRangeTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForRuntime(parameters)
-        .addInnerClasses(NewArrayInCatchRangeTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED);
-  }
-
-  @Test
-  public void testReleaseD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8(parameters.getBackend())
-        .release()
-        .setMinApi(parameters)
-        .addInnerClasses(NewArrayInCatchRangeTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkInstructions);
-  }
-
-  private void checkInstructions(CodeInspector inspector) {
-    MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithFinalName("foo");
-    List<InstructionSubject> filledArrayInstructions =
-        foo.streamInstructions()
-            .filter(i -> i.asDexInstruction().getInstruction() instanceof DexFilledNewArray)
-            .collect(Collectors.toList());
-    assertEquals(1, filledArrayInstructions.size());
-    InstructionOffsetSubject offset = filledArrayInstructions.get(0).getOffset(foo);
-    assertTrue(foo.streamTryCatches().allMatch(r -> r.getRange().includes(offset)));
-  }
-
-  static class TestClass {
-
-    public static int foo() {
-      int value = 1;
-      int[] array = null;
-      try {
-        array = new int[1];
-      } catch (Exception e) {
-        return array == null ? -1 : array.length;
-      }
-      array[0] = value;
-      return array[0];
-    }
-
-    public static void main(String[] args) {
-      System.out.println(foo());
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInTwoCatchRangesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInTwoCatchRangesTest.java
deleted file mode 100644
index 3ecd9de..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayInTwoCatchRangesTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.rewrite.arrays;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
-import com.android.tools.r8.utils.StringUtils;
-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;
-
-@RunWith(Parameterized.class)
-public class NewArrayInTwoCatchRangesTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("1");
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
-  }
-
-  public NewArrayInTwoCatchRangesTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForRuntime(parameters)
-        .addInnerClasses(NewArrayInTwoCatchRangesTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED);
-  }
-
-  @Test
-  public void testReleaseD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8(parameters.getBackend())
-        .release()
-        .setMinApi(parameters)
-        .addInnerClasses(NewArrayInTwoCatchRangesTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkHasFilledNewArray);
-  }
-
-  private void checkHasFilledNewArray(CodeInspector inspector) {
-    MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithFinalName("foo");
-    assertTrue(
-        foo.streamInstructions()
-            .anyMatch(i -> i.asDexInstruction().getInstruction() instanceof DexFilledNewArray));
-  }
-
-  static class TestClass {
-
-    public static int foo() {
-      int value = 1;
-      try {
-        int[] array = new int[2];
-        try {
-          array[0] = value;
-          try {
-            array[1] = value;
-          } catch (RuntimeException e) {
-            return array[1];
-          }
-        } catch (RuntimeException e) {
-          return array[0];
-        }
-        return array[0];
-      } catch (RuntimeException e) {
-        return 42;
-      }
-    }
-
-    public static void main(String[] args) {
-      System.out.println(foo());
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayMonitorTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayMonitorTest.java
deleted file mode 100644
index c7e3b2a..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayMonitorTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.rewrite.arrays;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionOffsetSubject;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class NewArrayMonitorTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("1");
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
-  }
-
-  public NewArrayMonitorTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForRuntime(parameters)
-        .addInnerClasses(NewArrayMonitorTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED);
-  }
-
-  @Test
-  public void testReleaseD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8(parameters.getBackend())
-        .release()
-        .setMinApi(parameters)
-        .addInnerClasses(NewArrayMonitorTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkInstructions);
-  }
-
-  private void checkInstructions(CodeInspector inspector) {
-    MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithFinalName("foo");
-    List<InstructionSubject> filledArrayInstructions =
-        foo.streamInstructions()
-            .filter(i -> i.asDexInstruction().getInstruction() instanceof DexFilledNewArray)
-            .collect(Collectors.toList());
-    assertEquals(1, filledArrayInstructions.size());
-    InstructionOffsetSubject offset = filledArrayInstructions.get(0).getOffset(foo);
-    assertTrue(foo.streamTryCatches().allMatch(r -> r.getRange().includes(offset)));
-  }
-
-  static class TestClass {
-
-    public static synchronized int foo() {
-      int value = 1;
-      int[] array = new int[1];
-      try {
-        array[0] = value;
-      } catch (RuntimeException e) {
-        return array[0];
-      }
-      return array[0];
-    }
-
-    public static void main(String[] args) {
-      System.out.println(foo());
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayOfInaccessibleTypeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayOfInaccessibleTypeTest.java
new file mode 100644
index 0000000..2282a4a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayOfInaccessibleTypeTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.rewrite.arrays;
+
+import com.android.tools.r8.NoAccessModification;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+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;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class NewArrayOfInaccessibleTypeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeCfRuntime();
+    testForJvm(parameters)
+        .addProgramClassFileData(getProgramClassFileData())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addProgramClassFileData(getProgramClassFileData())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepMainRule(Main.class)
+        .enableNoAccessModificationAnnotationsForClasses()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  private static List<byte[]> getProgramClassFileData() throws Exception {
+    return ImmutableList.of(
+        transformer(Main.class)
+            .transformTypeInsnInMethod(
+                "main",
+                (int opcode, String type, MethodVisitor visitor) ->
+                    visitor.visitTypeInsn(
+                        opcode,
+                        type.equals(binaryName(Inaccessible.class)) ? "pkg/Inaccessible" : type))
+            .replaceClassDescriptorInMethodInstructions(
+                descriptor(Inaccessible.class), "Lpkg/Inaccessible;")
+            .transform(),
+        transformer(Inaccessible.class).setClassDescriptor("Lpkg/Inaccessible;").transform());
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      // Moving the array creation into the else branch would change semantics as the code then no
+      // longer throws IllegalAccessError.
+      Inaccessible[] array = new Inaccessible[3];
+      array[0] = null;
+      array[1] = null;
+      if (System.currentTimeMillis() > 0) {
+        throw new RuntimeException("Unexpected");
+      } else {
+        array[2] = null;
+        System.out.println(Arrays.toString(array));
+      }
+    }
+  }
+
+  // TODO(b/320445632): Access modifier should not publicize items with illegal accesses.
+  @NoAccessModification
+  static class /*pkg.*/ Inaccessible {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayPutInCatchRangeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayPutInCatchRangeTest.java
deleted file mode 100644
index a3fd2da..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArrayPutInCatchRangeTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.rewrite.arrays;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.util.Collections;
-import java.util.stream.Collectors;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-// Regression test for issue found in b/259986613
-@RunWith(Parameterized.class)
-public class NewArrayPutInCatchRangeTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("1");
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
-  }
-
-  public NewArrayPutInCatchRangeTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForRuntime(parameters)
-        .addInnerClasses(NewArrayPutInCatchRangeTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED);
-  }
-
-  @Test
-  public void testReleaseD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8(parameters.getBackend())
-        .release()
-        .setMinApi(parameters)
-        .addInnerClasses(NewArrayPutInCatchRangeTest.class)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkHasFilledNewArray);
-  }
-
-  private void checkHasFilledNewArray(CodeInspector inspector) {
-    MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithFinalName("foo");
-    assertTrue(
-        foo.streamInstructions()
-            .anyMatch(i -> i.asDexInstruction().getInstruction() instanceof DexFilledNewArray));
-    assertEquals(Collections.emptyList(), foo.streamTryCatches().collect(Collectors.toList()));
-  }
-
-  static class TestClass {
-
-    public static int foo() {
-      int value = 1;
-      int[] array = new int[1];
-      try {
-        array[0] = value;
-      } catch (RuntimeException e) {
-        return array[0];
-      }
-      return array[0];
-    }
-
-    public static void main(String[] args) {
-      System.out.println(foo());
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArraySynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArraySynchronizedBlockTest.java
index c552566..5ed624e 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/NewArraySynchronizedBlockTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/NewArraySynchronizedBlockTest.java
@@ -72,7 +72,7 @@
       int[] array;
       synchronized (TestClass.class) {
         array = new int[1];
-      } // monitor exit here prohibits optimization as its failure could observe the lack of init.
+      }
       array[0] = value;
       return array[0];
     }
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
index c80436c..7011411 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
@@ -63,7 +63,18 @@
     "[1, null, 2]",
     "[1, null, 2]",
     "[1]",
-    "[1, 2]",
+    "[1, 2, 3]",
+    "[2, 2, 2]",
+    "[6, 7]",
+    "[7]",
+    "[3, 4]",
+    "[99]",
+    "[0, 1]",
+    "[0, 1]",
+    "[0, 1]",
+    "[0, 1]",
+    "[0, 1]",
+    "[0, 1]",
     "[1, 2, 3, 4, 5]",
     "[1]",
     "[a, 1, null, d, e, f]",
@@ -91,12 +102,16 @@
     "[0, 1, 2, 3, 4]",
     "[4, 0, 0, 0, 0]",
     "[4, 1, 2, 3, 4]",
+    "[9]",
+    "[*]",
+    "[*]",
+    "finally: [1, 2]",
+    "[1, 2]",
+    "[1, 2]",
+    "[1, 2]",
+    "[1, 2]",
     "[0, 1, 2]",
     "[0]",
-    "[0, 1, 2]",
-    "[1, 2, 3]",
-    "[1, 2, 3, 4, 5, 6]",
-    "[0]",
     "[null, null]",
   };
 
@@ -176,6 +191,20 @@
         mainClass.uniqueMethodWithOriginalName("interfaceArrayWithRawObject");
 
     MethodSubject phiFilledNewArray = mainClass.uniqueMethodWithOriginalName("phiFilledNewArray");
+    MethodSubject phiFilledNewArrayBlocks =
+        mainClass.uniqueMethodWithOriginalName("phiFilledNewArrayBlocks");
+    MethodSubject arrayWithDominatingPhiUsers =
+        mainClass.uniqueMethodWithOriginalName("arrayWithDominatingPhiUsers");
+    MethodSubject arrayWithNonDominatingPhiUsers =
+        mainClass.uniqueMethodWithOriginalName("arrayWithNonDominatingPhiUsers");
+    MethodSubject phiWithExceptionalPhiUser =
+        mainClass.uniqueMethodWithOriginalName("phiWithExceptionalPhiUser");
+    MethodSubject phiWithNestedCatchHandler =
+        mainClass.uniqueMethodWithOriginalName("phiWithNestedCatchHandler");
+    MethodSubject multiUseArray = mainClass.uniqueMethodWithOriginalName("multiUseArray");
+    MethodSubject arrayWithHole = mainClass.uniqueMethodWithOriginalName("arrayWithHole");
+    MethodSubject reassignmentDoesNotOptimize =
+        mainClass.uniqueMethodWithOriginalName("reassignmentDoesNotOptimize");
     MethodSubject intsThatUseFilledNewArray =
         mainClass.uniqueMethodWithOriginalName("intsThatUseFilledNewArray");
     MethodSubject twoDimensionalArrays =
@@ -191,26 +220,60 @@
         mainClass.uniqueMethodWithOriginalName("arrayWithCorrectCountButIncompleteCoverage");
     MethodSubject arrayWithExtraInitialPuts =
         mainClass.uniqueMethodWithOriginalName("arrayWithExtraInitialPuts");
-    MethodSubject catchHandlerThrowing =
-        mainClass.uniqueMethodWithOriginalName("catchHandlerThrowing");
-    MethodSubject catchHandlerNonThrowingFilledNewArray =
-        mainClass.uniqueMethodWithOriginalName("catchHandlerNonThrowingFilledNewArray");
-    MethodSubject catchHandlerNonThrowingFillArrayData =
-        mainClass.uniqueMethodWithOriginalName("catchHandlerNonThrowingFillArrayData");
+    MethodSubject catchHandlerWithoutSideeffects =
+        mainClass.uniqueMethodWithOriginalName("catchHandlerWithoutSideeffects");
+    MethodSubject allocationWithCatchHandler =
+        mainClass.uniqueMethodWithOriginalName("allocationWithCatchHandler");
+    MethodSubject allocationWithoutCatchHandler =
+        mainClass.uniqueMethodWithOriginalName("allocationWithoutCatchHandler");
+    MethodSubject catchHandlerWithFinally =
+        mainClass.uniqueMethodWithOriginalName("catchHandlerWithFinally");
+    MethodSubject simpleSynchronized1 =
+        mainClass.uniqueMethodWithOriginalName("simpleSynchronized1");
+    MethodSubject simpleSynchronized2 =
+        mainClass.uniqueMethodWithOriginalName("simpleSynchronized2");
+    MethodSubject simpleSynchronized3 =
+        mainClass.uniqueMethodWithOriginalName("simpleSynchronized3");
+    MethodSubject simpleSynchronized4 =
+        mainClass.uniqueMethodWithOriginalName("simpleSynchronized4");
+    MethodSubject arrayInsideCatchHandler =
+        mainClass.uniqueMethodWithOriginalName("arrayInsideCatchHandler");
     MethodSubject assumedValues = mainClass.uniqueMethodWithOriginalName("assumedValues");
 
+    // The explicit assignments can't be collapsed without breaking the debugger's ability to
+    // visit each line.
+    Class<?> filledNewArrayInRelease =
+        compilationMode == CompilationMode.DEBUG ? DexNewArray.class : DexFilledNewArray.class;
+
     assertArrayTypes(arraysThatUseNewArrayEmpty, DexNewArray.class);
     assertArrayTypes(intsThatUseFilledNewArray, DexFilledNewArray.class);
     assertFilledArrayData(arraysThatUseFilledData);
-    assertFilledArrayData(catchHandlerNonThrowingFillArrayData);
 
-    if (compilationMode == CompilationMode.DEBUG) {
-      // The explicit assignments can't be collapsed without breaking the debugger's ability to
-      // visit each line.
-      assertArrayTypes(reversedArray, DexNewArray.class);
-    } else {
-      assertArrayTypes(reversedArray, DexFilledNewArray.class);
-    }
+    // Algorithm does not support out-of-order assignment.
+    assertArrayTypes(reversedArray, DexNewArray.class);
+    // Algorithm does not support assigning to array elements multiple times.
+    assertArrayTypes(reassignmentDoesNotOptimize, DexNewArray.class);
+    // Algorithm does not support default-initialized array elements.
+    assertArrayTypes(arrayWithHole, DexNewArray.class);
+    // ArrayPuts not dominated by return statement.
+    assertArrayTypes(phiFilledNewArrayBlocks, DexNewArray.class);
+    assertArrayTypes(arrayWithDominatingPhiUsers, filledNewArrayInRelease);
+    assertArrayTypes(arrayWithNonDominatingPhiUsers, DexNewArray.class);
+    assertArrayTypes(phiWithNestedCatchHandler, DexNewArray.class);
+    assertArrayTypes(phiWithExceptionalPhiUser, DexFilledNewArray.class, filledNewArrayInRelease);
+    // Not safe to change catch handlers.
+    assertArrayTypes(allocationWithoutCatchHandler, DexNewArray.class);
+    // Not safe to change catch handlers.
+    assertArrayTypes(allocationWithCatchHandler, DexNewArray.class);
+    assertArrayTypes(catchHandlerWithFinally, DexNewArray.class);
+    assertArrayTypes(simpleSynchronized1, DexFilledNewArray.class);
+    assertArrayTypes(simpleSynchronized2, DexFilledNewArray.class);
+    assertArrayTypes(simpleSynchronized3, DexNewArray.class);
+    assertArrayTypes(simpleSynchronized4, DexNewArray.class);
+    // Could be optimized if we had side-effect analysis of exceptional blocks.
+    assertArrayTypes(catchHandlerWithoutSideeffects, DexNewArray.class);
+    assertArrayTypes(arrayInsideCatchHandler, filledNewArrayInRelease);
+    assertArrayTypes(multiUseArray, filledNewArrayInRelease);
 
     if (!canUseFilledNewArrayOfStringObjects(parameters)) {
       assertArrayTypes(stringArrays, DexNewArray.class);
@@ -239,9 +302,7 @@
         assertArrayTypes(referenceArraysWithInterfaceImplementations, DexNewArray.class);
       }
 
-      // TODO(b/246971330): Add support for arrays whose values have conditionals.
-      // assertArrayTypes(phiFilledNewArray, DexFilledNewArray.class);
-
+      assertArrayTypes(phiFilledNewArray, DexFilledNewArray.class);
       assertArrayTypes(
           objectArraysFilledNewArrayRange, DexFilledNewArrayRange.class, DexNewArray.class);
 
@@ -261,9 +322,6 @@
     // haven't bothered.
     assertArrayTypes(arrayWithExtraInitialPuts, DexNewArray.class);
     assertArrayTypes(arrayWithCorrectCountButIncompleteCoverage, DexNewArray.class);
-
-    assertArrayTypes(catchHandlerThrowing, DexNewArray.class);
-    assertArrayTypes(catchHandlerNonThrowingFilledNewArray, DexFilledNewArray.class);
   }
 
   private static Predicate<InstructionSubject> isInstruction(List<Class<?>> allowlist) {
@@ -310,6 +368,14 @@
       referenceArraysWithInterfaceImplementations();
       interfaceArrayWithRawObject();
       phiFilledNewArray();
+      phiFilledNewArrayBlocks();
+      arrayWithDominatingPhiUsers();
+      arrayWithNonDominatingPhiUsers();
+      phiWithNestedCatchHandler();
+      phiWithExceptionalPhiUser();
+      multiUseArray();
+      arrayWithHole();
+      reassignmentDoesNotOptimize();
       intsThatUseFilledNewArray();
       twoDimensionalArrays();
       objectArraysFilledNewArrayRange();
@@ -318,9 +384,15 @@
       reversedArray();
       arrayWithCorrectCountButIncompleteCoverage();
       arrayWithExtraInitialPuts();
-      catchHandlerThrowing();
-      catchHandlerNonThrowingFilledNewArray();
-      catchHandlerNonThrowingFillArrayData();
+      arrayInsideCatchHandler();
+      allocationWithCatchHandler();
+      allocationWithoutCatchHandler();
+      catchHandlerWithFinally();
+      simpleSynchronized1();
+      simpleSynchronized2();
+      simpleSynchronized3();
+      simpleSynchronized4();
+      catchHandlerWithoutSideeffects();
       arrayIntoAnotherArray();
       assumedValues();
     }
@@ -411,21 +483,12 @@
     }
 
     @NeverInline
-    private static void catchHandlerNonThrowingFilledNewArray() {
+    private static void arrayInsideCatchHandler() {
       try {
-        int[] arr1 = {1, 2, 3};
+        // Test filled-new-array with a throwing instruction before the last array-put.
+        int[] arr = new int[1];
         System.currentTimeMillis();
-        System.out.println(Arrays.toString(arr1));
-      } catch (Throwable t) {
-        throw new RuntimeException(t);
-      }
-    }
-
-    @NeverInline
-    private static void catchHandlerNonThrowingFillArrayData() {
-      try {
-        int[] arr = {1, 2, 3, 4, 5, 6};
-        System.currentTimeMillis();
+        arr[0] = 9;
         System.out.println(Arrays.toString(arr));
       } catch (Throwable t) {
         throw new RuntimeException(t);
@@ -433,37 +496,115 @@
     }
 
     @NeverInline
-    private static void catchHandlerThrowing() {
-      int[] arr1 = new int[3];
-      arr1[0] = 0;
-      arr1[1] = 1;
-      // Since the array is used in only one spot, and that spot is not within the try/catch, it
-      // should be safe to use filled-new-array, but we don't.
+    private static void allocationWithCatchHandler() {
+      Object[] arr;
       try {
-        System.currentTimeMillis();
+        arr = new Object[1];
+      } catch (NoClassDefFoundError | OutOfMemoryError t) {
+        throw new RuntimeException(t);
+      }
+
+      // new-array-empty dominates this, but catch handlers are relevant
+      arr[0] = "*";
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void allocationWithoutCatchHandler() {
+      Object[] arr = new Object[1];
+      try {
+        // new-array-empty dominates this, but catch handlers are relevant.
+        arr[0] = "*";
+      } catch (NoClassDefFoundError | OutOfMemoryError t) {
+        throw new RuntimeException(t);
+      }
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void catchHandlerWithFinally() {
+      Object[] arr = new Object[2];
+      try {
+        System.out.print("finally: ");
+      } finally {
+        // This will be duplicated into the throwing and non-throwing blocks.
+        arr[0] = "1";
+      }
+      arr[1] = "2";
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void simpleSynchronized1() {
+      // Should optimize since array is contained within a try block.
+      synchronized (Main.class) {
+        int[] arr = new int[] {1, 2};
+        System.out.println(Arrays.toString(arr));
+      }
+    }
+
+    @NeverInline
+    private static synchronized void simpleSynchronized2() {
+      // Should optimize since array is contained within a try block.
+      try {
+        try {
+          int[] arr = new int[] {1, 2};
+          System.out.println(Arrays.toString(arr));
+        } catch (Throwable t) {
+          throw new RuntimeException(t);
+        } finally {
+          System.currentTimeMillis();
+        }
+      } catch (Exception e) {
+        // Ignore.
+      }
+    }
+
+    @NeverInline
+    private static void simpleSynchronized3() {
+      // Does not optimize because allocation has different catch handlers.
+      int[] arr = new int[2];
+      synchronized (Main.class) {
+        arr[0] = 1;
+        arr[1] = 2;
+      }
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void simpleSynchronized4() {
+      // Does not optimize because allocation has different catch handlers.
+      int[] arr;
+      synchronized (Main.class) {
+        arr = new int[2];
+      }
+      arr[0] = 1;
+      arr[1] = 2;
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void catchHandlerWithoutSideeffects() {
+      // If we added logic to show that catch handlers exit without side-effects, we could optimize
+      // this case.z
+      int[] arr1;
+      try {
+        arr1 = new int[3];
+      } catch (Throwable t) {
+        throw new RuntimeException("1");
+      }
+      try {
+        arr1[0] = 0;
+      } catch (Throwable t) {
+        throw new RuntimeException("2");
+      }
+      arr1[1] = 1;
+      try {
         arr1[2] = 2;
       } catch (Throwable t) {
-        throw new RuntimeException(t);
+        throw new RuntimeException("3");
       }
       System.out.println(Arrays.toString(arr1));
-
-      try {
-        // Test filled-new-array with a throwing instruction before the last array-put.
-        int[] arr2 = new int[1];
-        System.currentTimeMillis();
-        arr2[0] = 0;
-        System.out.println(Arrays.toString(arr2));
-
-        // Test filled-array-data with a throwing instruction before the last array-put.
-        short[] arr3 = new short[3];
-        arr3[0] = 0;
-        arr3[1] = 1;
-        System.currentTimeMillis();
-        arr3[2] = 2;
-        System.out.println(Arrays.toString(arr3));
-      } catch (Throwable t) {
-        throw new RuntimeException(t);
-      }
     }
 
     @NeverInline
@@ -485,11 +626,123 @@
     @NeverInline
     private static void phiFilledNewArray() {
       // The presence of ? should not affect use of filled-new-array.
-      Integer[] phiArray = {1, System.nanoTime() > 0 ? 2 : 3};
+      Integer[] phiArray = {1, System.nanoTime() > 0 ? 2 : 3, 3};
       System.out.println(Arrays.toString(phiArray));
     }
 
     @NeverInline
+    private static void phiFilledNewArrayBlocks() {
+      int[] phiArray = new int[3];
+      if (System.currentTimeMillis() > 0) {
+        phiArray[0] = 2;
+        phiArray[1] = 2;
+        phiArray[2] = 2;
+      }
+      System.out.println(Arrays.toString(phiArray));
+    }
+
+    @NeverInline
+    private static void arrayWithDominatingPhiUsers() {
+      int[] phiArray = null;
+      try {
+        phiArray = new int[2];
+        phiArray[0] = 6;
+        phiArray[1] = 7;
+      } catch (Throwable t) {
+        System.out.println("Not reached");
+      }
+      System.out.println(Arrays.toString(phiArray));
+    }
+
+    @NeverInline
+    private static void arrayWithNonDominatingPhiUsers() {
+      int[] phiArray = null;
+      try {
+        phiArray = new int[1];
+        // If currentTimeMillis() throws, phiArray will have value of [0].
+        phiArray[0] = System.currentTimeMillis() > 0 ? 7 : 0;
+      } catch (Throwable t) {
+        System.out.println("Not reached");
+      }
+      System.out.println(Arrays.toString(phiArray));
+    }
+
+    @NeverInline
+    private static void phiWithNestedCatchHandler() {
+      int[] phiArray = null;
+      try {
+        phiArray = new int[2];
+        // If currentTimeMillis() throws, phiArray will have value of [0, 0].
+        try {
+          System.currentTimeMillis();
+        } catch (RuntimeException r) {
+          throw new RuntimeException(r);
+        }
+        phiArray[0] = 3;
+        phiArray[1] = 4;
+      } catch (Throwable t) {
+        System.out.println("Not reached");
+      }
+      System.out.println(Arrays.toString(phiArray));
+    }
+
+    @NeverInline
+    private static void phiWithExceptionalPhiUser() {
+      int[] arr = null;
+      try {
+        // Both of these should optimize, but care must be taken to ensure the phiUsers are properly
+        // dominated post-optimization.
+        if (System.currentTimeMillis() > 0) {
+          arr = new int[1];
+          arr[0] = 99;
+        } else {
+          arr = new int[] {1, 2};
+        }
+      } catch (RuntimeException e) {
+        // fall through
+      }
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void multiUseArray() {
+      int[] arr = new int[2];
+      arr[0] = 0;
+      arr[1] = System.nanoTime() > 0 ? 1 : 2;
+      System.out.println(Arrays.toString(arr));
+      System.out.println(Arrays.toString(arr));
+      // Usage in a different basic block.
+      if (System.nanoTime() > 0) {
+        System.nanoTime();
+      }
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void arrayWithHole() {
+      int[] arr = new int[2];
+      arr[1] = 1;
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
+    private static void reassignmentDoesNotOptimize() {
+      // Reassignment in same block, and of last index.
+      Integer[] arr = new Integer[2];
+      arr[0] = 0;
+      arr[1] = 2;
+      arr[1] = 1;
+      System.out.println(Arrays.toString(arr));
+
+      // Reassignment across blocks, of non-last index.
+      arr = new Integer[2];
+      arr[0] = 3;
+      arr[1] = System.nanoTime() > 0 ? 1 : 2;
+      arr[0] = 0;
+      System.out.println(Arrays.toString(arr));
+    }
+
+    @NeverInline
     private static void intsThatUseFilledNewArray() {
       // Up to 5 ints uses filled-new-array rather than filled-array-data.
       int[] intArr = {1, 2, 3, 4, 5};
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
index be54d34..68b80fc 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
@@ -46,9 +46,9 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "50");
 
-  private void inspect(MethodSubject method, int staticGets, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method, int staticGets, int puts, boolean isD8) {
+    // D8 cannot optimize due to risk of NoClassDefFoundError.
+    boolean expectingFilledNewArray = !isD8 && canUseFilledNewArrayOfNonStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -66,18 +66,9 @@
   }
 
   private void inspectD8(CodeInspector inspector) {
-    inspect(
-        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
-        canUseFilledNewArrayOfNonStringObjects(parameters) ? 100 : 1,
-        100,
-        false);
-    inspect(
-        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
-        canUseFilledNewArrayOfNonStringObjects(parameters)
-            ? 50
-            : (maxMaterializingConstants == 2 ? 42 : 10),
-        50,
-        false);
+    // D8 cannot optimize due to risk of NoClassDefFoundError.
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 100, 100, true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 50, 50, true);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
index fce1e79..f64d0e8 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
@@ -52,9 +52,9 @@
     EXPECTING_APUTOBJECT
   }
 
-  private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(boolean isR8, MethodSubject method, int puts) {
+    // D8 cannot optimize due to risk of NoClassDefFoundError.
+    boolean expectingFilledNewArray = isR8 && canUseFilledNewArrayOfNonStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -84,10 +84,10 @@
     }
   }
 
-  private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, true);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100, false);
+  private void inspect(CodeInspector inspector, boolean isR8) {
+    inspect(isR8, inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5);
+    inspect(isR8, inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5);
+    inspect(isR8, inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100);
   }
 
   @Test
@@ -97,7 +97,7 @@
         .addInnerClasses(getClass())
         .setMinApi(parameters)
         .run(parameters.getRuntime(), TestClass.class)
-        .inspect(this::inspect)
+        .inspect(inspector -> inspect(inspector, false))
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
 
@@ -110,7 +110,7 @@
         .enableInliningAnnotations()
         .addDontObfuscate()
         .run(parameters.getRuntime(), TestClass.class)
-        .inspect(this::inspect)
+        .inspect(inspector -> inspect(inspector, true))
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
 
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
index c433e6d..cf12961 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
@@ -46,10 +46,8 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
 
-  private void inspect(
-      MethodSubject method, int constStrings, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method, int constStrings, int puts) {
+    boolean expectingFilledNewArray = canUseFilledNewArrayOfStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -68,12 +66,11 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
         maxMaterializingConstants == 2 ? 32 : 26,
-        104,
-        false);
+        104);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
index 837bea2..53f7f1f 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
@@ -52,9 +52,8 @@
     EXPECTING_APUTOBJECT
   }
 
-  private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
-    boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
+  private void inspect(MethodSubject method, int puts) {
+    boolean expectingFilledNewArray = canUseFilledNewArrayOfStringObjects(parameters);
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -86,9 +85,9 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, true);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfApiDependentLibraryTypeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfApiDependentLibraryTypeTest.java
new file mode 100644
index 0000000..1f49507
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfApiDependentLibraryTypeTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.rewrite.arrays;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.function.Function;
+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 UnusedNewArrayOfApiDependentLibraryTypeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeCfRuntime();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addInnerClasses(getClass())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            hasFunctionAsRunTime(),
+            TestRunResult::assertSuccessWithEmptyOutput,
+            runResult -> runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .applyIf(
+            !hasFunctionAtCompileTime(), testBuilder -> testBuilder.addDontWarn(Function.class))
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            hasFunctionAsRunTime(),
+            TestRunResult::assertSuccessWithEmptyOutput,
+            runResult -> runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
+  }
+
+  private void inspect(CodeInspector inspector) {
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertEquals(canOptimize(), mainMethodSubject.getMethod().getCode().isEmptyVoidMethod());
+  }
+
+  private boolean canOptimize() {
+    return hasFunctionAtCompileTime() && parameters.isDexRuntime();
+  }
+
+  private boolean hasFunctionAtCompileTime() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+  }
+
+  private boolean hasFunctionAsRunTime() {
+    return parameters.isCfRuntime()
+        || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Function[] functions = new Function[0];
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfInaccessibleTypeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfInaccessibleTypeTest.java
new file mode 100644
index 0000000..aaf7c49
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfInaccessibleTypeTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.rewrite.arrays;
+
+import com.android.tools.r8.NoAccessModification;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class UnusedNewArrayOfInaccessibleTypeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeCfRuntime();
+    testForJvm(parameters)
+        .addProgramClassFileData(getProgramClassFileData())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addProgramClassFileData(getProgramClassFileData())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepMainRule(Main.class)
+        .enableNoAccessModificationAnnotationsForClasses()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  private static List<byte[]> getProgramClassFileData() throws Exception {
+    return ImmutableList.of(
+        transformer(Main.class)
+            .transformTypeInsnInMethod(
+                "main",
+                (int opcode, String type, MethodVisitor visitor) ->
+                    visitor.visitTypeInsn(
+                        opcode,
+                        type.equals(binaryName(Inaccessible.class)) ? "pkg/Inaccessible" : type))
+            .replaceClassDescriptorInMethodInstructions(
+                descriptor(Inaccessible.class), "Lpkg/Inaccessible;")
+            .transform(),
+        transformer(Inaccessible.class).setClassDescriptor("Lpkg/Inaccessible;").transform());
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Inaccessible[] array = new Inaccessible[0];
+    }
+  }
+
+  // TODO(b/320445632): Access modifier should not publicize items with illegal accesses.
+  @NoAccessModification
+  static class /*pkg.*/ Inaccessible {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfKnownLibraryTypeTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfKnownLibraryTypeTest.java
new file mode 100644
index 0000000..a013ceb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/UnusedNewArrayOfKnownLibraryTypeTest.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.rewrite.arrays;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 UnusedNewArrayOfKnownLibraryTypeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeCfRuntime();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addInnerClasses(getClass())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  private void inspect(CodeInspector inspector) {
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertTrue(mainMethodSubject.getMethod().getCode().isEmptyVoidMethod());
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      // Primitives.
+      boolean[] booleans = new boolean[0];
+      byte[] bytes = new byte[0];
+      char[] chars = new char[0];
+      double[] doubles = new double[0];
+      float[] floats = new float[0];
+      int[] ints = new int[0];
+      long[] longs = new long[0];
+      short[] shorts = new short[0];
+
+      // Boxed primitives.
+      Boolean[] boxedBooleans = new Boolean[0];
+      Byte[] boxedBytes = new Byte[0];
+      Character[] boxedChars = new Character[0];
+      Double[] boxedDoubles = new Double[0];
+      Float[] boxedFloats = new Float[0];
+      Integer[] boxedInts = new Integer[0];
+      Long[] boxedLongs = new Long[0];
+      Short[] boxedShorts = new Short[0];
+
+      // Common classes.
+      Enum<?>[] enums = new Enum<?>[0];
+      Object[] objects = new Object[0];
+      String[] strings = new String[0];
+      StringBuilder[] stringBuilders = new StringBuilder[0];
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index 2e03043..a2a1119 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -95,7 +95,7 @@
         "shaking1.Used -> a.a:",
         "# {'id':'sourceFile','fileName':'Used.java'}",
         "    java.lang.String name -> a",
-        "    1:14:void <init>(java.lang.String):0:13 -> <init>",
+        "    1:2:void <init>(java.lang.String):12:13 -> <init>",
         "    1:1:java.lang.String method():17:17 -> a",
         "    1:1:java.lang.String aMethodThatIsNotUsedButKept():21:21 "
             + "-> aMethodThatIsNotUsedButKept");
diff --git a/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingTest.java b/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingTest.java
index 5d90382..c46d024 100644
--- a/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingTest.java
@@ -10,15 +10,15 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.R8FullTestBuilder;
-import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.FileNotFoundException;
 import java.nio.file.Path;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -115,35 +115,32 @@
             .inspect(this::inspect)
             .writeToZip();
 
-    R8FullTestBuilder builder =
-        testForR8(parameters.getBackend())
-            .addLibraryClasses(BaseClass.class, UninstantiatedClass.class, TestClass.class)
-            .addDefaultRuntimeLibrary(parameters)
-            .addProgramClasses(Caller.class)
-            .addKeepMainRule(Caller.class)
-            .setMinApi(parameters);
-    R8TestRunResult result =
-        builder
-            .compile()
-            .addRunClasspathFiles(firstRunArchive)
-            .run(parameters.getRuntime(), Caller.class);
-    // TODO(b/117302947): Dex runtime should be able to find that framework class.
-    if (parameters.isDexRuntime()) {
-      result.assertFailureWithErrorThatMatches(containsString("NoClassDefFoundError"));
-      result.assertFailureWithErrorThatMatches(containsString("android.util.Log"));
-      return;
-    }
-    result
-        .assertFailureWithErrorThatMatches(
-            containsString(createExpectedMessage(UninstantiatedClass.class)))
-        .assertFailureWithErrorThatMatches(containsString("void <init>()"))
-        .assertFailureWithErrorThatMatches(containsString("void <init>(java.lang.String)"))
-        .assertFailureWithErrorThatMatches(
-            containsString(createExpectedMessage(TestClass.class)))
-        .assertFailureWithErrorThatMatches(containsString("void foo(int,long)"))
-        .assertFailureWithErrorThatMatches(
-            containsString("void bar(" + PACKAGE_NAME + ".TestClass" +")"))
-        .assertFailureWithErrorThatMatches(containsString("Reaching the end"));
+    testForR8(parameters.getBackend())
+        .addLibraryClasses(BaseClass.class, UninstantiatedClass.class, TestClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addProgramClasses(Caller.class)
+        .addKeepMainRule(Caller.class)
+        .setMinApi(parameters)
+        .compile()
+        .addRunClasspathFiles(firstRunArchive)
+        .run(parameters.getRuntime(), Caller.class)
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters.getDexRuntimeVersion().isEqualToOneOf(Version.V5_1_1, Version.V6_0_1),
+            runResult -> runResult.assertFailureWithErrorThatThrows(FileNotFoundException.class),
+            runResult ->
+                runResult
+                    .assertFailureWithErrorThatMatches(
+                        containsString(createExpectedMessage(UninstantiatedClass.class)))
+                    .assertFailureWithErrorThatMatches(containsString("void <init>()"))
+                    .assertFailureWithErrorThatMatches(
+                        containsString("void <init>(java.lang.String)"))
+                    .assertFailureWithErrorThatMatches(
+                        containsString(createExpectedMessage(TestClass.class)))
+                    .assertFailureWithErrorThatMatches(containsString("void foo(int,long)"))
+                    .assertFailureWithErrorThatMatches(
+                        containsString("void bar(" + PACKAGE_NAME + ".TestClass" + ")"))
+                    .assertFailureWithErrorThatMatches(containsString("Reaching the end")));
   }
 
   private String createExpectedMessage(Class<?> clazz) {
diff --git a/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingWithInliningTest.java b/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingWithInliningTest.java
index edd31ba..c916ae9 100644
--- a/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingWithInliningTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/addconfigurationdebugging/ConfigurationDebuggingWithInliningTest.java
@@ -10,6 +10,8 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import java.io.FileNotFoundException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -39,13 +41,9 @@
         .run(parameters.getRuntime(), Main.class)
         // AddConfigurationDebugging will insert a call to android.util.log.
         .applyIf(
-            parameters.isDexRuntime(),
-            result ->
-                result
-                    .assertFailureWithErrorThatThrows(NoClassDefFoundError.class)
-                    .assertFailureWithErrorThatMatches(containsString("Landroid/util/Log;")))
-        .applyIf(
-            parameters.isCfRuntime(),
+            parameters.isDexRuntime()
+                && parameters.getDexRuntimeVersion().isEqualToOneOf(Version.V5_1_1, Version.V6_0_1),
+            result -> result.assertFailureWithErrorThatThrows(FileNotFoundException.class),
             result ->
                 result.assertFailureWithErrorThatMatches(
                     containsString("Missing method in " + typeName(Bar.class))));
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
index 5b8f002..bf038dd 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
@@ -8,15 +8,20 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
+import java.util.HashSet;
+import java.util.Set;
 
 public class VerticallyMergedClassesInspector {
 
   private final DexItemFactory dexItemFactory;
   private final VerticallyMergedClasses verticallyMergedClasses;
 
+  private final Set<ClassReference> seen = new HashSet<>();
+
   public VerticallyMergedClassesInspector(
       DexItemFactory dexItemFactory, VerticallyMergedClasses verticallyMergedClasses) {
     this.dexItemFactory = dexItemFactory;
@@ -24,8 +29,7 @@
   }
 
   public VerticallyMergedClassesInspector assertMergedIntoSubtype(Class<?> clazz) {
-    assertTrue(verticallyMergedClasses.hasBeenMergedIntoSubtype(toDexType(clazz, dexItemFactory)));
-    return this;
+    return assertMergedIntoSubtype(Reference.classFromClass(clazz));
   }
 
   public VerticallyMergedClassesInspector assertMergedIntoSubtype(Class<?>... classes) {
@@ -39,6 +43,7 @@
     assertTrue(
         verticallyMergedClasses.hasBeenMergedIntoSubtype(
             toDexType(classReference, dexItemFactory)));
+    seen.add(classReference);
     return this;
   }
 
@@ -53,4 +58,11 @@
     assertTrue(verticallyMergedClasses.isEmpty());
     return this;
   }
+
+  public VerticallyMergedClassesInspector assertNoOtherClassesMerged() {
+    for (DexType source : verticallyMergedClasses.getSources()) {
+      assertTrue(source.getTypeName(), seen.contains(source.asClassReference()));
+    }
+    return this;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/workaround/FilledNewArrayFromSubtypeWithMissingInterfaceWorkaroundTest.java b/src/test/java/com/android/tools/r8/workaround/FilledNewArrayFromSubtypeWithMissingInterfaceWorkaroundTest.java
index 3f7aba6..53794e3 100644
--- a/src/test/java/com/android/tools/r8/workaround/FilledNewArrayFromSubtypeWithMissingInterfaceWorkaroundTest.java
+++ b/src/test/java/com/android/tools/r8/workaround/FilledNewArrayFromSubtypeWithMissingInterfaceWorkaroundTest.java
@@ -6,14 +6,13 @@
 
 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.assertFalse;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -44,7 +43,7 @@
         .release()
         .setMinApi(parameters)
         .compile()
-        .inspect(inspector -> inspect(inspector, true))
+        .inspect(this::inspect)
         .apply(
             compileResult ->
                 compileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors())
@@ -66,18 +65,17 @@
             parameters.isDexRuntime(),
             compileResult ->
                 compileResult
-                    .inspect(inspector -> inspect(inspector, false))
+                    .inspect(this::inspect)
                     .runDex2Oat(parameters.getRuntime())
                     .assertNoVerificationErrors())
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
   }
 
-  private void inspect(CodeInspector inspector, boolean isD8) {
+  private void inspect(CodeInspector inspector) {
     MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
     assertThat(mainMethodSubject, isPresent());
-    assertEquals(
-        isD8 && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N),
+    assertFalse(
         mainMethodSubject.streamInstructions().anyMatch(InstructionSubject::isFilledNewArray));
   }
 
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index 964d1dc..9ca17ac 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -299,6 +299,7 @@
         'url': 'https://github.com/signalapp/Signal-Android',
         'revision': '91ca19f294362ccee2c2b43c247eba228e2b30a1',
         'folder': 'signal-android',
+        'golem_duration': 300
     }),
     # TODO(b/172815827): Monkey runner does not work
     App({