Merge commit 'c9bdfe52acf8e16d98f6660f860f3da4216a8ebd' into dev-release

Change-Id: Ie3885b32d2d9a58d27444b28ba81015c229b0811
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexTypeAnnotation.java
index c343889..1a2a9e2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeAnnotation.java
@@ -34,7 +34,8 @@
 
   @Override
   public void collectIndexedItems(AppView<?> appView, IndexedItemCollection indexedItems) {
-    throw new Unreachable("Should not collect type annotation in DEX");
+    assert appView.options().isGeneratingClassFiles() : "Should not collect type annotation in DEX";
+    super.collectIndexedItems(appView, indexedItems);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/PrunedItems.java b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
index 3e96a43..00a3744 100644
--- a/src/main/java/com/android/tools/r8/graph/PrunedItems.java
+++ b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
@@ -21,7 +21,6 @@
   private final DexApplication prunedApp;
   private final Set<DexReference> additionalPinnedItems;
   private final Map<DexMethod, ProgramMethod> fullyInlinedMethods;
-  private final Set<DexType> noLongerSyntheticItems;
   private final Set<DexType> removedClasses;
   private final Set<DexField> removedFields;
   private final Set<DexMethod> removedMethods;
@@ -30,14 +29,12 @@
       DexApplication prunedApp,
       Set<DexReference> additionalPinnedItems,
       Map<DexMethod, ProgramMethod> fullyInlinedMethods,
-      Set<DexType> noLongerSyntheticItems,
       Set<DexType> removedClasses,
       Set<DexField> removedFields,
       Set<DexMethod> removedMethods) {
     this.prunedApp = prunedApp;
     this.additionalPinnedItems = additionalPinnedItems;
     this.fullyInlinedMethods = fullyInlinedMethods;
-    this.noLongerSyntheticItems = noLongerSyntheticItems;
     this.removedClasses = removedClasses;
     this.removedFields = removedFields;
     this.removedMethods = removedMethods;
@@ -62,7 +59,6 @@
   public boolean isEmpty() {
     return additionalPinnedItems.isEmpty()
         && fullyInlinedMethods.isEmpty()
-        && noLongerSyntheticItems.isEmpty()
         && removedClasses.isEmpty()
         && removedFields.isEmpty()
         && removedMethods.isEmpty();
@@ -105,10 +101,6 @@
     return fullyInlinedMethods;
   }
 
-  public Set<DexType> getNoLongerSyntheticItems() {
-    return noLongerSyntheticItems;
-  }
-
   public boolean hasRemovedClasses() {
     return !removedClasses.isEmpty();
   }
@@ -143,7 +135,6 @@
 
     private final Set<DexReference> additionalPinnedItems;
     private Map<DexMethod, ProgramMethod> fullyInlinedMethods;
-    private final Set<DexType> noLongerSyntheticItems;
     private Set<DexType> removedClasses;
     private final Set<DexField> removedFields;
     private Set<DexMethod> removedMethods;
@@ -151,7 +142,6 @@
     Builder() {
       additionalPinnedItems = newEmptySet();
       fullyInlinedMethods = newEmptyMap();
-      noLongerSyntheticItems = newEmptySet();
       removedClasses = newEmptySet();
       removedFields = newEmptySet();
       removedMethods = newEmptySet();
@@ -161,7 +151,6 @@
       this();
       assert prunedItems.getFullyInlinedMethods().isEmpty();
       additionalPinnedItems.addAll(prunedItems.getAdditionalPinnedItems());
-      noLongerSyntheticItems.addAll(prunedItems.getNoLongerSyntheticItems());
       prunedApp = prunedItems.getPrunedApp();
       removedClasses.addAll(prunedItems.getRemovedClasses());
       removedFields.addAll(prunedItems.getRemovedFields());
@@ -202,19 +191,12 @@
       fullyInlinedMethods.clear();
     }
 
-    public Builder addNoLongerSyntheticItems(Set<DexType> noLongerSyntheticItems) {
-      this.noLongerSyntheticItems.addAll(noLongerSyntheticItems);
-      return this;
-    }
-
     public Builder addRemovedClass(DexType removedClass) {
-      this.noLongerSyntheticItems.add(removedClass);
       this.removedClasses.add(removedClass);
       return this;
     }
 
     public Builder addRemovedClasses(Set<DexType> removedClasses) {
-      this.noLongerSyntheticItems.addAll(removedClasses);
       this.removedClasses.addAll(removedClasses);
       return this;
     }
@@ -265,7 +247,6 @@
           prunedApp,
           additionalPinnedItems,
           fullyInlinedMethods,
-          noLongerSyntheticItems,
           removedClasses,
           removedFields,
           removedMethods);
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java b/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
index bace0d4..35031ff 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/GetArrayOfMissingTypeVerifyErrorWorkaround.java
@@ -71,7 +71,7 @@
       ProgramMethod context,
       EnqueuerWorklist worklist) {
     if (isUnsafeToUseFieldOnDalvik(field)) {
-      enqueuer.getKeepInfo().joinMethod(context, Joiner::disallowOptimization);
+      enqueuer.mutateKeepInfo(context, (k, m) -> k.joinMethod(m, Joiner::disallowOptimization));
     }
   }
 
@@ -82,7 +82,7 @@
       ProgramMethod context,
       EnqueuerWorklist worklist) {
     if (isUnsafeToUseFieldOnDalvik(field)) {
-      enqueuer.getKeepInfo().joinMethod(context, Joiner::disallowOptimization);
+      enqueuer.mutateKeepInfo(context, (k, m) -> k.joinMethod(m, Joiner::disallowOptimization));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
index b5bf374..85ef04b 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
@@ -59,7 +59,7 @@
   public void traceInvokeVirtual(
       DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
     if (isInterfaceInSomeApiLevel(invokedMethod.getHolderType())) {
-      enqueuer.getKeepInfo().joinMethod(context, Joiner::disallowOptimization);
+      enqueuer.mutateKeepInfo(context, (k, m) -> k.joinMethod(m, Joiner::disallowOptimization));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleConstClassValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleConstClassValue.java
index 902f858..f970b80 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleConstClassValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleConstClassValue.java
@@ -55,8 +55,15 @@
   }
 
   @Override
-  public boolean equals(Object o) {
-    return this == o;
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof SingleConstClassValue)) {
+      return false;
+    }
+    SingleConstClassValue other = (SingleConstClassValue) obj;
+    return type.isIdenticalTo(other.type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
index 11eebdb..64d3845 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
@@ -140,13 +140,20 @@
   }
 
   @Override
-  public boolean equals(Object o) {
-    return this == o;
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof SingleNumberValue)) {
+      return false;
+    }
+    SingleNumberValue other = (SingleNumberValue) obj;
+    return value == other.value;
   }
 
   @Override
   public int hashCode() {
-    return System.identityHashCode(this);
+    return 31 * (31 + Long.hashCode(value)) + getClass().hashCode();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java
index 8be7c10..4a81b1f 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java
@@ -49,13 +49,20 @@
   }
 
   @Override
-  public boolean equals(Object o) {
-    return this == o;
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof SingleResourceNumberValue)) {
+      return false;
+    }
+    SingleResourceNumberValue other = (SingleResourceNumberValue) obj;
+    return value == other.value;
   }
 
   @Override
   public int hashCode() {
-    return System.identityHashCode(this);
+    return 31 * (31 + value) + getClass().hashCode();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
index 3257ca6..0b4af4d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
@@ -51,8 +51,15 @@
   }
 
   @Override
-  public boolean equals(Object o) {
-    return this == o;
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof SingleStringValue)) {
+      return false;
+    }
+    SingleStringValue other = (SingleStringValue) obj;
+    return string.isIdenticalTo(other.string);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
index ca07bf0..0b328ea 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
@@ -52,13 +52,20 @@
   }
 
   @Override
-  public boolean equals(Object o) {
-    return this == o;
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof KnownLengthArrayState)) {
+      return false;
+    }
+    KnownLengthArrayState other = (KnownLengthArrayState) obj;
+    return length == other.length;
   }
 
   @Override
   public int hashCode() {
-    return System.identityHashCode(this);
+    return 31 * (31 + length) + getClass().hashCode();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
index cbe7634..9492af5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.desugar.itf;
 
+import static com.android.tools.r8.graph.InvalidCode.isInvalidCode;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceMethodDesugaringEventConsumer.emptyInterfaceMethodDesugaringEventConsumer;
 
 import com.android.tools.r8.cf.CfVersion;
@@ -558,14 +559,21 @@
     assert method.getHolder().isInterface();
     assert definition.isNonAbstractNonNativeMethod();
     assert definition.getCode() != null;
-    assert !InvalidCode.isInvalidCode(definition.getCode());
     if (definition.isStatic()) {
+      assert !isInvalidCode(definition.getCode())
+          || appView.definitionFor(staticAsMethodOfCompanionClass(method)) != null;
       return ensureStaticAsMethodOfProgramCompanionClassStub(method, eventConsumer);
-    }
-    if (definition.isPrivate()) {
+    } else if (definition.isPrivate()) {
+      assert !isInvalidCode(definition.getCode())
+          || appView.definitionFor(privateAsMethodOfCompanionClass(method)) != null;
       return ensurePrivateAsMethodOfProgramCompanionClassStub(method, eventConsumer);
+    } else {
+      assert !isInvalidCode(definition.getCode())
+          || appView.definitionFor(
+                  defaultAsMethodOfCompanionClass(method.getReference(), appView.dexItemFactory()))
+              != null;
+      return ensureDefaultAsMethodOfProgramCompanionClassStub(method, eventConsumer);
     }
-    return ensureDefaultAsMethodOfProgramCompanionClassStub(method, eventConsumer);
   }
 
   private void ensureCompanionClassInitializesInterface(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ResourceGetOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourceGetOptimizer.java
index d0bfd83..e2cd8dc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ResourceGetOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourceGetOptimizer.java
@@ -58,7 +58,7 @@
     // TODO(b/312406163): Allow androidx classes that overwrite this, but don't change the value
     // or have side effects.
     allowStringInlining = Optional.of(true);
-    allowColorInlining = Optional.of(true);
+    allowColorInlining = Optional.of(appView.options().enableColorInlining);
     Map<DexClass, Boolean> cachedResults = new IdentityHashMap<>();
     for (DexClass clazz :
         Iterables.concat(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
index 7bda442..1512007 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
@@ -85,7 +85,6 @@
     }
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private void processCandidateForInstanceOfOptimization(
       ProgramMethod method, PrimaryMethodProcessor methodProcessor) {
     DexEncodedMethod definition = method.getDefinition();
@@ -132,7 +131,7 @@
     }
 
     DexEncodedMethod parentMethodDefinition = parentMethod.getDefinition();
-    if (abstractParentReturnValue == abstractReturnValue) {
+    if (abstractParentReturnValue.equals(abstractReturnValue)) {
       // The parent method is already guaranteed to return the same value.
     } else if (isClassAccessible(method.getHolder(), parentMethod, appView).isTrue()) {
       parentMethod.setCode(
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index b20df20..e18fa27 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -229,7 +229,7 @@
                       builder -> {
                         if (!targetDefinition.isAbstract()
                             && targetDefinition.getApiLevelForCode().isNotSetApiLevel()) {
-                          assert target.isLibraryMethod()
+                          assert !target.isProgramMethod()
                               || !appView.options().apiModelingOptions().isApiModelingEnabled();
                           builder.setApiLevelForCode(
                               appView
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index d328deb..a446d35 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.shaking;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.graph.UseRegistry.MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY;
 import static com.android.tools.r8.ir.desugar.constantdynamic.LibraryConstantDynamic.dispatchEnumDescConstantDynamic;
 import static com.android.tools.r8.ir.desugar.constantdynamic.LibraryConstantDynamic.isEnumDescConstantDynamic;
@@ -17,7 +18,6 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
-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;
@@ -287,15 +287,14 @@
   }
 
   private void registerEnumReferencedInTypeSwitchBootstrapArguments(DexType enumType) {
-    DexClass dexClass = appView.definitionFor(enumType);
-    if (dexClass == null || dexClass.isNotProgramClass()) {
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(enumType));
+    if (clazz == null) {
       return;
     }
     // The enum class cannot be unboxed or class merged. It can however be renamed.
-    enqueuer
-        .getKeepInfo()
-        .joinClass(
-            dexClass.asProgramClass(), joiner -> joiner.disallowOptimization().disallowShrinking());
+    enqueuer.mutateKeepInfo(
+        clazz,
+        (k, c) -> k.joinClass(c, joiner -> joiner.disallowOptimization().disallowShrinking()));
     DexItemFactory factory = dexItemFactory();
     DexMethod values =
         factory.createMethod(
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index f8116f2..f9980b1 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -145,6 +145,7 @@
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -167,6 +168,7 @@
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
@@ -250,6 +252,7 @@
   private AppInfoWithClassHierarchy appInfo;
   private final AppView<AppInfoWithClassHierarchy> appView;
   private final EnqueuerDeferredTracing deferredTracing;
+  private final EnqueuerDeferredAnnotationTracing deferredAnnotationTracing;
   private final ExecutorService executorService;
   private ImmediateAppSubtypingInfo subtypingInfo;
   private final InternalOptions options;
@@ -391,7 +394,7 @@
   private final Map<DexProgramClass, Map<ResolutionSearchKey, ProgramMethodSet>>
       reachableVirtualTargets = new IdentityHashMap<>();
 
-  /** Collection of keep requirements for the program. */
+  /** Collection of keep requirements for the program. Must only be accessed through its getters. */
   private final MutableKeepInfoCollection keepInfo;
 
   /**
@@ -419,23 +422,6 @@
    */
   private final Set<DexMethod> recordFieldValuesReferences = Sets.newIdentityHashSet();
 
-  /** Set of annotations that are live (needed for deferred (re)processing). */
-  private final Set<DexType> liveAnnotations = Sets.newIdentityHashSet();
-
-  /**
-   * A map from annotation classes to annotations that need to be processed should the classes ever
-   * become live.
-   */
-  private final Map<DexType, Map<DexAnnotation, List<ProgramDefinition>>> deferredAnnotations =
-      new IdentityHashMap<>();
-
-  /**
-   * A map from annotation classes to parameter annotations that need to be processed should the
-   * classes ever become live.
-   */
-  private final Map<DexType, Map<DexAnnotation, List<ProgramDefinition>>>
-      deferredParameterAnnotations = new IdentityHashMap<>();
-
   /**
    * A cache of ScopedDexMethodSet for each live type used for determining that virtual methods that
    * cannot be removed because they are widening access for another virtual method defined earlier
@@ -502,6 +488,7 @@
     this.mode = mode;
     this.profileCollectionAdditions = profileCollectionAdditions;
     this.deferredTracing = EnqueuerDeferredTracing.create(appView, this, mode);
+    this.deferredAnnotationTracing = new EnqueuerDeferredAnnotationTracing(this);
     this.executorService = executorService;
     this.subtypingInfo = subtypingInfo;
     this.forceProguardCompatibility = options.forceProguardCompatibility;
@@ -805,10 +792,16 @@
     return fieldAccessInfoCollection;
   }
 
-  public MutableKeepInfoCollection getKeepInfo() {
+  public KeepInfoCollection getKeepInfo() {
     return keepInfo;
   }
 
+  public <T extends ProgramDefinition> void mutateKeepInfo(
+      T itemToChange, BiConsumer<MutableKeepInfoCollection, T> consumer) {
+    consumer.accept(keepInfo, itemToChange);
+    deferredAnnotationTracing.notifyKeepInfoChanged(itemToChange);
+  }
+
   public KeepClassInfo getKeepInfo(DexProgramClass clazz) {
     return keepInfo.getClassInfo(clazz);
   }
@@ -1839,6 +1832,76 @@
     }
   }
 
+  void traceAnnotationFieldAccess(
+      DexField fieldReference, DexAnnotation annotation, ProgramDefinition context) {
+    recordFieldReference(fieldReference, context);
+    DexProgramClass holder = getProgramHolderOrNull(fieldReference, context);
+    if (holder == null) {
+      return;
+    }
+    ProgramField field = holder.lookupProgramField(fieldReference);
+    if (field == null) {
+      return;
+    }
+    // There is no dispatch on annotations, so only keep what is directly referenced.
+    if (field.getReference().isNotIdenticalTo(fieldReference)) {
+      return;
+    }
+    KeepReason reason = KeepReason.referencedInAnnotation(annotation, context);
+    if (field.getDefinition().isStatic()) {
+      FieldAccessInfoImpl fieldAccessInfo =
+          fieldAccessInfoCollection.contains(fieldReference)
+              ? fieldAccessInfoCollection.get(fieldReference)
+              : fieldAccessInfoCollection.extend(
+                  fieldReference, new FieldAccessInfoImpl(fieldReference));
+      fieldAccessInfo.setReadFromAnnotation();
+      markFieldAsLive(field, context, reason);
+      // In a class file an enum reference in an annotation is written as enum descriptor and
+      // enum name. At runtime the JVM use valueOf on the enum class with the name to get the
+      // instance. This indirectly use the values() method on that enum class. Also keep the
+      // name of the field and the name of the enum in sync as otherwise recovering the field to
+      // name relationship requires analysis of the enum <clinit> when this CF code is processed
+      // again (e.g. as input to D8 for converting to DEX). See b/236691999 for more info.
+      if (options.isGeneratingClassFiles() && field.getHolder().isEnum()) {
+        markEnumValuesAsReachable(field.getHolder(), reason);
+        applyMinimumKeepInfoWhenLive(field, KeepFieldInfo.newEmptyJoiner().disallowMinification());
+      }
+    } else {
+      // There is no dispatch on annotations, so only keep what is directly referenced.
+      worklist.enqueueMarkFieldAsReachableAction(field, context, reason);
+    }
+  }
+
+  void traceAnnotationMethodAccess(
+      DexMethod method, DexAnnotation annotation, ProgramDefinition context) {
+    // Record the references in case they are not program types.
+    recordMethodReference(method, context);
+    DexProgramClass holder = getProgramHolderOrNull(method, context);
+    if (holder == null) {
+      return;
+    }
+    KeepReason reason = KeepReason.referencedInAnnotation(annotation, context);
+    DexEncodedMethod target = holder.lookupDirectMethod(method);
+    if (target != null) {
+      // There is no dispatch on annotations, so only keep what is directly referenced.
+      if (target.getReference().isIdenticalTo(method)) {
+        markDirectStaticOrConstructorMethodAsLive(new ProgramMethod(holder, target), reason);
+      }
+    } else {
+      target = holder.lookupVirtualMethod(method);
+      // There is no dispatch on annotations, so only keep what is directly referenced.
+      if (target != null && target.getReference().isIdenticalTo(method)) {
+        markMethodAsTargeted(new ProgramMethod(holder, target), reason);
+      }
+    }
+  }
+
+  void traceAnnotationTypeAccess(
+      DexType type, DexAnnotation annotation, ProgramDefinition context) {
+    KeepReason reason = KeepReason.referencedInAnnotation(annotation, context);
+    markTypeAsLive(type, context, reason);
+  }
+
   void traceInstanceFieldRead(
       DexField fieldReference, ProgramMethod currentMethod, FieldAccessMetadata metadata) {
     traceInstanceFieldAccess(
@@ -1962,7 +2025,7 @@
           appView.withGeneratedExtensionRegistryShrinker(
               shrinker ->
                   shrinker.isDeadProtoExtensionField(
-                      resolutionResult, fieldAccessInfoCollection, keepInfo),
+                      resolutionResult, fieldAccessInfoCollection, getKeepInfo()),
               false);
       if (skipTracing) {
         addDeadProtoTypeCandidate(resolutionResult.getSingleProgramField().getHolder());
@@ -2221,13 +2284,13 @@
     applyMinimumKeepInfo(clazz);
     applyMinimumKeepInfoDependentOn(new LiveClassEnqueuerEvent(clazz));
     if (hasAlternativeLibraryDefinition(clazz)) {
-      getKeepInfo().keepClass(clazz);
+      mutateKeepInfo(clazz, MutableKeepInfoCollection::keepClass);
     }
 
     processAnnotations(clazz);
 
     if (clazz.isAnnotation()) {
-      liveAnnotations.add(clazz.getType());
+      deferredAnnotationTracing.notifyNewlyLiveAnnotation(clazz);
     }
 
     compatEnqueueHolderIfDependentNonStaticMember(
@@ -2236,11 +2299,12 @@
     analyses.processNewlyLiveClass(clazz, worklist);
   }
 
-  private void processOnClickMethods() {
+  private void processOnClickMethods(Timing timing) {
     if (onClickMethodReferences.isEmpty()) {
       return;
     }
-    for (DexProgramClass item : liveTypes.items) {
+    timing.begin("Process onclick methods");
+    for (DexProgramClass item : liveTypes.getItems()) {
       for (ProgramMethod method :
           item.virtualProgramMethods(
               p ->
@@ -2248,7 +2312,7 @@
                       && p.getParameter(0)
                           .isIdenticalTo(appInfo.dexItemFactory().androidViewViewType)
                       && onClickMethodReferences.containsKey(p.getName()))) {
-        KeepMethodInfo methodInfo = keepInfo.getMethodInfo(method);
+        KeepMethodInfo methodInfo = getKeepInfo().getMethodInfo(method);
         if (!methodInfo.isOptimizationAllowed(options)
             && !methodInfo.isShrinkingAllowed(options)
             && !methodInfo.isMinificationAllowed(options)) {
@@ -2265,30 +2329,7 @@
         applyMinimumKeepInfo(method, minimumKeepInfo);
       }
     }
-  }
-
-  private void processDeferredAnnotations(
-      Map<DexType, Map<DexAnnotation, List<ProgramDefinition>>> deferredAnnotations,
-      Function<ProgramDefinition, AnnotatedKind> kindProvider) {
-    // Collect annotations to process as processing the annotation can modify liveAnnotations.
-    Set<Map<DexAnnotation, List<ProgramDefinition>>> toProcess = Sets.newIdentityHashSet();
-    for (DexType annotationType : liveAnnotations) {
-      Map<DexAnnotation, List<ProgramDefinition>> annotations =
-          deferredAnnotations.remove(annotationType);
-      if (annotations != null) {
-        assert annotations.keySet().stream()
-            .allMatch(annotation -> annotationType.isIdenticalTo(annotation.getAnnotationType()));
-        toProcess.add(annotations);
-      }
-    }
-    toProcess.forEach(
-        annotations ->
-            annotations.forEach(
-                (annotation, annotatedItems) ->
-                    annotatedItems.forEach(
-                        annotatedItem ->
-                            processAnnotation(
-                                annotatedItem, annotation, kindProvider.apply(annotatedItem)))));
+    timing.end();
   }
 
   private void ensureMethodsContinueToWidenAccess(ClassDefinition clazz) {
@@ -2412,25 +2453,36 @@
   void processAnnotation(
       ProgramDefinition annotatedItem, DexAnnotation annotation, AnnotatedKind kind) {
     DexType type = annotation.getAnnotationType();
-    DexClass clazz = definitionFor(type, annotatedItem);
-    boolean annotationTypeIsNotProgramClass = clazz == null || clazz.isNotProgramClass();
-    boolean isLive = annotationTypeIsNotProgramClass || liveTypes.contains(clazz.asProgramClass());
-    if (!shouldKeepAnnotation(annotatedItem, annotation, kind, isLive)) {
-      // Remember this annotation for later.
-      Map<DexType, Map<DexAnnotation, List<ProgramDefinition>>> deferredAnnotations =
-          kind.isParameter() ? deferredParameterAnnotations : this.deferredAnnotations;
-      Map<DexAnnotation, List<ProgramDefinition>> deferredAnnotationsForAnnotationType =
-          deferredAnnotations.computeIfAbsent(type, ignore -> new IdentityHashMap<>());
-      deferredAnnotationsForAnnotationType
-          .computeIfAbsent(annotation, ignore -> new ArrayList<>())
-          .add(annotatedItem);
-      // Also, non-program annotations should be considered live w.r.t the deferred processing.
-      if (annotationTypeIsNotProgramClass) {
-        liveAnnotations.add(type);
-      }
+    DexClass annotationClass = definitionFor(type, annotatedItem);
+    processAnnotation(annotatedItem, annotation, kind, annotationClass);
+  }
+
+  void processAnnotation(
+      ProgramDefinition annotatedItem,
+      DexAnnotation annotation,
+      AnnotatedKind kind,
+      DexClass annotationClass) {
+    boolean annotationTypeIsNotProgramClass =
+        annotationClass == null || annotationClass.isNotProgramClass();
+    boolean isLive =
+        annotationTypeIsNotProgramClass || liveTypes.contains(annotationClass.asProgramClass());
+    if (shouldKeepAnnotation(annotatedItem, annotation, kind, isLive)) {
+      traceAnnotation(annotatedItem, annotation);
       return;
     }
+    if (isLive) {
+      // Enqueue for reprocessing when the keep info of the annotated item changes.
+      deferredAnnotationTracing.enqueueAnnotationForProcessingWhenKeepInfoChanges(
+          annotatedItem, annotation, annotationClass, kind);
+    } else {
+      // Enqueue for reprocessing when the annotation type becomes live.
+      assert annotationClass.isProgramClass();
+      deferredAnnotationTracing.enqueueAnnotationForProcessingWhenAnnotationTypeBecomesLive(
+          annotatedItem, annotation, annotationClass.asProgramClass(), kind);
+    }
+  }
 
+  private void traceAnnotation(ProgramDefinition annotatedItem, DexAnnotation annotation) {
     // Report that the annotation is retained due to the annotated item.
     graphReporter.registerAnnotation(annotation, annotatedItem);
 
@@ -2438,7 +2490,7 @@
     // annotation.
     AnnotationReferenceMarker referenceMarker =
         new AnnotationReferenceMarker(annotation, annotatedItem);
-    annotation.annotation.collectIndexedItems(appView, referenceMarker);
+    annotation.collectIndexedItems(appView, referenceMarker);
   }
 
   private boolean shouldKeepAnnotation(
@@ -2451,7 +2503,7 @@
       assert mode.isInitialTreeShaking();
       return true;
     }
-    KeepInfo<?, ?> itemKeepInfo = keepInfo.getInfo(annotatedItem);
+    KeepInfo<?, ?> itemKeepInfo = getKeepInfo().getInfo(annotatedItem);
     return AnnotationRemover.shouldKeepAnnotation(
         appView, annotatedItem, annotation, isLive, annotatedKind, mode, itemKeepInfo);
   }
@@ -2761,17 +2813,17 @@
   private void keepClassAndAllMembers(DexProgramClass clazz, KeepReason keepReason) {
     KeepReasonWitness keepReasonWitness = graphReporter.registerClass(clazz, keepReason);
     markClassAsInstantiatedWithCompatRule(clazz.asProgramClass(), () -> keepReasonWitness);
-    keepInfo.keepClass(clazz);
+    mutateKeepInfo(clazz, MutableKeepInfoCollection::keepClass);
     shouldNotBeMinified(clazz);
     clazz.forEachProgramField(
         field -> {
-          keepInfo.keepField(field);
+          mutateKeepInfo(field, MutableKeepInfoCollection::keepField);
           shouldNotBeMinified(field);
           markFieldAsKept(field, keepReasonWitness);
         });
     clazz.forEachProgramMethod(
         method -> {
-          keepInfo.keepMethod(method);
+          mutateKeepInfo(method, MutableKeepInfoCollection::keepMethod);
           shouldNotBeMinified(method);
           markMethodAsKept(method, keepReasonWitness);
         });
@@ -3039,7 +3091,8 @@
                                     context,
                                     appView,
                                     instantiation,
-                                    definition -> keepInfo.isPinned(definition, options, appInfo));
+                                    definition ->
+                                        getKeepInfo().isPinned(definition, options, appInfo));
                             if (lookupResult.isLookupResultSuccess()) {
                               lookupResultSuccess = lookupResult.asLookupResultSuccess();
                               break;
@@ -3306,7 +3359,7 @@
     // Update keep info.
     applyMinimumKeepInfo(field);
     if (hasAlternativeLibraryDefinition(field.getHolder()) && !field.getDefinition().isPrivate()) {
-      getKeepInfo().keepField(field);
+      mutateKeepInfo(field, MutableKeepInfoCollection::keepField);
     }
 
     // Notify analyses.
@@ -3600,7 +3653,7 @@
                   (type, subTypeConsumer, lambdaConsumer) ->
                       objectAllocationInfoCollection.forEachInstantiatedSubType(
                           type, subTypeConsumer, lambdaConsumer, appInfo),
-                  definition -> keepInfo.isPinned(definition, options, appInfo));
+                  definition -> getKeepInfo().isPinned(definition, options, appInfo));
           handleVirtualDispatchTargets(lookupResult, resolution);
         });
     return resolutionResults;
@@ -3711,9 +3764,13 @@
       // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the
       // marking for not renaming it is in the root set.
       worklist.enqueueMarkMethodKeptAction(valuesMethod, reason);
-      keepInfo.joinMethod(
+      mutateKeepInfo(
           valuesMethod,
-          joiner -> joiner.disallowMinification().disallowOptimization().disallowShrinking());
+          (k, m) ->
+              k.joinMethod(
+                  m,
+                  joiner ->
+                      joiner.disallowMinification().disallowOptimization().disallowShrinking()));
       shouldNotBeMinified(valuesMethod);
     }
   }
@@ -3905,7 +3962,7 @@
     KeepClassInfo.Joiner minimumKeepInfoForClass =
         dependentMinimumKeepInfo.remove(preconditionEvent, clazz.getType());
     if (minimumKeepInfoForClass != null) {
-      keepInfo.joinClass(clazz, info -> info.merge(minimumKeepInfoForClass));
+      mutateKeepInfo(clazz, (k, c) -> k.joinClass(c, info -> info.merge(minimumKeepInfoForClass)));
       enqueueClassIfShrinkingIsDisallowed(clazz, preconditionEvent, minimumKeepInfoForClass);
     }
   }
@@ -3920,7 +3977,7 @@
       KeepClassInfo.Joiner minimumKeepInfo,
       EnqueuerEvent preconditionEvent) {
     if (liveTypes.contains(clazz)) {
-      keepInfo.joinClass(clazz, info -> info.merge(minimumKeepInfo));
+      mutateKeepInfo(clazz, (k, c) -> k.joinClass(c, info -> info.merge(minimumKeepInfo)));
     } else {
       dependentMinimumKeepInfo
           .getOrCreateUnconditionalMinimumKeepInfo()
@@ -3961,7 +4018,7 @@
     KeepFieldInfo.Joiner minimumKeepInfoForField =
         dependentMinimumKeepInfo.remove(preconditionEvent, field.getReference());
     if (minimumKeepInfoForField != null) {
-      keepInfo.joinField(field, info -> info.merge(minimumKeepInfoForField));
+      mutateKeepInfo(field, (k, f) -> k.joinField(f, info -> info.merge(minimumKeepInfoForField)));
       enqueueFieldIfShrinkingIsDisallowed(field, preconditionEvent, minimumKeepInfoForField);
     }
   }
@@ -3974,7 +4031,7 @@
   private void applyMinimumKeepInfoWhenLive(
       ProgramField field, KeepFieldInfo.Joiner minimumKeepInfo, EnqueuerEvent preconditionEvent) {
     if (liveFields.contains(field)) {
-      keepInfo.joinField(field, info -> info.merge(minimumKeepInfo));
+      mutateKeepInfo(field, (k, f) -> k.joinField(f, info -> info.merge(minimumKeepInfo)));
     } else {
       dependentMinimumKeepInfo
           .getOrCreateUnconditionalMinimumKeepInfo()
@@ -4011,13 +4068,14 @@
     KeepMethodInfo.Joiner minimumKeepInfoForMethod =
         dependentMinimumKeepInfo.remove(preconditionEvent, method.getReference());
     if (minimumKeepInfoForMethod != null) {
-      keepInfo.joinMethod(method, info -> info.merge(minimumKeepInfoForMethod));
+      mutateKeepInfo(
+          method, (k, m) -> k.joinMethod(m, info -> info.merge(minimumKeepInfoForMethod)));
       enqueueMethodIfShrinkingIsDisallowed(method, preconditionEvent, minimumKeepInfoForMethod);
     }
   }
 
   private void applyMinimumKeepInfo(ProgramMethod method, KeepMethodInfo.Joiner minimumKeepInfo) {
-    keepInfo.joinMethod(method, info -> info.merge(minimumKeepInfo));
+    mutateKeepInfo(method, (k, m) -> k.joinMethod(m, info -> info.merge(minimumKeepInfo)));
     enqueueMethodIfShrinkingIsDisallowed(method, UnconditionalKeepInfoEvent.get(), minimumKeepInfo);
   }
 
@@ -4031,7 +4089,7 @@
       KeepMethodInfo.Joiner minimumKeepInfo,
       EnqueuerEvent preconditionEvent) {
     if (liveMethods.contains(method) || targetedMethods.contains(method)) {
-      keepInfo.joinMethod(method, info -> info.merge(minimumKeepInfo));
+      mutateKeepInfo(method, (k, m) -> k.joinMethod(m, info -> info.merge(minimumKeepInfo)));
     } else {
       dependentMinimumKeepInfo
           .getOrCreateUnconditionalMinimumKeepInfo()
@@ -4606,7 +4664,7 @@
             fieldAccessInfoCollection,
             objectAllocationInfoCollection.build(appInfo),
             callSites,
-            keepInfo,
+            getKeepInfo(),
             rootSet.mayHaveSideEffects,
             amendWithCompanionMethods(rootSet.alwaysInline),
             amendWithCompanionMethods(rootSet.whyAreYouNotInlining),
@@ -4640,14 +4698,17 @@
         (methodReference, companionReference) -> {
           ProgramMethod companion = appView.definitionFor(companionReference).asProgramMethod();
           KeepMethodInfo.Joiner minimumKeepInfoForCompanion =
-              keepInfo.getMethodInfoWithDefinitionLookup(methodReference, appInfo).joiner();
+              getKeepInfo().getMethodInfoWithDefinitionLookup(methodReference, appInfo).joiner();
           KeepMethodInfo.Joiner extraMinimumKeepInfoForCompanion =
               dependentMinimumKeepInfo
                   .getUnconditionalMinimumKeepInfoOrDefault(MinimumKeepInfoCollection.empty())
                   .getOrDefault(methodReference, KeepMethodInfo.newEmptyJoiner())
                   .asMethodJoiner();
-          keepInfo.evaluateMethodRule(
-              companion, minimumKeepInfoForCompanion.merge(extraMinimumKeepInfoForCompanion));
+          mutateKeepInfo(
+              companion,
+              (k, m) ->
+                  k.evaluateMethodRule(
+                      m, minimumKeepInfoForCompanion.merge(extraMinimumKeepInfoForCompanion)));
         });
   }
 
@@ -4776,16 +4837,7 @@
           }
         }
 
-        // Process all deferred annotations.
-        timing.begin("Process deferred annotations");
-        processDeferredAnnotations(deferredAnnotations, AnnotatedKind::from);
-        processDeferredAnnotations(
-            deferredParameterAnnotations, annotatedItem -> AnnotatedKind.PARAMETER);
-        timing.end();
-
-        timing.begin("Process onclick methods");
-        processOnClickMethods();
-        timing.end();
+        processOnClickMethods(timing);
         if (worklist.hasNext()) {
           timing.end();
           continue;
@@ -4887,7 +4939,7 @@
             liveMethods::contains, interfaceProcessor);
     CfPostProcessingDesugaringCollection.createForR8CfToCfDesugaring(
             appView, interfaceDesugaring, this)
-        .postProcessingDesugaring(liveTypes.items, eventConsumer, executorService, timing);
+        .postProcessingDesugaring(liveTypes.getItems(), eventConsumer, executorService, timing);
 
     if (syntheticAdditions.isEmpty() && !options.testing.forceEnqueuerFullProcessingDesugaring) {
       return;
@@ -5163,7 +5215,7 @@
     applyMinimumKeepInfo(method);
     if (hasAlternativeLibraryDefinition(method.getHolder())
         && !method.getDefinition().isPrivateMethod()) {
-      getKeepInfo().keepMethod(method);
+      mutateKeepInfo(method, MutableKeepInfoCollection::keepMethod);
     }
   }
 
@@ -5262,34 +5314,39 @@
         method, method, graphReporter.reportCompatKeepMethod(method));
   }
 
-  private static class SetWithReportedReason<T> {
+  private static class SetWithReportedReason<T extends DexDefinition> {
 
-    private final Set<T> items = Sets.newIdentityHashSet();
+    private final Map<DexReference, T> items = new IdentityHashMap<>();
     private final Map<T, List<Action>> deferredActions = new IdentityHashMap<>();
 
     boolean add(T item, KeepReasonWitness witness) {
       assert witness != null;
-      if (items.add(item)) {
+      T existing = items.put(item.getReference(), item);
+      if (existing == null) {
         deferredActions.getOrDefault(item, Collections.emptyList()).forEach(Action::execute);
         return true;
       }
       return false;
     }
 
+    boolean contains(DexReference reference) {
+      return items.containsKey(reference);
+    }
+
     boolean contains(T item) {
-      return items.contains(item);
+      return contains(item.getReference());
     }
 
     boolean registerDeferredAction(T item, Action action) {
-      if (!items.contains(item)) {
+      if (!contains(item)) {
         deferredActions.computeIfAbsent(item, ignore -> new ArrayList<>()).add(action);
         return true;
       }
       return false;
     }
 
-    Set<T> getItems() {
-      return SetUtils.unmodifiableForTesting(items);
+    Collection<T> getItems() {
+      return CollectionUtils.unmodifiableForTesting(items.values());
     }
   }
 
@@ -5351,12 +5408,37 @@
 
   private class AnnotationReferenceMarker implements IndexedItemCollection {
 
+    private final DexAnnotation annotation;
     private final ProgramDefinition context;
-    private final KeepReason reason;
 
     private AnnotationReferenceMarker(DexAnnotation annotation, ProgramDefinition context) {
+      this.annotation = annotation;
       this.context = context;
-      this.reason = KeepReason.referencedInAnnotation(annotation, context);
+    }
+
+    @Override
+    public boolean addField(DexField field) {
+      worklist.enqueueTraceFieldAccessFromAnnotationAction(field, annotation, context);
+      return false;
+    }
+
+    @Override
+    public boolean addMethod(DexMethod method) {
+      worklist.enqueueTraceMethodAccessFromAnnotationAction(method, annotation, context);
+      return false;
+    }
+
+    @Override
+    public boolean addType(DexType type) {
+      DexType baseType = type.toBaseType(appView.dexItemFactory());
+      if (!baseType.isClassType()) {
+        return false;
+      }
+      if (!graphReporter.hasConsumer() && liveTypes.contains(baseType)) {
+        return false;
+      }
+      worklist.enqueueTraceTypeAccessFromAnnotationAction(baseType, annotation, context);
+      return false;
     }
 
     @Override
@@ -5365,73 +5447,6 @@
     }
 
     @Override
-    @SuppressWarnings("ReferenceEquality")
-    public boolean addField(DexField fieldReference) {
-      recordFieldReference(fieldReference, context);
-      DexProgramClass holder = getProgramHolderOrNull(fieldReference, context);
-      if (holder == null) {
-        return false;
-      }
-      ProgramField field = holder.lookupProgramField(fieldReference);
-      if (field == null) {
-        return false;
-      }
-      // There is no dispatch on annotations, so only keep what is directly referenced.
-      if (field.getReference() != fieldReference) {
-        return false;
-      }
-      if (field.getDefinition().isStatic()) {
-        FieldAccessInfoImpl fieldAccessInfo =
-            fieldAccessInfoCollection.contains(fieldReference)
-                ? fieldAccessInfoCollection.get(fieldReference)
-                : fieldAccessInfoCollection.extend(
-                    fieldReference, new FieldAccessInfoImpl(fieldReference));
-        fieldAccessInfo.setReadFromAnnotation();
-        markFieldAsLive(field, context, reason);
-        // In a class file an enum reference in an annotation is written as enum descriptor and
-        // enum name. At runtime the JVM use valueOf on the enum class with the name to get the
-        // instance. This indirectly use the values() method on that enum class. Also keep the
-        // name of the field and the name of the enum in sync as otherwise recovering the field to
-        // name relationship requires analysis of the enum <clinit> when this CF code is processed
-        // again (e.g. as input to D8 for converting to DEX). See b/236691999 for more info.
-        if (options.isGeneratingClassFiles() && field.getHolder().isEnum()) {
-          markEnumValuesAsReachable(field.getHolder(), reason);
-          applyMinimumKeepInfoWhenLive(
-              field, KeepFieldInfo.newEmptyJoiner().disallowMinification());
-        }
-      } else {
-        // There is no dispatch on annotations, so only keep what is directly referenced.
-        worklist.enqueueMarkFieldAsReachableAction(field, context, reason);
-      }
-      return false;
-    }
-
-    @Override
-    @SuppressWarnings("ReferenceEquality")
-    public boolean addMethod(DexMethod method) {
-      // Record the references in case they are not program types.
-      recordMethodReference(method, context);
-      DexProgramClass holder = getProgramHolderOrNull(method, context);
-      if (holder == null) {
-        return false;
-      }
-      DexEncodedMethod target = holder.lookupDirectMethod(method);
-      if (target != null) {
-        // There is no dispatch on annotations, so only keep what is directly referenced.
-        if (target.getReference() == method) {
-          markDirectStaticOrConstructorMethodAsLive(new ProgramMethod(holder, target), reason);
-        }
-      } else {
-        target = holder.lookupVirtualMethod(method);
-        // There is no dispatch on annotations, so only keep what is directly referenced.
-        if (target != null && target.getReference() == method) {
-          markMethodAsTargeted(new ProgramMethod(holder, target), reason);
-        }
-      }
-      return false;
-    }
-
-    @Override
     public boolean addString(DexString string) {
       return false;
     }
@@ -5450,12 +5465,6 @@
     public boolean addMethodHandle(DexMethodHandle methodHandle) {
       return false;
     }
-
-    @Override
-    public boolean addType(DexType type) {
-      markTypeAsLive(type, context, reason);
-      return false;
-    }
   }
 
   public static class EnqueuerDefinitionSupplier {
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredAnnotationTracing.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredAnnotationTracing.java
new file mode 100644
index 0000000..aafe94b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredAnnotationTracing.java
@@ -0,0 +1,137 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotation.AnnotatedKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexDefinition;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class EnqueuerDeferredAnnotationTracing {
+
+  private final Enqueuer enqueuer;
+
+  /**
+   * A map from annotation classes to annotations that need to be processed should the classes ever
+   * become live.
+   *
+   * <p>Concurrent instance since the concurrent processing of deferred annotations may update this.
+   */
+  private final Map<DexProgramClass, Set<DeferredAnnotation>> deferredAnnotationsByAnnotationClass =
+      new IdentityHashMap<>();
+
+  /**
+   * A map from annotated items to the annotations that need to be processed if/when the keep info
+   * of the annotated item changes.
+   *
+   * <p>Concurrent instance since the concurrent processing of deferred annotations may update this.
+   */
+  private final Map<DexDefinition, Set<DeferredAnnotation>> deferredAnnotationsByAnnotatedItem =
+      new IdentityHashMap<>();
+
+  EnqueuerDeferredAnnotationTracing(Enqueuer enqueuer) {
+    this.enqueuer = enqueuer;
+  }
+
+  public void enqueueAnnotationForProcessingWhenAnnotationTypeBecomesLive(
+      ProgramDefinition annotatedItem,
+      DexAnnotation annotation,
+      DexProgramClass annotationClass,
+      AnnotatedKind kind) {
+    assert annotationClass.isAnnotation();
+    deferredAnnotationsByAnnotationClass
+        .computeIfAbsent(annotationClass, ignoreKey(HashSet::new))
+        .add(new DeferredAnnotation(annotatedItem, annotation, annotationClass, kind));
+  }
+
+  public void enqueueAnnotationForProcessingWhenKeepInfoChanges(
+      ProgramDefinition annotatedItem,
+      DexAnnotation annotation,
+      DexClass annotationClass,
+      AnnotatedKind kind) {
+    deferredAnnotationsByAnnotatedItem
+        .computeIfAbsent(annotatedItem.getDefinition(), ignoreKey(Sets::newHashSet))
+        .add(new DeferredAnnotation(annotatedItem, annotation, annotationClass, kind));
+  }
+
+  // Called by the Enqueuer when an item has its keep info changed.
+  public void notifyKeepInfoChanged(ProgramDefinition definition) {
+    processDeferredAnnotations(definition.getDefinition(), deferredAnnotationsByAnnotatedItem);
+  }
+
+  // Called by the Enqueuer when an annotation class becomes live.
+  public void notifyNewlyLiveAnnotation(DexProgramClass newlyLiveAnnotationClass) {
+    assert newlyLiveAnnotationClass.isAnnotation();
+    processDeferredAnnotations(newlyLiveAnnotationClass, deferredAnnotationsByAnnotationClass);
+  }
+
+  private <T> void processDeferredAnnotations(
+      T key, Map<T, Set<DeferredAnnotation>> deferredAnnotations) {
+    Set<DeferredAnnotation> annotations =
+        MapUtils.removeOrDefault(deferredAnnotations, key, Collections.emptySet());
+    for (var annotation : annotations) {
+      enqueuer.processAnnotation(
+          annotation.annotatedItem,
+          annotation.annotation,
+          annotation.kind,
+          annotation.annotationClass);
+    }
+  }
+
+  private static class DeferredAnnotation {
+
+    private final ProgramDefinition annotatedItem;
+    private final DexAnnotation annotation;
+    private final DexClass annotationClass;
+    private final AnnotatedKind kind;
+
+    DeferredAnnotation(
+        ProgramDefinition annotatedItem,
+        DexAnnotation annotation,
+        DexClass annotationClass,
+        AnnotatedKind kind) {
+      this.annotatedItem = annotatedItem;
+      this.annotation = annotation;
+      this.annotationClass = annotationClass;
+      this.kind = kind;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (!(obj instanceof DeferredAnnotation)) {
+        return false;
+      }
+      DeferredAnnotation other = (DeferredAnnotation) obj;
+      if (ObjectUtils.identical(annotation, other.annotation)
+          && annotatedItem.getDefinition() == other.annotatedItem.getDefinition()) {
+        assert annotatedItem.getDefinition() == other.annotatedItem.getDefinition();
+        assert annotationClass == other.annotationClass;
+        assert kind == other.kind;
+        return true;
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return (31 * (31 + System.identityHashCode(annotation)))
+          + annotatedItem.getDefinition().hashCode();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
index 4511156..792ad59 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
@@ -181,16 +181,17 @@
 
   private void ensureClassIsMockable(DexProgramClass programClass) {
     // Ensures the type is not made final so that it can still be subclassed.
-    enqueuer.getKeepInfo().joinClass(programClass, Joiner::disallowOptimization);
+    enqueuer.mutateKeepInfo(programClass, (k, c) -> k.joinClass(c, Joiner::disallowOptimization));
 
     // disallowOptimization --> prevent method from being marked final.
     // allowCodeReplacement --> do not inline or optimize based on method body.
     programClass.forEachProgramVirtualMethodMatching(
         enqueuer::isMethodLive,
         virtualMethod ->
-            enqueuer.getKeepInfo()
-                .joinMethod(
-                    virtualMethod,
-                    joiner -> joiner.disallowOptimization().allowCodeReplacement()));
+            enqueuer.mutateKeepInfo(
+                virtualMethod,
+                (k, m) ->
+                    k.joinMethod(
+                        m, joiner -> joiner.disallowOptimization().allowCodeReplacement())));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerWorklist.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerWorklist.java
index 5ff3b75..8b76da8 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerWorklist.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerWorklist.java
@@ -64,6 +64,19 @@
     }
   }
 
+  static class ConditionalRuleConsequencesAction extends EnqueuerAction {
+    private final MinimumKeepInfoCollection consequences;
+
+    ConditionalRuleConsequencesAction(MinimumKeepInfoCollection consequences) {
+      this.consequences = consequences;
+    }
+
+    @Override
+    public void run(Enqueuer enqueuer) {
+      enqueuer.includeMinimumKeepInfo(consequences);
+    }
+  }
+
   static class MarkReachableDirectAction extends EnqueuerAction {
     private final DexMethod target;
     // TODO(b/175854431): Avoid pushing context on worklist.
@@ -248,6 +261,60 @@
     }
   }
 
+  static class TraceFieldAccessFromAnnotationAction extends EnqueuerAction {
+    private final DexField field;
+    private final DexAnnotation annotation;
+    private final ProgramDefinition context;
+
+    TraceFieldAccessFromAnnotationAction(
+        DexField field, DexAnnotation annotation, ProgramDefinition context) {
+      this.field = field;
+      this.annotation = annotation;
+      this.context = context;
+    }
+
+    @Override
+    public void run(Enqueuer enqueuer) {
+      enqueuer.traceAnnotationFieldAccess(field, annotation, context);
+    }
+  }
+
+  static class TraceMethodAccessFromAnnotationAction extends EnqueuerAction {
+    private final DexMethod method;
+    private final DexAnnotation annotation;
+    private final ProgramDefinition context;
+
+    TraceMethodAccessFromAnnotationAction(
+        DexMethod method, DexAnnotation annotation, ProgramDefinition context) {
+      this.method = method;
+      this.annotation = annotation;
+      this.context = context;
+    }
+
+    @Override
+    public void run(Enqueuer enqueuer) {
+      enqueuer.traceAnnotationMethodAccess(method, annotation, context);
+    }
+  }
+
+  static class TraceTypeAccessFromAnnotationAction extends EnqueuerAction {
+    private final DexType type;
+    private final DexAnnotation annotation;
+    private final ProgramDefinition context;
+
+    TraceTypeAccessFromAnnotationAction(
+        DexType type, DexAnnotation annotation, ProgramDefinition context) {
+      this.type = type;
+      this.annotation = annotation;
+      this.context = context;
+    }
+
+    @Override
+    public void run(Enqueuer enqueuer) {
+      enqueuer.traceAnnotationTypeAccess(type, annotation, context);
+    }
+  }
+
   static class TraceCodeAction extends EnqueuerAction {
     private final ProgramMethod method;
 
@@ -636,6 +703,11 @@
 
   abstract boolean enqueueAssertAction(Action assertion);
 
+  public final void enqueueConditionalRuleConsequencesAction(
+      MinimumKeepInfoCollection consequences) {
+    enqueue(new ConditionalRuleConsequencesAction(consequences));
+  }
+
   abstract void enqueueFuture(Action action);
 
   abstract void enqueueMarkReachableDirectAction(
@@ -668,6 +740,15 @@
   abstract void enqueueTraceAnnotationAction(
       ProgramDefinition annotatedItem, DexAnnotation annotation, AnnotatedKind annotatedKind);
 
+  abstract void enqueueTraceFieldAccessFromAnnotationAction(
+      DexField field, DexAnnotation annotation, ProgramDefinition context);
+
+  abstract void enqueueTraceMethodAccessFromAnnotationAction(
+      DexMethod method, DexAnnotation annotation, ProgramDefinition context);
+
+  abstract void enqueueTraceTypeAccessFromAnnotationAction(
+      DexType type, DexAnnotation annotation, ProgramDefinition context);
+
   public abstract void enqueueTraceCodeAction(ProgramMethod method);
 
   public abstract void enqueueTraceConstClassAction(
@@ -810,6 +891,24 @@
     }
 
     @Override
+    void enqueueTraceFieldAccessFromAnnotationAction(
+        DexField field, DexAnnotation annotation, ProgramDefinition context) {
+      queue.add(new TraceFieldAccessFromAnnotationAction(field, annotation, context));
+    }
+
+    @Override
+    void enqueueTraceMethodAccessFromAnnotationAction(
+        DexMethod method, DexAnnotation annotation, ProgramDefinition context) {
+      queue.add(new TraceMethodAccessFromAnnotationAction(method, annotation, context));
+    }
+
+    @Override
+    void enqueueTraceTypeAccessFromAnnotationAction(
+        DexType type, DexAnnotation annotation, ProgramDefinition context) {
+      queue.add(new TraceTypeAccessFromAnnotationAction(type, annotation, context));
+    }
+
+    @Override
     public void enqueueTraceCodeAction(ProgramMethod method) {
       queue.add(new TraceCodeAction(method));
     }
@@ -983,6 +1082,24 @@
     }
 
     @Override
+    void enqueueTraceFieldAccessFromAnnotationAction(
+        DexField field, DexAnnotation annotation, ProgramDefinition context) {
+      throw attemptToEnqueue("TraceFieldAccessFromAnnotationAction " + field);
+    }
+
+    @Override
+    void enqueueTraceMethodAccessFromAnnotationAction(
+        DexMethod method, DexAnnotation annotation, ProgramDefinition context) {
+      throw attemptToEnqueue("TraceMethodAccessFromAnnotationAction " + method);
+    }
+
+    @Override
+    void enqueueTraceTypeAccessFromAnnotationAction(
+        DexType type, DexAnnotation annotation, ProgramDefinition context) {
+      throw attemptToEnqueue("TraceTypeAccessFromAnnotationAction " + type);
+    }
+
+    @Override
     public void enqueueTraceCodeAction(ProgramMethod method) {
       throw attemptToEnqueue("TraceCodeAction " + method);
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
index d261cf3..27ffcc0 100644
--- a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
+++ b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
@@ -75,6 +75,10 @@
     return KeepReasonWitness.INSTANCE;
   }
 
+  public boolean hasConsumer() {
+    return keptGraphConsumer != null;
+  }
+
   public boolean verifyRootedPath(DexProgramClass liveType) {
     assert verificationGraphConsumer != null;
     ClassGraphNode node = getClassGraphNode(liveType.type);
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
index 889c466..7df2ab5 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
@@ -351,7 +351,9 @@
 
     public Consumer<DexType> addDependencyAllowSyntheticRoot(SyntheticItems syntheticItems) {
       return type -> {
-        assert !roots.contains(type) || syntheticItems.isCommittedSynthetic(type);
+        assert !roots.contains(type)
+            || syntheticItems.isCommittedSynthetic(type)
+            || syntheticItems.isFinalizedSynthetic(type);
         addDependencyIfNotRoot(type);
       };
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 0da300d..622d966 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.errors.InlinableStaticFinalFieldPreconditionDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.errors.UnusedProguardKeepRuleDiagnostic;
+import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
@@ -43,6 +44,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
+import com.android.tools.r8.graph.InvalidCode;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
@@ -855,7 +857,7 @@
         }
         ProgramMethod resolutionMethod = resolutionResult.getResolvedProgramMethod();
         ProgramMethod methodToKeep;
-        if (canInsertForwardingMethod(originalClazz, resolutionMethod.getDefinition())) {
+        if (canInsertForwardingMethod(originalClazz, resolutionMethod)) {
           DexMethod methodToKeepReference =
               resolutionMethod.getReference().withHolder(originalClazz, appView.dexItemFactory());
           methodToKeep =
@@ -885,9 +887,14 @@
       }
     }
 
-    private boolean canInsertForwardingMethod(DexClass holder, DexEncodedMethod target) {
+    private boolean canInsertForwardingMethod(DexProgramClass holder, ProgramMethod method) {
+      DexProgramClass resolvedHolder = method.getHolder();
+      if (AccessControl.isMemberAccessible(method, resolvedHolder, holder, appView)
+          .isPossiblyFalse()) {
+        return false;
+      }
       return appView.options().isGeneratingDex()
-          || ArrayUtils.contains(holder.interfaces.values, target.getHolderType());
+          || ArrayUtils.contains(holder.interfaces.values, resolvedHolder.getType());
     }
 
     private void markMatchingOverriddenMethods(
@@ -1877,7 +1884,9 @@
             interfaceDesugaringSyntheticHelper.ensureMethodOfProgramCompanionClassStub(
                 method, eventConsumer);
         // Add the method to the inverse map as tracing will now directly target the CC method.
-        pendingMethodMoveInverse.put(companion, method);
+        if (InvalidCode.isInvalidCode(companion.getDefinition().getCode())) {
+          pendingMethodMoveInverse.put(companion, method);
+        }
 
         LazyBox<Joiner<?, ?, ?>> companionJoiner =
             new LazyBox<>(
diff --git a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
index 20aa5be..0941d2a 100644
--- a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
@@ -58,9 +58,9 @@
       // To ensure we are not moving the class because we cannot prune it when there is a reflective
       // use of it.
       if (enqueuer.getKeepInfo().getClassInfo(programClass).isShrinkingAllowed(options)) {
-        enqueuer
-            .getKeepInfo()
-            .joinClass(programClass, joiner -> joiner.disallowOptimization().disallowShrinking());
+        enqueuer.mutateKeepInfo(
+            programClass,
+            (k, c) -> k.joinClass(c, joiner -> joiner.disallowOptimization().disallowShrinking()));
       }
     } else {
       enqueuer.recordNonProgramClassWithNoMissingReporting(clazz, context);
@@ -122,19 +122,19 @@
 
       // Keep this interface to ensure that we do not merge the interface into its unique subtype,
       // or merge other interfaces into it horizontally.
-      enqueuer
-          .getKeepInfo()
-          .joinClass(clazz, joiner -> joiner.disallowOptimization().disallowShrinking());
+      enqueuer.mutateKeepInfo(
+          clazz,
+          (k, c) -> k.joinClass(c, joiner -> joiner.disallowOptimization().disallowShrinking()));
 
       // Also keep all of its virtual methods to ensure that the devirtualizer does not perform
       // illegal rewritings of invoke-interface instructions into invoke-virtual instructions.
       if (enqueuer.getMode().isInitialTreeShaking()) {
         clazz.forEachProgramVirtualMethod(
             virtualMethod -> {
-              enqueuer
-                  .getKeepInfo()
-                  .joinMethod(
-                      virtualMethod, joiner -> joiner.disallowOptimization().disallowShrinking());
+              enqueuer.mutateKeepInfo(
+                  virtualMethod,
+                  (k, m) ->
+                      k.joinMethod(m, joiner -> joiner.disallowOptimization().disallowShrinking()));
               enqueuer.markVirtualMethodAsReachable(
                   virtualMethod.getReference(), true, context, reason);
             });
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java
index d8b3d66..3242fd3 100644
--- a/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java
+++ b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java
@@ -58,16 +58,23 @@
     }
     // TODO(b/323816623): If we tracked newly live, we could speed up finding rules.
     // TODO(b/323816623): Parallelize this.
+    MinimumKeepInfoCollection consequences = null;
     for (int i = 0; i < pendingConditionalRules.size(); i++) {
       R rule = pendingConditionalRules.get(i);
       if (rule != null && rule.isSatisfiedAfterUpdate(enqueuer)) {
         ++prunedCount;
         pendingConditionalRules.set(i, null);
-        enqueuer.includeMinimumKeepInfo(rule.getConsequences());
         materializedRules.add(rule.asMaterialized());
         onSatisfiedRuleCallback.accept(rule, enqueuer);
+        if (consequences == null) {
+          consequences = MinimumKeepInfoCollection.create();
+        }
+        consequences.merge(rule.getConsequences());
       }
     }
+    if (consequences != null) {
+      enqueuer.getWorklist().enqueueConditionalRuleConsequencesAction(consequences);
+    }
 
     if (prunedCount == pendingConditionalRules.size()) {
       assert Iterables.all(pendingConditionalRules, Objects::isNull);
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
index ea5a020..5ce9d49 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
@@ -29,6 +29,7 @@
   final DexApplication application;
   final SyntheticItems.State state;
   final CommittedSyntheticsCollection committed;
+  final CommittedSyntheticsCollection finalized;
   final ImmutableList<DexType> committedProgramTypes;
   final GlobalSyntheticsStrategy globalSyntheticsStrategy;
 
@@ -36,11 +37,13 @@
       State state,
       DexApplication application,
       CommittedSyntheticsCollection committed,
+      CommittedSyntheticsCollection finalized,
       ImmutableList<DexType> committedProgramTypes,
       GlobalSyntheticsStrategy globalSyntheticsStrategy) {
     this.state = state;
     this.application = application;
     this.committed = committed;
+    this.finalized = finalized;
     this.committedProgramTypes = committedProgramTypes;
     this.globalSyntheticsStrategy = globalSyntheticsStrategy;
     assert committed.verifyTypesAreInApp(application);
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
index 8663f34..fa6cff0 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
@@ -24,6 +24,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 /**
@@ -32,7 +33,7 @@
  * <p>This structure is to make it easier to pass the items from SyntheticItems to CommittedItems
  * and back while also providing a builder for updating the committed synthetics.
  */
-class CommittedSyntheticsCollection {
+public class CommittedSyntheticsCollection {
 
   static class Builder {
     private final CommittedSyntheticsCollection parent;
@@ -179,6 +180,37 @@
     assert verifySyntheticInputsSubsetOfSynthetics();
   }
 
+  public CommittedSyntheticsCollection merge(
+      ImmutableMap<DexType, List<SyntheticMethodReference>> methods,
+      ImmutableMap<DexType, List<SyntheticProgramClassReference>> classes,
+      ImmutableMap<DexType, Set<DexType>> globalContexts,
+      ImmutableSet<DexType> syntheticInputs) {
+    if (isEmpty()) {
+      return new CommittedSyntheticsCollection(methods, classes, globalContexts, syntheticInputs);
+    }
+    return new CommittedSyntheticsCollection(
+        ImmutableMap.<DexType, List<SyntheticMethodReference>>builderWithExpectedSize(
+                this.methods.size() + methods.size())
+            .putAll(this.methods)
+            .putAll(methods)
+            .build(),
+        ImmutableMap.<DexType, List<SyntheticProgramClassReference>>builderWithExpectedSize(
+                this.classes.size() + classes.size())
+            .putAll(this.classes)
+            .putAll(classes)
+            .build(),
+        ImmutableMap.<DexType, Set<DexType>>builderWithExpectedSize(
+                this.globalContexts.size() + globalContexts.size())
+            .putAll(this.globalContexts)
+            .putAll(globalContexts)
+            .build(),
+        ImmutableSet.<DexType>builderWithExpectedSize(
+                this.syntheticInputs.size() + syntheticInputs.size())
+            .addAll(this.syntheticInputs)
+            .addAll(syntheticInputs)
+            .build());
+  }
+
   private boolean verifySyntheticInputsSubsetOfSynthetics() {
     Set<DexType> synthetics =
         ImmutableSet.<DexType>builder().addAll(methods.keySet()).addAll(classes.keySet()).build();
@@ -236,6 +268,32 @@
     return syntheticInputs.contains(type);
   }
 
+  public void forEachClass(BiConsumer<DexType, List<SyntheticProgramClassReference>> consumer) {
+    classes.forEach(consumer);
+  }
+
+  public void forEachClassFlattened(BiConsumer<DexType, SyntheticProgramClassReference> consumer) {
+    classes.forEach(
+        (type, references) -> {
+          for (SyntheticProgramClassReference reference : references) {
+            consumer.accept(type, reference);
+          }
+        });
+  }
+
+  public void forEachMethod(BiConsumer<DexType, List<SyntheticMethodReference>> consumer) {
+    methods.forEach(consumer);
+  }
+
+  public void forEachMethodFlattened(BiConsumer<DexType, SyntheticMethodReference> consumer) {
+    methods.forEach(
+        (type, references) -> {
+          for (SyntheticMethodReference reference : references) {
+            consumer.accept(type, reference);
+          }
+        });
+  }
+
   public Set<DexType> getContextsForGlobal(DexType globalSynthetic) {
     return globalContexts.get(globalSynthetic);
   }
@@ -267,7 +325,7 @@
   }
 
   CommittedSyntheticsCollection pruneItems(PrunedItems prunedItems) {
-    Set<DexType> removed = prunedItems.getNoLongerSyntheticItems();
+    Set<DexType> removed = prunedItems.getRemovedClasses();
     if (removed.isEmpty()) {
       return this;
     }
@@ -296,8 +354,6 @@
     }
     // Global synthetic contexts are only collected for per-file modes which only prune synthetic
     // items, not inputs.
-    assert globalContexts.isEmpty()
-        || prunedItems.getNoLongerSyntheticItems().size() == prunedItems.getRemovedClasses().size();
     return changed ? builder.build() : this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index d1db8ba..f7cc1e5 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -160,10 +160,15 @@
 
   private final SyntheticItems synthetics;
   private final CommittedSyntheticsCollection committed;
+  private final CommittedSyntheticsCollection finalized;
 
-  SyntheticFinalization(SyntheticItems synthetics, CommittedSyntheticsCollection committed) {
+  SyntheticFinalization(
+      SyntheticItems synthetics,
+      CommittedSyntheticsCollection committed,
+      CommittedSyntheticsCollection finalized) {
     this.synthetics = synthetics;
     this.committed = committed;
+    this.finalized = finalized;
   }
 
   public static void finalize(
@@ -286,11 +291,9 @@
         new CommittedItems(
             State.FINALIZED,
             application,
-            new CommittedSyntheticsCollection(
-                finalMethods,
-                finalClasses,
-                committed.getGlobalContexts(),
-                finalInputSynthetics),
+            CommittedSyntheticsCollection.empty(),
+            finalized.merge(
+                finalMethods, finalClasses, committed.getGlobalContexts(), finalInputSynthetics),
             ImmutableList.of(),
             synthetics.getGlobalSyntheticsStrategy()),
         syntheticFinalizationGraphLens,
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 2f4e348..942465b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -77,7 +77,8 @@
     if (definition != null) {
       return definition.getKind().isShareable();
     }
-    Iterable<SyntheticReference<?, ?, ?>> references = committed.getItems(clazz.type);
+    Iterable<SyntheticReference<?, ?, ?>> references =
+        Iterables.concat(committed.getItems(clazz.type), finalized.getItems(clazz.getType()));
     Iterator<SyntheticReference<?, ?, ?>> iterator = references.iterator();
     if (iterator.hasNext()) {
       boolean sharable = iterator.next().getKind().isShareable();
@@ -218,38 +219,38 @@
   private final State state;
   private final SyntheticNaming naming;
   private final CommittedSyntheticsCollection committed;
+  private final CommittedSyntheticsCollection finalized;
   private final PendingSynthetics pending = new PendingSynthetics();
   private final ContextsForGlobalSynthetics globalContexts;
   private final GlobalSyntheticsStrategy globalSyntheticsStrategy;
 
-  @SuppressWarnings("ReferenceEquality")
   public Set<DexType> collectSyntheticsFromContext(DexType context) {
     Set<DexType> result = Sets.newIdentityHashSet();
-    committed
-        .getMethods()
-        .forEach(
-            (synthetic, methodReferences) -> {
-              methodReferences.forEach(
-                  methodReference -> {
-                    if (methodReference.getContext().getSynthesizingContextType() == context) {
-                      result.add(synthetic);
-                    }
-                  });
-            });
-    committed
-        .getClasses()
-        .forEach(
-            (synthetic, classReferences) -> {
-              classReferences.forEach(
-                  classReference -> {
-                    if (classReference.getContext().getSynthesizingContextType() == context) {
-                      result.add(synthetic);
-                    }
-                  });
-            });
+    internalCollectSyntheticsFromContext(context, committed, result);
+    internalCollectSyntheticsFromContext(context, finalized, result);
     return result;
   }
 
+  private static void internalCollectSyntheticsFromContext(
+      DexType context, CommittedSyntheticsCollection committed, Set<DexType> result) {
+    committed.forEachMethodFlattened(
+        (synthetic, methodReference) -> {
+          if (context.isIdenticalTo(methodReference.getContext().getSynthesizingContextType())) {
+            result.add(synthetic);
+          }
+        });
+    committed.forEachClassFlattened(
+        (synthetic, classReference) -> {
+          if (context.isIdenticalTo(classReference.getContext().getSynthesizingContextType())) {
+            result.add(synthetic);
+          }
+        });
+  }
+
+  public CommittedSyntheticsCollection getCommitted() {
+    return committed;
+  }
+
   public SyntheticNaming getNaming() {
     return naming;
   }
@@ -265,6 +266,7 @@
         State.OPEN,
         application,
         CommittedSyntheticsCollection.empty(),
+        CommittedSyntheticsCollection.empty(),
         ImmutableList.of(),
         globalSyntheticsStrategy);
   }
@@ -274,6 +276,7 @@
     this(
         commit.state,
         commit.committed,
+        commit.finalized,
         commit.globalSyntheticsStrategy,
         commit.getApplication().dexItemFactory().getSyntheticNaming());
   }
@@ -281,10 +284,12 @@
   private SyntheticItems(
       State state,
       CommittedSyntheticsCollection committed,
+      CommittedSyntheticsCollection finalized,
       GlobalSyntheticsStrategy globalSyntheticsStrategy,
       SyntheticNaming naming) {
     this.state = state;
     this.committed = committed;
+    this.finalized = finalized;
     this.naming = naming;
     this.globalContexts = globalSyntheticsStrategy.getStrategy();
     this.globalSyntheticsStrategy = globalSyntheticsStrategy;
@@ -293,17 +298,23 @@
   public Map<DexType, Set<DexType>> getFinalGlobalSyntheticContexts(AppView<?> appView) {
     assert isFinalized();
     DexItemFactory factory = appView.dexItemFactory();
-    ImmutableMap<DexType, Set<DexType>> globalContexts = committed.getGlobalContexts();
+    ImmutableMap<DexType, Set<DexType>> committedGlobalContexts = committed.getGlobalContexts();
+    ImmutableMap<DexType, Set<DexType>> finalizedGlobalContexts = finalized.getGlobalContexts();
     NamingLens namingLens = appView.getNamingLens();
-    Map<DexType, Set<DexType>> rewritten = new IdentityHashMap<>(globalContexts.size());
-    globalContexts.forEach(
-        (global, contexts) -> {
-          Set<DexType> old =
-              rewritten.put(
-                  namingLens.lookupType(global, factory),
-                  SetUtils.mapIdentityHashSet(contexts, c -> namingLens.lookupType(c, factory)));
-          assert old == null;
-        });
+    Map<DexType, Set<DexType>> rewritten =
+        new IdentityHashMap<>(committedGlobalContexts.size() + finalizedGlobalContexts.size());
+    Iterables.concat(committedGlobalContexts.entrySet(), finalizedGlobalContexts.entrySet())
+        .forEach(
+            entry -> {
+              var global = entry.getKey();
+              var contexts = entry.getValue();
+              Set<DexType> old =
+                  rewritten.put(
+                      namingLens.lookupType(global, factory),
+                      SetUtils.mapIdentityHashSet(
+                          contexts, c -> namingLens.lookupType(c, factory)));
+              assert old == null;
+            });
     return rewritten;
   }
 
@@ -311,6 +322,7 @@
     // Collecting synthetic items must be the very first task after application build.
     SyntheticItems synthetics = appView.getSyntheticItems();
     assert synthetics.committed.isEmpty();
+    assert synthetics.finalized.isEmpty();
     assert synthetics.pending.isEmpty();
     CommittedSyntheticsCollection.Builder builder = synthetics.committed.builder();
     if (appView.options().intermediate) {
@@ -336,6 +348,7 @@
             synthetics.state,
             appView.appInfo().app(),
             committed,
+            synthetics.finalized,
             ImmutableList.of(),
             synthetics.globalSyntheticsStrategy);
     if (appView.appInfo().hasClassHierarchy()) {
@@ -463,6 +476,10 @@
     return committed.containsType(type);
   }
 
+  public boolean isFinalizedSynthetic(DexType type) {
+    return finalized.containsType(type);
+  }
+
   public boolean isPendingSynthetic(DexType type) {
     return pending.containsType(type);
   }
@@ -472,7 +489,7 @@
   }
 
   public boolean isSynthetic(DexType type) {
-    return committed.containsType(type) || pending.definitions.containsKey(type);
+    return isCommittedSynthetic(type) || isFinalizedSynthetic(type) || isPendingSynthetic(type);
   }
 
   public boolean isSubjectToKeepRules(DexProgramClass clazz) {
@@ -493,7 +510,23 @@
     if (definition != null) {
       return definition.getKind().isGlobal();
     }
-    return isGlobalReferences(committed.getClasses().get(type));
+    List<SyntheticProgramClassReference> committedReferences =
+        committed.getClasses().getOrDefault(type, Collections.emptyList());
+    List<SyntheticProgramClassReference> finalizedReferences =
+        finalized.getClasses().getOrDefault(type, Collections.emptyList());
+    SyntheticProgramClassReference singleReference;
+    if (committedReferences.size() + finalizedReferences.size() == 1) {
+      singleReference =
+          committedReferences.size() == 1 ? committedReferences.get(0) : finalizedReferences.get(0);
+    } else {
+      singleReference = null;
+    }
+    if (singleReference != null && singleReference.getKind().isGlobal()) {
+      return true;
+    }
+    assert verifyNoGlobals(committedReferences);
+    assert verifyNoGlobals(finalizedReferences);
+    return false;
   }
 
   public boolean isGlobalSyntheticClass(DexProgramClass clazz) {
@@ -513,6 +546,10 @@
       // Only a single context should exist for a globally derived synthetic, so return early.
       return isGlobalSyntheticClass(reference.getContext().getSynthesizingContextType());
     }
+    for (SyntheticReference<?, ?, ?> reference : finalized.getItems(type)) {
+      // Only a single context should exist for a globally derived synthetic, so return early.
+      return isGlobalSyntheticClass(reference.getContext().getSynthesizingContextType());
+    }
     SyntheticDefinition<?, ?, ?> definition = pending.definitions.get(type);
     if (definition != null) {
       return isGlobalSyntheticClass(definition.getContext().getSynthesizingContextType());
@@ -520,17 +557,6 @@
     return false;
   }
 
-  private static boolean isGlobalReferences(List<SyntheticProgramClassReference> references) {
-    if (references == null) {
-      return false;
-    }
-    if (references.size() == 1 && references.get(0).getKind().isGlobal()) {
-      return true;
-    }
-    assert verifyNoGlobals(references);
-    return false;
-  }
-
   private static boolean verifyNoGlobals(List<SyntheticProgramClassReference> references) {
     for (SyntheticProgramClassReference reference : references) {
       assert !reference.getKind().isGlobal();
@@ -545,12 +571,16 @@
 
   public boolean isSyntheticOfKind(DexType type, SyntheticKindSelector kindSelector) {
     SyntheticKind kind = kindSelector.select(naming);
-    return pending.containsTypeOfKind(type, kind) || committed.containsTypeOfKind(type, kind);
+    return pending.containsTypeOfKind(type, kind)
+        || committed.containsTypeOfKind(type, kind)
+        || finalized.containsTypeOfKind(type, kind);
   }
 
   public Iterable<SyntheticKind> getSyntheticKinds(DexType type) {
     Iterable<SyntheticKind> references =
-        IterableUtils.transform(committed.getItems(type), SyntheticReference::getKind);
+        Iterables.concat(
+            Iterables.transform(committed.getItems(type), SyntheticReference::getKind),
+            Iterables.transform(finalized.getItems(type), SyntheticReference::getKind));
     SyntheticDefinition<?, ?, ?> definition = pending.definitions.get(type);
     if (definition != null) {
       references = Iterables.concat(references, IterableUtils.singleton(definition.getKind()));
@@ -559,7 +589,8 @@
   }
 
   boolean isSyntheticInput(DexProgramClass clazz) {
-    return committed.containsSyntheticInput(clazz.getType());
+    return committed.containsSyntheticInput(clazz.getType())
+        || finalized.containsSyntheticInput(clazz.getType());
   }
 
   public FeatureSplit getContextualFeatureSplitOrDefault(DexType type, FeatureSplit defaultValue) {
@@ -591,6 +622,9 @@
     for (SyntheticReference<?, ?, ?> reference : committed.getItems(type)) {
       consumer.accept(reference.getContext());
     }
+    for (SyntheticReference<?, ?, ?> reference : finalized.getItems(type)) {
+      consumer.accept(reference.getContext());
+    }
     SyntheticDefinition<?, ?, ?> definition = pending.definitions.get(type);
     if (definition != null) {
       consumer.accept(definition.getContext());
@@ -648,6 +682,7 @@
   }
 
   public boolean isSyntheticMethodThatShouldNotBeDoubleProcessed(ProgramMethod method) {
+    assert finalized.isEmpty();
     for (SyntheticMethodReference reference :
         committed.getMethods().getOrDefault(method.getHolderType(), Collections.emptyList())) {
       if (reference.getKind().equals(naming.STATIC_INTERFACE_CALL)) {
@@ -667,6 +702,7 @@
       DexProgramClass clazz,
       Predicate<DexProgramClass> ifIsLambda,
       Predicate<DexProgramClass> ifNotLambda) {
+    assert finalized.isEmpty();
     Iterable<SyntheticReference<?, ?, ?>> references = committed.getItems(clazz.getType());
     SyntheticDefinition<?, ?, ?> definition = pending.definitions.get(clazz.getType());
     if (definition != null) {
@@ -693,7 +729,8 @@
     if (existingDefinition != null) {
       return existingDefinition.getContext();
     }
-    Iterable<SyntheticReference<?, ?, ?>> existingReferences = committed.getItems(contextType);
+    Iterable<SyntheticReference<?, ?, ?>> existingReferences =
+        Iterables.concat(committed.getItems(contextType), finalized.getItems(contextType));
     if (!Iterables.isEmpty(existingReferences)) {
       // Use a deterministic synthesizing context from the set of contexts.
       return IterableUtils.min(
@@ -1171,7 +1208,6 @@
       AppView<?> appView,
       Consumer<SyntheticMethodBuilder> fn,
       Supplier<String> syntheticIdSupplier) {
-    assert !isFinalized();
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
     // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = getSynthesizingContext(context, appView);
@@ -1206,7 +1242,14 @@
   }
 
   public CommittedItems commitPrunedItems(PrunedItems prunedItems) {
-    return commit(prunedItems, pending, globalContexts, committed, state, globalSyntheticsStrategy);
+    return commit(
+        prunedItems,
+        pending,
+        globalContexts,
+        committed,
+        finalized,
+        state,
+        globalSyntheticsStrategy);
   }
 
   public CommittedItems commitRewrittenWithLens(
@@ -1219,6 +1262,7 @@
             pending,
             globalContexts,
             committed.rewriteWithLens(lens, timing),
+            finalized.rewriteWithLens(lens, timing),
             state,
             globalSyntheticsStrategy);
     timing.end();
@@ -1230,15 +1274,16 @@
       PendingSynthetics pending,
       ContextsForGlobalSynthetics globalContexts,
       CommittedSyntheticsCollection committed,
+      CommittedSyntheticsCollection finalized,
       State state,
       GlobalSyntheticsStrategy globalSyntheticsStrategy) {
     DexApplication application = prunedItems.getPrunedApp();
-    Set<DexType> removedClasses = prunedItems.getNoLongerSyntheticItems();
-    CommittedSyntheticsCollection.Builder builder = committed.builder();
+    Set<DexType> removedClasses = prunedItems.getRemovedClasses();
+    CommittedSyntheticsCollection.Builder committedBuilder = committed.builder();
     // Compute the synthetic additions and add them to the application.
     ImmutableList<DexType> committedProgramTypes;
     DexApplication amendedApplication;
-    if (pending.definitions.isEmpty()) {
+    if (pending.isEmpty()) {
       committedProgramTypes = ImmutableList.of();
       amendedApplication = application;
     } else {
@@ -1258,30 +1303,32 @@
             assert definition.isClasspathDefinition();
             appBuilder.addClasspathClass(definition.asClasspathDefinition().getHolder());
           }
-          builder.addItem(definition);
+          committedBuilder.addItem(definition);
         }
       }
-      builder.addGlobalContexts(globalContexts);
+      committedBuilder.addGlobalContexts(globalContexts);
       committedProgramTypes = committedProgramTypesBuilder.build();
       amendedApplication = appBuilder.build();
     }
     return new CommittedItems(
         state,
         amendedApplication,
-        builder.build().pruneItems(prunedItems),
+        committedBuilder.build().pruneItems(prunedItems),
+        finalized.pruneItems(prunedItems),
         committedProgramTypes,
         globalSyntheticsStrategy);
   }
 
   public void writeAttributeIfIntermediateSyntheticClass(
       ClassWriter writer, DexProgramClass clazz, AppView<?> appView) {
+    assert committed.isEmpty();
     if (appView.options().testing.disableSyntheticMarkerAttributeWriting) {
       return;
     }
     if (!appView.options().intermediate || !appView.options().isGeneratingClassFiles()) {
       return;
     }
-    Iterator<SyntheticReference<?, ?, ?>> it = committed.getItems(clazz.getType()).iterator();
+    Iterator<SyntheticReference<?, ?, ?>> it = finalized.getItems(clazz.getType()).iterator();
     if (it.hasNext()) {
       SyntheticKind kind = it.next().getKind();
       // When compiling intermediates there should not be any mergings as they may invalidate the
@@ -1296,19 +1343,20 @@
 
   Result computeFinalSynthetics(AppView<?> appView, Timing timing) {
     assert !hasPendingSyntheticClasses();
-    return new SyntheticFinalization(this, committed).computeFinalSynthetics(appView, timing);
+    return new SyntheticFinalization(this, committed, finalized)
+        .computeFinalSynthetics(appView, timing);
   }
 
-  @SuppressWarnings("ReferenceEquality")
   public void reportSyntheticsInformation(SyntheticInfoConsumer consumer) {
     assert isFinalized();
+    assert committed.isEmpty();
     Map<DexType, DexType> seen = new IdentityHashMap<>();
-    committed.forEachItem(
+    finalized.forEachItem(
         ref -> {
           DexType holder = ref.getHolder();
           DexType context = ref.getContext().getSynthesizingContextType();
           DexType old = seen.put(holder, context);
-          assert old == null || old == context;
+          assert old == null || old.isIdenticalTo(context);
           if (old == null) {
             consumer.acceptSyntheticInfo(new SyntheticInfoConsumerDataImpl(holder, context));
           }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
index edfdb92..fb1b109 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
@@ -17,7 +17,7 @@
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-class SyntheticMethodDefinition
+public class SyntheticMethodDefinition
     extends SyntheticDefinition<
         SyntheticMethodReference, SyntheticMethodDefinition, DexProgramClass>
     implements SyntheticProgramDefinition {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
index e649f48..e6b6760 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
@@ -18,7 +18,7 @@
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-class SyntheticMethodReference
+public class SyntheticMethodReference
     extends SyntheticReference<SyntheticMethodReference, SyntheticMethodDefinition, DexProgramClass>
     implements SyntheticProgramReference, Rewritable<SyntheticMethodReference> {
   final DexMethod method;
@@ -39,7 +39,7 @@
   }
 
   @Override
-  SyntheticMethodDefinition lookupDefinition(Function<DexType, DexClass> definitions) {
+  public SyntheticMethodDefinition lookupDefinition(Function<DexType, DexClass> definitions) {
     DexClass clazz = definitions.apply(method.holder);
     if (clazz == null) {
       return null;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 83beaff..676989f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -86,6 +86,8 @@
   public final SyntheticKind OBJECT_CLONE_OUTLINE = generator.forSingleMethod("ObjectCloneOutline");
   public final SyntheticKind TO_STRING_IF_NOT_NULL =
       generator.forSingleMethodWithGlobalMerging("ToStringIfNotNull");
+  public final SyntheticKind THROW_CCE_IF_NOT_EQUALS =
+      generator.forSingleMethodWithGlobalMerging("ThrowCCEIfNotEquals");
   public final SyntheticKind THROW_CCE_IF_NOT_NULL =
       generator.forSingleMethodWithGlobalMerging("ThrowCCEIfNotNull");
   public final SyntheticKind NON_NULL = generator.forSingleMethodWithGlobalMerging("NonNull");
diff --git a/src/main/java/com/android/tools/r8/utils/CollectionUtils.java b/src/main/java/com/android/tools/r8/utils/CollectionUtils.java
index 5434871..c08d714 100644
--- a/src/main/java/com/android/tools/r8/utils/CollectionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/CollectionUtils.java
@@ -83,4 +83,8 @@
     }
     return returnArr;
   }
+
+  public static <K> Collection<K> unmodifiableForTesting(Collection<K> map) {
+    return InternalOptions.assertionsEnabled() ? Collections.unmodifiableCollection(map) : map;
+  }
 }
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 1e182f5..c1120ae 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -785,6 +785,9 @@
       System.getProperty("com.android.tools.r8.experimentalTraceAndroidEnumSerialization") != null;
   public boolean enableXmlInlining =
       System.getProperty("com.android.tools.r8.enableXmlInlining") != null;
+  // Enable color inlining in code, i.e `getResources().getColor(..)`.
+  public boolean enableColorInlining =
+      System.getProperty("com.android.tools.r8.enableColorInlining") != null;
 
   // Flag to turn on/offLoad/store optimization in the Cf back-end.
   public boolean enableLoadStoreOptimization = true;
diff --git a/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
index 08d30ac..99c3df9 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
@@ -30,7 +30,7 @@
   @Override
   public List<MappedPosition> getMappedPositions(
       ProgramMethod method,
-      PositionRemapper positionRemapper,
+      ClassPositionRemapper positionRemapper,
       boolean hasOverloads,
       boolean canUseDexPc,
       int pcEncodingCutoff) {
@@ -45,7 +45,7 @@
   }
 
   private List<MappedPosition> getMappedPositionsRemapped(
-      ProgramMethod method, PositionRemapper positionRemapper, boolean hasOverloads) {
+      ProgramMethod method, ClassPositionRemapper positionRemapper, boolean hasOverloads) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     CfCode oldCode = method.getDefinition().getCode().asCfCode();
@@ -101,7 +101,7 @@
 
   @SuppressWarnings("UnusedVariable")
   private List<MappedPosition> getPcEncodedPositions(
-      ProgramMethod method, PositionRemapper positionRemapper) {
+      ProgramMethod method, ClassPositionRemapper positionRemapper) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     CfCode oldCode = method.getDefinition().getCode().asCfCode();
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java b/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
similarity index 94%
rename from src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java
rename to src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
index e769917..5444395 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
@@ -1,7 +1,6 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2025, 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.positions;
 
 import com.android.tools.r8.ResourceException;
@@ -27,14 +26,14 @@
 
 // PositionRemapper is a stateful function which takes a position (represented by a
 // DexDebugPositionState) and returns a remapped Position.
-public interface PositionRemapper {
+public interface ClassPositionRemapper {
 
   Pair<Position, Position> createRemappedPosition(Position position);
 
-  static PositionRemapper getPositionRemapper(
+  static ClassPositionRemapper getPositionRemapper(
       AppView<?> appView, CfLineToMethodMapper cfLineToMethodMapper) {
     boolean identityMapping = appView.options().lineNumberOptimization.isOff();
-    PositionRemapper positionRemapper =
+    ClassPositionRemapper positionRemapper =
         identityMapping
             ? new IdentityPositionRemapper()
             : new OptimizingPositionRemapper(appView.options());
@@ -48,7 +47,7 @@
 
   void setCurrentMethod(DexEncodedMethod definition);
 
-  class IdentityPositionRemapper implements PositionRemapper {
+  class IdentityPositionRemapper implements ClassPositionRemapper {
 
     @Override
     public Pair<Position, Position> createRemappedPosition(Position position) {
@@ -63,7 +62,7 @@
     }
   }
 
-  class OptimizingPositionRemapper implements PositionRemapper {
+  class OptimizingPositionRemapper implements ClassPositionRemapper {
     private final int maxLineDelta;
     private DexMethod previousMethod = null;
     private int previousSourceLine = -1;
@@ -104,13 +103,13 @@
     }
   }
 
-  class KotlinInlineFunctionPositionRemapper implements PositionRemapper {
+  class KotlinInlineFunctionPositionRemapper implements ClassPositionRemapper {
 
     private final AppView<?> appView;
     private final DexItemFactory factory;
     private final Map<DexType, Result> parsedKotlinSourceDebugExtensions = new IdentityHashMap<>();
     private final CfLineToMethodMapper lineToMethodMapper;
-    private final PositionRemapper baseRemapper;
+    private final ClassPositionRemapper baseRemapper;
 
     // Fields for the current context.
     private DexEncodedMethod currentMethod;
@@ -118,7 +117,7 @@
 
     private KotlinInlineFunctionPositionRemapper(
         AppView<?> appView,
-        PositionRemapper baseRemapper,
+        ClassPositionRemapper baseRemapper,
         CfLineToMethodMapper lineToMethodMapper) {
       this.appView = appView;
       this.factory = appView.dexItemFactory();
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
index 099e233..0b28e8e 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
@@ -84,7 +84,7 @@
 
     private final PositionEventEmitter positionEventEmitter;
     private final List<MappedPosition> mappedPositions;
-    private final PositionRemapper positionRemapper;
+    private final ClassPositionRemapper positionRemapper;
     private final List<DexDebugEvent> processedEvents;
 
     // Keep track of what PC has been emitted.
@@ -95,7 +95,7 @@
     public DexDebugPositionStateVisitor(
         PositionEventEmitter positionEventEmitter,
         List<MappedPosition> mappedPositions,
-        PositionRemapper positionRemapper,
+        ClassPositionRemapper positionRemapper,
         List<DexDebugEvent> processedEvents,
         DexItemFactory factory,
         int startLine,
@@ -181,7 +181,7 @@
   }
 
   public List<MappedPosition> optimizeDexCodePositions(
-      ProgramMethod method, PositionRemapper positionRemapper) {
+      ProgramMethod method, ClassPositionRemapper positionRemapper) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexApplication application = appView.appInfo().app();
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
index a583e5f..7b67169 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
@@ -34,7 +34,7 @@
   }
 
   public List<MappedPosition> optimizeDexCodePositionsForPc(
-      ProgramMethod method, PositionRemapper positionRemapper, int pcEncodingCutoff) {
+      ProgramMethod method, ClassPositionRemapper positionRemapper, int pcEncodingCutoff) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexCode dexCode = method.getDefinition().getCode().asDexCode();
@@ -96,7 +96,7 @@
       int startPc,
       int endPc,
       Position position,
-      PositionRemapper remapper,
+      ClassPositionRemapper remapper,
       List<MappedPosition> mappedPositions) {
     Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
     Position oldPosition = remappedPosition.getFirst();
diff --git a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
index 12b0d73..aa55443 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
@@ -61,7 +61,7 @@
     // used. We still run the line number optimizer to collect line numbers and inline frame
     // information for the mapping file.
     timing.begin("Line number remapping");
-    ClassNameMapper mapper = run(appView, inputApp, originalSourceFiles, representation);
+    ClassNameMapper mapper = run(appView, inputApp, originalSourceFiles, representation, timing);
     timing.end();
     if (appView.options().mappingComposeOptions().generatedClassNameMapperConsumer != null) {
       appView.options().mappingComposeOptions().generatedClassNameMapperConsumer.accept(mapper);
@@ -104,7 +104,8 @@
       AppView<?> appView,
       AndroidApp inputApp,
       OriginalSourceFiles originalSourceFiles,
-      DebugRepresentationPredicate representation) {
+      DebugRepresentationPredicate representation,
+      Timing timing) {
     // For finding methods in kotlin files based on SourceDebugExtensions, we use a line method map.
     // We create it here to ensure it is only reading class files once.
     // TODO(b/220999985): Make this threaded per virtual file. Possibly pull the kotlin line mapping
@@ -118,6 +119,7 @@
         MappedPositionToClassNameMapperBuilder.builder(appView, originalSourceFiles);
 
     // Collect which files contain which classes that need to have their line numbers optimized.
+    timing.begin("Process classes");
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (shouldRun(clazz, appView)) {
         runForClass(
@@ -126,12 +128,16 @@
             representation,
             builder,
             cfLineToMethodMapper,
-            positionToMappedRangeMapper);
+            positionToMappedRangeMapper,
+            timing);
       }
     }
+    timing.end();
 
     // Update all the debug-info objects.
+    timing.begin("Update debug info in code objects");
     positionToMappedRangeMapper.updateDebugInfoInCodeObjects();
+    timing.end();
 
     return builder.build();
   }
@@ -151,7 +157,9 @@
       DebugRepresentationPredicate representation,
       MappedPositionToClassNameMapperBuilder builder,
       CfLineToMethodMapper cfLineToMethodMapper,
-      PositionToMappedRangeMapper positionToMappedRangeMapper) {
+      PositionToMappedRangeMapper positionToMappedRangeMapper,
+      Timing timing) {
+    timing.begin("Prelude");
     IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
         groupMethodsByRenamedName(appView, clazz);
 
@@ -160,6 +168,8 @@
     // Process methods ordered by renamed name.
     List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
     renamedMethodNames.sort(DexString::compareTo);
+    timing.end();
+
     for (DexString methodName : renamedMethodNames) {
       List<ProgramMethod> methods = methodsByRenamedName.get(methodName);
       if (methods.size() > 1) {
@@ -175,38 +185,64 @@
         assert verifyMethodsAreKeptDirectlyOrIndirectly(appView, methods);
       }
 
-      PositionRemapper positionRemapper =
-          PositionRemapper.getPositionRemapper(appView, cfLineToMethodMapper);
+      ClassPositionRemapper positionRemapper =
+          ClassPositionRemapper.getPositionRemapper(appView, cfLineToMethodMapper);
 
+      timing.begin("Process methods");
       for (ProgramMethod method : methods) {
-        DexEncodedMethod definition = method.getDefinition();
-        if (method.getName().isIdenticalTo(methodName)
-            && !mustHaveResidualDebugInfo(appView.options(), definition)
-            && !definition.isD8R8Synthesized()
-            && methods.size() <= 1) {
-          continue;
-        }
-        positionRemapper.setCurrentMethod(definition);
-        List<MappedPosition> mappedPositions;
-        int pcEncodingCutoff =
-            methods.size() == 1 ? representation.getDexPcEncodingCutoff(method) : -1;
-        boolean canUseDexPc = pcEncodingCutoff > 0;
-        if (definition.getCode() != null
-            && (definition.getCode().isCfCode() || definition.getCode().isDexCode())
-            && !appView.isCfByteCodePassThrough(method)) {
-          mappedPositions =
-              positionToMappedRangeMapper.getMappedPositions(
-                  method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff);
-        } else {
-          mappedPositions = new ArrayList<>();
-        }
-
-        classNamingBuilder.addMappedPositions(
-            method, mappedPositions, positionRemapper, canUseDexPc);
-      } // for each method of the group
+        runForMethod(
+            method,
+            appView,
+            classNamingBuilder,
+            methodName,
+            methods,
+            positionRemapper,
+            positionToMappedRangeMapper,
+            representation,
+            timing);
+      }
+      timing.end();
     } // for each method group, grouped by name
   }
 
+  private static void runForMethod(
+      ProgramMethod method,
+      AppView<?> appView,
+      MappedPositionToClassNamingBuilder classNamingBuilder,
+      DexString methodName,
+      List<ProgramMethod> methods,
+      ClassPositionRemapper positionRemapper,
+      PositionToMappedRangeMapper positionToMappedRangeMapper,
+      DebugRepresentationPredicate representation,
+      Timing timing) {
+    DexEncodedMethod definition = method.getDefinition();
+    if (method.getName().isIdenticalTo(methodName)
+        && !mustHaveResidualDebugInfo(appView.options(), definition)
+        && !definition.isD8R8Synthesized()
+        && methods.size() <= 1) {
+      return;
+    }
+    positionRemapper.setCurrentMethod(definition);
+    List<MappedPosition> mappedPositions;
+    int pcEncodingCutoff = methods.size() == 1 ? representation.getDexPcEncodingCutoff(method) : -1;
+    boolean canUseDexPc = pcEncodingCutoff > 0;
+    if (definition.getCode() != null
+        && (definition.getCode().isCfCode() || definition.getCode().isDexCode())
+        && !appView.isCfByteCodePassThrough(method)) {
+      timing.begin("Get mapped positions");
+      mappedPositions =
+          positionToMappedRangeMapper.getMappedPositions(
+              method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff);
+      timing.end();
+    } else {
+      mappedPositions = new ArrayList<>();
+    }
+
+    timing.begin("Add mapped positions");
+    classNamingBuilder.addMappedPositions(method, mappedPositions, positionRemapper, canUseDexPc);
+    timing.end();
+  }
+
   @SuppressWarnings("ComplexBooleanConstant")
   private static boolean verifyMethodsAreKeptDirectlyOrIndirectly(
       AppView<?> appView, List<ProgramMethod> methods) {
@@ -283,14 +319,12 @@
         });
   }
 
-  @SuppressWarnings("UnusedVariable")
   public static IdentityHashMap<DexString, List<ProgramMethod>> groupMethodsByRenamedName(
       AppView<?> appView, DexProgramClass clazz) {
     IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
         new IdentityHashMap<>(clazz.getMethodCollection().size());
     for (ProgramMethod programMethod : clazz.programMethods()) {
       // Add method only if renamed, moved, or if it has debug info to map.
-      DexEncodedMethod definition = programMethod.getDefinition();
       DexMethod method = programMethod.getReference();
       DexString renamedName = appView.getNamingLens().lookupName(method);
       methodsByRenamedName
diff --git a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
index 516241e..b854592 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
@@ -220,7 +220,7 @@
     public MappedPositionToClassNamingBuilder addMappedPositions(
         ProgramMethod method,
         List<MappedPosition> mappedPositions,
-        PositionRemapper positionRemapper,
+        ClassPositionRemapper positionRemapper,
         boolean canUseDexPc) {
       DexEncodedMethod definition = method.getDefinition();
       DexMethod residualMethod =
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
index 78fb8d0..10a79ac 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
@@ -19,7 +19,7 @@
 
   List<MappedPosition> getMappedPositions(
       ProgramMethod method,
-      PositionRemapper positionRemapper,
+      ClassPositionRemapper positionRemapper,
       boolean hasOverloads,
       boolean canUseDexPc,
       int pcEncodingCutoff);
@@ -51,7 +51,7 @@
     @Override
     public List<MappedPosition> getMappedPositions(
         ProgramMethod method,
-        PositionRemapper positionRemapper,
+        ClassPositionRemapper positionRemapper,
         boolean hasOverloads,
         boolean canUseDexPc,
         int pcEncodingCutoff) {
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java b/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
index 0b0b20a..9852eeb 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
@@ -20,7 +20,7 @@
 public class PositionUtils {
 
   public static Position remapAndAdd(
-      Position position, PositionRemapper remapper, List<MappedPosition> mappedPositions) {
+      Position position, ClassPositionRemapper remapper, List<MappedPosition> mappedPositions) {
     Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
     Position oldPosition = remappedPosition.getFirst();
     Position newPosition = remappedPosition.getSecond();
diff --git a/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java b/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
index ad0627c..c3d5cf9 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
@@ -87,8 +87,18 @@
         .addKeepMainRule(FooBar.class)
         .applyIf(
             parameters.getPartialCompilationTestParameters().isSome(),
-            rr -> rr.addR8PartialR8OptionsModification(o -> o.enableXmlInlining = true),
-            rr -> rr.addOptionsModification(o -> o.enableXmlInlining = true))
+            rr ->
+                rr.addR8PartialR8OptionsModification(
+                    o -> {
+                      o.enableXmlInlining = true;
+                      o.enableColorInlining = true;
+                    }),
+            rr ->
+                rr.addOptionsModification(
+                    o -> {
+                      o.enableXmlInlining = true;
+                      o.enableColorInlining = true;
+                    }))
         .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
         .applyIf(
             addResourcesSubclass,
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index b4d42d9..282b101 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -626,7 +626,7 @@
     private final ProguardTestBuilder builder;
     private final List<Consumer<List<String>>> configConsumers = new ArrayList<>();
     private final List<Consumer<List<String>>> extractedRulesConsumers = new ArrayList<>();
-    private final List<String> extractedRules = new ArrayList();
+    private final List<String> extractedRules = new ArrayList<>();
 
     public PGBuilder(
         KeepAnnoParameters params,
@@ -638,11 +638,7 @@
       builder =
           TestBase.testForProguard(KeepAnnoTestUtils.PG_VERSION, temp)
               .applyIf(
-                  keepAnnotationLibrary == ANDROIDX,
-                  b ->
-                      b.addDefaultRuntimeLibrary(parameters())
-                          .addLibraryFiles(
-                              kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar()))
+                  keepAnnotationLibrary == ANDROIDX, b -> b.addDefaultRuntimeLibrary(parameters()))
               .addProgramFiles(KeepAnnoTestUtils.getKeepAnnoLib(temp, keepAnnotationLibrary))
               .setMinApi(parameters());
     }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternAnyRetentionTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternAnyRetentionTest.java
index a9b13e0..1f60688 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternAnyRetentionTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternAnyRetentionTest.java
@@ -13,6 +13,7 @@
 import androidx.annotation.keep.KeepTarget;
 import androidx.annotation.keep.UsedByReflection;
 import androidx.annotation.keep.UsesReflection;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.keepanno.KeepAnnoParameters;
 import com.android.tools.r8.keepanno.KeepAnnoTestBase;
 import com.android.tools.r8.utils.StringUtils;
@@ -47,6 +48,16 @@
   public void test() throws Exception {
     testForKeepAnnoAndroidX(parameters)
         .addProgramClasses(getInputClasses())
+        .applyIfPG(
+            b ->
+                b.addProgramFiles(
+                    ImmutableList.of(
+                        KotlinCompilerVersion.MAX_SUPPORTED_VERSION
+                            .getCompiler()
+                            .getKotlinStdlibJar(),
+                        KotlinCompilerVersion.MAX_SUPPORTED_VERSION
+                            .getCompiler()
+                            .getKotlinAnnotationJar())))
         .setExcludedOuterClass(getClass())
         .run(TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternClassRetentionTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternClassRetentionTest.java
index 869b1bc..8ce1296 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternClassRetentionTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/AnnotationPatternClassRetentionTest.java
@@ -14,6 +14,7 @@
 import androidx.annotation.keep.KeepTarget;
 import androidx.annotation.keep.UsedByReflection;
 import androidx.annotation.keep.UsesReflection;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.keepanno.KeepAnnoParameters;
 import com.android.tools.r8.keepanno.KeepAnnoTestBase;
 import com.android.tools.r8.utils.StringUtils;
@@ -48,6 +49,16 @@
   public void test() throws Exception {
     testForKeepAnnoAndroidX(parameters)
         .addProgramClasses(getInputClasses())
+        .applyIfPG(
+            b ->
+                b.addProgramFiles(
+                    ImmutableList.of(
+                        KotlinCompilerVersion.MAX_SUPPORTED_VERSION
+                            .getCompiler()
+                            .getKotlinStdlibJar(),
+                        KotlinCompilerVersion.MAX_SUPPORTED_VERSION
+                            .getCompiler()
+                            .getKotlinAnnotationJar())))
         .setExcludedOuterClass(getClass())
         .run(TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
new file mode 100644
index 0000000..9da5d33
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.regress.b426351560;
+
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.regress.b426351560.testclasses.Regress426351560TestClasses;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class Regress426351560Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().withPartialCompilation().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/427887773)
+    assumeFalse(parameters.isRandomPartialCompilation());
+    testForR8(parameters)
+        .addInnerClasses(Regress426351560TestClasses.class, getClass())
+        .addKeepRules("-keep class " + Main.class.getTypeName() + " { *; }")
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static class Main extends Regress426351560TestClasses.A {
+    public static void main(String[] strArr) {
+      Main test = new Main();
+      System.out.println(test.defaultMethod());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/testclasses/Regress426351560TestClasses.java b/src/test/java/com/android/tools/r8/regress/b426351560/testclasses/Regress426351560TestClasses.java
new file mode 100644
index 0000000..95e16df
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b426351560/testclasses/Regress426351560TestClasses.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.regress.b426351560.testclasses;
+
+public class Regress426351560TestClasses {
+  interface I {
+    default String defaultMethod() {
+      return "Hello, world!";
+    }
+  }
+
+  public static class A implements I {}
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
index 39aab56..6a55ea8 100644
--- a/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/testbase/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -111,10 +111,6 @@
       this.defaultTargetVersion = defaultTargetVersion;
     }
 
-    public static KotlinCompilerVersion latest() {
-      return ArrayUtils.last(values());
-    }
-
     public KotlinCompiler getCompiler() {
       return new KotlinCompiler(this);
     }