Merge commit '42f9d5566fd9a449f3207f5f288394f3f0904267' into dev-release
diff --git a/.gitignore b/.gitignore
index 424a67a..080826a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -131,6 +131,12 @@
 third_party/openjdk/desugar_jdk_libs_releases/1.1.1.tar.gz
 third_party/openjdk/desugar_jdk_libs_releases/1.1.5
 third_party/openjdk/desugar_jdk_libs_releases/1.1.5.tar.gz
+third_party/openjdk/jdk-17/linux
+third_party/openjdk/jdk-17/linux.tar.gz
+third_party/openjdk/jdk-17/osx
+third_party/openjdk/jdk-17/osx.tar.gz
+third_party/openjdk/jdk-17/windows
+third_party/openjdk/jdk-17/windows.tar.gz
 third_party/openjdk/jdk-16/linux
 third_party/openjdk/jdk-16/linux.tar.gz
 third_party/openjdk/jdk-16/osx
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index c3902b8..8fbb76e 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -3,6 +3,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 from os import path
+import datetime
 from subprocess import check_output, Popen, PIPE, STDOUT
 
 FMT_CMD = path.join(
@@ -78,14 +79,15 @@
       continue
     if not CopyRightInContents(f, contents):
       results.append(
-          output_api.PresubmitError('Could not find Copyright in file: %s' % f))
+          output_api.PresubmitError('Could not find correctly formatted '
+                                    'copyright in file: %s' % f))
   return results
 
 def CopyRightInContents(f, contents):
   expected = '//'
   if f.LocalPath().endswith('.py') or f.LocalPath().endswith('.sh'):
     expected = '#'
-  expected = expected + ' Copyright'
+  expected = expected + ' Copyright (c) ' + str(datetime.datetime.now().year)
   for content_line in contents:
     if expected in content_line:
       return True
diff --git a/build.gradle b/build.gradle
index 3321e41..e83113a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -131,9 +131,9 @@
             srcDirs = ['src/test/examplesJava11']
         }
     }
-    examplesJava16 {
+    examplesJava17 {
         java {
-            srcDirs = ['src/test/examplesJava16']
+            srcDirs = ['src/test/examplesJava17']
         }
     }
     jdk11TimeTests {
@@ -374,18 +374,18 @@
                 "third_party": ["openjdk/openjdk-9.0.4/linux",
                                 "openjdk/jdk8/linux-x86",
                                 "openjdk/jdk-11/linux",
-                                "openjdk/jdk-16/linux"],
+                                "openjdk/jdk-17/linux"],
         ],
         osx: [
                 "third_party": ["openjdk/openjdk-9.0.4/osx",
                                 "openjdk/jdk8/darwin-x86",
                                 "openjdk/jdk-11/osx",
-                                "openjdk/jdk-16/osx"],
+                                "openjdk/jdk-17/osx"],
         ],
         windows: [
                 "third_party": ["openjdk/openjdk-9.0.4/windows",
                                 "openjdk/jdk-11/windows",
-                                "openjdk/jdk-16/windows"],
+                                "openjdk/jdk-17/windows"],
         ],
 ]
 
@@ -624,10 +624,10 @@
         JavaVersion.VERSION_11,
         false)
 setJdkCompilationWithCompatibility(
-        sourceSets.examplesJava16.compileJavaTaskName,
-        'jdk-16',
-        JavaVersion.VERSION_16,
-        true)
+        sourceSets.examplesJava17.compileJavaTaskName,
+        'jdk-17',
+        JavaVersion.VERSION_17,
+        false)
 
 task compileMainWithJava11 (type: JavaCompile) {
     dependsOn downloadDeps
@@ -1618,7 +1618,7 @@
 buildExampleJarsCreateTask("Java9", sourceSets.examplesJava9)
 buildExampleJarsCreateTask("Java10", sourceSets.examplesJava10)
 buildExampleJarsCreateTask("Java11", sourceSets.examplesJava11)
-buildExampleJarsCreateTask("Java16", sourceSets.examplesJava16)
+buildExampleJarsCreateTask("Java17", sourceSets.examplesJava17)
 
 task provideArtFrameworksDependencies {
     cloudDependencies.tools.forEach({ art ->
@@ -1701,7 +1701,7 @@
     dependsOn buildExampleJava9Jars
     dependsOn buildExampleJava10Jars
     dependsOn buildExampleJava11Jars
-    dependsOn buildExampleJava16Jars
+    dependsOn buildExampleJava17Jars
     dependsOn buildExampleAndroidApi
     def examplesDir = file("src/test/examples")
     def noDexTests = [
diff --git a/scripts/add-openjdk.sh b/scripts/add-openjdk.sh
old mode 100644
new mode 100755
index 8587b93..6f8bada
--- a/scripts/add-openjdk.sh
+++ b/scripts/add-openjdk.sh
@@ -16,9 +16,9 @@
 # Prepare README.google
 # Update JDK_VERSION below
 
-# Now run script wit fingers crossed!
+# Now run script with fingers crossed!
 
-JDK_VERSION=16.0.2
+JDK_VERSION=17
 
 tar xf ~/Downloads/openjdk-${JDK_VERSION}_linux-x64_bin.tar.gz
 cp -rL jdk-${JDK_VERSION} linux
@@ -28,7 +28,7 @@
 rm -rf linux
 rm linux.tar.gz
 
-tar xf ~/Downloads/openjdk-${JDK_VERSION}_osx-x64_bin.tar.gz
+tar xf ~/Downloads/openjdk-${JDK_VERSION}_macos-x64_bin.tar.gz
 cp -rL jdk-${JDK_VERSION}.jdk osx
 cp README.google osx
 upload_to_google_storage.py -a --bucket r8-deps osx
@@ -43,3 +43,7 @@
 rm -rf windows
 rm -rf jdk-${JDK_VERSION}
 rm windows.tar.gz
+
+git add *.sha1
+
+echo "Update additional files, see https://r8-review.googlesource.com/c/r8/+/61909"
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabase.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabase.java
new file mode 100644
index 0000000..5dda285
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabase.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2021, 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.androidapi;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.AndroidApiLevel;
+
+public interface AndroidApiLevelDatabase {
+
+  AndroidApiLevel getTypeApiLevel(DexType type);
+
+  AndroidApiLevel getMethodApiLevel(DexMethod method);
+
+  AndroidApiLevel getFieldApiLevel(DexField field);
+}
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabaseImpl.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabaseImpl.java
new file mode 100644
index 0000000..d98013e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelDatabaseImpl.java
@@ -0,0 +1,120 @@
+// Copyright (c) 2021, 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.androidapi;
+
+import com.android.tools.r8.apimodel.AndroidApiDatabaseBuilder;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.util.HashMap;
+import java.util.function.BiFunction;
+
+public class AndroidApiLevelDatabaseImpl implements AndroidApiLevelDatabase {
+
+  private final HashMap<DexType, AndroidApiClass> predefinedApiTypeLookup;
+
+  private final AndroidApiClass SENTINEL =
+      new AndroidApiClass(null) {
+
+        @Override
+        public AndroidApiLevel getApiLevel() {
+          return null;
+        }
+
+        @Override
+        public int getMemberCount() {
+          return 0;
+        }
+
+        @Override
+        protected TraversalContinuation visitFields(
+            BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor,
+            ClassReference holder,
+            int minApiClass) {
+          return null;
+        }
+
+        @Override
+        protected TraversalContinuation visitMethods(
+            BiFunction<MethodReference, AndroidApiLevel, TraversalContinuation> visitor,
+            ClassReference holder,
+            int minApiClass) {
+          return null;
+        }
+      };
+
+  public AndroidApiLevelDatabaseImpl(HashMap<DexType, AndroidApiClass> predefinedApiTypeLookup) {
+    this.predefinedApiTypeLookup = predefinedApiTypeLookup;
+  }
+
+  @Override
+  public AndroidApiLevel getTypeApiLevel(DexType type) {
+    return lookupDefinedApiLevel(type);
+  }
+
+  @Override
+  public AndroidApiLevel getMethodApiLevel(DexMethod method) {
+    return lookupDefinedApiLevel(method);
+  }
+
+  @Override
+  public AndroidApiLevel getFieldApiLevel(DexField field) {
+    return lookupDefinedApiLevel(field);
+  }
+
+  private AndroidApiLevel lookupDefinedApiLevel(DexReference reference) {
+    AndroidApiClass foundClass =
+        predefinedApiTypeLookup.getOrDefault(reference.getContextType(), SENTINEL);
+    if (foundClass == null) {
+      return AndroidApiLevel.UNKNOWN;
+    }
+    AndroidApiClass androidApiClass;
+    if (foundClass == SENTINEL) {
+      androidApiClass =
+          AndroidApiDatabaseBuilder.buildClass(reference.getContextType().asClassReference());
+      if (androidApiClass == null) {
+        predefinedApiTypeLookup.put(reference.getContextType(), null);
+        return AndroidApiLevel.UNKNOWN;
+      }
+    } else {
+      androidApiClass = foundClass;
+    }
+    return reference.apply(
+        type -> androidApiClass.getApiLevel(),
+        field -> {
+          FieldReference fieldReference = field.asFieldReference();
+          Box<AndroidApiLevel> apiLevelBox = new Box<>(AndroidApiLevel.UNKNOWN);
+          androidApiClass.visitFields(
+              (fieldRef, apiLevel) -> {
+                if (fieldReference.equals(fieldRef)) {
+                  apiLevelBox.set(apiLevel);
+                  return TraversalContinuation.BREAK;
+                }
+                return TraversalContinuation.CONTINUE;
+              });
+          return apiLevelBox.get();
+        },
+        method -> {
+          MethodReference methodReference = method.asMethodReference();
+          Box<AndroidApiLevel> apiLevelBox = new Box<>(AndroidApiLevel.UNKNOWN);
+          androidApiClass.visitMethods(
+              (methodRef, apiLevel) -> {
+                if (methodReference.equals(methodRef)) {
+                  apiLevelBox.set(apiLevel);
+                  return TraversalContinuation.BREAK;
+                }
+                return TraversalContinuation.CONTINUE;
+              });
+          return apiLevelBox.get();
+        });
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java
index e3fa030..8ca1b59 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java
@@ -4,37 +4,28 @@
 
 package com.android.tools.r8.androidapi;
 
-import com.android.tools.r8.apimodel.AndroidApiDatabaseBuilder;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryConfiguration;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.TraversalContinuation;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.HashMap;
 
 public class AndroidApiReferenceLevelCache {
 
-  private static final int BUILD_CACHE_TRESHOLD = 20;
-
-  private final ConcurrentHashMap<DexType, AndroidApiClass> apiTypeLookup;
-  private final ConcurrentHashMap<DexReference, AndroidApiLevel> apiMemberLookup =
-      new ConcurrentHashMap<>();
   private final DesugaredLibraryConfiguration desugaredLibraryConfiguration;
+  private final AndroidApiLevelDatabase androidApiLevelDatabase;
   private final AppView<?> appView;
 
   private AndroidApiReferenceLevelCache(AppView<?> appView) {
-    this(appView, new ConcurrentHashMap<>());
+    this(appView, new HashMap<>());
   }
 
   private AndroidApiReferenceLevelCache(
-      AppView<?> appView, ConcurrentHashMap<DexType, AndroidApiClass> apiTypeLookup) {
+      AppView<?> appView, HashMap<DexType, AndroidApiClass> predefinedApiTypeLookup) {
     this.appView = appView;
-    this.apiTypeLookup = apiTypeLookup;
+    androidApiLevelDatabase = new AndroidApiLevelDatabaseImpl(predefinedApiTypeLookup);
     desugaredLibraryConfiguration = appView.options().desugaredLibraryConfiguration;
   }
 
@@ -51,16 +42,16 @@
     }
     // The apiTypeLookup is build lazily except for the mocked api types that we define in tests
     // externally.
-    ConcurrentHashMap<DexType, AndroidApiClass> apiTypeLookup = new ConcurrentHashMap<>();
+    HashMap<DexType, AndroidApiClass> predefinedApiTypeLookup = new HashMap<>();
     appView
         .options()
         .apiModelingOptions()
         .visitMockedApiReferences(
             (classReference, androidApiClass) ->
-                apiTypeLookup.put(
+                predefinedApiTypeLookup.put(
                     appView.dexItemFactory().createType(classReference.getDescriptor()),
                     androidApiClass));
-    return new AndroidApiReferenceLevelCache(appView, apiTypeLookup);
+    return new AndroidApiReferenceLevelCache(appView, predefinedApiTypeLookup);
   }
 
   public AndroidApiLevel lookupMax(DexReference reference, AndroidApiLevel minApiLevel) {
@@ -87,73 +78,9 @@
       // of the program.
       return appView.options().minApiLevel;
     }
-    AndroidApiClass androidApiClass =
-        apiTypeLookup.computeIfAbsent(
-            contextType, type -> AndroidApiDatabaseBuilder.buildClass(type.asClassReference()));
-    if (androidApiClass == null) {
-      // This is a library class but we have no api model for it. This happens if using an older
-      // version of R8 to compile a new target. We simply have to disallow inlining of methods
-      // that has such references.
-      return AndroidApiLevel.UNKNOWN;
-    }
-    if (reference.isDexType()) {
-      return androidApiClass.getApiLevel();
-    }
-    return androidApiClass.getMemberCount() > BUILD_CACHE_TRESHOLD
-        ? findMemberByCaching(reference, androidApiClass)
-        : findMemberByIteration(reference.asDexMember(), androidApiClass);
-  }
-
-  private AndroidApiLevel findMemberByIteration(
-      DexMember<?, ?> reference, AndroidApiClass apiClass) {
-    DexItemFactory factory = appView.dexItemFactory();
-    // Similar to the case for api classes we are unable to find, if the member
-    // is unknown we have to be conservative.
-    Box<AndroidApiLevel> apiLevelBox = new Box<>(AndroidApiLevel.UNKNOWN);
-    reference.apply(
-        field ->
-            apiClass.visitFields(
-                (fieldReference, apiLevel) -> {
-                  if (factory.createField(fieldReference) == field) {
-                    apiLevelBox.set(apiLevel);
-                    return TraversalContinuation.BREAK;
-                  }
-                  return TraversalContinuation.CONTINUE;
-                }),
-        method ->
-            apiClass.visitMethods(
-                (methodReference, apiLevel) -> {
-                  if (factory.createMethod(methodReference) == method) {
-                    apiLevelBox.set(apiLevel);
-                    return TraversalContinuation.BREAK;
-                  }
-                  return TraversalContinuation.CONTINUE;
-                }));
-    return apiLevelBox.get();
-  }
-
-  private AndroidApiLevel findMemberByCaching(DexReference reference, AndroidApiClass apiClass) {
-    buildCacheForMembers(reference.getContextType(), apiClass);
-    return apiMemberLookup.getOrDefault(reference, AndroidApiLevel.UNKNOWN);
-  }
-
-  private void buildCacheForMembers(DexType context, AndroidApiClass apiClass) {
-    assert apiClass.getMemberCount() > BUILD_CACHE_TRESHOLD;
-    // Use the context type as a token for us having build a cache for it.
-    if (apiMemberLookup.containsKey(context)) {
-      return;
-    }
-    DexItemFactory factory = appView.dexItemFactory();
-    apiClass.visitFields(
-        (fieldReference, apiLevel) -> {
-          apiMemberLookup.put(factory.createField(fieldReference), apiLevel);
-          return TraversalContinuation.CONTINUE;
-        });
-    apiClass.visitMethods(
-        (methodReference, apiLevel) -> {
-          apiMemberLookup.put(factory.createMethod(methodReference), apiLevel);
-          return TraversalContinuation.CONTINUE;
-        });
-    apiMemberLookup.put(context, AndroidApiLevel.UNKNOWN);
+    return reference.apply(
+        androidApiLevelDatabase::getTypeApiLevel,
+        androidApiLevelDatabase::getFieldApiLevel,
+        androidApiLevelDatabase::getMethodApiLevel);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/CfVersion.java b/src/main/java/com/android/tools/r8/cf/CfVersion.java
index 5b877b8..b23d44d 100644
--- a/src/main/java/com/android/tools/r8/cf/CfVersion.java
+++ b/src/main/java/com/android/tools/r8/cf/CfVersion.java
@@ -36,6 +36,8 @@
   public static final CfVersion V15_PREVIEW = new CfVersion(Opcodes.V15 | Opcodes.V_PREVIEW);
   public static final CfVersion V16 = new CfVersion(Opcodes.V16);
   public static final CfVersion V16_PREVIEW = new CfVersion(Opcodes.V16 | Opcodes.V_PREVIEW);
+  public static final CfVersion V17 = new CfVersion(Opcodes.V17);
+  public static final CfVersion V17_PREVIEW = new CfVersion(Opcodes.V17 | Opcodes.V_PREVIEW);
 
   private final int version;
 
@@ -55,7 +57,8 @@
     CfVersion.V13,
     CfVersion.V14,
     CfVersion.V15,
-    CfVersion.V16
+    CfVersion.V16,
+    CfVersion.V17
   };
 
   // Private constructor in case we want to canonicalize versions.
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 4050961..08e18ca4 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -331,7 +331,6 @@
     // as there was a dex resource.
     private boolean hasReadProgramResourceFromCf = false;
     private boolean hasReadProgramResourceFromDex = false;
-    private boolean hasReadProgramRecord = false;
 
     ClassReader(ExecutorService executorService, List<Future<?>> futures) {
       this.executorService = executorService;
@@ -340,7 +339,9 @@
 
     public DexApplicationReadFlags getDexApplicationReadFlags() {
       return new DexApplicationReadFlags(
-          hasReadProgramResourceFromDex, hasReadProgramResourceFromCf, hasReadProgramRecord);
+          hasReadProgramResourceFromDex,
+          hasReadProgramResourceFromCf,
+          application.hasReadRecordReferenceFromProgramClass());
     }
 
     private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
@@ -382,15 +383,7 @@
       }
       hasReadProgramResourceFromCf = true;
       JarClassFileReader<DexProgramClass> reader =
-          new JarClassFileReader<>(
-              application,
-              clazz -> {
-                classes.add(clazz);
-                if (clazz.isRecord()) {
-                  hasReadProgramRecord = true;
-                }
-              },
-              PROGRAM);
+          new JarClassFileReader<>(application, classes::add, PROGRAM);
       // Read classes in parallel.
       for (ProgramResource input : classSources) {
         futures.add(
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index dc8e0bb..12a5e8d 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -794,15 +794,23 @@
     return builder.resolve(clazz);
   }
 
-  // Non-private lookup (ie, not resolution) to find interface targets.
-  DexClassAndMethod lookupMaximallySpecificTarget(DexClass clazz, DexMethod method) {
-    MaximallySpecificMethodsBuilder builder = new MaximallySpecificMethodsBuilder();
-    resolveMethodStep3Helper(method.getProto(), method.getName(), clazz, builder);
-    return builder.lookup();
+  MethodResolutionResult resolveMaximallySpecificTarget(DexClass clazz, DexMethod method) {
+    return resolveMaximallySpecificTargetHelper(clazz, method).resolve(clazz);
   }
 
-  // Non-private lookup (ie, not resolution) to find interface targets.
-  DexClassAndMethod lookupMaximallySpecificTarget(LambdaDescriptor lambda, DexMethod method) {
+  private MaximallySpecificMethodsBuilder resolveMaximallySpecificTargetHelper(
+      DexClass clazz, DexMethod method) {
+    MaximallySpecificMethodsBuilder builder = new MaximallySpecificMethodsBuilder();
+    resolveMethodStep3Helper(method.getProto(), method.getName(), clazz, builder);
+    return builder;
+  }
+
+  MethodResolutionResult resolveMaximallySpecificTarget(LambdaDescriptor lambda, DexMethod method) {
+    return resolveMaximallySpecificTargetHelper(lambda, method).internalResolve(null);
+  }
+
+  private MaximallySpecificMethodsBuilder resolveMaximallySpecificTargetHelper(
+      LambdaDescriptor lambda, DexMethod method) {
     MaximallySpecificMethodsBuilder builder = new MaximallySpecificMethodsBuilder();
     resolveMethodStep3Helper(
         method.getProto(),
@@ -810,7 +818,17 @@
         dexItemFactory().objectType,
         lambda.interfaces,
         builder);
-    return builder.lookup();
+    return builder;
+  }
+
+  // Non-private lookup (ie, not resolution) to find interface targets.
+  DexClassAndMethod lookupMaximallySpecificTarget(DexClass clazz, DexMethod method) {
+    return resolveMaximallySpecificTargetHelper(clazz, method).lookup();
+  }
+
+  // Non-private lookup (ie, not resolution) to find interface targets.
+  DexClassAndMethod lookupMaximallySpecificTarget(LambdaDescriptor lambda, DexMethod method) {
+    return resolveMaximallySpecificTargetHelper(lambda, method).lookup();
   }
 
   /** Helper method that builds the set of maximally specific methods. */
@@ -1059,10 +1077,7 @@
     }
 
     DexClassAndMethod lookup() {
-      SingleResolutionResult result = internalResolve(null).asSingleResolution();
-      return result != null
-          ? DexClassAndMethod.create(result.getResolvedHolder(), result.getResolvedMethod())
-          : null;
+      return internalResolve(null).getResolutionPair();
     }
 
     MethodResolutionResult resolve(DexClass initialResolutionHolder) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 72cddc2..0225c1d 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -97,7 +97,7 @@
   private InitializedClassesInInstanceMethods initializedClassesInInstanceMethods;
   private HorizontallyMergedClasses horizontallyMergedClasses = HorizontallyMergedClasses.empty();
   private VerticallyMergedClasses verticallyMergedClasses;
-  private EnumDataMap unboxedEnums = EnumDataMap.empty();
+  private EnumDataMap unboxedEnums = null;
   // TODO(b/169115389): Remove
   private Set<DexMethod> cfByteCodePassThrough = ImmutableSet.of();
   private Map<DexType, DexValueString> sourceDebugExtensions = new IdentityHashMap<>();
@@ -575,12 +575,16 @@
     testing().verticallyMergedClassesConsumer.accept(dexItemFactory(), verticallyMergedClasses);
   }
 
+  public boolean hasUnboxedEnums() {
+    return unboxedEnums != null;
+  }
+
   public EnumDataMap unboxedEnums() {
-    return unboxedEnums;
+    return hasUnboxedEnums() ? unboxedEnums : EnumDataMap.empty();
   }
 
   public void setUnboxedEnums(EnumDataMap unboxedEnums) {
-    assert this.unboxedEnums.isEmpty();
+    assert !hasUnboxedEnums();
     this.unboxedEnums = unboxedEnums;
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
index d5089a7..bb3bcf8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
@@ -9,15 +9,15 @@
 
   private final boolean hasReadProgramClassFromDex;
   private final boolean hasReadProgramClassFromCf;
-  private final boolean hasReadProgramRecord;
+  private final boolean hasReadRecordReferenceFromProgramClass;
 
   public DexApplicationReadFlags(
       boolean hasReadProgramClassFromDex,
       boolean hasReadProgramClassFromCf,
-      boolean hasReadProgramRecord) {
+      boolean hasReadRecordReferenceFromProgramClass) {
     this.hasReadProgramClassFromDex = hasReadProgramClassFromDex;
     this.hasReadProgramClassFromCf = hasReadProgramClassFromCf;
-    this.hasReadProgramRecord = hasReadProgramRecord;
+    this.hasReadRecordReferenceFromProgramClass = hasReadRecordReferenceFromProgramClass;
   }
 
   public boolean hasReadProgramClassFromCf() {
@@ -28,7 +28,7 @@
     return hasReadProgramClassFromDex;
   }
 
-  public boolean hasReadProgramRecord() {
-    return hasReadProgramRecord;
+  public boolean hasReadRecordReferenceFromProgramClass() {
+    return hasReadRecordReferenceFromProgramClass;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index b4a382f..82f09f8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -1675,6 +1675,11 @@
       return this;
     }
 
+    public Builder setOptimizationInfo(MethodOptimizationInfo optimizationInfo) {
+      this.optimizationInfo = optimizationInfo;
+      return this;
+    }
+
     public Builder modifyOptimizationInfo(
         BiConsumer<DexEncodedMethod, MutableMethodOptimizationInfo> consumer) {
       return addBuildConsumer(
diff --git a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
index 67e7b5b..047be58 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
+import com.android.tools.r8.ir.desugar.records.RecordRewriter;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
@@ -26,6 +27,8 @@
   private final ConcurrentHashMap<String, DexString> stringCache = new ConcurrentHashMap<>();
   private final Map<String, String> typeDescriptorMap;
 
+  private boolean hasReadRecordReferenceFromProgramClass = false;
+
   public JarApplicationReader(InternalOptions options) {
     this.options = options;
     typeDescriptorMap = ApplicationReaderMap.getDescriptorMap(options);
@@ -149,4 +152,24 @@
   public Type getReturnType(final String methodDescriptor) {
     return getAsmType(DescriptorUtils.getReturnTypeDescriptor(methodDescriptor));
   }
+
+  public void setHasReadRecordReferenceFromProgramClass() {
+    hasReadRecordReferenceFromProgramClass = true;
+  }
+
+  public boolean hasReadRecordReferenceFromProgramClass() {
+    return hasReadRecordReferenceFromProgramClass;
+  }
+
+  public void checkFieldForRecord(DexField dexField) {
+    if (options.shouldDesugarRecords() && RecordRewriter.refersToRecord(dexField, getFactory())) {
+      setHasReadRecordReferenceFromProgramClass();
+    }
+  }
+
+  public void checkMethodForRecord(DexMethod dexMethod) {
+    if (options.shouldDesugarRecords() && RecordRewriter.refersToRecord(dexMethod, getFactory())) {
+      setHasReadRecordReferenceFromProgramClass();
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 4c6c165..296f792 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -512,9 +512,13 @@
     }
 
     private void checkRecord() {
+      if (!application.options.shouldDesugarRecords()) {
+        return;
+      }
       if (!accessFlags.isRecord()) {
         return;
       }
+      application.setHasReadRecordReferenceFromProgramClass();
       // TODO(b/169645628): Change this logic if we start stripping the record components.
       // Another approach would be to mark a bit in fields that are record components instead.
       String message = "Records are expected to have one record component per instance field.";
@@ -661,6 +665,7 @@
     public void visitEnd() {
       FieldAccessFlags flags = createFieldAccessFlags(access);
       DexField dexField = parent.application.getField(parent.type, name, desc);
+      parent.application.checkFieldForRecord(dexField);
       Wrapper<DexField> signature = FieldSignatureEquivalence.get().wrap(dexField);
       if (parent.fieldSignatures.add(signature)) {
         DexAnnotationSet annotationSet =
@@ -878,6 +883,7 @@
     @Override
     public void visitEnd() {
       InternalOptions options = parent.application.options;
+      parent.application.checkMethodForRecord(method);
       if (!flags.isAbstract() && !flags.isNative() && classRequiresCode()) {
         code = new LazyCfCode(method, parent.origin, parent.context, parent.application);
       }
diff --git a/src/main/java/com/android/tools/r8/graph/LookupResult.java b/src/main/java/com/android/tools/r8/graph/LookupResult.java
index 17d7f55..bc85814 100644
--- a/src/main/java/com/android/tools/r8/graph/LookupResult.java
+++ b/src/main/java/com/android/tools/r8/graph/LookupResult.java
@@ -5,9 +5,10 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.LookupResult.LookupResultSuccess.LookupResultCollectionState;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.function.Consumer;
 
 public abstract class LookupResult {
@@ -28,18 +29,23 @@
     return null;
   }
 
-  public final void forEach(Consumer<LookupTarget> onTarget) {
-    forEach(onTarget::accept, onTarget::accept);
+  public final void forEach(Consumer<? super LookupTarget> onTarget) {
+    forEach(onTarget, onTarget);
   }
 
   public abstract void forEach(
-      Consumer<DexClassAndMethod> onMethodTarget, Consumer<LookupLambdaTarget> onLambdaTarget);
+      Consumer<? super DexClassAndMethod> onMethodTarget,
+      Consumer<? super LookupLambdaTarget> onLambdaTarget);
+
+  public abstract void forEachFailureDependency(
+      Consumer<? super DexEncodedMethod> methodCausingFailureConsumer);
 
   public static LookupResultSuccess createResult(
-      Map<DexEncodedMethod, DexClassAndMethod> methodTargets,
+      DexClassAndMethodSet methodTargets,
       List<LookupLambdaTarget> lambdaTargets,
+      List<DexEncodedMethod> methodsCausingFailure,
       LookupResultCollectionState state) {
-    return new LookupResultSuccess(methodTargets, lambdaTargets, state);
+    return new LookupResultSuccess(methodTargets, lambdaTargets, methodsCausingFailure, state);
   }
 
   public static LookupResultFailure createFailedResult() {
@@ -54,23 +60,31 @@
 
     private static final LookupResultSuccess EMPTY_INSTANCE =
         new LookupResultSuccess(
-            Collections.emptyMap(),
+            DexClassAndMethodSet.empty(),
+            Collections.emptyList(),
             Collections.emptyList(),
             LookupResultCollectionState.Incomplete);
 
-    private final Map<DexEncodedMethod, DexClassAndMethod> methodTargets;
+    private final DexClassAndMethodSet methodTargets;
     private final List<LookupLambdaTarget> lambdaTargets;
+    private final List<DexEncodedMethod> methodsCausingFailure;
     private LookupResultCollectionState state;
 
     private LookupResultSuccess(
-        Map<DexEncodedMethod, DexClassAndMethod> methodTargets,
+        DexClassAndMethodSet methodTargets,
         List<LookupLambdaTarget> lambdaTargets,
+        List<DexEncodedMethod> methodsCausingFailure,
         LookupResultCollectionState state) {
       this.methodTargets = methodTargets;
       this.lambdaTargets = lambdaTargets;
+      this.methodsCausingFailure = methodsCausingFailure;
       this.state = state;
     }
 
+    public static Builder builder() {
+      return new Builder();
+    }
+
     public boolean isEmpty() {
       return methodTargets.isEmpty() && lambdaTargets.isEmpty();
     }
@@ -85,14 +99,21 @@
 
     @Override
     public void forEach(
-        Consumer<DexClassAndMethod> onMethodTarget, Consumer<LookupLambdaTarget> onLambdaTarget) {
-      methodTargets.forEach((ignore, method) -> onMethodTarget.accept(method));
+        Consumer<? super DexClassAndMethod> onMethodTarget,
+        Consumer<? super LookupLambdaTarget> onLambdaTarget) {
+      methodTargets.forEach(onMethodTarget);
       lambdaTargets.forEach(onLambdaTarget);
     }
 
+    @Override
+    public void forEachFailureDependency(
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
+      methodsCausingFailure.forEach(methodCausingFailureConsumer);
+    }
+
     public boolean contains(DexEncodedMethod method) {
       // Containment of a method in the lookup results only pertains to the method targets.
-      return methodTargets.containsKey(method);
+      return methodTargets.contains(method);
     }
 
     @Override
@@ -124,7 +145,7 @@
       }
       // TODO(b/150932978): Check lambda targets implementation methods.
       if (methodTargets.size() == 1) {
-        return methodTargets.values().iterator().next();
+        return methodTargets.iterator().next();
       } else if (lambdaTargets.size() == 1) {
         return lambdaTargets.get(0);
       }
@@ -135,6 +156,38 @@
       Complete,
       Incomplete,
     }
+
+    public static class Builder {
+
+      private final DexClassAndMethodSet methodTargets = DexClassAndMethodSet.create();
+      private final List<LookupLambdaTarget> lambdaTargets = new ArrayList<>();
+      private final List<DexEncodedMethod> methodsCausingFailure = new ArrayList<>();
+      private LookupResultCollectionState state;
+
+      public Builder addMethodTarget(DexClassAndMethod methodTarget) {
+        methodTargets.add(methodTarget);
+        return this;
+      }
+
+      public Builder addLambdaTarget(LookupLambdaTarget lambdaTarget) {
+        lambdaTargets.add(lambdaTarget);
+        return this;
+      }
+
+      public Builder addMethodCausingFailure(DexEncodedMethod methodCausingFailure) {
+        methodsCausingFailure.add(methodCausingFailure);
+        return this;
+      }
+
+      public Builder setState(LookupResultCollectionState state) {
+        this.state = state;
+        return this;
+      }
+
+      public LookupResultSuccess build() {
+        return new LookupResultSuccess(methodTargets, lambdaTargets, methodsCausingFailure, state);
+      }
+    }
   }
 
   public static class LookupResultFailure extends LookupResult {
@@ -157,8 +210,15 @@
 
     @Override
     public void forEach(
-        Consumer<DexClassAndMethod> onMethodTarget, Consumer<LookupLambdaTarget> onLambdaTarget) {
+        Consumer<? super DexClassAndMethod> onMethodTarget,
+        Consumer<? super LookupLambdaTarget> onLambdaTarget) {
       // Nothing to iterate for a failed lookup.
     }
+
+    @Override
+    public void forEachFailureDependency(
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
+      // TODO: record and emit failure dependencies.
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
index c2cb1d4..c1ff02b 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
+
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
 import com.android.tools.r8.graph.LookupResult.LookupResultSuccess.LookupResultCollectionState;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -10,12 +13,9 @@
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.OptionalBool;
-import java.util.ArrayList;
+import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
@@ -84,6 +84,10 @@
     return null;
   }
 
+  public DexEncodedMethod getResolvedMethod() {
+    return null;
+  }
+
   /** Short-hand to get the single resolution method if resolution finds it, null otherwise. */
   public final DexEncodedMethod getSingleTarget() {
     return isSingleResolution() ? asSingleResolution().getResolvedMethod() : null;
@@ -148,7 +152,9 @@
       DexClass dynamicInstance, AppInfoWithClassHierarchy appInfo);
 
   public abstract LookupTarget lookupVirtualDispatchTarget(
-      LambdaDescriptor lambdaInstance, AppInfoWithClassHierarchy appInfo);
+      LambdaDescriptor lambdaInstance,
+      AppInfoWithClassHierarchy appInfo,
+      Consumer<? super DexEncodedMethod> methodCausingFailureConsumer);
 
   /** Result for a resolution that succeeds with a known declaration/definition. */
   public static class SingleResolutionResult extends MethodResolutionResult
@@ -187,6 +193,7 @@
       return resolvedMethod;
     }
 
+    @Override
     public DexEncodedMethod getResolvedMethod() {
       return resolvedMethod;
     }
@@ -431,46 +438,47 @@
         boolean isIncomplete =
             pinnedPredicate.isPinned(resolvedHolder) && pinnedPredicate.isPinned(resolvedMethod);
         return LookupResult.createResult(
-            Collections.singletonMap(
-                resolvedMethod, DexClassAndMethod.create(resolvedHolder, resolvedMethod)),
+            DexClassAndMethodSet.create(getResolutionPair()),
+            Collections.emptyList(),
             Collections.emptyList(),
             isIncomplete
                 ? LookupResultCollectionState.Incomplete
                 : LookupResultCollectionState.Complete);
       }
       assert resolvedMethod.isNonPrivateVirtualMethod();
-      Map<DexEncodedMethod, DexClassAndMethod> methodTargets = new IdentityHashMap<>();
-      List<LookupLambdaTarget> lambdaTargets = new ArrayList<>();
+      LookupResultSuccess.Builder resultBuilder = LookupResultSuccess.builder();
       LookupCompletenessHelper incompleteness = new LookupCompletenessHelper(pinnedPredicate);
       instantiatedInfo.forEachInstantiatedSubType(
           initialResolutionHolder.type,
           subClass -> {
             incompleteness.checkClass(subClass);
             DexClassAndMethod dexClassAndMethod =
-                lookupVirtualDispatchTarget(subClass, appInfo, resolvedHolder.type);
+                lookupVirtualDispatchTarget(
+                    subClass, appInfo, resolvedHolder.type, resultBuilder::addMethodCausingFailure);
             if (dexClassAndMethod != null) {
               incompleteness.checkDexClassAndMethod(dexClassAndMethod);
               addVirtualDispatchTarget(
-                  dexClassAndMethod, resolvedHolder.isInterface(), methodTargets);
+                  dexClassAndMethod, resolvedHolder.isInterface(), resultBuilder);
             }
           },
           lambda -> {
             assert resolvedHolder.isInterface()
                 || resolvedHolder.type == appInfo.dexItemFactory().objectType;
-            LookupTarget target = lookupVirtualDispatchTarget(lambda, appInfo);
+            LookupTarget target =
+                lookupVirtualDispatchTarget(
+                    lambda, appInfo, resultBuilder::addMethodCausingFailure);
             if (target != null) {
               if (target.isLambdaTarget()) {
-                lambdaTargets.add(target.asLambdaTarget());
+                resultBuilder.addLambdaTarget(target.asLambdaTarget());
               } else {
                 addVirtualDispatchTarget(
-                    target.asMethodTarget(), resolvedHolder.isInterface(), methodTargets);
+                    target.asMethodTarget(), resolvedHolder.isInterface(), resultBuilder);
               }
             }
           });
-      return LookupResult.createResult(
-          methodTargets,
-          lambdaTargets,
-          incompleteness.computeCollectionState(resolvedMethod.getReference(), appInfo));
+      return resultBuilder
+          .setState(incompleteness.computeCollectionState(resolvedMethod.getReference(), appInfo))
+          .build();
     }
 
     @Override
@@ -532,7 +540,7 @@
     private static void addVirtualDispatchTarget(
         DexClassAndMethod target,
         boolean holderIsInterface,
-        Map<DexEncodedMethod, DexClassAndMethod> result) {
+        LookupResultSuccess.Builder resultBuilder) {
       DexEncodedMethod targetMethod = target.getDefinition();
       assert !targetMethod.isPrivateMethod();
       if (holderIsInterface) {
@@ -559,17 +567,17 @@
         //   }
         //
         if (targetMethod.isDefaultMethod()) {
-          result.putIfAbsent(targetMethod, target);
+          resultBuilder.addMethodTarget(target);
         }
         // Default methods are looked up when looking at a specific subtype that does not override
         // them. Otherwise, we would look up default methods that are actually never used.
         // However, we have to add bridge methods, otherwise we can remove a bridge that will be
         // used.
         if (!targetMethod.accessFlags.isAbstract() && targetMethod.accessFlags.isBridge()) {
-          result.putIfAbsent(targetMethod, target);
+          resultBuilder.addMethodTarget(target);
         }
       } else {
-        result.putIfAbsent(targetMethod, target);
+        resultBuilder.addMethodTarget(target);
       }
     }
 
@@ -583,18 +591,21 @@
         InstantiatedObject instance, AppInfoWithClassHierarchy appInfo) {
       return instance.isClass()
           ? lookupVirtualDispatchTarget(instance.asClass(), appInfo)
-          : lookupVirtualDispatchTarget(instance.asLambda(), appInfo);
+          : lookupVirtualDispatchTarget(instance.asLambda(), appInfo, emptyConsumer());
     }
 
     @Override
     public DexClassAndMethod lookupVirtualDispatchTarget(
         DexClass dynamicInstance, AppInfoWithClassHierarchy appInfo) {
-      return lookupVirtualDispatchTarget(dynamicInstance, appInfo, initialResolutionHolder.type);
+      return lookupVirtualDispatchTarget(
+          dynamicInstance, appInfo, initialResolutionHolder.type, emptyConsumer());
     }
 
     @Override
     public LookupTarget lookupVirtualDispatchTarget(
-        LambdaDescriptor lambdaInstance, AppInfoWithClassHierarchy appInfo) {
+        LambdaDescriptor lambdaInstance,
+        AppInfoWithClassHierarchy appInfo,
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       if (lambdaInstance.getMainMethod().match(resolvedMethod)) {
         DexMethod methodReference = lambdaInstance.implHandle.asMethod();
         DexClass holder = appInfo.definitionForHolder(methodReference);
@@ -605,11 +616,15 @@
         }
         return new LookupLambdaTarget(lambdaInstance, method);
       }
-      return lookupMaximallySpecificDispatchTarget(lambdaInstance, appInfo);
+      return lookupMaximallySpecificDispatchTarget(
+          lambdaInstance, appInfo, methodCausingFailureConsumer);
     }
 
     private DexClassAndMethod lookupVirtualDispatchTarget(
-        DexClass dynamicInstance, AppInfoWithClassHierarchy appInfo, DexType resolutionHolder) {
+        DexClass dynamicInstance,
+        AppInfoWithClassHierarchy appInfo,
+        DexType resolutionHolder,
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       assert appInfo.isSubtype(dynamicInstance.type, resolutionHolder)
           : dynamicInstance.type + " is not a subtype of " + resolutionHolder;
       // TODO(b/148591377): Enable this assertion.
@@ -618,7 +633,7 @@
       if (resolvedMethod.isPrivateMethod()) {
         // If the resolved reference is private there is no dispatch.
         // This is assuming that the method is accessible, which implies self/nest access.
-        return DexClassAndMethod.create(resolvedHolder, resolvedMethod);
+        return getResolutionPair();
       }
       boolean allowPackageBlocked = resolvedMethod.accessFlags.isPackagePrivate();
       DexClass current = dynamicInstance;
@@ -645,17 +660,46 @@
       if (!resolvedHolder.isInterface()) {
         return null;
       }
-      return lookupMaximallySpecificDispatchTarget(dynamicInstance, appInfo);
+      return lookupMaximallySpecificDispatchTarget(
+          dynamicInstance, appInfo, methodCausingFailureConsumer);
     }
 
     private DexClassAndMethod lookupMaximallySpecificDispatchTarget(
-        DexClass dynamicInstance, AppInfoWithClassHierarchy appInfo) {
-      return appInfo.lookupMaximallySpecificMethod(dynamicInstance, resolvedMethod.getReference());
+        DexClass dynamicInstance,
+        AppInfoWithClassHierarchy appInfo,
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
+      MethodResolutionResult maximallySpecificResolutionResult =
+          appInfo.resolveMaximallySpecificTarget(dynamicInstance, resolvedMethod.getReference());
+      if (maximallySpecificResolutionResult.isSingleResolution()) {
+        return maximallySpecificResolutionResult.getResolutionPair();
+      }
+      if (maximallySpecificResolutionResult.isFailedResolution()) {
+        maximallySpecificResolutionResult
+            .asFailedResolution()
+            .forEachFailureDependency(methodCausingFailureConsumer);
+        return null;
+      }
+      assert maximallySpecificResolutionResult.isArrayCloneMethodResult();
+      return null;
     }
 
     private DexClassAndMethod lookupMaximallySpecificDispatchTarget(
-        LambdaDescriptor lambdaDescriptor, AppInfoWithClassHierarchy appInfo) {
-      return appInfo.lookupMaximallySpecificMethod(lambdaDescriptor, resolvedMethod.getReference());
+        LambdaDescriptor lambdaDescriptor,
+        AppInfoWithClassHierarchy appInfo,
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
+      MethodResolutionResult maximallySpecificResolutionResult =
+          appInfo.resolveMaximallySpecificTarget(lambdaDescriptor, resolvedMethod.getReference());
+      if (maximallySpecificResolutionResult.isSingleResolution()) {
+        return maximallySpecificResolutionResult.getResolutionPair();
+      }
+      if (maximallySpecificResolutionResult.isFailedResolution()) {
+        maximallySpecificResolutionResult
+            .asFailedResolution()
+            .forEachFailureDependency(methodCausingFailureConsumer);
+        return null;
+      }
+      assert maximallySpecificResolutionResult.isArrayCloneMethodResult();
+      return null;
     }
 
     /**
@@ -773,7 +817,9 @@
 
     @Override
     public DexClassAndMethod lookupVirtualDispatchTarget(
-        LambdaDescriptor lambdaInstance, AppInfoWithClassHierarchy appInfo) {
+        LambdaDescriptor lambdaInstance,
+        AppInfoWithClassHierarchy appInfo,
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       return null;
     }
   }
@@ -823,7 +869,8 @@
       return this;
     }
 
-    public void forEachFailureDependency(Consumer<DexEncodedMethod> methodCausingFailureConsumer) {
+    public void forEachFailureDependency(
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       // Default failure has no dependencies.
     }
 
@@ -871,7 +918,8 @@
     }
 
     @Override
-    public void forEachFailureDependency(Consumer<DexEncodedMethod> methodCausingFailureConsumer) {
+    public void forEachFailureDependency(
+        Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       this.methodsCausingError.forEach(methodCausingFailureConsumer);
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
index 265d050..4c869ef 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
@@ -107,7 +107,7 @@
    */
   @Override
   public BitSet fixupNonNullParamOnNormalExits(BitSet nonNullParamOnNormalExits) {
-    return fixupNonNullParamInfo(nonNullParamOnNormalExits);
+    return fixupArgumentInfo(nonNullParamOnNormalExits);
   }
 
   /**
@@ -116,27 +116,7 @@
    */
   @Override
   public BitSet fixupNonNullParamOrThrow(BitSet nonNullParamOrThrow) {
-    return fixupNonNullParamInfo(nonNullParamOrThrow);
-  }
-
-  private BitSet fixupNonNullParamInfo(BitSet nonNullParamInfo) {
-    if (getArgumentInfoCollection().isEmpty() || nonNullParamInfo == null) {
-      return nonNullParamInfo;
-    }
-    int n = nonNullParamInfo.length();
-    BitSet rewrittenNonNullParamOnNormalExits = new BitSet(n);
-    for (int argumentIndex = 0; argumentIndex < n; argumentIndex++) {
-      if (!nonNullParamInfo.get(argumentIndex)) {
-        continue;
-      }
-      ArgumentInfo argumentInfo = getArgumentInfoCollection().getArgumentInfo(argumentIndex);
-      if (argumentInfo.isRemovedArgumentInfo() || argumentInfo.isRewrittenTypeInfo()) {
-        continue;
-      }
-      rewrittenNonNullParamOnNormalExits.set(
-          getArgumentInfoCollection().getNewArgumentIndex(argumentIndex));
-    }
-    return rewrittenNonNullParamOnNormalExits.isEmpty() ? null : rewrittenNonNullParamOnNormalExits;
+    return fixupArgumentInfo(nonNullParamOrThrow);
   }
 
   /**
@@ -167,4 +147,33 @@
     }
     return constraint.fixupAfterParametersChanged(appView, getArgumentInfoCollection(), factory);
   }
+
+  /**
+   * Function for rewriting the unused arguments on a piece of method optimization info after
+   * prototype changes were made.
+   */
+  @Override
+  public BitSet fixupUnusedArguments(BitSet unusedArguments) {
+    return fixupArgumentInfo(unusedArguments);
+  }
+
+  private BitSet fixupArgumentInfo(BitSet bitSet) {
+    if (getArgumentInfoCollection().isEmpty() || bitSet == null) {
+      return bitSet;
+    }
+    int n = bitSet.length();
+    BitSet rewrittenNonNullParamOnNormalExits = new BitSet(n);
+    for (int argumentIndex = 0; argumentIndex < n; argumentIndex++) {
+      if (!bitSet.get(argumentIndex)) {
+        continue;
+      }
+      ArgumentInfo argumentInfo = getArgumentInfoCollection().getArgumentInfo(argumentIndex);
+      if (argumentInfo.isRemovedArgumentInfo() || argumentInfo.isRewrittenTypeInfo()) {
+        continue;
+      }
+      rewrittenNonNullParamOnNormalExits.set(
+          getArgumentInfoCollection().getNewArgumentIndex(argumentIndex));
+    }
+    return rewrittenNonNullParamOnNormalExits.isEmpty() ? null : rewrittenNonNullParamOnNormalExits;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
index 58a44a3..190e1b5 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
@@ -37,6 +37,10 @@
     return createSingleNumberValue(0);
   }
 
+  public SingleNumberValue createZeroValue() {
+    return createSingleNumberValue(0);
+  }
+
   public SingleStringValue createSingleStringValue(DexString string) {
     return singleStringValues.computeIfAbsent(string, SingleStringValue::new);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
index 8230766..33829cd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
@@ -18,9 +18,11 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
+import com.android.tools.r8.ir.optimize.enums.EnumUnboxerImpl;
 
 /**
- * Special instruction used by {@link com.android.tools.r8.ir.optimize.enums.EnumUnboxer}.
+ * Special instruction used by {@link EnumUnboxerImpl}.
  *
  * <p>When applying the enum unboxer to the application, we move the class initializer of each
  * unboxed enum to its utility class, and change each {@link NewInstance} instruction that
@@ -32,11 +34,10 @@
  * code to type check until lens code rewriting, which replaces the {@link NewUnboxedEnumInstance}
  * instructions by {@link ConstNumber} instructions.
  *
- * <p>Note: The {@link NewUnboxedEnumInstance} is only used from {@link
- * com.android.tools.r8.ir.optimize.enums.EnumUnboxer#unboxEnums} until the execution of the {@link
- * com.android.tools.r8.ir.conversion.PostMethodProcessor}. There should be no instances of {@link
- * NewUnboxedEnumInstance} (nor {@link CfNewUnboxedEnum}, {@link DexNewUnboxedEnumInstance}) after
- * IR processing has finished.
+ * <p>Note: The {@link NewUnboxedEnumInstance} is only used from {@link EnumUnboxer#unboxEnums}
+ * until the execution of the {@link com.android.tools.r8.ir.conversion.PostMethodProcessor}. There
+ * should be no instances of {@link NewUnboxedEnumInstance} (nor {@link CfNewUnboxedEnum}, {@link
+ * DexNewUnboxedEnumInstance}) after IR processing has finished.
  */
 public class NewUnboxedEnumInstance extends Instruction {
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 5bde78e..a165209 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -78,7 +78,6 @@
 import com.android.tools.r8.ir.optimize.ReflectionOptimizer;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
-import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
 import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfoCollector;
@@ -239,7 +238,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumValueOptimizer = null;
-      this.enumUnboxer = null;
+      this.enumUnboxer = EnumUnboxer.empty();
       this.assumeInserter = null;
       return;
     }
@@ -267,7 +266,7 @@
           options.enableTreeShakingOfLibraryMethodOverrides
               ? new LibraryMethodOverrideAnalysis(appViewWithLiveness)
               : null;
-      this.enumUnboxer = options.enableEnumUnboxing ? new EnumUnboxer(appViewWithLiveness) : null;
+      this.enumUnboxer = EnumUnboxer.create(appViewWithLiveness);
       this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, enumUnboxer);
       this.inliner = new Inliner(appViewWithLiveness, lensCodeRewriter);
       this.outliner = Outliner.create(appViewWithLiveness);
@@ -306,7 +305,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumValueOptimizer = null;
-      this.enumUnboxer = null;
+      this.enumUnboxer = EnumUnboxer.empty();
     }
     this.stringSwitchRemover =
         options.isStringSwitchConversionEnabled()
@@ -640,10 +639,7 @@
           optimization.abandonCallSitePropagationForPinnedMethodsAndOverrides(
               executorService, timing);
         });
-    ConsumerUtils.acceptIfNotNull(
-        enumUnboxer,
-        enumUnboxer ->
-            enumUnboxer.initializeEnumUnboxingCandidates(graphLensForPrimaryOptimizationPass));
+    enumUnboxer.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
     ConsumerUtils.acceptIfNotNull(
         classStaticizer,
         classStaticizer ->
@@ -724,12 +720,8 @@
           .run(executorService, feedback, timing);
     }
 
-    if (enumUnboxer != null) {
-      outliner.rewriteWithLens();
-      enumUnboxer.unboxEnums(this, postMethodProcessorBuilder, executorService, feedback);
-    } else {
-      appView.setUnboxedEnums(EnumDataMap.empty());
-    }
+    outliner.rewriteWithLens();
+    enumUnboxer.unboxEnums(appView, this, postMethodProcessorBuilder, executorService, feedback);
 
     GraphLens graphLensForSecondaryOptimizationPass = appView.graphLens();
 
@@ -757,9 +749,7 @@
     }
     timing.end();
 
-    if (enumUnboxer != null) {
-      enumUnboxer.unsetRewriter();
-    }
+    enumUnboxer.unsetRewriter();
 
     // All the code that should be impacted by the lenses inserted between phase 1 and phase 2
     // have now been processed and rewritten, we clear code lens rewriting so that the class
@@ -848,9 +838,7 @@
     if (appView.options().protoShrinking().enableRemoveProtoEnumSwitchMap()) {
       appView.protoShrinker().protoEnumSwitchMapRemover.updateVisibleStaticFieldValues();
     }
-    if (enumUnboxer != null) {
-      enumUnboxer.updateEnumUnboxingCandidatesInfo();
-    }
+    enumUnboxer.updateEnumUnboxingCandidatesInfo();
     assert delayedOptimizationFeedback.noUpdatesLeft();
     onWaveDoneActions.forEach(com.android.tools.r8.utils.Action::execute);
     onWaveDoneActions = null;
@@ -1548,7 +1536,7 @@
     appView.withArgumentPropagator(
         argumentPropagator -> argumentPropagator.scan(method, code, methodProcessor, timing));
 
-    if (enumUnboxer != null && methodProcessor.isPrimaryMethodProcessor()) {
+    if (methodProcessor.isPrimaryMethodProcessor()) {
       enumUnboxer.analyzeEnums(code, conversionOptions);
     }
 
@@ -1593,9 +1581,7 @@
                 appView, code, classInitializerDefaultsResult, feedback, timing);
       }
     }
-    if (enumUnboxer != null) {
-      enumUnboxer.recordEnumState(method.getHolder(), staticFieldValues);
-    }
+    enumUnboxer.recordEnumState(method.getHolder(), staticFieldValues);
     if (appView.options().protoShrinking().enableRemoveProtoEnumSwitchMap()) {
       appView
           .protoShrinker()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index 13b7140..5219a50 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -66,6 +66,8 @@
   void setEnumUnboxerMethodClassification(
       ProgramMethod method, EnumUnboxerMethodClassification enumUnboxerMethodClassification);
 
+  void unsetEnumUnboxerMethodClassification(ProgramMethod method);
+
   void setInstanceInitializerInfoCollection(
       DexEncodedMethod method, InstanceInitializerInfoCollection instanceInitializerInfoCollection);
 
@@ -78,4 +80,6 @@
   void setSimpleInliningConstraint(ProgramMethod method, SimpleInliningConstraint constraint);
 
   void classInitializerMayBePostponed(DexEncodedMethod method);
+
+  void setUnusedArguments(ProgramMethod method, BitSet unusedArguments);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/AccessorMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/AccessorMethodSourceCode.java
index f07a802..8236617 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/AccessorMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/AccessorMethodSourceCode.java
@@ -4,20 +4,25 @@
 package com.android.tools.r8.ir.desugar;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 
 // Source code representing synthesized accessor method.
 
 public class AccessorMethodSourceCode {
 
-  public static CfCode build(LambdaClass lambda, DexMethod accessor) {
-    DexMethod target = lambda.descriptor.implHandle.asMethod();
+  public static CfCode build(
+      DexMethod target,
+      boolean isInterface,
+      MethodHandleType type,
+      DexMethod accessor,
+      AppView<?> appView) {
     ForwardMethodBuilder forwardMethodBuilder =
-        ForwardMethodBuilder.builder(lambda.appView.dexItemFactory()).setStaticSource(accessor);
-    boolean isInterface = lambda.descriptor.implHandle.isInterface;
-    switch (lambda.descriptor.implHandle.type) {
+        ForwardMethodBuilder.builder(appView.dexItemFactory()).setStaticSource(accessor);
+    switch (type) {
       case INVOKE_INSTANCE:
         {
           forwardMethodBuilder.setVirtualTarget(target, isInterface);
@@ -35,7 +40,7 @@
         }
       case INVOKE_CONSTRUCTOR:
         {
-          forwardMethodBuilder.setConstructorTarget(target, lambda.appView.dexItemFactory());
+          forwardMethodBuilder.setConstructorTarget(target, appView.dexItemFactory());
           break;
         }
       case INVOKE_INTERFACE:
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 2493dd3..929b5a8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -343,12 +343,13 @@
       for (ConstantDynamicClass constantDynamicClass : synthesizedConstantDynamicClasses) {
         constantDynamicClass.getConstantDynamicProgramClass().forEachProgramMethod(needsProcessing);
       }
-      synthesizedLambdaClasses.clear();
+      synthesizedConstantDynamicClasses.clear();
     }
 
     public boolean verifyNothingToFinalize() {
       assert pendingInvokeSpecialBridges.isEmpty();
       assert synthesizedLambdaClasses.isEmpty();
+      assert synthesizedConstantDynamicClasses.isEmpty();
       return true;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index c2d95ca..afedd24 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -312,14 +312,18 @@
           appView.appInfoForDesugaring().resolveMethod(implMethod, implHandle.isInterface);
       if (resolution.isFailedResolution()) {
         return new InvalidLambdaImplTarget(
-            implMethod, Type.STATIC, appView.dexItemFactory().icceType);
+            implMethod,
+            Type.STATIC,
+            appView.dexItemFactory().icceType,
+            descriptor.implHandle.isInterface);
       }
       SingleResolutionResult result = resolution.asSingleResolution();
       assert result.getResolvedMethod().isStatic();
       assert result.getResolvedHolder().isProgramClass();
       return new StaticLambdaImplTarget(
           new ProgramMethod(
-              result.getResolvedHolder().asProgramClass(), result.getResolvedMethod()));
+              result.getResolvedHolder().asProgramClass(), result.getResolvedMethod()),
+          descriptor.implHandle.isInterface);
     }
 
     assert implHandle.type.isInvokeDirect();
@@ -334,18 +338,24 @@
 
       DexProto newProto = appView.dexItemFactory().createProto(implProto.returnType, newParams);
       return new InterfaceLambdaImplTarget(
-          appView.dexItemFactory().createMethod(implMethod.holder, newProto, implMethod.name));
+          descriptor.implHandle.asMethod(),
+          descriptor.implHandle.isInterface,
+          appView.dexItemFactory().createMethod(implMethod.holder, newProto, implMethod.name),
+          appView);
     } else {
       // Otherwise we need to ensure the method can be reached publicly by virtual dispatch.
       // To avoid potential conflicts on the name of the lambda method once dispatch becomes virtual
       // we add the fully qualified method-holder name as suffix to the lambda-method name.
       return new InstanceLambdaImplTarget(
+          descriptor.implHandle.asMethod(),
+          descriptor.implHandle.isInterface,
           appView
               .dexItemFactory()
               .createMethod(
                   implMethod.holder,
                   implMethod.proto,
-                  appendFullyQualifiedHolderToMethodName(implMethod, appView.dexItemFactory())));
+                  appendFullyQualifiedHolderToMethodName(implMethod, appView.dexItemFactory())),
+          appView);
     }
   }
 
@@ -356,7 +366,8 @@
         descriptor.implHandle.type.isInvokeDirect();
 
     if (doesNotNeedAccessor(accessedFrom)) {
-      return new NoAccessorMethodTarget(Invoke.Type.VIRTUAL);
+      return new NoAccessorMethodTarget(
+          descriptor.implHandle.asMethod(), Type.VIRTUAL, descriptor.implHandle.isInterface);
     }
     // We need to generate an accessor method in `accessedFrom` class/interface
     // for accessing the original instance impl-method. Note that impl-method's
@@ -379,7 +390,12 @@
             .createMethod(
                 accessedFrom.getHolderType(), accessorProto, generateUniqueLambdaMethodName());
 
-    return new ClassMethodWithAccessorTarget(accessorMethod);
+    return new ClassMethodWithAccessorTarget(
+        descriptor.implHandle.asMethod(),
+        descriptor.implHandle.isInterface,
+        descriptor.implHandle.type,
+        accessorMethod,
+        appView);
   }
 
   // Create targets for static method referenced directly without
@@ -388,7 +404,8 @@
     assert descriptor.implHandle.type.isInvokeStatic();
 
     if (doesNotNeedAccessor(accessedFrom)) {
-      return new NoAccessorMethodTarget(Invoke.Type.STATIC);
+      return new NoAccessorMethodTarget(
+          descriptor.implHandle.asMethod(), Type.STATIC, descriptor.implHandle.isInterface);
     }
 
     // We need to generate an accessor method in `accessedFrom` class/interface
@@ -401,7 +418,12 @@
                 accessedFrom.getHolderType(),
                 descriptor.implHandle.asMethod().proto,
                 generateUniqueLambdaMethodName());
-    return new ClassMethodWithAccessorTarget(accessorMethod);
+    return new ClassMethodWithAccessorTarget(
+        descriptor.implHandle.asMethod(),
+        descriptor.implHandle.isInterface,
+        descriptor.implHandle.type,
+        accessorMethod,
+        appView);
   }
 
   // Create targets for constructor referenced directly without lambda$ methods.
@@ -412,7 +434,8 @@
     assert implHandle.type.isInvokeConstructor();
 
     if (doesNotNeedAccessor(accessedFrom)) {
-      return new NoAccessorMethodTarget(Invoke.Type.DIRECT);
+      return new NoAccessorMethodTarget(
+          descriptor.implHandle.asMethod(), Type.DIRECT, descriptor.implHandle.isInterface);
     }
 
     // We need to generate an accessor method in `accessedFrom` class/interface for
@@ -428,14 +451,20 @@
             .dexItemFactory()
             .createMethod(
                 accessedFrom.getHolderType(), accessorProto, generateUniqueLambdaMethodName());
-    return new ClassMethodWithAccessorTarget(accessorMethod);
+    return new ClassMethodWithAccessorTarget(
+        descriptor.implHandle.asMethod(),
+        descriptor.implHandle.isInterface,
+        descriptor.implHandle.type,
+        accessorMethod,
+        appView);
   }
 
   // Create targets for interface methods.
   private Target createInterfaceMethodTarget(ProgramMethod accessedFrom) {
     assert descriptor.implHandle.type.isInvokeInterface();
     assert doesNotNeedAccessor(accessedFrom);
-    return new NoAccessorMethodTarget(Invoke.Type.INTERFACE);
+    return new NoAccessorMethodTarget(
+        descriptor.implHandle.asMethod(), Type.INTERFACE, descriptor.implHandle.isInterface);
   }
 
   private DexString generateUniqueLambdaMethodName() {
@@ -447,19 +476,20 @@
   // Represents information about the method lambda class need to delegate the call to. It may
   // be the same method as specified in lambda descriptor or a newly synthesized accessor.
   // Also provides action for ensuring accessibility of the referenced symbols.
-  public abstract class Target {
+  public abstract static class Target {
 
     final DexMethod callTarget;
     final Invoke.Type invokeType;
+    final boolean isInterface;
 
     private boolean hasEnsuredAccessibility;
-    private ProgramMethod accessibilityBridge;
 
-    Target(DexMethod callTarget, Invoke.Type invokeType) {
+    Target(DexMethod callTarget, Type invokeType, boolean isInterface) {
       assert callTarget != null;
       assert invokeType != null;
       this.callTarget = callTarget;
       this.invokeType = invokeType;
+      this.isInterface = isInterface;
     }
 
     // Ensure access of the referenced symbol(s).
@@ -476,29 +506,27 @@
         ForcefullyMovedLambdaMethodConsumer forcefullyMovedLambdaMethodConsumer,
         Consumer<ProgramMethod> needsProcessingConsumer) {
       if (!hasEnsuredAccessibility) {
-        accessibilityBridge =
-            ensureAccessibility(forcefullyMovedLambdaMethodConsumer, needsProcessingConsumer);
+        ensureAccessibility(forcefullyMovedLambdaMethodConsumer, needsProcessingConsumer);
         hasEnsuredAccessibility = true;
       }
     }
 
     boolean isInterface() {
-      return descriptor.implHandle.isInterface;
+      return isInterface;
     }
   }
 
-  public abstract class D8SpecificTarget extends Target {
-    D8SpecificTarget(DexMethod callTarget, Type invokeType) {
-      super(callTarget, invokeType);
-      assert !appView.enableWholeProgramOptimizations();
+  public abstract static class D8SpecificTarget extends Target {
+    D8SpecificTarget(DexMethod callTarget, Type invokeType, boolean isInterface) {
+      super(callTarget, invokeType, isInterface);
     }
   }
 
   // Used for targeting methods referenced directly without creating accessors.
-  private final class NoAccessorMethodTarget extends Target {
+  private static final class NoAccessorMethodTarget extends Target {
 
-    NoAccessorMethodTarget(Invoke.Type invokeType) {
-      super(descriptor.implHandle.asMethod(), invokeType);
+    NoAccessorMethodTarget(DexMethod method, Type invokeType, boolean isInterface) {
+      super(method, invokeType, isInterface);
     }
 
     @Override
@@ -510,12 +538,12 @@
   }
 
   // Used for static private lambda$ methods. Only needs access relaxation.
-  private final class StaticLambdaImplTarget extends D8SpecificTarget {
+  private static final class StaticLambdaImplTarget extends D8SpecificTarget {
 
     final ProgramMethod target;
 
-    StaticLambdaImplTarget(ProgramMethod target) {
-      super(descriptor.implHandle.asMethod(), Invoke.Type.STATIC);
+    StaticLambdaImplTarget(ProgramMethod target, boolean isInterface) {
+      super(target.getReference(), Invoke.Type.STATIC, isInterface);
       this.target = target;
     }
 
@@ -535,10 +563,16 @@
 
   // Used for instance private lambda$ methods on interfaces which need to be converted to public
   // static methods. They can't remain instance methods as they will end up on the companion class.
-  private class InterfaceLambdaImplTarget extends D8SpecificTarget {
+  private static final class InterfaceLambdaImplTarget extends D8SpecificTarget {
 
-    InterfaceLambdaImplTarget(DexMethod staticMethod) {
-      super(staticMethod, Type.STATIC);
+    private final AppView<?> appView;
+    private final DexMethod implMethod;
+
+    InterfaceLambdaImplTarget(
+        DexMethod implMethod, boolean isInterface, DexMethod staticMethod, AppView<?> appView) {
+      super(staticMethod, Type.STATIC, isInterface);
+      this.implMethod = implMethod;
+      this.appView = appView;
     }
 
     @Override
@@ -547,7 +581,6 @@
         Consumer<ProgramMethod> needsProcessingConsumer) {
       // For all instantiation points for which the compiler creates lambda$
       // methods, it creates these methods in the same class/interface.
-      DexMethod implMethod = descriptor.implHandle.asMethod();
       DexProgramClass implMethodHolder = appView.definitionFor(implMethod.holder).asProgramClass();
 
       DexEncodedMethod replacement =
@@ -603,12 +636,13 @@
     }
   }
 
-  class InvalidLambdaImplTarget extends Target {
+  static final class InvalidLambdaImplTarget extends Target {
 
     final DexType exceptionType;
 
-    public InvalidLambdaImplTarget(DexMethod callTarget, Type invokeType, DexType exceptionType) {
-      super(callTarget, invokeType);
+    public InvalidLambdaImplTarget(
+        DexMethod callTarget, Type invokeType, DexType exceptionType, boolean isInterface) {
+      super(callTarget, invokeType, isInterface);
       this.exceptionType = exceptionType;
     }
 
@@ -621,10 +655,16 @@
   }
 
   // Used for instance private lambda$ methods which need to be converted to public methods.
-  private class InstanceLambdaImplTarget extends D8SpecificTarget {
+  private static final class InstanceLambdaImplTarget extends D8SpecificTarget {
 
-    InstanceLambdaImplTarget(DexMethod staticMethod) {
-      super(staticMethod, Type.VIRTUAL);
+    private final DexMethod implMethod;
+    private final AppView<?> appView;
+
+    InstanceLambdaImplTarget(
+        DexMethod implMethod, boolean isInterface, DexMethod staticMethod, AppView<?> appView) {
+      super(staticMethod, Type.VIRTUAL, isInterface);
+      this.implMethod = implMethod;
+      this.appView = appView;
     }
 
     @Override
@@ -634,7 +674,6 @@
       // When compiling with whole program optimization, check that we are not inplace modifying.
       // For all instantiation points for which the compiler creates lambda$
       // methods, it creates these methods in the same class/interface.
-      DexMethod implMethod = descriptor.implHandle.asMethod();
       DexProgramClass implMethodHolder = appView.definitionFor(implMethod.holder).asProgramClass();
 
       DexEncodedMethod replacement =
@@ -687,12 +726,25 @@
 
   // Used for instance/static methods or constructors accessed via
   // synthesized accessor method. Needs accessor method to be created.
-  private class ClassMethodWithAccessorTarget extends Target {
+  private static class ClassMethodWithAccessorTarget extends Target {
 
-    ClassMethodWithAccessorTarget(DexMethod accessorMethod) {
-      super(accessorMethod, Invoke.Type.STATIC);
+    private final AppView<?> appView;
+    private final DexMethod implMethod;
+    private final MethodHandleType type;
+
+    ClassMethodWithAccessorTarget(
+        DexMethod implMethod,
+        boolean isInterface,
+        MethodHandleType type,
+        DexMethod accessorMethod,
+        AppView<?> appView) {
+      super(accessorMethod, Invoke.Type.STATIC, isInterface);
+      this.appView = appView;
+      this.implMethod = implMethod;
+      this.type = type;
     }
 
+
     @Override
     ProgramMethod ensureAccessibility(
         ForcefullyMovedLambdaMethodConsumer forcefullyMovedLambdaMethodConsumer,
@@ -718,7 +770,9 @@
               DexEncodedMethod.syntheticBuilder()
                   .setMethod(callTarget)
                   .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
-                  .setCode(AccessorMethodSourceCode.build(LambdaClass.this, callTarget))
+                  .setCode(
+                      AccessorMethodSourceCode.build(
+                          implMethod, isInterface, type, callTarget, appView))
                   // The api level is computed when tracing.
                   .disableAndroidApiLevelCheck()
                   .build());
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index b4da192..da0a96a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.RetargetingInfo;
+import com.android.tools.r8.ir.desugar.icce.AlwaysThrowingInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodProcessorFacade;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
@@ -29,8 +30,10 @@
 import com.android.tools.r8.ir.desugar.twr.TwrInstructionDesugaring;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThrowingConsumer;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -55,6 +58,13 @@
       AppView<?> appView, AndroidApiLevelCompute apiLevelCompute) {
     this.appView = appView;
     this.apiLevelCompute = apiLevelCompute;
+    AlwaysThrowingInstructionDesugaring alwaysThrowingInstructionDesugaring =
+        appView.enableWholeProgramOptimizations()
+            ? new AlwaysThrowingInstructionDesugaring(appView.withClassHierarchy())
+            : null;
+    if (alwaysThrowingInstructionDesugaring != null) {
+      desugarings.add(alwaysThrowingInstructionDesugaring);
+    }
     if (appView.options().desugarState.isOff()) {
       this.nestBasedAccessDesugaring = null;
       this.recordRewriter = null;
@@ -75,14 +85,17 @@
     if (appView.options().enableBackportedMethodRewriting()) {
       backportedMethodRewriter = new BackportedMethodRewriter(appView);
     }
-    // Place TWR before Interface desugaring to eliminate potential $closeResource interface calls.
     if (appView.options().enableTryWithResourcesDesugaring()) {
       desugarings.add(new TwrInstructionDesugaring(appView));
     }
     if (appView.options().isInterfaceMethodDesugaringEnabled()) {
       interfaceMethodRewriter =
           new InterfaceMethodRewriter(
-              appView, backportedMethodRewriter, desugaredLibraryRetargeter);
+              appView,
+              SetUtils.newImmutableSetExcludingNullItems(
+                  alwaysThrowingInstructionDesugaring,
+                  backportedMethodRewriter,
+                  desugaredLibraryRetargeter));
       desugarings.add(interfaceMethodRewriter);
     } else {
       interfaceMethodRewriter = null;
@@ -91,9 +104,11 @@
         appView.rewritePrefix.isRewriting()
             ? new DesugaredLibraryAPIConverter(
                 appView,
-                interfaceMethodRewriter,
-                desugaredLibraryRetargeter,
-                backportedMethodRewriter)
+                SetUtils.newImmutableSetExcludingNullItems(
+                    interfaceMethodRewriter, desugaredLibraryRetargeter, backportedMethodRewriter),
+                interfaceMethodRewriter != null
+                    ? interfaceMethodRewriter.getEmulatedMethods()
+                    : ImmutableSet.of())
             : null;
     if (desugaredLibraryAPIConverter != null) {
       desugarings.add(desugaredLibraryAPIConverter);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryAPIConverter.java
index 828e70c..4634977 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryAPIConverter.java
@@ -23,22 +23,22 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.ValueType;
-import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryClasspathWrapperSynthesizeEventConsumer;
-import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConversionCfCodeProvider;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -67,31 +67,24 @@
 
   private final AppView<?> appView;
   private final DexItemFactory factory;
-  // This is used to filter out double desugaring on backported methods.
-  private final BackportedMethodRewriter backportedMethodRewriter;
-  private final InterfaceMethodRewriter interfaceMethodRewriter;
-  private final DesugaredLibraryRetargeter retargeter;
+  private final Set<CfInstructionDesugaring> precedingDesugarings;
+  private final Set<DexString> emulatedMethods;
 
   private final DesugaredLibraryWrapperSynthesizer wrapperSynthesizor;
-  private final Set<DexMethod> trackedCallBackAPIs;
   private final Set<DexMethod> trackedAPIs;
 
   public DesugaredLibraryAPIConverter(
       AppView<?> appView,
-      InterfaceMethodRewriter interfaceMethodRewriter,
-      DesugaredLibraryRetargeter retargeter,
-      BackportedMethodRewriter backportedMethodRewriter) {
+      Set<CfInstructionDesugaring> precedingDesugarings,
+      Set<DexString> emulatedMethods) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
-    this.interfaceMethodRewriter = interfaceMethodRewriter;
-    this.retargeter = retargeter;
-    this.backportedMethodRewriter = backportedMethodRewriter;
+    this.precedingDesugarings = precedingDesugarings;
+    this.emulatedMethods = emulatedMethods;
     this.wrapperSynthesizor = new DesugaredLibraryWrapperSynthesizer(appView);
     if (appView.options().testing.trackDesugaredAPIConversions) {
-      trackedCallBackAPIs = Sets.newConcurrentHashSet();
       trackedAPIs = Sets.newConcurrentHashSet();
     } else {
-      trackedCallBackAPIs = null;
       trackedAPIs = null;
     }
   }
@@ -176,10 +169,7 @@
   // The problem is that a method can resolve into a library method which is not present at runtime,
   // the code relies in that case on emulated interface dispatch. We should not convert such API.
   private boolean isEmulatedInterfaceOverride(DexClassAndMethod invokedMethod) {
-    if (interfaceMethodRewriter == null) {
-      return false;
-    }
-    if (!interfaceMethodRewriter.getEmulatedMethods().contains(invokedMethod.getName())) {
+    if (!emulatedMethods.contains(invokedMethod.getName())) {
       return false;
     }
     DexClassAndMethod interfaceResult =
@@ -195,18 +185,8 @@
   }
 
   private boolean isAlreadyDesugared(CfInvoke invoke, ProgramMethod context) {
-    if (interfaceMethodRewriter != null
-        && interfaceMethodRewriter.needsDesugaring(invoke, context)) {
-      return true;
-    }
-    if (retargeter != null && retargeter.needsDesugaring(invoke, context)) {
-      return true;
-    }
-    if (backportedMethodRewriter != null
-        && backportedMethodRewriter.needsDesugaring(invoke, context)) {
-      return true;
-    }
-    return false;
+    return Iterables.any(
+        precedingDesugarings, desugaring -> desugaring.needsDesugaring(invoke, context));
   }
 
   public static DexMethod methodWithVivifiedTypeInSignature(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/icce/AlwaysThrowingInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/icce/AlwaysThrowingInstructionDesugaring.java
new file mode 100644
index 0000000..28b6981
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/icce/AlwaysThrowingInstructionDesugaring.java
@@ -0,0 +1,198 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.icce;
+
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.FailedResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
+import com.android.tools.r8.ir.desugar.FreshLocalProvider;
+import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.MethodSynthesizerConsumer;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class AlwaysThrowingInstructionDesugaring implements CfInstructionDesugaring {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+
+  public AlwaysThrowingInstructionDesugaring(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      DexItemFactory dexItemFactory) {
+    return computeDesugarDescription(instruction)
+        .desugarInstruction(
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            dexItemFactory);
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    return computeDesugarDescription(instruction).needsDesugaring();
+  }
+
+  private DesugarDescription computeDesugarDescription(CfInstruction instruction) {
+    if (instruction.isInvoke()) {
+      CfInvoke invoke = instruction.asInvoke();
+      DexMethod invokedMethod = invoke.getMethod();
+      MethodResolutionResult resolutionResult =
+          appView.appInfo().resolveMethod(invokedMethod, invoke.isInterface());
+      if (shouldRewriteInvokeToThrow(invoke, resolutionResult)) {
+        return computeInvokeAsThrowRewrite(appView, invoke, resolutionResult);
+      }
+    }
+    return DesugarDescription.nothing();
+  }
+
+  private boolean shouldRewriteInvokeToThrow(
+      CfInvoke invoke, MethodResolutionResult resolutionResult) {
+    if (resolutionResult.isArrayCloneMethodResult()) {
+      return false;
+    }
+    if (resolutionResult.isFailedResolution()) {
+      // For now don't materialize NSMEs from failed resolutions.
+      return resolutionResult.asFailedResolution().hasMethodsCausingError();
+    }
+    assert resolutionResult.isSingleResolution();
+    return resolutionResult.getResolvedMethod().isStatic() != invoke.isInvokeStatic();
+  }
+
+  public static DesugarDescription computeInvokeAsThrowRewrite(
+      AppView<?> appView, CfInvoke invoke, MethodResolutionResult resolutionResult) {
+    return DesugarDescription.builder()
+        .setDesugarRewrite(
+            (freshLocalProvider,
+                localStackAllocator,
+                eventConsumer,
+                context,
+                methodProcessingContext,
+                dexItemFactory) ->
+                getThrowInstructions(
+                    appView,
+                    invoke,
+                    resolutionResult,
+                    localStackAllocator,
+                    eventConsumer,
+                    context,
+                    methodProcessingContext))
+        .build();
+  }
+
+  private static Collection<CfInstruction> getThrowInstructions(
+      AppView<?> appView,
+      CfInvoke invoke,
+      MethodResolutionResult resolutionResult,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+    MethodSynthesizerConsumer methodSynthesizerConsumer = null;
+    if (resolutionResult == null) {
+      methodSynthesizerConsumer =
+          UtilityMethodsForCodeOptimizations::synthesizeThrowNoSuchMethodErrorMethod;
+    } else if (resolutionResult.isSingleResolution()) {
+      if (resolutionResult.getResolvedMethod().isStatic() != invoke.isInvokeStatic()) {
+        methodSynthesizerConsumer =
+            UtilityMethodsForCodeOptimizations::synthesizeThrowIncompatibleClassChangeErrorMethod;
+      }
+    } else if (resolutionResult.isFailedResolution()) {
+      FailedResolutionResult failedResolutionResult = resolutionResult.asFailedResolution();
+      AppInfoWithClassHierarchy appInfo = appView.appInfoForDesugaring();
+      if (failedResolutionResult.isIllegalAccessErrorResult(context.getHolder(), appInfo)) {
+        methodSynthesizerConsumer =
+            UtilityMethodsForCodeOptimizations::synthesizeThrowIllegalAccessErrorMethod;
+      } else if (failedResolutionResult.isNoSuchMethodErrorResult(context.getHolder(), appInfo)) {
+        methodSynthesizerConsumer =
+            UtilityMethodsForCodeOptimizations::synthesizeThrowNoSuchMethodErrorMethod;
+      } else if (failedResolutionResult.isIncompatibleClassChangeErrorResult()) {
+        methodSynthesizerConsumer =
+            UtilityMethodsForCodeOptimizations::synthesizeThrowIncompatibleClassChangeErrorMethod;
+      }
+    }
+
+    if (methodSynthesizerConsumer == null) {
+      assert false;
+      return null;
+    }
+
+    // Replace the entire effect of the invoke by by call to the throwing helper:
+    //   ...
+    //   invoke <method> [receiver] args*
+    // =>
+    //   ...
+    //   (pop arg)*
+    //   [pop receiver]
+    //   invoke <throwing-method>
+    //   pop exception result
+    //   [push fake result for <method>]
+    UtilityMethodForCodeOptimizations throwMethod =
+        methodSynthesizerConsumer.synthesizeMethod(appView, methodProcessingContext);
+    ProgramMethod throwProgramMethod = throwMethod.uncheckedGetMethod();
+    eventConsumer.acceptThrowMethod(throwProgramMethod, context);
+
+    ArrayList<CfInstruction> replacement = new ArrayList<>();
+    DexTypeList parameters = invoke.getMethod().getParameters();
+    for (int i = parameters.values.length - 1; i >= 0; i--) {
+      replacement.add(
+          new CfStackInstruction(
+              parameters.get(i).isWideType()
+                  ? CfStackInstruction.Opcode.Pop2
+                  : CfStackInstruction.Opcode.Pop));
+    }
+    if (!invoke.isInvokeStatic()) {
+      replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
+    }
+
+    CfInvoke throwInvoke =
+        new CfInvoke(
+            org.objectweb.asm.Opcodes.INVOKESTATIC, throwProgramMethod.getReference(), false);
+    assert throwInvoke.getMethod().getReturnType().isClassType();
+    replacement.add(throwInvoke);
+    replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
+
+    DexType returnType = invoke.getMethod().getReturnType();
+    if (!returnType.isVoidType()) {
+      replacement.add(
+          returnType.isPrimitiveType()
+              ? new CfConstNumber(0, ValueType.fromDexType(returnType))
+              : new CfConstNull());
+    } else {
+      // If the return type is void, the stack may need an extra slot to fit the return type of
+      // the call to the throwing method.
+      localStackAllocator.allocateLocalStack(1);
+    }
+    return replacement;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index 9ee3f54..87e0d65 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -6,11 +6,8 @@
 
 import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.cf.code.CfConstNull;
-import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
@@ -27,27 +24,20 @@
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.code.ValueType;
-import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.DesugarDescription;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryConfiguration;
-import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeter;
+import com.android.tools.r8.ir.desugar.icce.AlwaysThrowingInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
-import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
-import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.MethodSynthesizerConsumer;
-import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.MethodPosition;
@@ -56,8 +46,8 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -108,8 +98,7 @@
   private final Map<DexType, DefaultMethodsHelper.Collection> cache = new ConcurrentHashMap<>();
 
   // This is used to filter out double desugaring on backported methods.
-  private final BackportedMethodRewriter backportedMethodRewriter;
-  private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
+  private final Set<CfInstructionDesugaring> precedingDesugarings;
 
   /** Defines a minor variation in desugaring. */
   public enum Flavor {
@@ -119,26 +108,10 @@
     ExcludeDexResources
   }
 
-  // Constructor for cf to cf desugaring.
   public InterfaceMethodRewriter(
-      AppView<?> appView,
-      BackportedMethodRewriter rewriter,
-      DesugaredLibraryRetargeter desugaredLibraryRetargeter) {
+      AppView<?> appView, Set<CfInstructionDesugaring> precedingDesugarings) {
     this.appView = appView;
-    this.backportedMethodRewriter = rewriter;
-    this.desugaredLibraryRetargeter = desugaredLibraryRetargeter;
-    this.options = appView.options();
-    this.factory = appView.dexItemFactory();
-    this.helper = new InterfaceDesugaringSyntheticHelper(appView);
-    initializeEmulatedInterfaceVariables();
-  }
-
-  // Constructor for IR desugaring.
-  public InterfaceMethodRewriter(AppView<?> appView, IRConverter converter) {
-    assert converter != null;
-    this.appView = appView;
-    this.backportedMethodRewriter = null;
-    this.desugaredLibraryRetargeter = null;
+    this.precedingDesugarings = precedingDesugarings;
     this.options = appView.options();
     this.factory = appView.dexItemFactory();
     this.helper = new InterfaceDesugaringSyntheticHelper(appView);
@@ -228,13 +201,8 @@
   }
 
   private boolean isAlreadyDesugared(CfInvoke invoke, ProgramMethod context) {
-    // In Cf to Cf it is forbidden to desugar twice the same instruction, if the backported
-    // method rewriter or the desugared library retargeter already desugar the instruction, they
-    // take precedence and nothing has to be done here.
-    return (backportedMethodRewriter != null
-            && backportedMethodRewriter.needsDesugaring(invoke, context))
-        || (desugaredLibraryRetargeter != null
-            && desugaredLibraryRetargeter.needsDesugaring(invoke, context));
+    return Iterables.any(
+        precedingDesugarings, desugaring -> desugaring.needsDesugaring(invoke, context));
   }
 
   @Override
@@ -375,7 +343,7 @@
     if (target != null && target.isDefaultMethod()) {
       // Rewrite the invoke to a throw ICCE as the default method forward would otherwise hide the
       // static / virtual mismatch.
-      return computeInvokeAsThrowRewrite(invoke, resolution.asSingleResolution());
+      return computeInvokeAsThrowRewrite(invoke, resolution.asSingleResolution(), context);
     }
     return DesugarDescription.nothing();
   }
@@ -469,7 +437,7 @@
             .resolveMethodOnInterface(holder, invoke.getMethod())
             .asSingleResolution();
     if (holder.isInterface() && shouldRewriteToInvokeToThrow(resolutionResult, true)) {
-      return computeInvokeAsThrowRewrite(invoke, resolutionResult);
+      return computeInvokeAsThrowRewrite(invoke, resolutionResult, context);
     }
 
     assert resolutionResult != null;
@@ -504,7 +472,7 @@
       return computeInvokeDirect(holder, invoke, context);
     }
     if (resolution != null && resolution.getResolvedMethod().isStatic()) {
-      return computeInvokeAsThrowRewrite(invoke, resolution);
+      return computeInvokeAsThrowRewrite(invoke, resolution, context);
     }
     DesugarDescription description = computeEmulatedInterfaceVirtualDispatchOrNull(invoke);
     return description != null ? description : DesugarDescription.nothing();
@@ -549,7 +517,7 @@
     MethodResolutionResult resolution =
         appView.appInfoForDesugaring().resolveMethod(invokedMethod, invoke.isInterface());
     if (resolution.isFailedResolution()) {
-      return computeInvokeAsThrowRewrite(invoke, null);
+      return computeInvokeAsThrowRewrite(invoke, null, context);
     }
 
     SingleResolutionResult singleResolution = resolution.asSingleResolution();
@@ -628,23 +596,10 @@
   }
 
   private DesugarDescription computeInvokeAsThrowRewrite(
-      CfInvoke invoke, SingleResolutionResult resolution) {
-    return DesugarDescription.builder()
-        .setDesugarRewrite(
-            (freshLocalProvider,
-                localStackAllocator,
-                eventConsumer,
-                context,
-                methodProcessingContext,
-                dexItemFactory) ->
-                getThrowInstructions(
-                    invoke,
-                    resolution,
-                    localStackAllocator,
-                    eventConsumer,
-                    context,
-                    methodProcessingContext))
-        .build();
+      CfInvoke invoke, SingleResolutionResult resolution, ProgramMethod context) {
+    assert !isAlreadyDesugared(invoke, context);
+    return AlwaysThrowingInstructionDesugaring.computeInvokeAsThrowRewrite(
+        appView, invoke, resolution);
   }
 
   private Collection<CfInstruction> getInvokeStaticInstructions(DexMethod newTarget) {
@@ -652,76 +607,6 @@
         new CfInvoke(org.objectweb.asm.Opcodes.INVOKESTATIC, newTarget, false));
   }
 
-  private Collection<CfInstruction> getThrowInstructions(
-      CfInvoke invoke,
-      SingleResolutionResult resolutionResult,
-      LocalStackAllocator localStackAllocator,
-      CfInstructionDesugaringEventConsumer eventConsumer,
-      ProgramMethod context,
-      MethodProcessingContext methodProcessingContext) {
-    assert !isAlreadyDesugared(invoke, context);
-
-    MethodSynthesizerConsumer methodSynthesizerConsumer;
-    if (resolutionResult == null) {
-      methodSynthesizerConsumer =
-          UtilityMethodsForCodeOptimizations::synthesizeThrowNoSuchMethodErrorMethod;
-    } else if (resolutionResult.getResolvedMethod().isStatic() != invoke.isInvokeStatic()) {
-      methodSynthesizerConsumer =
-          UtilityMethodsForCodeOptimizations::synthesizeThrowIncompatibleClassChangeErrorMethod;
-    } else {
-      assert false;
-      return null;
-    }
-
-    // Replace the entire effect of the invoke by by call to the throwing helper:
-    //   ...
-    //   invoke <method> [receiver] args*
-    // =>
-    //   ...
-    //   (pop arg)*
-    //   [pop receiver]
-    //   invoke <throwing-method>
-    //   pop exception result
-    //   [push fake result for <method>]
-    UtilityMethodForCodeOptimizations throwMethod =
-        methodSynthesizerConsumer.synthesizeMethod(appView, methodProcessingContext);
-    ProgramMethod throwProgramMethod = throwMethod.uncheckedGetMethod();
-    eventConsumer.acceptThrowMethod(throwProgramMethod, context);
-
-    ArrayList<CfInstruction> replacement = new ArrayList<>();
-    DexTypeList parameters = invoke.getMethod().getParameters();
-    for (int i = parameters.values.length - 1; i >= 0; i--) {
-      replacement.add(
-          new CfStackInstruction(
-              parameters.get(i).isWideType()
-                  ? CfStackInstruction.Opcode.Pop2
-                  : CfStackInstruction.Opcode.Pop));
-    }
-    if (!invoke.isInvokeStatic()) {
-      replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
-    }
-
-    CfInvoke throwInvoke =
-        new CfInvoke(
-            org.objectweb.asm.Opcodes.INVOKESTATIC, throwProgramMethod.getReference(), false);
-    assert throwInvoke.getMethod().getReturnType().isClassType();
-    replacement.add(throwInvoke);
-    replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
-
-    DexType returnType = invoke.getMethod().getReturnType();
-    if (returnType != factory.voidType) {
-      replacement.add(
-          returnType.isPrimitiveType()
-              ? new CfConstNumber(0, ValueType.fromDexType(returnType))
-              : new CfConstNull());
-    } else {
-      // If the return type is void, the stack may need an extra slot to fit the return type of
-      // the call to the throwing method.
-      localStackAllocator.allocateLocalStack(1);
-    }
-    return replacement;
-  }
-
   private void leavingStaticInvokeToInterface(ProgramMethod method) {
     // When leaving static interface method invokes possibly upgrade the class file
     // version, but don't go above the initial class file version. If the input was
@@ -778,7 +663,7 @@
     SingleResolutionResult resolutionResult =
         appView.appInfoForDesugaring().resolveMethodOn(clazz, invokedMethod).asSingleResolution();
     if (clazz.isInterface() && shouldRewriteToInvokeToThrow(resolutionResult, false)) {
-      return computeInvokeAsThrowRewrite(invoke, resolutionResult);
+      return computeInvokeAsThrowRewrite(invoke, resolutionResult, context);
     }
 
     if (clazz.isInterface() && !clazz.isLibraryClass()) {
@@ -794,7 +679,7 @@
       if (resolutionResult.getResolvedMethod().isPrivateMethod()) {
         if (resolutionResult.isAccessibleFrom(context, appView.appInfoForDesugaring()).isFalse()) {
           // TODO(b/145775365): This should throw IAE.
-          return computeInvokeAsThrowRewrite(invoke, null);
+          return computeInvokeAsThrowRewrite(invoke, null, context);
         }
         return DesugarDescription.builder()
             .setDesugarRewrite(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
index 7fa0410..1f16c1b 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
@@ -137,21 +137,21 @@
     assert !instruction.isInitClass();
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
-      if (refersToRecord(cfInvoke.getMethod())) {
+      if (refersToRecord(cfInvoke.getMethod(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isFieldInstruction()) {
       CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
-      if (refersToRecord(fieldInstruction.getField())) {
+      if (refersToRecord(fieldInstruction.getField(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isTypeInstruction()) {
       CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
-      if (refersToRecord(typeInstruction.getType())) {
+      if (refersToRecord(typeInstruction.getType(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
@@ -452,35 +452,35 @@
     }
   }
 
-  private boolean refersToRecord(DexField field) {
-    assert !refersToRecord(field.holder) : "The java.lang.Record class has no fields.";
-    return refersToRecord(field.type);
+  public static boolean refersToRecord(DexField field, DexItemFactory factory) {
+    assert !refersToRecord(field.holder, factory) : "The java.lang.Record class has no fields.";
+    return refersToRecord(field.type, factory);
   }
 
-  private boolean refersToRecord(DexMethod method) {
-    if (refersToRecord(method.holder)) {
+  public static boolean refersToRecord(DexMethod method, DexItemFactory factory) {
+    if (refersToRecord(method.holder, factory)) {
       return true;
     }
-    return refersToRecord(method.proto);
+    return refersToRecord(method.proto, factory);
   }
 
-  private boolean refersToRecord(DexProto proto) {
-    if (refersToRecord(proto.returnType)) {
+  private static boolean refersToRecord(DexProto proto, DexItemFactory factory) {
+    if (refersToRecord(proto.returnType, factory)) {
       return true;
     }
-    return refersToRecord(proto.parameters.values);
+    return refersToRecord(proto.parameters.values, factory);
   }
 
-  private boolean refersToRecord(DexType[] types) {
+  private static boolean refersToRecord(DexType[] types, DexItemFactory factory) {
     for (DexType type : types) {
-      if (refersToRecord(type)) {
+      if (refersToRecord(type, factory)) {
         return true;
       }
     }
     return false;
   }
 
-  private boolean refersToRecord(DexType type) {
+  private static boolean refersToRecord(DexType type, DexItemFactory factory) {
     return type == factory.recordType;
   }
 
@@ -600,7 +600,7 @@
 
   @Override
   public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
-    if (appView.appInfo().app().getFlags().hasReadProgramRecord()) {
+    if (appView.appInfo().app().getFlags().hasReadRecordReferenceFromProgramClass()) {
       ensureRecordClass(eventConsumer);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
new file mode 100644
index 0000000..2fb3b5a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+public class EmptyEnumUnboxer extends EnumUnboxer {
+
+  private static final EmptyEnumUnboxer INSTANCE = new EmptyEnumUnboxer();
+
+  private EmptyEnumUnboxer() {}
+
+  static EmptyEnumUnboxer get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public void prepareForPrimaryOptimizationPass(GraphLens graphLensForPrimaryOptimizationPass) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void analyzeEnums(IRCode code, MutableMethodConversionOptions conversionOptions) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public Set<Phi> rewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    // Intentionally empty.
+    return Collections.emptySet();
+  }
+
+  @Override
+  public void unboxEnums(
+      AppView<AppInfoWithLiveness> appView,
+      IRConverter converter,
+      Builder postMethodProcessorBuilder,
+      ExecutorService executorService,
+      OptimizationFeedbackDelayed feedback) {
+    appView.setUnboxedEnums(EnumDataMap.empty());
+  }
+
+  @Override
+  public void unsetRewriter() {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void updateEnumUnboxingCandidatesInfo() {
+    // Intentionally empty.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 705ce93..2cdf22f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -1,1575 +1,53 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
 package com.android.tools.r8.ir.optimize.enums;
 
-import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-import static com.android.tools.r8.ir.code.Opcodes.ARRAY_GET;
-import static com.android.tools.r8.ir.code.Opcodes.ARRAY_LENGTH;
-import static com.android.tools.r8.ir.code.Opcodes.ARRAY_PUT;
-import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
-import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
-import static com.android.tools.r8.ir.code.Opcodes.IF;
-import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
-import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
-import static com.android.tools.r8.ir.code.Opcodes.RETURN;
-import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT;
-import static com.android.tools.r8.utils.MapUtils.ignoreKey;
-
-import com.android.tools.r8.graph.AccessFlags;
 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.DexClassAndField;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMember;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
-import com.android.tools.r8.graph.MethodResolutionResult;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
-import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues.EnumStaticFieldValues;
-import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState;
-import com.android.tools.r8.ir.analysis.value.ObjectState;
-import com.android.tools.r8.ir.code.ArrayGet;
-import com.android.tools.r8.ir.code.ArrayLength;
-import com.android.tools.r8.ir.code.ArrayPut;
-import com.android.tools.r8.ir.code.Assume;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.CheckCast;
-import com.android.tools.r8.ir.code.ConstClass;
-import com.android.tools.r8.ir.code.FieldInstruction;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.If;
-import com.android.tools.r8.ir.code.InstanceGet;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeCustom;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.code.InvokeVirtual;
-import com.android.tools.r8.ir.code.MemberType;
-import com.android.tools.r8.ir.code.Opcodes;
 import com.android.tools.r8.ir.code.Phi;
-import com.android.tools.r8.ir.code.Return;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.PostMethodProcessor;
-import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
-import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
-import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
-import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldOrdinalData;
-import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldUnknownData;
-import com.android.tools.r8.ir.optimize.enums.classification.CheckNotNullEnumUnboxerMethodClassification;
-import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.IllegalInvokeWithImpreciseParameterTypeReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingContentsForEnumValuesArrayReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingEnumStaticFieldValuesReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingInstanceFieldValueForEnumInstanceReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingObjectStateForEnumInstanceReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedInstanceFieldValueForEnumInstanceReason;
-import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedLibraryInvokeReason;
-import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
-import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.KeepInfoCollection;
-import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.StringDiagnostic;
-import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap;
-import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
-import com.android.tools.r8.utils.collections.ProgramMethodMap;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.HashMultiset;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import it.unimi.dsi.fastutil.objects.Object2IntMap;
-import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.OptionalInt;
 import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
 
-public class EnumUnboxer {
+public abstract class EnumUnboxer {
 
-  private final AppView<AppInfoWithLiveness> appView;
-  private final DexItemFactory factory;
-  // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given
-  // enum if the optimization eventually decides to unbox it.
-  private EnumUnboxingCandidateInfoCollection enumUnboxingCandidatesInfo;
-  private final Set<DexProgramClass> candidatesToRemoveInWave = Sets.newConcurrentHashSet();
-  private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap =
-      new ConcurrentHashMap<>();
-
-  // Methods depending on library modelisation need to be reprocessed so they are peephole
-  // optimized.
-  private LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodsDependingOnLibraryModelisation;
-
-  // Map from checkNotNull() methods to the enums that use the given method.
-  private final ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods =
-      ProgramMethodMap.createConcurrent();
-
-  private final DexClassAndField ordinalField;
-
-  private EnumUnboxingRewriter enumUnboxerRewriter;
-
-  private final boolean debugLogEnabled;
-  private final Map<DexType, List<Reason>> debugLogs;
-
-  public EnumUnboxer(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
-    this.factory = appView.dexItemFactory();
-    if (appView.options().testing.enableEnumUnboxingDebugLogs) {
-      debugLogEnabled = true;
-      debugLogs = new ConcurrentHashMap<>();
-    } else {
-      debugLogEnabled = false;
-      debugLogs = null;
-    }
-    assert !appView.options().debug;
-    ordinalField =
-        appView.appInfo().resolveField(factory.enumMembers.ordinalField).getResolutionPair();
+  public static EnumUnboxer create(AppView<AppInfoWithLiveness> appView) {
+    return appView.options().enableEnumUnboxing ? new EnumUnboxerImpl(appView) : empty();
   }
 
-  public static int ordinalToUnboxedInt(int ordinal) {
-    return ordinal + 1;
+  public static EmptyEnumUnboxer empty() {
+    return EmptyEnumUnboxer.get();
   }
 
-  public DexClassAndField getOrdinalField() {
-    return ordinalField;
-  }
+  public abstract void prepareForPrimaryOptimizationPass(
+      GraphLens graphLensForPrimaryOptimizationPass);
 
-  public void updateEnumUnboxingCandidatesInfo() {
-    for (DexProgramClass candidate : candidatesToRemoveInWave) {
-      enumUnboxingCandidatesInfo.removeCandidate(candidate);
-    }
-    candidatesToRemoveInWave.clear();
-  }
+  public abstract void analyzeEnums(IRCode code, MutableMethodConversionOptions conversionOptions);
 
-  /**
-   * Returns true if {@param enumClass} was marked as being unboxable.
-   *
-   * <p>Note that, if debug logging is enabled, {@param enumClass} is not marked unboxable until the
-   * enum unboxing analysis has finished. This is to ensure completeness of the reason reporting.
-   */
-  private boolean markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
-    assert enumClass.isEnum();
-    if (!reportFailure(enumClass, reason)) {
-      // The failure was not reported, meaning debug logging is disabled.
-      candidatesToRemoveInWave.add(enumClass);
-      return true;
-    }
-    return false;
-  }
+  public abstract void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues);
 
-  private void markMethodDependsOnLibraryModelisation(ProgramMethod method) {
-    methodsDependingOnLibraryModelisation.add(method, appView.graphLens());
-  }
+  public abstract Set<Phi> rewriteCode(IRCode code, MethodProcessor methodProcessor);
 
-  private DexProgramClass getEnumUnboxingCandidateOrNull(TypeElement lattice) {
-    if (lattice.isClassType()) {
-      DexType classType = lattice.asClassType().getClassType();
-      return getEnumUnboxingCandidateOrNull(classType);
-    }
-    if (lattice.isArrayType()) {
-      ArrayTypeElement arrayType = lattice.asArrayType();
-      if (arrayType.getBaseType().isClassType()) {
-        return getEnumUnboxingCandidateOrNull(arrayType.getBaseType());
-      }
-    }
-    return null;
-  }
-
-  private DexProgramClass getEnumUnboxingCandidateOrNull(DexType type) {
-    if (type.isArrayType()) {
-      return getEnumUnboxingCandidateOrNull(type.toBaseType(appView.dexItemFactory()));
-    }
-    if (type.isPrimitiveType() || type.isVoidType()) {
-      return null;
-    }
-    assert type.isClassType();
-    return enumUnboxingCandidatesInfo.getCandidateClassOrNull(type);
-  }
-
-  public void analyzeEnums(IRCode code, MutableMethodConversionOptions conversionOptions) {
-    Set<DexType> eligibleEnums = Sets.newIdentityHashSet();
-    for (BasicBlock block : code.blocks) {
-      for (Instruction instruction : block.getInstructions()) {
-        Value outValue = instruction.outValue();
-        if (outValue != null) {
-          DexProgramClass enumClass =
-              getEnumUnboxingCandidateOrNull(outValue.getDynamicUpperBoundType(appView));
-          if (enumClass != null) {
-            Reason reason = validateEnumUsages(code, outValue, enumClass);
-            if (reason == Reason.ELIGIBLE) {
-              eligibleEnums.add(enumClass.type);
-            }
-          }
-          if (outValue.getType().isNullType()) {
-            addNullDependencies(code, outValue.uniqueUsers(), eligibleEnums);
-          }
-        } else {
-          if (instruction.isInvokeMethod()) {
-            DexProgramClass enumClass =
-                getEnumUnboxingCandidateOrNull(instruction.asInvokeMethod().getReturnType());
-            if (enumClass != null) {
-              eligibleEnums.add(enumClass.type);
-            }
-          }
-        }
-        switch (instruction.opcode()) {
-          case Opcodes.CONST_CLASS:
-            analyzeConstClass(instruction.asConstClass(), eligibleEnums, code.context());
-            break;
-          case Opcodes.CHECK_CAST:
-            analyzeCheckCast(instruction.asCheckCast(), eligibleEnums);
-            break;
-          case Opcodes.INVOKE_CUSTOM:
-            analyzeInvokeCustom(instruction.asInvokeCustom(), eligibleEnums);
-            break;
-          case INVOKE_STATIC:
-            analyzeInvokeStatic(instruction.asInvokeStatic(), eligibleEnums, code.context());
-            break;
-          case Opcodes.STATIC_GET:
-          case Opcodes.INSTANCE_GET:
-          case Opcodes.STATIC_PUT:
-          case INSTANCE_PUT:
-            analyzeFieldInstruction(
-                instruction.asFieldInstruction(), eligibleEnums, code.context());
-            break;
-          default: // Nothing to do for other instructions.
-        }
-      }
-      for (Phi phi : block.getPhis()) {
-        DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getType());
-        if (enumClass != null) {
-          Reason reason = validateEnumUsages(code, phi, enumClass);
-          if (reason == Reason.ELIGIBLE) {
-            eligibleEnums.add(enumClass.type);
-          }
-        }
-        if (phi.getType().isNullType()) {
-          addNullDependencies(code, phi.uniqueUsers(), eligibleEnums);
-        }
-      }
-    }
-    if (!eligibleEnums.isEmpty()) {
-      for (DexType eligibleEnum : eligibleEnums) {
-        enumUnboxingCandidatesInfo.addMethodDependency(eligibleEnum, code.context());
-      }
-    }
-    if (methodsDependingOnLibraryModelisation.contains(code.context(), appView.graphLens())) {
-      conversionOptions.disablePeepholeOptimizations();
-    }
-  }
-
-  private void analyzeInvokeCustom(InvokeCustom invoke, Set<DexType> eligibleEnums) {
-    Consumer<DexType> typeReferenceConsumer =
-        type -> {
-          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(type);
-          if (enumClass != null) {
-            eligibleEnums.add(enumClass.getType());
-          }
-        };
-    invoke.getCallSite().getMethodProto().forEachType(typeReferenceConsumer);
-    invoke
-        .getCallSite()
-        .getBootstrapArgs()
-        .forEach(
-            bootstrapArgument -> {
-              if (bootstrapArgument.isDexValueMethodHandle()) {
-                DexMethodHandle methodHandle =
-                    bootstrapArgument.asDexValueMethodHandle().getValue();
-                if (methodHandle.isMethodHandle()) {
-                  DexMethod method = methodHandle.asMethod();
-                  DexProgramClass enumClass =
-                      getEnumUnboxingCandidateOrNull(method.getHolderType());
-                  if (enumClass != null) {
-                    markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass);
-                  } else {
-                    method.getProto().forEachType(typeReferenceConsumer);
-                  }
-                } else {
-                  assert methodHandle.isFieldHandle();
-                  DexField field = methodHandle.asField();
-                  DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.getHolderType());
-                  if (enumClass != null) {
-                    markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass);
-                  } else {
-                    typeReferenceConsumer.accept(field.getType());
-                  }
-                }
-              } else if (bootstrapArgument.isDexValueMethodType()) {
-                DexProto proto = bootstrapArgument.asDexValueMethodType().getValue();
-                proto.forEachType(typeReferenceConsumer);
-              }
-            });
-  }
-
-  private void analyzeFieldInstruction(
-      FieldInstruction fieldInstruction, Set<DexType> eligibleEnums, ProgramMethod context) {
-    DexField field = fieldInstruction.getField();
-    DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.holder);
-    if (enumClass != null) {
-      FieldResolutionResult resolutionResult = appView.appInfo().resolveField(field, context);
-      if (resolutionResult.isSuccessfulResolution()) {
-        eligibleEnums.add(enumClass.getType());
-      } else {
-        markEnumAsUnboxable(Reason.UNRESOLVABLE_FIELD, enumClass);
-      }
-    }
-  }
-
-  private void analyzeInvokeStatic(
-      InvokeStatic invokeStatic, Set<DexType> eligibleEnums, ProgramMethod context) {
-    DexMethod invokedMethod = invokeStatic.getInvokedMethod();
-    DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
-    if (enumClass != null) {
-      DexClassAndMethod method = invokeStatic.lookupSingleTarget(appView, context);
-      if (method != null) {
-        eligibleEnums.add(enumClass.type);
-      } else {
-        markEnumAsUnboxable(Reason.INVALID_INVOKE, enumClass);
-      }
-    }
-  }
-
-  private void analyzeCheckCast(CheckCast checkCast, Set<DexType> eligibleEnums) {
-    // Casts to enum array types are fine as long all enum array creations are valid and have valid
-    // usages. Since creations of enum arrays are rewritten to primitive int arrays, enum array
-    // casts will continue to work after rewriting to int[] casts. Casts that failed with
-    // ClassCastException: "T[] cannot be cast to MyEnum[]" will continue to fail, but with "T[]
-    // cannot be cast to int[]".
-    //
-    // Note that strictly speaking, the rewriting from MyEnum[] to int[] could change the semantics
-    // of code that would fail with "int[] cannot be cast to MyEnum[]" in the input. However, javac
-    // does not allow such code ("incompatible types"), so we should generally not see such code.
-    if (checkCast.getType().isArrayType()) {
-      return;
-    }
-
-    // We are doing a type check, which typically means the in-value is of an upper
-    // type and cannot be dealt with.
-    // If the cast is on a dynamically typed object, the checkCast can be simply removed.
-    // This allows enum array clone and valueOf to work correctly.
-    DexProgramClass enumClass =
-        getEnumUnboxingCandidateOrNull(checkCast.getType().toBaseType(factory));
-    if (enumClass == null) {
-      return;
-    }
-    if (allowCheckCast(checkCast)) {
-      eligibleEnums.add(enumClass.type);
-      return;
-    }
-    markEnumAsUnboxable(Reason.DOWN_CAST, enumClass);
-  }
-
-  private boolean allowCheckCast(CheckCast checkCast) {
-    TypeElement objectType = checkCast.object().getDynamicUpperBoundType(appView);
-    return objectType.equalUpToNullability(
-        TypeElement.fromDexType(checkCast.getType(), definitelyNotNull(), appView));
-  }
-
-  private void analyzeConstClass(
-      ConstClass constClass, Set<DexType> eligibleEnums, ProgramMethod context) {
-    // We are using the ConstClass of an enum, which typically means the enum cannot be unboxed.
-    // We however allow unboxing if the ConstClass is used only:
-    // - as an argument to java.lang.reflect.Array#newInstance(java.lang.Class, int[]), to allow
-    //   unboxing of:
-    //    MyEnum[][] a = new MyEnum[x][y];
-    // - as an argument to Enum#valueOf, to allow unboxing of:
-    //    MyEnum a = Enum.valueOf(MyEnum.class, "A");
-    // - as a receiver for a name method, to allow unboxing of:
-    //    MyEnum.class.getName();
-    DexType enumType = constClass.getValue();
-    if (!enumUnboxingCandidatesInfo.isCandidate(enumType)) {
-      return;
-    }
-    if (constClass.outValue() == null) {
-      eligibleEnums.add(enumType);
-      return;
-    }
-    DexProgramClass enumClass = appView.definitionFor(enumType).asProgramClass();
-    if (constClass.outValue().hasPhiUsers()) {
-      markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
-      return;
-    }
-    for (Instruction user : constClass.outValue().aliasedUsers()) {
-      if (!isLegitimateConstClassUser(user, context, enumClass)) {
-        markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
-        return;
-      }
-    }
-    eligibleEnums.add(enumType);
-  }
-
-  private boolean isLegitimateConstClassUser(
-      Instruction user, ProgramMethod context, DexProgramClass enumClass) {
-    if (user.isAssume()) {
-      if (user.outValue().hasPhiUsers()) {
-        return false;
-      }
-      return true;
-    }
-
-    if (user.isInvokeStatic()) {
-      DexClassAndMethod singleTarget = user.asInvokeStatic().lookupSingleTarget(appView, context);
-      if (singleTarget == null) {
-        return false;
-      }
-      if (singleTarget.getReference() == factory.enumMembers.valueOf) {
-        // The name data is required for the correct mapping from the enum name to the ordinal
-        // in the valueOf utility method.
-        addRequiredNameData(enumClass);
-        markMethodDependsOnLibraryModelisation(context);
-        return true;
-      }
-      if (singleTarget.getReference()
-          == factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions) {
-        markMethodDependsOnLibraryModelisation(context);
-        return true;
-      }
-    }
-
-    if (user.isInvokeVirtual()) {
-      InvokeVirtual invoke = user.asInvokeVirtual();
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (invokedMethod == factory.classMethods.desiredAssertionStatus) {
-        // Only valid in the enum's class initializer, since the class constant must be rewritten
-        // to LocalEnumUtility.class instead of int.class.
-        return context.getDefinition().isClassInitializer() && context.getHolder() == enumClass;
-      }
-      if (isUnboxableNameMethod(invokedMethod)) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  private void addRequiredNameData(DexProgramClass enumClass) {
-    enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData(
-        enumClass, factory.enumMembers.nameField);
-  }
-
-  private boolean isUnboxableNameMethod(DexMethod method) {
-    return method == factory.classMethods.getName
-        || method == factory.classMethods.getCanonicalName
-        || method == factory.classMethods.getSimpleName;
-  }
-
-  private void addNullDependencies(IRCode code, Set<Instruction> uses, Set<DexType> eligibleEnums) {
-    for (Instruction use : uses) {
-      if (use.isInvokeMethod()) {
-        InvokeMethod invokeMethod = use.asInvokeMethod();
-        DexMethod invokedMethod = invokeMethod.getInvokedMethod();
-        for (DexType paramType : invokedMethod.proto.parameters.values) {
-          if (enumUnboxingCandidatesInfo.isCandidate(paramType)) {
-            eligibleEnums.add(paramType);
-          }
-        }
-        if (invokeMethod.isInvokeMethodWithReceiver()) {
-          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
-          if (enumClass != null) {
-            markEnumAsUnboxable(Reason.ENUM_METHOD_CALLED_WITH_NULL_RECEIVER, enumClass);
-          }
-        }
-      } else if (use.isFieldPut()) {
-        DexType type = use.asFieldInstruction().getField().type;
-        if (enumUnboxingCandidatesInfo.isCandidate(type)) {
-          eligibleEnums.add(type);
-        }
-      } else if (use.isReturn()) {
-        DexType returnType = code.method().getReference().proto.returnType;
-        if (enumUnboxingCandidatesInfo.isCandidate(returnType)) {
-          eligibleEnums.add(returnType);
-        }
-      }
-    }
-  }
-
-  private Reason validateEnumUsages(IRCode code, Value value, DexProgramClass enumClass) {
-    Reason result = Reason.ELIGIBLE;
-    for (Instruction user : value.uniqueUsers()) {
-      Reason reason = instructionAllowEnumUnboxing(user, code, enumClass, value);
-      if (reason != Reason.ELIGIBLE) {
-        if (markEnumAsUnboxable(reason, enumClass)) {
-          return reason;
-        }
-        // Record that the enum is ineligible, and continue analysis to collect all reasons for
-        // debugging.
-        result = reason;
-      }
-    }
-    for (Phi phi : value.uniquePhiUsers()) {
-      for (Value operand : phi.getOperands()) {
-        if (!operand.getType().isNullType()
-            && getEnumUnboxingCandidateOrNull(operand.getType()) != enumClass) {
-          // All reported reasons from here will be the same (INVALID_PHI), so just return
-          // immediately.
-          markEnumAsUnboxable(Reason.INVALID_PHI, enumClass);
-          return Reason.INVALID_PHI;
-        }
-      }
-    }
-    return result;
-  }
-
-  public void initializeEnumUnboxingCandidates(GraphLens graphLensForPrimaryOptimizationPass) {
-    assert enumUnboxingCandidatesInfo == null;
-    enumUnboxingCandidatesInfo =
-        new EnumUnboxingCandidateAnalysis(appView, this)
-            .findCandidates(graphLensForPrimaryOptimizationPass);
-    methodsDependingOnLibraryModelisation =
-        LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(
-            graphLensForPrimaryOptimizationPass);
-  }
-
-  public void unboxEnums(
+  public abstract void unboxEnums(
+      AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
-      PostMethodProcessor.Builder postBuilder,
+      Builder postMethodProcessorBuilder,
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback)
-      throws ExecutionException {
-    assert candidatesToRemoveInWave.isEmpty();
-    EnumDataMap enumDataMap = finishAnalysis();
-    assert candidatesToRemoveInWave.isEmpty();
+      throws ExecutionException;
 
-    // At this point the enum unboxing candidates are no longer candidates, they will all be
-    // unboxed. We extract the now immutable enums to unbox information and clear the candidate
-    // info.
-    if (enumUnboxingCandidatesInfo.isEmpty()) {
-      assert enumDataMap.isEmpty();
-      appView.setUnboxedEnums(enumDataMap);
-      return;
-    }
+  public abstract void unsetRewriter();
 
-    ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates();
-    ImmutableSet<DexProgramClass> enumClassesToUnbox =
-        enumUnboxingCandidatesInfo.candidateClasses();
-    LongLivedProgramMethodSetBuilder<ProgramMethodSet> dependencies =
-        enumUnboxingCandidatesInfo.allMethodDependencies();
-    enumUnboxingCandidatesInfo.clear();
-    // Update keep info on any of the enum methods of the removed classes.
-    updateKeepInfo(enumsToUnbox);
-
-    EnumUnboxingUtilityClasses utilityClasses =
-        EnumUnboxingUtilityClasses.builder(appView)
-            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox, enumDataMap)
-            .build(converter, executorService);
-
-    // Fixup the application.
-    EnumUnboxingTreeFixer.Result treeFixerResult =
-        new EnumUnboxingTreeFixer(
-                appView, checkNotNullMethods, enumDataMap, enumClassesToUnbox, utilityClasses)
-            .fixupTypeReferences(converter, executorService);
-    EnumUnboxingLens enumUnboxingLens = treeFixerResult.getLens();
-    appView.setUnboxedEnums(enumDataMap);
-
-    // Update the graph lens.
-    appView.rewriteWithLens(enumUnboxingLens);
-
-    // Enqueue the (lens rewritten) methods that require reprocessing.
-    //
-    // Note that the reprocessing set must be rewritten to the new enum unboxing lens before pruning
-    // the builders with the methods removed by the tree fixer (since these methods references are
-    // already fully lens rewritten).
-    postBuilder
-        .getMethodsToReprocessBuilder()
-        .rewrittenWithLens(appView)
-        .merge(dependencies)
-        .merge(methodsDependingOnLibraryModelisation)
-        .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods());
-    methodsDependingOnLibraryModelisation.clear();
-
-    updateOptimizationInfos(executorService, feedback, treeFixerResult.getPrunedItems());
-
-    enumUnboxerRewriter =
-        new EnumUnboxingRewriter(
-            appView,
-            treeFixerResult.getCheckNotNullToCheckNotZeroMapping(),
-            converter,
-            enumUnboxingLens,
-            enumDataMap,
-            utilityClasses);
-  }
-
-  private void updateOptimizationInfos(
-      ExecutorService executorService,
-      OptimizationFeedbackDelayed feedback,
-      PrunedItems prunedItems)
-      throws ExecutionException {
-    feedback.fixupOptimizationInfos(
-        appView,
-        executorService,
-        new OptimizationInfoFixer() {
-          @Override
-          public void fixup(DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) {
-            optimizationInfo
-                .fixupClassTypeReferences(appView, appView.graphLens())
-                .fixupAbstractValue(appView, appView.graphLens());
-          }
-
-          @Override
-          public void fixup(
-              DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) {
-            optimizationInfo
-                .fixupClassTypeReferences(appView, appView.graphLens())
-                .fixupAbstractReturnValue(appView, appView.graphLens())
-                .fixupInstanceInitializerInfo(appView, appView.graphLens(), prunedItems);
-          }
-        });
-  }
-
-  private void updateKeepInfo(Set<DexType> enumsToUnbox) {
-    KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo();
-    keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(enumsToUnbox));
-  }
-
-  public EnumDataMap finishAnalysis() {
-    analyzeInitializers();
-    updateEnumUnboxingCandidatesInfo();
-    EnumDataMap enumDataMap = analyzeEnumInstances();
-    if (debugLogEnabled) {
-      // Remove all enums that have been reported as being unboxable.
-      debugLogs.keySet().forEach(enumUnboxingCandidatesInfo::removeCandidate);
-      reportEnumsAnalysis();
-    }
-    assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size();
-    return enumDataMap;
-  }
-
-  private EnumDataMap analyzeEnumInstances() {
-    ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
-    enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData(
-        (enumClass, instanceFields) -> {
-          EnumData data = buildData(enumClass, instanceFields);
-          if (data == null) {
-            // Reason is already reported at this point.
-            enumUnboxingCandidatesInfo.removeCandidate(enumClass);
-            return;
-          }
-          if (!debugLogEnabled || !debugLogs.containsKey(enumClass.getType())) {
-            builder.put(enumClass.type, data);
-          }
-        });
-    staticFieldValuesMap.clear();
-    return new EnumDataMap(builder.build());
-  }
-
-  private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) {
-    if (!enumClass.hasStaticFields()) {
-      return new EnumData(ImmutableMap.of(), ImmutableMap.of(), ImmutableSet.of(), -1);
-    }
-
-    // This map holds all the accessible fields to their unboxed value, so we can remap the field
-    // read to the unboxed value.
-    ImmutableMap.Builder<DexField, Integer> unboxedValues = ImmutableMap.builder();
-    // This maps the ordinal to the object state, note that some fields may have been removed,
-    // hence the entry is in this map but not the enumToOrdinalMap.
-    Int2ReferenceMap<ObjectState> ordinalToObjectState = new Int2ReferenceArrayMap<>();
-    // Any fields matching the expected $VALUES content can be recorded here, they have however
-    // all the same content.
-    ImmutableSet.Builder<DexField> valuesField = ImmutableSet.builder();
-    EnumValuesObjectState valuesContents = null;
-
-    EnumStaticFieldValues enumStaticFieldValues = staticFieldValuesMap.get(enumClass.type);
-    if (enumStaticFieldValues == null) {
-      reportFailure(enumClass, new MissingEnumStaticFieldValuesReason());
-      return null;
-    }
-
-    // Step 1: We iterate over the field to find direct enum instance information and the values
-    // fields.
-    for (DexEncodedField staticField : enumClass.staticFields()) {
-      if (factory.enumMembers.isEnumField(staticField, enumClass.type)) {
-        ObjectState enumState =
-            enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
-        if (enumState == null) {
-          if (staticField.getOptimizationInfo().isDead()) {
-            // We don't care about unused field data.
-            continue;
-          }
-          // We could not track the content of that field. We bail out.
-          reportFailure(
-              enumClass, new MissingObjectStateForEnumInstanceReason(staticField.getReference()));
-          return null;
-        }
-        OptionalInt optionalOrdinal = getOrdinal(enumState);
-        if (!optionalOrdinal.isPresent()) {
-          reportFailure(
-              enumClass,
-              new MissingInstanceFieldValueForEnumInstanceReason(
-                  staticField.getReference(), factory.enumMembers.ordinalField));
-          return null;
-        }
-        int ordinal = optionalOrdinal.getAsInt();
-        unboxedValues.put(staticField.getReference(), ordinalToUnboxedInt(ordinal));
-        ordinalToObjectState.put(ordinal, enumState);
-      } else if (factory.enumMembers.isValuesFieldCandidate(staticField, enumClass.type)) {
-        ObjectState valuesState =
-            enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
-        if (valuesState == null) {
-          if (staticField.getOptimizationInfo().isDead()) {
-            // We don't care about unused field data.
-            continue;
-          }
-          // We could not track the content of that field. We bail out.
-          // We could not track the content of that field, and the field could be a values field.
-          // We conservatively bail out.
-          reportFailure(
-              enumClass, new MissingContentsForEnumValuesArrayReason(staticField.getReference()));
-          return null;
-        }
-        assert valuesState.isEnumValuesObjectState();
-        assert valuesContents == null
-            || valuesContents.equals(valuesState.asEnumValuesObjectState());
-        valuesContents = valuesState.asEnumValuesObjectState();
-        valuesField.add(staticField.getReference());
-      }
-    }
-
-    // Step 2: We complete the information based on the values content, since some enum instances
-    // may be reachable only though the $VALUES field.
-    if (valuesContents != null) {
-      for (int ordinal = 0; ordinal < valuesContents.getEnumValuesSize(); ordinal++) {
-        if (!ordinalToObjectState.containsKey(ordinal)) {
-          ObjectState enumState = valuesContents.getObjectStateForOrdinal(ordinal);
-          if (enumState.isEmpty()) {
-            // If $VALUES is used, we need data for all enums, at least the ordinal.
-            return null;
-          }
-          assert getOrdinal(enumState).isPresent();
-          assert getOrdinal(enumState).getAsInt() == ordinal;
-          ordinalToObjectState.put(ordinal, enumState);
-        }
-      }
-    }
-
-    // The ordinalToObjectState map may have holes at this point, if some enum instances are never
-    // used ($VALUES unused or removed, and enum instance field unused or removed), it contains
-    // only data for reachable enum instance, that is what we're interested in.
-    ImmutableMap<DexField, EnumInstanceFieldKnownData> instanceFieldsData =
-        computeRequiredEnumInstanceFieldsData(enumClass, instanceFields, ordinalToObjectState);
-    if (instanceFieldsData == null) {
-      return null;
-    }
-
-    return new EnumData(
-        instanceFieldsData,
-        unboxedValues.build(),
-        valuesField.build(),
-        valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize());
-  }
-
-  private ImmutableMap<DexField, EnumInstanceFieldKnownData> computeRequiredEnumInstanceFieldsData(
-      DexProgramClass enumClass,
-      Set<DexField> instanceFields,
-      Int2ReferenceMap<ObjectState> ordinalToObjectState) {
-    ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> builder = ImmutableMap.builder();
-    for (DexField instanceField : instanceFields) {
-      EnumInstanceFieldData fieldData =
-          computeRequiredEnumInstanceFieldData(instanceField, enumClass, ordinalToObjectState);
-      if (fieldData.isUnknown()) {
-        if (!debugLogEnabled) {
-          return null;
-        }
-        builder = null;
-      }
-      if (builder != null) {
-        builder.put(instanceField, fieldData.asEnumFieldKnownData());
-      }
-    }
-    return builder != null ? builder.build() : null;
-  }
-
-  private EnumInstanceFieldData computeRequiredEnumInstanceFieldData(
-      DexField instanceField,
-      DexProgramClass enumClass,
-      Int2ReferenceMap<ObjectState> ordinalToObjectState) {
-    DexEncodedField encodedInstanceField =
-        appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField();
-    assert encodedInstanceField != null;
-    boolean canBeOrdinal = instanceField.type.isIntType();
-    ImmutableInt2ReferenceSortedMap.Builder<AbstractValue> data =
-        ImmutableInt2ReferenceSortedMap.builder();
-    for (Integer ordinal : ordinalToObjectState.keySet()) {
-      ObjectState state = ordinalToObjectState.get(ordinal);
-      AbstractValue fieldValue = state.getAbstractFieldValue(encodedInstanceField);
-      if (!fieldValue.isSingleValue()) {
-        reportFailure(
-            enumClass, new MissingInstanceFieldValueForEnumInstanceReason(ordinal, instanceField));
-        return EnumInstanceFieldUnknownData.getInstance();
-      }
-      if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) {
-        reportFailure(
-            enumClass,
-            new UnsupportedInstanceFieldValueForEnumInstanceReason(ordinal, instanceField));
-        return EnumInstanceFieldUnknownData.getInstance();
-      }
-      data.put(ordinalToUnboxedInt(ordinal), fieldValue);
-      if (canBeOrdinal) {
-        assert fieldValue.isSingleNumberValue();
-        int computedValue = fieldValue.asSingleNumberValue().getIntValue();
-        if (computedValue != ordinal) {
-          canBeOrdinal = false;
-        }
-      }
-    }
-    if (canBeOrdinal) {
-      return new EnumInstanceFieldOrdinalData();
-    }
-    return new EnumInstanceFieldMappingData(data.build());
-  }
-
-  private OptionalInt getOrdinal(ObjectState state) {
-    AbstractValue field = state.getAbstractFieldValue(getOrdinalField().getDefinition());
-    if (field.isSingleNumberValue()) {
-      return OptionalInt.of(field.asSingleNumberValue().getIntValue());
-    }
-    return OptionalInt.empty();
-  }
-
-  public void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues) {
-    if (staticFieldValues == null || !staticFieldValues.isEnumStaticFieldValues()) {
-      return;
-    }
-    assert clazz.isEnum();
-    EnumStaticFieldValues enumStaticFieldValues = staticFieldValues.asEnumStaticFieldValues();
-    if (getEnumUnboxingCandidateOrNull(clazz.type) != null) {
-      staticFieldValuesMap.put(clazz.type, enumStaticFieldValues);
-    }
-  }
-
-  private class EnumAccessibilityUseRegistry extends UseRegistry {
-
-    private ProgramMethod context;
-    private Constraint constraint;
-
-    public EnumAccessibilityUseRegistry(DexItemFactory factory) {
-      super(factory);
-    }
-
-    public Constraint computeConstraint(ProgramMethod method) {
-      constraint = Constraint.ALWAYS;
-      context = method;
-      method.registerCodeReferences(this);
-      return constraint;
-    }
-
-    public Constraint deriveConstraint(DexType targetHolder, AccessFlags<?> flags) {
-      DexProgramClass contextHolder = context.getHolder();
-      if (targetHolder == contextHolder.type) {
-        return Constraint.ALWAYS;
-      }
-      if (flags.isPublic()) {
-        return Constraint.ALWAYS;
-      }
-      if (flags.isPrivate()) {
-        // Enum unboxing is currently happening only cf to dex, and no class should be in a nest
-        // at this point. If that is the case, we just don't unbox the enum, or we would need to
-        // support Constraint.SAMENEST in the enum unboxer.
-        assert !contextHolder.isInANest();
-        // Only accesses within the enum are allowed since all enum methods and fields will be
-        // moved to the same class, and the enum itself becomes an integer, which is
-        // accessible everywhere.
-        return Constraint.NEVER;
-      }
-      assert flags.isProtected() || flags.isPackagePrivate();
-      // Protected is in practice equivalent to package private in this analysis since we are
-      // accessing the member from an enum context where subclassing is limited.
-      // At this point we don't support unboxing enums with subclasses, so we assume either
-      // same package access, or we just don't unbox.
-      // The only protected methods in java.lang.Enum are clone, finalize and the constructor.
-      // Besides calls to the constructor in the instance initializer, Enums with calls to such
-      // methods cannot be unboxed.
-      return targetHolder.isSamePackage(contextHolder.type) ? Constraint.PACKAGE : Constraint.NEVER;
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {
-      if (type.isArrayType()) {
-        registerTypeReference(type.toBaseType(factory));
-        return;
-      }
-
-      if (type.isPrimitiveType()) {
-        return;
-      }
-
-      DexClass definition = appView.definitionFor(type);
-      if (definition == null) {
-        constraint = Constraint.NEVER;
-        return;
-      }
-      constraint = constraint.meet(deriveConstraint(type, definition.accessFlags));
-    }
-
-    @Override
-    public void registerInitClass(DexType type) {
-      registerTypeReference(type);
-    }
-
-    @Override
-    public void registerInstanceOf(DexType type) {
-      registerTypeReference(type);
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      registerTypeReference(type);
-    }
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {
-      registerVirtualInvoke(method, false);
-    }
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {
-      registerVirtualInvoke(method, true);
-    }
-
-    private void registerVirtualInvoke(DexMethod method, boolean isInterface) {
-      if (method.holder.isArrayType()) {
-        return;
-      }
-      // Perform resolution and derive unboxing constraints based on the accessibility of the
-      // resolution result.
-      MethodResolutionResult resolutionResult =
-          appView.appInfo().resolveMethod(method, isInterface);
-      if (!resolutionResult.isVirtualTarget()) {
-        constraint = Constraint.NEVER;
-        return;
-      }
-      registerTarget(
-          resolutionResult.getInitialResolutionHolder(), resolutionResult.getSingleTarget());
-    }
-
-    private void registerTarget(DexClass initialResolutionHolder, DexEncodedMember<?, ?> target) {
-      if (target == null) {
-        // This will fail at runtime.
-        constraint = Constraint.NEVER;
-        return;
-      }
-      DexType resolvedHolder = target.getHolderType();
-      if (initialResolutionHolder == null) {
-        constraint = Constraint.NEVER;
-        return;
-      }
-      Constraint memberConstraint = deriveConstraint(resolvedHolder, target.getAccessFlags());
-      // We also have to take the constraint of the initial resolution holder into account.
-      Constraint classConstraint =
-          deriveConstraint(initialResolutionHolder.type, initialResolutionHolder.accessFlags);
-      Constraint instructionConstraint = memberConstraint.meet(classConstraint);
-      constraint = instructionConstraint.meet(constraint);
-    }
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {
-      registerSingleTargetInvoke(method, DexEncodedMethod::isDirectMethod);
-    }
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {
-      registerSingleTargetInvoke(method, DexEncodedMethod::isStatic);
-    }
-
-    private void registerSingleTargetInvoke(
-        DexMethod method, Predicate<DexEncodedMethod> methodValidator) {
-      if (method.holder.isArrayType()) {
-        return;
-      }
-      MethodResolutionResult resolutionResult =
-          appView.appInfo().unsafeResolveMethodDueToDexFormat(method);
-      DexEncodedMethod target = resolutionResult.getSingleTarget();
-      if (target == null || !methodValidator.test(target)) {
-        constraint = Constraint.NEVER;
-        return;
-      }
-      registerTarget(resolutionResult.getInitialResolutionHolder(), target);
-    }
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {
-      // Invoke-super can only target java.lang.Enum methods since we do not unbox enums with
-      // subclasses. Calls to java.lang.Object methods would have resulted in the enum to be marked
-      // as unboxable. The methods of java.lang.Enum called are already analyzed in the enum
-      // unboxer analysis, so invoke-super is always valid.
-      assert method.holder == factory.enumType;
-    }
-
-    @Override
-    public void registerCallSite(DexCallSite callSite) {
-      // This is reached after lambda desugaring, so this should not be a lambda call site.
-      // We do not unbox enums with invoke custom since it's not clear the accessibility
-      // constraints would be correct if the method holding the invoke custom is moved to
-      // another class.
-      assert appView.options().isGeneratingClassFiles()
-          || !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod());
-      constraint = Constraint.NEVER;
-    }
-
-    private void registerFieldInstruction(DexField field) {
-      FieldResolutionResult fieldResolutionResult = appView.appInfo().resolveField(field, context);
-      registerTarget(
-          fieldResolutionResult.getInitialResolutionHolder(),
-          fieldResolutionResult.getResolvedField());
-    }
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {
-      registerFieldInstruction(field);
-    }
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {
-      registerFieldInstruction(field);
-    }
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {
-      registerFieldInstruction(field);
-    }
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {
-      registerFieldInstruction(field);
-    }
-  }
-
-  private void analyzeInitializers() {
-    enumUnboxingCandidatesInfo.forEachCandidate(
-        enumClass -> {
-          for (DexEncodedMethod directMethod : enumClass.directMethods()) {
-            if (directMethod.isInstanceInitializer()) {
-              if (directMethod
-                  .getOptimizationInfo()
-                  .getContextInsensitiveInstanceInitializerInfo()
-                  .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
-                if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
-                  break;
-                }
-              }
-            }
-          }
-          if (enumClass.classInitializationMayHaveSideEffects(appView)) {
-            markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
-          }
-        });
-  }
-
-  private Reason instructionAllowEnumUnboxing(
-      Instruction instruction, IRCode code, DexProgramClass enumClass, Value enumValue) {
-    ProgramMethod context = code.context();
-    switch (instruction.opcode()) {
-      case ASSUME:
-        return analyzeAssumeUser(instruction.asAssume(), code, context, enumClass, enumValue);
-      case ARRAY_GET:
-        return analyzeArrayGetUser(instruction.asArrayGet(), code, context, enumClass, enumValue);
-      case ARRAY_LENGTH:
-        return analyzeArrayLengthUser(
-            instruction.asArrayLength(), code, context, enumClass, enumValue);
-      case ARRAY_PUT:
-        return analyzeArrayPutUser(instruction.asArrayPut(), code, context, enumClass, enumValue);
-      case CHECK_CAST:
-        return analyzeCheckCastUser(instruction.asCheckCast(), code, context, enumClass, enumValue);
-      case IF:
-        return analyzeIfUser(instruction.asIf(), code, context, enumClass, enumValue);
-      case INSTANCE_GET:
-        return analyzeInstanceGetUser(
-            instruction.asInstanceGet(), code, context, enumClass, enumValue);
-      case INSTANCE_PUT:
-        return analyzeFieldPutUser(
-            instruction.asInstancePut(), code, context, enumClass, enumValue);
-      case INVOKE_DIRECT:
-      case INVOKE_INTERFACE:
-      case INVOKE_STATIC:
-      case INVOKE_SUPER:
-      case INVOKE_VIRTUAL:
-        return analyzeInvokeUser(instruction.asInvokeMethod(), code, context, enumClass, enumValue);
-      case RETURN:
-        return analyzeReturnUser(instruction.asReturn(), code, context, enumClass, enumValue);
-      case STATIC_PUT:
-        return analyzeFieldPutUser(instruction.asStaticPut(), code, context, enumClass, enumValue);
-      default:
-        return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
-    }
-  }
-
-  private Reason analyzeAssumeUser(
-      Assume assume,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    return validateEnumUsages(code, assume.outValue(), enumClass);
-  }
-
-  private Reason analyzeArrayGetUser(
-      ArrayGet arrayGet,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    // MyEnum[] array = ...; array[0]; is valid.
-    return Reason.ELIGIBLE;
-  }
-
-  private Reason analyzeArrayLengthUser(
-      ArrayLength arrayLength,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    // MyEnum[] array = ...; array.length; is valid.
-    return Reason.ELIGIBLE;
-  }
-
-  private Reason analyzeArrayPutUser(
-      ArrayPut arrayPut,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    // MyEnum[] array; array[0] = MyEnum.A; is valid.
-    // MyEnum[][] array2d; MyEnum[] array; array2d[0] = array; is valid.
-    // MyEnum[]^N array; MyEnum[]^(N-1) element; array[0] = element; is valid.
-    // We need to prove that the value to put in and the array have correct types.
-    assert arrayPut.getMemberType() == MemberType.OBJECT;
-    TypeElement arrayType = arrayPut.array().getType();
-    assert arrayType.isArrayType();
-    assert arrayType.asArrayType().getBaseType().isClassType();
-    ClassTypeElement arrayBaseType = arrayType.asArrayType().getBaseType().asClassType();
-    TypeElement valueBaseType = arrayPut.value().getType();
-    if (valueBaseType.isArrayType()) {
-      assert valueBaseType.asArrayType().getBaseType().isClassType();
-      assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
-      valueBaseType = valueBaseType.asArrayType().getBaseType();
-    }
-    if (arrayBaseType.equalUpToNullability(valueBaseType)
-        && arrayBaseType.getClassType() == enumClass.type) {
-      return Reason.ELIGIBLE;
-    }
-    return Reason.INVALID_ARRAY_PUT;
-  }
-
-  private Reason analyzeCheckCastUser(
-      CheckCast checkCast,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    if (allowCheckCast(checkCast)) {
-      return Reason.ELIGIBLE;
-    }
-    return Reason.DOWN_CAST;
-  }
-
-  // A field put is valid only if the field is not on an enum, and the field type and the valuePut
-  // have identical enum type.
-  private Reason analyzeFieldPutUser(
-      FieldInstruction fieldPut,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    assert fieldPut.isInstancePut() || fieldPut.isStaticPut();
-    DexEncodedField field = appView.appInfo().resolveField(fieldPut.getField()).getResolvedField();
-    if (field == null) {
-      return Reason.INVALID_FIELD_PUT;
-    }
-    DexProgramClass dexClass = appView.programDefinitionFor(field.getHolderType(), code.context());
-    if (dexClass == null) {
-      return Reason.INVALID_FIELD_PUT;
-    }
-    if (fieldPut.isInstancePut() && fieldPut.asInstancePut().object() == enumValue) {
-      return Reason.ELIGIBLE;
-    }
-    // The put value has to be of the field type.
-    if (field.getReference().type.toBaseType(factory) != enumClass.type) {
-      return Reason.TYPE_MISMATCH_FIELD_PUT;
-    }
-    return Reason.ELIGIBLE;
-  }
-
-  // An If using enum as inValue is valid if it matches e == null
-  // or e == X with X of same enum type as e. Ex: if (e == MyEnum.A).
-  private Reason analyzeIfUser(
-      If theIf, IRCode code, ProgramMethod context, DexProgramClass enumClass, Value enumValue) {
-    assert (theIf.getType() == If.Type.EQ || theIf.getType() == If.Type.NE)
-        : "Comparing a reference with " + theIf.getType().toString();
-    // e == null.
-    if (theIf.isZeroTest()) {
-      return Reason.ELIGIBLE;
-    }
-    // e == MyEnum.X
-    TypeElement leftType = theIf.lhs().getType();
-    TypeElement rightType = theIf.rhs().getType();
-    if (leftType.equalUpToNullability(rightType)) {
-      assert leftType.isClassType();
-      assert leftType.asClassType().getClassType() == enumClass.type;
-      return Reason.ELIGIBLE;
-    }
-    return Reason.INVALID_IF_TYPES;
-  }
-
-  private Reason analyzeInstanceGetUser(
-      InstanceGet instanceGet,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    assert instanceGet.getField().holder == enumClass.type;
-    DexField field = instanceGet.getField();
-    enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData(enumClass, field);
-    return Reason.ELIGIBLE;
-  }
-
-  // All invokes in the library are invalid, besides a few cherry picked cases such as ordinal().
-  private Reason analyzeInvokeUser(
-      InvokeMethod invoke,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    if (invoke.getInvokedMethod().holder.isArrayType()) {
-      // The only valid methods is clone for values() to be correct.
-      if (invoke.getInvokedMethod().name == factory.cloneMethodName) {
-        return Reason.ELIGIBLE;
-      }
-      return Reason.INVALID_INVOKE_ON_ARRAY;
-    }
-    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
-    if (singleTarget == null) {
-      return Reason.INVALID_INVOKE;
-    }
-    DexMethod singleTargetReference = singleTarget.getReference();
-    DexClass targetHolder = singleTarget.getHolder();
-    if (targetHolder.isProgramClass()) {
-      if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
-        if (code.context().getHolder() == targetHolder && code.method().isClassInitializer()) {
-          // The enum instance initializer is allowed to be called only from the enum clinit.
-          return Reason.ELIGIBLE;
-        } else {
-          return Reason.INVALID_INIT;
-        }
-      }
-
-      // Check if this is a checkNotNull() user. In this case, we can create a copy of the method
-      // that takes an int instead of java.lang.Object and call that method instead.
-      EnumUnboxerMethodClassification classification =
-          singleTarget.getOptimizationInfo().getEnumUnboxerMethodClassification();
-      if (classification.isCheckNotNullClassification()) {
-        CheckNotNullEnumUnboxerMethodClassification checkNotNullClassification =
-            classification.asCheckNotNullClassification();
-        if (checkNotNullClassification.isUseEligibleForUnboxing(
-            invoke.asInvokeStatic(), enumValue)) {
-          checkNotNullMethods
-              .computeIfAbsent(
-                  singleTarget.asProgramMethod(), ignoreKey(Sets::newConcurrentHashSet))
-              .add(enumClass);
-          return Reason.ELIGIBLE;
-        }
-      }
-
-      // Check that the enum-value only flows into parameters whose type exactly matches the
-      // enum's type.
-      for (int i = 0; i < singleTarget.getParameters().size(); i++) {
-        if (invoke.getArgumentForParameter(i) == enumValue
-            && singleTarget.getParameter(i).toBaseType(factory) != enumClass.getType()) {
-          return new IllegalInvokeWithImpreciseParameterTypeReason(singleTargetReference);
-        }
-      }
-      if (invoke.isInvokeMethodWithReceiver()) {
-        Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
-        if (receiver == enumValue && targetHolder.isInterface()) {
-          return Reason.DEFAULT_METHOD_INVOKE;
-        }
-      }
-      return Reason.ELIGIBLE;
-    }
-
-    if (targetHolder.isClasspathClass()) {
-      return Reason.INVALID_INVOKE_CLASSPATH;
-    }
-
-    assert targetHolder.isLibraryClass();
-
-    Reason reason =
-        analyzeLibraryInvoke(
-            invoke, code, context, enumClass, enumValue, singleTargetReference, targetHolder);
-
-    if (reason == Reason.ELIGIBLE) {
-      markMethodDependsOnLibraryModelisation(context);
-    }
-
-    return reason;
-  }
-
-  private Reason analyzeLibraryInvoke(
-      InvokeMethod invoke,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue,
-      DexMethod singleTargetReference,
-      DexClass targetHolder) {
-    // Calls to java.lang.Enum.
-    if (targetHolder.getType() == factory.enumType) {
-      // TODO(b/147860220): EnumSet and EnumMap may be interesting to model.
-      if (singleTargetReference == factory.enumMembers.compareTo
-          || singleTargetReference == factory.enumMembers.compareToWithObject) {
-        DexProgramClass otherEnumClass =
-            getEnumUnboxingCandidateOrNull(invoke.getLastArgument().getType());
-        if (otherEnumClass == enumClass || invoke.getLastArgument().getType().isNullType()) {
-          return Reason.ELIGIBLE;
-        }
-      } else if (singleTargetReference == factory.enumMembers.equals) {
-        return Reason.ELIGIBLE;
-      } else if (singleTargetReference == factory.enumMembers.nameMethod
-          || singleTargetReference == factory.enumMembers.toString) {
-        assert invoke.asInvokeMethodWithReceiver().getReceiver() == enumValue;
-        addRequiredNameData(enumClass);
-        return Reason.ELIGIBLE;
-      } else if (singleTargetReference == factory.enumMembers.ordinalMethod) {
-        return Reason.ELIGIBLE;
-      } else if (singleTargetReference == factory.enumMembers.hashCode) {
-        return Reason.ELIGIBLE;
-      } else if (singleTargetReference == factory.enumMembers.constructor) {
-        // Enum constructor call is allowed only if called from an enum initializer.
-        if (code.method().isInstanceInitializer() && code.context().getHolder() == enumClass) {
-          return Reason.ELIGIBLE;
-        }
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Calls to java.lang.Object.
-    if (targetHolder.getType() == factory.objectType) {
-      // Object#getClass without outValue is important since R8 rewrites explicit null checks to
-      // such instructions.
-      if (singleTargetReference == factory.objectMembers.getClass && invoke.hasUnusedOutValue()) {
-        // This is a hidden null check.
-        return Reason.ELIGIBLE;
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Calls to java.lang.Objects.
-    if (targetHolder.getType() == factory.objectsType) {
-      // Objects#requireNonNull is important since R8 rewrites explicit null checks to such
-      // instructions.
-      if (singleTargetReference == factory.objectsMethods.requireNonNull
-          || singleTargetReference == factory.objectsMethods.requireNonNullWithMessage) {
-        return Reason.ELIGIBLE;
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Calls to java.lang.String.
-    if (targetHolder.getType() == factory.stringType) {
-      if (singleTargetReference == factory.stringMembers.valueOf) {
-        addRequiredNameData(enumClass);
-        return Reason.ELIGIBLE;
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Calls to java.lang.StringBuilder and java.lang.StringBuffer.
-    if (targetHolder.getType() == factory.stringBuilderType
-        || targetHolder.getType() == factory.stringBufferType) {
-      if (singleTargetReference == factory.stringBuilderMethods.appendObject
-          || singleTargetReference == factory.stringBufferMethods.appendObject) {
-        addRequiredNameData(enumClass);
-        return Reason.ELIGIBLE;
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Calls to java.lang.System.
-    if (targetHolder.getType() == factory.javaLangSystemType) {
-      if (singleTargetReference == factory.javaLangSystemMethods.arraycopy) {
-        // Important for Kotlin 1.5 enums, which use arraycopy to create a copy of $VALUES instead
-        // of int[].clone().
-        return Reason.ELIGIBLE;
-      }
-      if (singleTargetReference == factory.javaLangSystemMethods.identityHashCode) {
-        // Important for proto enum unboxing.
-        return Reason.ELIGIBLE;
-      }
-      return new UnsupportedLibraryInvokeReason(singleTargetReference);
-    }
-
-    // Unsupported holder.
-    return new UnsupportedLibraryInvokeReason(singleTargetReference);
-  }
-
-  // Return is used for valueOf methods.
-  private Reason analyzeReturnUser(
-      Return theReturn,
-      IRCode code,
-      ProgramMethod context,
-      DexProgramClass enumClass,
-      Value enumValue) {
-    DexType returnType = context.getReturnType();
-    if (returnType != enumClass.type && returnType.toBaseType(factory) != enumClass.type) {
-      return Reason.IMPLICIT_UP_CAST_IN_RETURN;
-    }
-    return Reason.ELIGIBLE;
-  }
-
-  private void reportEnumsAnalysis() {
-    assert debugLogEnabled;
-    Reporter reporter = appView.reporter();
-    Set<DexType> candidates = enumUnboxingCandidatesInfo.candidates();
-    reporter.info(
-        new StringDiagnostic(
-            "Unboxed " + candidates.size() + " enums: " + Arrays.toString(candidates.toArray())));
-
-    StringBuilder sb =
-        new StringBuilder("Unable to unbox ")
-            .append(debugLogs.size())
-            .append(" enums.")
-            .append(System.lineSeparator())
-            .append(System.lineSeparator());
-
-    // Sort by the number of reasons that prevent enum unboxing.
-    TreeMap<DexType, List<Reason>> sortedDebugLogs =
-        new TreeMap<>(
-            Comparator.<DexType>comparingInt(x -> debugLogs.get(x).size())
-                .thenComparing(Function.identity()));
-    sortedDebugLogs.putAll(debugLogs);
-
-    // Print the pinned enums and remove them from further reporting.
-    List<DexType> pinned = new ArrayList<>();
-    Iterator<Entry<DexType, List<Reason>>> sortedDebugLogIterator =
-        sortedDebugLogs.entrySet().iterator();
-    while (sortedDebugLogIterator.hasNext()) {
-      Entry<DexType, List<Reason>> entry = sortedDebugLogIterator.next();
-      List<Reason> reasons = entry.getValue();
-      if (reasons.size() > 1) {
-        break;
-      }
-      if (reasons.get(0) == Reason.PINNED) {
-        pinned.add(entry.getKey());
-        sortedDebugLogIterator.remove();
-      }
-    }
-    if (!pinned.isEmpty()) {
-      sb.append("Pinned: ").append(Arrays.toString(pinned.toArray()));
-    }
-
-    // Print the reasons for each unboxable enum.
-    sortedDebugLogs.forEach(
-        (type, reasons) -> {
-          sb.append(type).append(" (").append(reasons.size()).append(" reasons):");
-          HashMultiset.create(reasons)
-              .forEachEntry(
-                  (reason, count) ->
-                      sb.append(System.lineSeparator())
-                          .append(" - ")
-                          .append(reason)
-                          .append(" (")
-                          .append(count)
-                          .append(")"));
-          sb.append(System.lineSeparator());
-        });
-
-    sb.append(System.lineSeparator());
-
-    // Print information about how often a given Reason kind prevents enum unboxing.
-    Object2IntMap<Object> reasonKindCount = new Object2IntOpenHashMap<>();
-    debugLogs.forEach(
-        (type, reasons) ->
-            reasons.forEach(
-                reason ->
-                    reasonKindCount.put(reason.getKind(), reasonKindCount.getInt(reason) + 1)));
-    List<Object> differentReasonKinds = new ArrayList<>(reasonKindCount.keySet());
-    differentReasonKinds.sort(
-        (reasonKind, other) -> {
-          int freq = reasonKindCount.getInt(reasonKind) - reasonKindCount.getInt(other);
-          return freq != 0
-              ? freq
-              : System.identityHashCode(reasonKind) - System.identityHashCode(other);
-        });
-    differentReasonKinds.forEach(
-        reasonKind ->
-            sb.append(reasonKind)
-                .append(" (")
-                .append(reasonKindCount.getInt(reasonKind))
-                .append(")")
-                .append(System.lineSeparator()));
-
-    reporter.info(new StringDiagnostic(sb.toString()));
-  }
-
-  boolean reportFailure(DexProgramClass enumClass, Reason reason) {
-    return reportFailure(enumClass.getType(), reason);
-  }
-
-  /** Returns true if the failure was reported. */
-  boolean reportFailure(DexType enumType, Reason reason) {
-    if (debugLogEnabled) {
-      debugLogs
-          .computeIfAbsent(enumType, ignore -> Collections.synchronizedList(new ArrayList<>()))
-          .add(reason);
-      return true;
-    }
-    return false;
-  }
-
-  public Set<Phi> rewriteCode(IRCode code, MethodProcessor methodProcessor) {
-    // This has no effect during primary processing since the enumUnboxerRewriter is set
-    // in between primary and post processing.
-    if (enumUnboxerRewriter != null) {
-      return enumUnboxerRewriter.rewriteCode(code, methodProcessor);
-    }
-    return Sets.newIdentityHashSet();
-  }
-
-  public void unsetRewriter() {
-    enumUnboxerRewriter = null;
-  }
+  public abstract void updateEnumUnboxingCandidatesInfo();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
new file mode 100644
index 0000000..e4229c2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -0,0 +1,1620 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.code.Opcodes.ARRAY_GET;
+import static com.android.tools.r8.ir.code.Opcodes.ARRAY_LENGTH;
+import static com.android.tools.r8.ir.code.Opcodes.ARRAY_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
+import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS;
+import static com.android.tools.r8.ir.code.Opcodes.IF;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_CUSTOM;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
+import static com.android.tools.r8.ir.code.Opcodes.RETURN;
+import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
+import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AccessFlags;
+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.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues.EnumStaticFieldValues;
+import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState;
+import com.android.tools.r8.ir.analysis.value.ObjectState;
+import com.android.tools.r8.ir.code.ArrayGet;
+import com.android.tools.r8.ir.code.ArrayLength;
+import com.android.tools.r8.ir.code.ArrayPut;
+import com.android.tools.r8.ir.code.Assume;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeCustom;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldOrdinalData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldUnknownData;
+import com.android.tools.r8.ir.optimize.enums.classification.CheckNotNullEnumUnboxerMethodClassification;
+import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.IllegalInvokeWithImpreciseParameterTypeReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingContentsForEnumValuesArrayReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingEnumStaticFieldValuesReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingInstanceFieldValueForEnumInstanceReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingObjectStateForEnumInstanceReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedInstanceFieldValueForEnumInstanceReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedLibraryInvokeReason;
+import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap;
+import com.android.tools.r8.utils.collections.LongLivedClassSetBuilder;
+import com.android.tools.r8.utils.collections.LongLivedProgramMethodMapBuilder;
+import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class EnumUnboxerImpl extends EnumUnboxer {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final DexItemFactory factory;
+  // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given
+  // enum if the optimization eventually decides to unbox it.
+  private EnumUnboxingCandidateInfoCollection enumUnboxingCandidatesInfo;
+  private final Set<DexProgramClass> candidatesToRemoveInWave = Sets.newConcurrentHashSet();
+  private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap =
+      new ConcurrentHashMap<>();
+
+  // Methods depending on library modelisation need to be reprocessed so they are peephole
+  // optimized.
+  private LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodsDependingOnLibraryModelisation;
+
+  // Map from checkNotNull() methods to the enums that use the given method.
+  private LongLivedProgramMethodMapBuilder<LongLivedClassSetBuilder<DexProgramClass>>
+      checkNotNullMethodsBuilder;
+
+  private final DexClassAndField ordinalField;
+
+  private EnumUnboxingRewriter enumUnboxerRewriter;
+
+  private final boolean debugLogEnabled;
+  private final Map<DexType, List<Reason>> debugLogs;
+
+  EnumUnboxerImpl(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.factory = appView.dexItemFactory();
+    if (appView.options().testing.enableEnumUnboxingDebugLogs) {
+      debugLogEnabled = true;
+      debugLogs = new ConcurrentHashMap<>();
+    } else {
+      debugLogEnabled = false;
+      debugLogs = null;
+    }
+    assert !appView.options().debug;
+    ordinalField =
+        appView.appInfo().resolveField(factory.enumMembers.ordinalField).getResolutionPair();
+  }
+
+  public static int ordinalToUnboxedInt(int ordinal) {
+    return ordinal + 1;
+  }
+
+  public DexClassAndField getOrdinalField() {
+    return ordinalField;
+  }
+
+  @Override
+  public void updateEnumUnboxingCandidatesInfo() {
+    for (DexProgramClass candidate : candidatesToRemoveInWave) {
+      enumUnboxingCandidatesInfo.removeCandidate(candidate);
+    }
+    candidatesToRemoveInWave.clear();
+  }
+
+  /**
+   * Returns true if {@param enumClass} was marked as being unboxable.
+   *
+   * <p>Note that, if debug logging is enabled, {@param enumClass} is not marked unboxable until the
+   * enum unboxing analysis has finished. This is to ensure completeness of the reason reporting.
+   */
+  private boolean markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
+    assert enumClass.isEnum();
+    if (!reportFailure(enumClass, reason)) {
+      // The failure was not reported, meaning debug logging is disabled.
+      candidatesToRemoveInWave.add(enumClass);
+      return true;
+    }
+    return false;
+  }
+
+  private void markMethodDependsOnLibraryModelisation(ProgramMethod method) {
+    methodsDependingOnLibraryModelisation.add(method, appView.graphLens());
+  }
+
+  private DexProgramClass getEnumUnboxingCandidateOrNull(TypeElement lattice) {
+    if (lattice.isClassType()) {
+      DexType classType = lattice.asClassType().getClassType();
+      return getEnumUnboxingCandidateOrNull(classType);
+    }
+    if (lattice.isArrayType()) {
+      ArrayTypeElement arrayType = lattice.asArrayType();
+      if (arrayType.getBaseType().isClassType()) {
+        return getEnumUnboxingCandidateOrNull(arrayType.getBaseType());
+      }
+    }
+    return null;
+  }
+
+  private DexProgramClass getEnumUnboxingCandidateOrNull(DexType type) {
+    if (type.isArrayType()) {
+      return getEnumUnboxingCandidateOrNull(type.toBaseType(appView.dexItemFactory()));
+    }
+    if (type.isPrimitiveType() || type.isVoidType()) {
+      return null;
+    }
+    assert type.isClassType();
+    return enumUnboxingCandidatesInfo.getCandidateClassOrNull(type);
+  }
+
+  @Override
+  public void analyzeEnums(IRCode code, MutableMethodConversionOptions conversionOptions) {
+    Set<DexType> eligibleEnums = Sets.newIdentityHashSet();
+    for (BasicBlock block : code.blocks) {
+      for (Instruction instruction : block.getInstructions()) {
+        Value outValue = instruction.outValue();
+        if (outValue != null) {
+          DexProgramClass enumClass =
+              getEnumUnboxingCandidateOrNull(outValue.getDynamicUpperBoundType(appView));
+          if (enumClass != null) {
+            Reason reason = validateEnumUsages(code, outValue, enumClass);
+            if (reason == Reason.ELIGIBLE) {
+              eligibleEnums.add(enumClass.type);
+            }
+          }
+          if (outValue.getType().isNullType()) {
+            addNullDependencies(code, outValue.uniqueUsers(), eligibleEnums);
+          }
+        } else {
+          if (instruction.isInvokeMethod()) {
+            DexProgramClass enumClass =
+                getEnumUnboxingCandidateOrNull(instruction.asInvokeMethod().getReturnType());
+            if (enumClass != null) {
+              eligibleEnums.add(enumClass.type);
+            }
+          }
+        }
+        switch (instruction.opcode()) {
+          case CONST_CLASS:
+            analyzeConstClass(instruction.asConstClass(), eligibleEnums, code.context());
+            break;
+          case CHECK_CAST:
+            analyzeCheckCast(instruction.asCheckCast(), eligibleEnums);
+            break;
+          case INVOKE_CUSTOM:
+            analyzeInvokeCustom(instruction.asInvokeCustom(), eligibleEnums);
+            break;
+          case INVOKE_STATIC:
+            analyzeInvokeStatic(instruction.asInvokeStatic(), eligibleEnums, code.context());
+            break;
+          case STATIC_GET:
+          case INSTANCE_GET:
+          case STATIC_PUT:
+          case INSTANCE_PUT:
+            analyzeFieldInstruction(
+                instruction.asFieldInstruction(), eligibleEnums, code.context());
+            break;
+          default: // Nothing to do for other instructions.
+        }
+      }
+      for (Phi phi : block.getPhis()) {
+        DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getType());
+        if (enumClass != null) {
+          Reason reason = validateEnumUsages(code, phi, enumClass);
+          if (reason == Reason.ELIGIBLE) {
+            eligibleEnums.add(enumClass.type);
+          }
+        }
+        if (phi.getType().isNullType()) {
+          addNullDependencies(code, phi.uniqueUsers(), eligibleEnums);
+        }
+      }
+    }
+    if (!eligibleEnums.isEmpty()) {
+      for (DexType eligibleEnum : eligibleEnums) {
+        enumUnboxingCandidatesInfo.addMethodDependency(eligibleEnum, code.context());
+      }
+    }
+    if (methodsDependingOnLibraryModelisation.contains(code.context(), appView.graphLens())) {
+      conversionOptions.disablePeepholeOptimizations();
+    }
+  }
+
+  private void analyzeInvokeCustom(InvokeCustom invoke, Set<DexType> eligibleEnums) {
+    Consumer<DexType> typeReferenceConsumer =
+        type -> {
+          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(type);
+          if (enumClass != null) {
+            eligibleEnums.add(enumClass.getType());
+          }
+        };
+    invoke.getCallSite().getMethodProto().forEachType(typeReferenceConsumer);
+    invoke
+        .getCallSite()
+        .getBootstrapArgs()
+        .forEach(
+            bootstrapArgument -> {
+              if (bootstrapArgument.isDexValueMethodHandle()) {
+                DexMethodHandle methodHandle =
+                    bootstrapArgument.asDexValueMethodHandle().getValue();
+                if (methodHandle.isMethodHandle()) {
+                  DexMethod method = methodHandle.asMethod();
+                  DexProgramClass enumClass =
+                      getEnumUnboxingCandidateOrNull(method.getHolderType());
+                  if (enumClass != null) {
+                    markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass);
+                  } else {
+                    method.getProto().forEachType(typeReferenceConsumer);
+                  }
+                } else {
+                  assert methodHandle.isFieldHandle();
+                  DexField field = methodHandle.asField();
+                  DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.getHolderType());
+                  if (enumClass != null) {
+                    markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass);
+                  } else {
+                    typeReferenceConsumer.accept(field.getType());
+                  }
+                }
+              } else if (bootstrapArgument.isDexValueMethodType()) {
+                DexProto proto = bootstrapArgument.asDexValueMethodType().getValue();
+                proto.forEachType(typeReferenceConsumer);
+              }
+            });
+  }
+
+  private void analyzeFieldInstruction(
+      FieldInstruction fieldInstruction, Set<DexType> eligibleEnums, ProgramMethod context) {
+    DexField field = fieldInstruction.getField();
+    DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.holder);
+    if (enumClass != null) {
+      FieldResolutionResult resolutionResult = appView.appInfo().resolveField(field, context);
+      if (resolutionResult.isSuccessfulResolution()) {
+        eligibleEnums.add(enumClass.getType());
+      } else {
+        markEnumAsUnboxable(Reason.UNRESOLVABLE_FIELD, enumClass);
+      }
+    }
+  }
+
+  private void analyzeInvokeStatic(
+      InvokeStatic invokeStatic, Set<DexType> eligibleEnums, ProgramMethod context) {
+    DexMethod invokedMethod = invokeStatic.getInvokedMethod();
+    DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
+    if (enumClass != null) {
+      DexClassAndMethod method = invokeStatic.lookupSingleTarget(appView, context);
+      if (method != null) {
+        eligibleEnums.add(enumClass.type);
+      } else {
+        markEnumAsUnboxable(Reason.INVALID_INVOKE, enumClass);
+      }
+    }
+  }
+
+  private void analyzeCheckCast(CheckCast checkCast, Set<DexType> eligibleEnums) {
+    // Casts to enum array types are fine as long all enum array creations are valid and have valid
+    // usages. Since creations of enum arrays are rewritten to primitive int arrays, enum array
+    // casts will continue to work after rewriting to int[] casts. Casts that failed with
+    // ClassCastException: "T[] cannot be cast to MyEnum[]" will continue to fail, but with "T[]
+    // cannot be cast to int[]".
+    //
+    // Note that strictly speaking, the rewriting from MyEnum[] to int[] could change the semantics
+    // of code that would fail with "int[] cannot be cast to MyEnum[]" in the input. However, javac
+    // does not allow such code ("incompatible types"), so we should generally not see such code.
+    if (checkCast.getType().isArrayType()) {
+      return;
+    }
+
+    // We are doing a type check, which typically means the in-value is of an upper
+    // type and cannot be dealt with.
+    // If the cast is on a dynamically typed object, the checkCast can be simply removed.
+    // This allows enum array clone and valueOf to work correctly.
+    DexProgramClass enumClass =
+        getEnumUnboxingCandidateOrNull(checkCast.getType().toBaseType(factory));
+    if (enumClass == null) {
+      return;
+    }
+    if (allowCheckCast(checkCast)) {
+      eligibleEnums.add(enumClass.type);
+      return;
+    }
+    markEnumAsUnboxable(Reason.DOWN_CAST, enumClass);
+  }
+
+  private boolean allowCheckCast(CheckCast checkCast) {
+    TypeElement objectType = checkCast.object().getDynamicUpperBoundType(appView);
+    return objectType.equalUpToNullability(
+        TypeElement.fromDexType(checkCast.getType(), definitelyNotNull(), appView));
+  }
+
+  private void analyzeConstClass(
+      ConstClass constClass, Set<DexType> eligibleEnums, ProgramMethod context) {
+    // We are using the ConstClass of an enum, which typically means the enum cannot be unboxed.
+    // We however allow unboxing if the ConstClass is used only:
+    // - as an argument to java.lang.reflect.Array#newInstance(java.lang.Class, int[]), to allow
+    //   unboxing of:
+    //    MyEnum[][] a = new MyEnum[x][y];
+    // - as an argument to Enum#valueOf, to allow unboxing of:
+    //    MyEnum a = Enum.valueOf(MyEnum.class, "A");
+    // - as a receiver for a name method, to allow unboxing of:
+    //    MyEnum.class.getName();
+    DexType enumType = constClass.getValue();
+    if (!enumUnboxingCandidatesInfo.isCandidate(enumType)) {
+      return;
+    }
+    if (constClass.outValue() == null) {
+      eligibleEnums.add(enumType);
+      return;
+    }
+    DexProgramClass enumClass = appView.definitionFor(enumType).asProgramClass();
+    if (constClass.outValue().hasPhiUsers()) {
+      markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
+      return;
+    }
+    for (Instruction user : constClass.outValue().aliasedUsers()) {
+      if (!isLegitimateConstClassUser(user, context, enumClass)) {
+        markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
+        return;
+      }
+    }
+    eligibleEnums.add(enumType);
+  }
+
+  private boolean isLegitimateConstClassUser(
+      Instruction user, ProgramMethod context, DexProgramClass enumClass) {
+    if (user.isAssume()) {
+      if (user.outValue().hasPhiUsers()) {
+        return false;
+      }
+      return true;
+    }
+
+    if (user.isInvokeStatic()) {
+      DexClassAndMethod singleTarget = user.asInvokeStatic().lookupSingleTarget(appView, context);
+      if (singleTarget == null) {
+        return false;
+      }
+      if (singleTarget.getReference() == factory.enumMembers.valueOf) {
+        // The name data is required for the correct mapping from the enum name to the ordinal
+        // in the valueOf utility method.
+        addRequiredNameData(enumClass);
+        markMethodDependsOnLibraryModelisation(context);
+        return true;
+      }
+      if (singleTarget.getReference()
+          == factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions) {
+        markMethodDependsOnLibraryModelisation(context);
+        return true;
+      }
+    }
+
+    if (user.isInvokeVirtual()) {
+      InvokeVirtual invoke = user.asInvokeVirtual();
+      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (invokedMethod == factory.classMethods.desiredAssertionStatus) {
+        // Only valid in the enum's class initializer, since the class constant must be rewritten
+        // to LocalEnumUtility.class instead of int.class.
+        return context.getDefinition().isClassInitializer() && context.getHolder() == enumClass;
+      }
+      if (isUnboxableNameMethod(invokedMethod)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  private void addRequiredNameData(DexProgramClass enumClass) {
+    enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData(
+        enumClass, factory.enumMembers.nameField);
+  }
+
+  private boolean isUnboxableNameMethod(DexMethod method) {
+    return method == factory.classMethods.getName
+        || method == factory.classMethods.getCanonicalName
+        || method == factory.classMethods.getSimpleName;
+  }
+
+  private void addNullDependencies(IRCode code, Set<Instruction> uses, Set<DexType> eligibleEnums) {
+    for (Instruction use : uses) {
+      if (use.isInvokeMethod()) {
+        InvokeMethod invokeMethod = use.asInvokeMethod();
+        DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+        for (DexType paramType : invokedMethod.proto.parameters.values) {
+          if (enumUnboxingCandidatesInfo.isCandidate(paramType)) {
+            eligibleEnums.add(paramType);
+          }
+        }
+        if (invokeMethod.isInvokeMethodWithReceiver()) {
+          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
+          if (enumClass != null) {
+            markEnumAsUnboxable(Reason.ENUM_METHOD_CALLED_WITH_NULL_RECEIVER, enumClass);
+          }
+        }
+      } else if (use.isFieldPut()) {
+        DexType type = use.asFieldInstruction().getField().type;
+        if (enumUnboxingCandidatesInfo.isCandidate(type)) {
+          eligibleEnums.add(type);
+        }
+      } else if (use.isReturn()) {
+        DexType returnType = code.method().getReference().proto.returnType;
+        if (enumUnboxingCandidatesInfo.isCandidate(returnType)) {
+          eligibleEnums.add(returnType);
+        }
+      }
+    }
+  }
+
+  private Reason validateEnumUsages(IRCode code, Value value, DexProgramClass enumClass) {
+    Reason result = Reason.ELIGIBLE;
+    for (Instruction user : value.uniqueUsers()) {
+      Reason reason = instructionAllowEnumUnboxing(user, code, enumClass, value);
+      if (reason != Reason.ELIGIBLE) {
+        if (markEnumAsUnboxable(reason, enumClass)) {
+          return reason;
+        }
+        // Record that the enum is ineligible, and continue analysis to collect all reasons for
+        // debugging.
+        result = reason;
+      }
+    }
+    for (Phi phi : value.uniquePhiUsers()) {
+      for (Value operand : phi.getOperands()) {
+        if (!operand.getType().isNullType()
+            && getEnumUnboxingCandidateOrNull(operand.getType()) != enumClass) {
+          // All reported reasons from here will be the same (INVALID_PHI), so just return
+          // immediately.
+          markEnumAsUnboxable(Reason.INVALID_PHI, enumClass);
+          return Reason.INVALID_PHI;
+        }
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public void prepareForPrimaryOptimizationPass(GraphLens graphLensForPrimaryOptimizationPass) {
+    assert appView.graphLens() == graphLensForPrimaryOptimizationPass;
+    initializeCheckNotNullMethods(graphLensForPrimaryOptimizationPass);
+    initializeEnumUnboxingCandidates(graphLensForPrimaryOptimizationPass);
+  }
+
+  private void initializeCheckNotNullMethods(GraphLens graphLensForPrimaryOptimizationPass) {
+    assert checkNotNullMethodsBuilder == null;
+    checkNotNullMethodsBuilder =
+        LongLivedProgramMethodMapBuilder.createConcurrentBuilderForNonConcurrentMap(
+            graphLensForPrimaryOptimizationPass);
+  }
+
+  private void initializeEnumUnboxingCandidates(GraphLens graphLensForPrimaryOptimizationPass) {
+    assert enumUnboxingCandidatesInfo == null;
+    enumUnboxingCandidatesInfo =
+        new EnumUnboxingCandidateAnalysis(appView, this)
+            .findCandidates(graphLensForPrimaryOptimizationPass);
+    methodsDependingOnLibraryModelisation =
+        LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(
+            graphLensForPrimaryOptimizationPass);
+  }
+
+  @Override
+  public void unboxEnums(
+      AppView<AppInfoWithLiveness> appView,
+      IRConverter converter,
+      Builder postMethodProcessorBuilder,
+      ExecutorService executorService,
+      OptimizationFeedbackDelayed feedback)
+      throws ExecutionException {
+    assert feedback.noUpdatesLeft();
+
+    assert candidatesToRemoveInWave.isEmpty();
+    EnumDataMap enumDataMap = finishAnalysis();
+    assert candidatesToRemoveInWave.isEmpty();
+
+    // At this point the enum unboxing candidates are no longer candidates, they will all be
+    // unboxed. We extract the now immutable enums to unbox information and clear the candidate
+    // info.
+    appView.setUnboxedEnums(enumDataMap);
+
+    if (enumUnboxingCandidatesInfo.isEmpty()) {
+      assert enumDataMap.isEmpty();
+      return;
+    }
+
+    ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates();
+    ImmutableSet<DexProgramClass> enumClassesToUnbox =
+        enumUnboxingCandidatesInfo.candidateClasses();
+    LongLivedProgramMethodSetBuilder<ProgramMethodSet> dependencies =
+        enumUnboxingCandidatesInfo.allMethodDependencies();
+    enumUnboxingCandidatesInfo.clear();
+    // Update keep info on any of the enum methods of the removed classes.
+    updateKeepInfo(enumsToUnbox);
+
+    EnumUnboxingUtilityClasses utilityClasses =
+        EnumUnboxingUtilityClasses.builder(appView)
+            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox, enumDataMap)
+            .build(converter, executorService);
+
+    // Fixup the application.
+    ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods =
+        checkNotNullMethodsBuilder
+            .rewrittenWithLens(appView, (enumClasses, appliedGraphLens) -> enumClasses)
+            .build(appView, builder -> builder.build(appView));
+    EnumUnboxingTreeFixer.Result treeFixerResult =
+        new EnumUnboxingTreeFixer(
+                appView, checkNotNullMethods, enumDataMap, enumClassesToUnbox, utilityClasses)
+            .fixupTypeReferences(converter, executorService);
+    EnumUnboxingLens enumUnboxingLens = treeFixerResult.getLens();
+
+    // Update the graph lens.
+    appView.rewriteWithLens(enumUnboxingLens);
+
+    // Enqueue the (lens rewritten) methods that require reprocessing.
+    //
+    // Note that the reprocessing set must be rewritten to the new enum unboxing lens before pruning
+    // the builders with the methods removed by the tree fixer (since these methods references are
+    // already fully lens rewritten).
+    postMethodProcessorBuilder
+        .getMethodsToReprocessBuilder()
+        .rewrittenWithLens(appView)
+        .merge(dependencies)
+        .merge(methodsDependingOnLibraryModelisation)
+        .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods());
+    methodsDependingOnLibraryModelisation.clear();
+
+    updateOptimizationInfos(executorService, feedback, treeFixerResult);
+
+    enumUnboxerRewriter =
+        new EnumUnboxingRewriter(
+            appView,
+            treeFixerResult.getCheckNotNullToCheckNotZeroMapping(),
+            converter,
+            enumUnboxingLens,
+            enumDataMap,
+            utilityClasses);
+  }
+
+  private void updateOptimizationInfos(
+      ExecutorService executorService,
+      OptimizationFeedbackDelayed feedback,
+      EnumUnboxingTreeFixer.Result treeFixerResult)
+      throws ExecutionException {
+    feedback.fixupOptimizationInfos(
+        appView,
+        executorService,
+        new OptimizationInfoFixer() {
+          @Override
+          public void fixup(DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) {
+            optimizationInfo
+                .fixupClassTypeReferences(appView, appView.graphLens())
+                .fixupAbstractValue(appView, appView.graphLens());
+          }
+
+          @Override
+          public void fixup(
+              DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) {
+            optimizationInfo
+                .fixupClassTypeReferences(appView, appView.graphLens())
+                .fixupAbstractReturnValue(appView, appView.graphLens())
+                .fixupInstanceInitializerInfo(
+                    appView, appView.graphLens(), treeFixerResult.getPrunedItems());
+
+            // Clear the enum unboxer method classification for check-not-null methods (these
+            // classifications are transferred to the synthesized check-not-zero methods by now).
+            if (!treeFixerResult
+                .getCheckNotNullToCheckNotZeroMapping()
+                .containsValue(method.getReference())) {
+              optimizationInfo.unsetEnumUnboxerMethodClassification();
+            }
+          }
+        });
+  }
+
+  private void updateKeepInfo(Set<DexType> enumsToUnbox) {
+    KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo();
+    keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(enumsToUnbox));
+  }
+
+  public EnumDataMap finishAnalysis() {
+    analyzeInitializers();
+    updateEnumUnboxingCandidatesInfo();
+    EnumDataMap enumDataMap = analyzeEnumInstances();
+    if (debugLogEnabled) {
+      // Remove all enums that have been reported as being unboxable.
+      debugLogs.keySet().forEach(enumUnboxingCandidatesInfo::removeCandidate);
+      reportEnumsAnalysis();
+    }
+    assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size();
+    return enumDataMap;
+  }
+
+  private EnumDataMap analyzeEnumInstances() {
+    ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
+    enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData(
+        (enumClass, instanceFields) -> {
+          EnumData data = buildData(enumClass, instanceFields);
+          if (data == null) {
+            // Reason is already reported at this point.
+            enumUnboxingCandidatesInfo.removeCandidate(enumClass);
+            return;
+          }
+          if (!debugLogEnabled || !debugLogs.containsKey(enumClass.getType())) {
+            builder.put(enumClass.type, data);
+          }
+        });
+    staticFieldValuesMap.clear();
+    return new EnumDataMap(builder.build());
+  }
+
+  private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) {
+    if (!enumClass.hasStaticFields()) {
+      return new EnumData(ImmutableMap.of(), ImmutableMap.of(), ImmutableSet.of(), -1);
+    }
+
+    // This map holds all the accessible fields to their unboxed value, so we can remap the field
+    // read to the unboxed value.
+    ImmutableMap.Builder<DexField, Integer> unboxedValues = ImmutableMap.builder();
+    // This maps the ordinal to the object state, note that some fields may have been removed,
+    // hence the entry is in this map but not the enumToOrdinalMap.
+    Int2ReferenceMap<ObjectState> ordinalToObjectState = new Int2ReferenceArrayMap<>();
+    // Any fields matching the expected $VALUES content can be recorded here, they have however
+    // all the same content.
+    ImmutableSet.Builder<DexField> valuesField = ImmutableSet.builder();
+    EnumValuesObjectState valuesContents = null;
+
+    EnumStaticFieldValues enumStaticFieldValues = staticFieldValuesMap.get(enumClass.type);
+    if (enumStaticFieldValues == null) {
+      reportFailure(enumClass, new MissingEnumStaticFieldValuesReason());
+      return null;
+    }
+
+    // Step 1: We iterate over the field to find direct enum instance information and the values
+    // fields.
+    for (DexEncodedField staticField : enumClass.staticFields()) {
+      if (factory.enumMembers.isEnumField(staticField, enumClass.type)) {
+        ObjectState enumState =
+            enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
+        if (enumState == null) {
+          if (staticField.getOptimizationInfo().isDead()) {
+            // We don't care about unused field data.
+            continue;
+          }
+          // We could not track the content of that field. We bail out.
+          reportFailure(
+              enumClass, new MissingObjectStateForEnumInstanceReason(staticField.getReference()));
+          return null;
+        }
+        OptionalInt optionalOrdinal = getOrdinal(enumState);
+        if (!optionalOrdinal.isPresent()) {
+          reportFailure(
+              enumClass,
+              new MissingInstanceFieldValueForEnumInstanceReason(
+                  staticField.getReference(), factory.enumMembers.ordinalField));
+          return null;
+        }
+        int ordinal = optionalOrdinal.getAsInt();
+        unboxedValues.put(staticField.getReference(), ordinalToUnboxedInt(ordinal));
+        ordinalToObjectState.put(ordinal, enumState);
+      } else if (factory.enumMembers.isValuesFieldCandidate(staticField, enumClass.type)) {
+        ObjectState valuesState =
+            enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
+        if (valuesState == null) {
+          if (staticField.getOptimizationInfo().isDead()) {
+            // We don't care about unused field data.
+            continue;
+          }
+          // We could not track the content of that field. We bail out.
+          // We could not track the content of that field, and the field could be a values field.
+          // We conservatively bail out.
+          reportFailure(
+              enumClass, new MissingContentsForEnumValuesArrayReason(staticField.getReference()));
+          return null;
+        }
+        assert valuesState.isEnumValuesObjectState();
+        assert valuesContents == null
+            || valuesContents.equals(valuesState.asEnumValuesObjectState());
+        valuesContents = valuesState.asEnumValuesObjectState();
+        valuesField.add(staticField.getReference());
+      }
+    }
+
+    // Step 2: We complete the information based on the values content, since some enum instances
+    // may be reachable only though the $VALUES field.
+    if (valuesContents != null) {
+      for (int ordinal = 0; ordinal < valuesContents.getEnumValuesSize(); ordinal++) {
+        if (!ordinalToObjectState.containsKey(ordinal)) {
+          ObjectState enumState = valuesContents.getObjectStateForOrdinal(ordinal);
+          if (enumState.isEmpty()) {
+            // If $VALUES is used, we need data for all enums, at least the ordinal.
+            return null;
+          }
+          assert getOrdinal(enumState).isPresent();
+          assert getOrdinal(enumState).getAsInt() == ordinal;
+          ordinalToObjectState.put(ordinal, enumState);
+        }
+      }
+    }
+
+    // The ordinalToObjectState map may have holes at this point, if some enum instances are never
+    // used ($VALUES unused or removed, and enum instance field unused or removed), it contains
+    // only data for reachable enum instance, that is what we're interested in.
+    ImmutableMap<DexField, EnumInstanceFieldKnownData> instanceFieldsData =
+        computeRequiredEnumInstanceFieldsData(enumClass, instanceFields, ordinalToObjectState);
+    if (instanceFieldsData == null) {
+      return null;
+    }
+
+    return new EnumData(
+        instanceFieldsData,
+        unboxedValues.build(),
+        valuesField.build(),
+        valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize());
+  }
+
+  private ImmutableMap<DexField, EnumInstanceFieldKnownData> computeRequiredEnumInstanceFieldsData(
+      DexProgramClass enumClass,
+      Set<DexField> instanceFields,
+      Int2ReferenceMap<ObjectState> ordinalToObjectState) {
+    ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> builder = ImmutableMap.builder();
+    for (DexField instanceField : instanceFields) {
+      EnumInstanceFieldData fieldData =
+          computeRequiredEnumInstanceFieldData(instanceField, enumClass, ordinalToObjectState);
+      if (fieldData.isUnknown()) {
+        if (!debugLogEnabled) {
+          return null;
+        }
+        builder = null;
+      }
+      if (builder != null) {
+        builder.put(instanceField, fieldData.asEnumFieldKnownData());
+      }
+    }
+    return builder != null ? builder.build() : null;
+  }
+
+  private EnumInstanceFieldData computeRequiredEnumInstanceFieldData(
+      DexField instanceField,
+      DexProgramClass enumClass,
+      Int2ReferenceMap<ObjectState> ordinalToObjectState) {
+    DexEncodedField encodedInstanceField =
+        appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField();
+    assert encodedInstanceField != null;
+    boolean canBeOrdinal = instanceField.type.isIntType();
+    ImmutableInt2ReferenceSortedMap.Builder<AbstractValue> data =
+        ImmutableInt2ReferenceSortedMap.builder();
+    for (Integer ordinal : ordinalToObjectState.keySet()) {
+      ObjectState state = ordinalToObjectState.get(ordinal);
+      AbstractValue fieldValue = state.getAbstractFieldValue(encodedInstanceField);
+      if (!fieldValue.isSingleValue()) {
+        reportFailure(
+            enumClass, new MissingInstanceFieldValueForEnumInstanceReason(ordinal, instanceField));
+        return EnumInstanceFieldUnknownData.getInstance();
+      }
+      if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) {
+        reportFailure(
+            enumClass,
+            new UnsupportedInstanceFieldValueForEnumInstanceReason(ordinal, instanceField));
+        return EnumInstanceFieldUnknownData.getInstance();
+      }
+      data.put(ordinalToUnboxedInt(ordinal), fieldValue);
+      if (canBeOrdinal) {
+        assert fieldValue.isSingleNumberValue();
+        int computedValue = fieldValue.asSingleNumberValue().getIntValue();
+        if (computedValue != ordinal) {
+          canBeOrdinal = false;
+        }
+      }
+    }
+    if (canBeOrdinal) {
+      return new EnumInstanceFieldOrdinalData();
+    }
+    return new EnumInstanceFieldMappingData(data.build());
+  }
+
+  private OptionalInt getOrdinal(ObjectState state) {
+    AbstractValue field = state.getAbstractFieldValue(getOrdinalField().getDefinition());
+    if (field.isSingleNumberValue()) {
+      return OptionalInt.of(field.asSingleNumberValue().getIntValue());
+    }
+    return OptionalInt.empty();
+  }
+
+  @Override
+  public void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues) {
+    if (staticFieldValues == null || !staticFieldValues.isEnumStaticFieldValues()) {
+      return;
+    }
+    assert clazz.isEnum();
+    EnumStaticFieldValues enumStaticFieldValues = staticFieldValues.asEnumStaticFieldValues();
+    if (getEnumUnboxingCandidateOrNull(clazz.type) != null) {
+      staticFieldValuesMap.put(clazz.type, enumStaticFieldValues);
+    }
+  }
+
+  private class EnumAccessibilityUseRegistry extends UseRegistry {
+
+    private ProgramMethod context;
+    private Constraint constraint;
+
+    public EnumAccessibilityUseRegistry(DexItemFactory factory) {
+      super(factory);
+    }
+
+    public Constraint computeConstraint(ProgramMethod method) {
+      constraint = Constraint.ALWAYS;
+      context = method;
+      method.registerCodeReferences(this);
+      return constraint;
+    }
+
+    public Constraint deriveConstraint(DexType targetHolder, AccessFlags<?> flags) {
+      DexProgramClass contextHolder = context.getHolder();
+      if (targetHolder == contextHolder.type) {
+        return Constraint.ALWAYS;
+      }
+      if (flags.isPublic()) {
+        return Constraint.ALWAYS;
+      }
+      if (flags.isPrivate()) {
+        // Enum unboxing is currently happening only cf to dex, and no class should be in a nest
+        // at this point. If that is the case, we just don't unbox the enum, or we would need to
+        // support Constraint.SAMENEST in the enum unboxer.
+        assert !contextHolder.isInANest();
+        // Only accesses within the enum are allowed since all enum methods and fields will be
+        // moved to the same class, and the enum itself becomes an integer, which is
+        // accessible everywhere.
+        return Constraint.NEVER;
+      }
+      assert flags.isProtected() || flags.isPackagePrivate();
+      // Protected is in practice equivalent to package private in this analysis since we are
+      // accessing the member from an enum context where subclassing is limited.
+      // At this point we don't support unboxing enums with subclasses, so we assume either
+      // same package access, or we just don't unbox.
+      // The only protected methods in java.lang.Enum are clone, finalize and the constructor.
+      // Besides calls to the constructor in the instance initializer, Enums with calls to such
+      // methods cannot be unboxed.
+      return targetHolder.isSamePackage(contextHolder.type) ? Constraint.PACKAGE : Constraint.NEVER;
+    }
+
+    @Override
+    public void registerTypeReference(DexType type) {
+      if (type.isArrayType()) {
+        registerTypeReference(type.toBaseType(factory));
+        return;
+      }
+
+      if (type.isPrimitiveType()) {
+        return;
+      }
+
+      DexClass definition = appView.definitionFor(type);
+      if (definition == null) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      constraint = constraint.meet(deriveConstraint(type, definition.accessFlags));
+    }
+
+    @Override
+    public void registerInitClass(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerInstanceOf(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerNewInstance(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerInvokeVirtual(DexMethod method) {
+      registerVirtualInvoke(method, false);
+    }
+
+    @Override
+    public void registerInvokeInterface(DexMethod method) {
+      registerVirtualInvoke(method, true);
+    }
+
+    private void registerVirtualInvoke(DexMethod method, boolean isInterface) {
+      if (method.holder.isArrayType()) {
+        return;
+      }
+      // Perform resolution and derive unboxing constraints based on the accessibility of the
+      // resolution result.
+      MethodResolutionResult resolutionResult =
+          appView.appInfo().resolveMethod(method, isInterface);
+      if (!resolutionResult.isVirtualTarget()) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      registerTarget(
+          resolutionResult.getInitialResolutionHolder(), resolutionResult.getSingleTarget());
+    }
+
+    private void registerTarget(DexClass initialResolutionHolder, DexEncodedMember<?, ?> target) {
+      if (target == null) {
+        // This will fail at runtime.
+        constraint = Constraint.NEVER;
+        return;
+      }
+      DexType resolvedHolder = target.getHolderType();
+      if (initialResolutionHolder == null) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      Constraint memberConstraint = deriveConstraint(resolvedHolder, target.getAccessFlags());
+      // We also have to take the constraint of the initial resolution holder into account.
+      Constraint classConstraint =
+          deriveConstraint(initialResolutionHolder.type, initialResolutionHolder.accessFlags);
+      Constraint instructionConstraint = memberConstraint.meet(classConstraint);
+      constraint = instructionConstraint.meet(constraint);
+    }
+
+    @Override
+    public void registerInvokeDirect(DexMethod method) {
+      registerSingleTargetInvoke(method, DexEncodedMethod::isDirectMethod);
+    }
+
+    @Override
+    public void registerInvokeStatic(DexMethod method) {
+      registerSingleTargetInvoke(method, DexEncodedMethod::isStatic);
+    }
+
+    private void registerSingleTargetInvoke(
+        DexMethod method, Predicate<DexEncodedMethod> methodValidator) {
+      if (method.holder.isArrayType()) {
+        return;
+      }
+      MethodResolutionResult resolutionResult =
+          appView.appInfo().unsafeResolveMethodDueToDexFormat(method);
+      DexEncodedMethod target = resolutionResult.getSingleTarget();
+      if (target == null || !methodValidator.test(target)) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      registerTarget(resolutionResult.getInitialResolutionHolder(), target);
+    }
+
+    @Override
+    public void registerInvokeSuper(DexMethod method) {
+      // Invoke-super can only target java.lang.Enum methods since we do not unbox enums with
+      // subclasses. Calls to java.lang.Object methods would have resulted in the enum to be marked
+      // as unboxable. The methods of java.lang.Enum called are already analyzed in the enum
+      // unboxer analysis, so invoke-super is always valid.
+      assert method.holder == factory.enumType;
+    }
+
+    @Override
+    public void registerCallSite(DexCallSite callSite) {
+      // This is reached after lambda desugaring, so this should not be a lambda call site.
+      // We do not unbox enums with invoke custom since it's not clear the accessibility
+      // constraints would be correct if the method holding the invoke custom is moved to
+      // another class.
+      assert appView.options().isGeneratingClassFiles()
+          || !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod());
+      constraint = Constraint.NEVER;
+    }
+
+    private void registerFieldInstruction(DexField field) {
+      FieldResolutionResult fieldResolutionResult = appView.appInfo().resolveField(field, context);
+      registerTarget(
+          fieldResolutionResult.getInitialResolutionHolder(),
+          fieldResolutionResult.getResolvedField());
+    }
+
+    @Override
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldInstruction(field);
+    }
+  }
+
+  private void analyzeInitializers() {
+    enumUnboxingCandidatesInfo.forEachCandidate(
+        enumClass -> {
+          for (DexEncodedMethod directMethod : enumClass.directMethods()) {
+            if (directMethod.isInstanceInitializer()) {
+              if (directMethod
+                  .getOptimizationInfo()
+                  .getContextInsensitiveInstanceInitializerInfo()
+                  .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
+                if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
+                  break;
+                }
+              }
+            }
+          }
+          if (enumClass.classInitializationMayHaveSideEffects(appView)) {
+            markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
+          }
+        });
+  }
+
+  private Reason instructionAllowEnumUnboxing(
+      Instruction instruction, IRCode code, DexProgramClass enumClass, Value enumValue) {
+    ProgramMethod context = code.context();
+    switch (instruction.opcode()) {
+      case ASSUME:
+        return analyzeAssumeUser(instruction.asAssume(), code, context, enumClass, enumValue);
+      case ARRAY_GET:
+        return analyzeArrayGetUser(instruction.asArrayGet(), code, context, enumClass, enumValue);
+      case ARRAY_LENGTH:
+        return analyzeArrayLengthUser(
+            instruction.asArrayLength(), code, context, enumClass, enumValue);
+      case ARRAY_PUT:
+        return analyzeArrayPutUser(instruction.asArrayPut(), code, context, enumClass, enumValue);
+      case CHECK_CAST:
+        return analyzeCheckCastUser(instruction.asCheckCast(), code, context, enumClass, enumValue);
+      case IF:
+        return analyzeIfUser(instruction.asIf(), code, context, enumClass, enumValue);
+      case INSTANCE_GET:
+        return analyzeInstanceGetUser(
+            instruction.asInstanceGet(), code, context, enumClass, enumValue);
+      case INSTANCE_PUT:
+        return analyzeFieldPutUser(
+            instruction.asInstancePut(), code, context, enumClass, enumValue);
+      case INVOKE_DIRECT:
+      case INVOKE_INTERFACE:
+      case INVOKE_STATIC:
+      case INVOKE_SUPER:
+      case INVOKE_VIRTUAL:
+        return analyzeInvokeUser(instruction.asInvokeMethod(), code, context, enumClass, enumValue);
+      case RETURN:
+        return analyzeReturnUser(instruction.asReturn(), code, context, enumClass, enumValue);
+      case STATIC_PUT:
+        return analyzeFieldPutUser(instruction.asStaticPut(), code, context, enumClass, enumValue);
+      default:
+        return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
+    }
+  }
+
+  private Reason analyzeAssumeUser(
+      Assume assume,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    return validateEnumUsages(code, assume.outValue(), enumClass);
+  }
+
+  private Reason analyzeArrayGetUser(
+      ArrayGet arrayGet,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    // MyEnum[] array = ...; array[0]; is valid.
+    return Reason.ELIGIBLE;
+  }
+
+  private Reason analyzeArrayLengthUser(
+      ArrayLength arrayLength,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    // MyEnum[] array = ...; array.length; is valid.
+    return Reason.ELIGIBLE;
+  }
+
+  private Reason analyzeArrayPutUser(
+      ArrayPut arrayPut,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    // MyEnum[] array; array[0] = MyEnum.A; is valid.
+    // MyEnum[][] array2d; MyEnum[] array; array2d[0] = array; is valid.
+    // MyEnum[]^N array; MyEnum[]^(N-1) element; array[0] = element; is valid.
+    // We need to prove that the value to put in and the array have correct types.
+    assert arrayPut.getMemberType() == MemberType.OBJECT;
+    TypeElement arrayType = arrayPut.array().getType();
+    assert arrayType.isArrayType();
+    assert arrayType.asArrayType().getBaseType().isClassType();
+    ClassTypeElement arrayBaseType = arrayType.asArrayType().getBaseType().asClassType();
+    TypeElement valueBaseType = arrayPut.value().getType();
+    if (valueBaseType.isArrayType()) {
+      assert valueBaseType.asArrayType().getBaseType().isClassType();
+      assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
+      valueBaseType = valueBaseType.asArrayType().getBaseType();
+    }
+    if (arrayBaseType.equalUpToNullability(valueBaseType)
+        && arrayBaseType.getClassType() == enumClass.type) {
+      return Reason.ELIGIBLE;
+    }
+    return Reason.INVALID_ARRAY_PUT;
+  }
+
+  private Reason analyzeCheckCastUser(
+      CheckCast checkCast,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    if (allowCheckCast(checkCast)) {
+      return Reason.ELIGIBLE;
+    }
+    return Reason.DOWN_CAST;
+  }
+
+  // A field put is valid only if the field is not on an enum, and the field type and the valuePut
+  // have identical enum type.
+  private Reason analyzeFieldPutUser(
+      FieldInstruction fieldPut,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    assert fieldPut.isInstancePut() || fieldPut.isStaticPut();
+    DexEncodedField field = appView.appInfo().resolveField(fieldPut.getField()).getResolvedField();
+    if (field == null) {
+      return Reason.INVALID_FIELD_PUT;
+    }
+    DexProgramClass dexClass = appView.programDefinitionFor(field.getHolderType(), code.context());
+    if (dexClass == null) {
+      return Reason.INVALID_FIELD_PUT;
+    }
+    if (fieldPut.isInstancePut() && fieldPut.asInstancePut().object() == enumValue) {
+      return Reason.ELIGIBLE;
+    }
+    // The put value has to be of the field type.
+    if (field.getReference().type.toBaseType(factory) != enumClass.type) {
+      return Reason.TYPE_MISMATCH_FIELD_PUT;
+    }
+    return Reason.ELIGIBLE;
+  }
+
+  // An If using enum as inValue is valid if it matches e == null
+  // or e == X with X of same enum type as e. Ex: if (e == MyEnum.A).
+  private Reason analyzeIfUser(
+      If theIf, IRCode code, ProgramMethod context, DexProgramClass enumClass, Value enumValue) {
+    assert (theIf.getType() == If.Type.EQ || theIf.getType() == If.Type.NE)
+        : "Comparing a reference with " + theIf.getType().toString();
+    // e == null.
+    if (theIf.isZeroTest()) {
+      return Reason.ELIGIBLE;
+    }
+    // e == MyEnum.X
+    TypeElement leftType = theIf.lhs().getType();
+    TypeElement rightType = theIf.rhs().getType();
+    if (leftType.equalUpToNullability(rightType)) {
+      assert leftType.isClassType();
+      assert leftType.asClassType().getClassType() == enumClass.type;
+      return Reason.ELIGIBLE;
+    }
+    return Reason.INVALID_IF_TYPES;
+  }
+
+  private Reason analyzeInstanceGetUser(
+      InstanceGet instanceGet,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    assert instanceGet.getField().holder == enumClass.type;
+    DexField field = instanceGet.getField();
+    enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData(enumClass, field);
+    return Reason.ELIGIBLE;
+  }
+
+  // All invokes in the library are invalid, besides a few cherry picked cases such as ordinal().
+  private Reason analyzeInvokeUser(
+      InvokeMethod invoke,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    if (invoke.getInvokedMethod().holder.isArrayType()) {
+      // The only valid methods is clone for values() to be correct.
+      if (invoke.getInvokedMethod().name == factory.cloneMethodName) {
+        return Reason.ELIGIBLE;
+      }
+      return Reason.INVALID_INVOKE_ON_ARRAY;
+    }
+    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+    if (singleTarget == null) {
+      return Reason.INVALID_INVOKE;
+    }
+    DexMethod singleTargetReference = singleTarget.getReference();
+    DexClass targetHolder = singleTarget.getHolder();
+    if (targetHolder.isProgramClass()) {
+      if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
+        if (code.context().getHolder() == targetHolder && code.method().isClassInitializer()) {
+          // The enum instance initializer is allowed to be called only from the enum clinit.
+          return Reason.ELIGIBLE;
+        } else {
+          return Reason.INVALID_INIT;
+        }
+      }
+
+      // Check if this is a checkNotNull() user. In this case, we can create a copy of the method
+      // that takes an int instead of java.lang.Object and call that method instead.
+      EnumUnboxerMethodClassification classification =
+          singleTarget.getOptimizationInfo().getEnumUnboxerMethodClassification();
+      if (classification.isCheckNotNullClassification()) {
+        CheckNotNullEnumUnboxerMethodClassification checkNotNullClassification =
+            classification.asCheckNotNullClassification();
+        if (checkNotNullClassification.isUseEligibleForUnboxing(
+            invoke.asInvokeStatic(), enumValue)) {
+          GraphLens graphLens = appView.graphLens();
+          checkNotNullMethodsBuilder
+              .computeIfAbsent(
+                  singleTarget.asProgramMethod(),
+                  ignoreKey(
+                      () ->
+                          LongLivedClassSetBuilder.createConcurrentBuilderForIdentitySet(
+                              graphLens)),
+                  graphLens)
+              .add(enumClass, graphLens);
+          return Reason.ELIGIBLE;
+        }
+      }
+
+      // Check that the enum-value only flows into parameters whose type exactly matches the
+      // enum's type.
+      for (int i = 0; i < singleTarget.getParameters().size(); i++) {
+        if (invoke.getArgumentForParameter(i) == enumValue
+            && singleTarget.getParameter(i).toBaseType(factory) != enumClass.getType()) {
+          return new IllegalInvokeWithImpreciseParameterTypeReason(singleTargetReference);
+        }
+      }
+      if (invoke.isInvokeMethodWithReceiver()) {
+        Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
+        if (receiver == enumValue && targetHolder.isInterface()) {
+          return Reason.DEFAULT_METHOD_INVOKE;
+        }
+      }
+      return Reason.ELIGIBLE;
+    }
+
+    if (targetHolder.isClasspathClass()) {
+      return Reason.INVALID_INVOKE_CLASSPATH;
+    }
+
+    assert targetHolder.isLibraryClass();
+
+    Reason reason =
+        analyzeLibraryInvoke(
+            invoke, code, context, enumClass, enumValue, singleTargetReference, targetHolder);
+
+    if (reason == Reason.ELIGIBLE) {
+      markMethodDependsOnLibraryModelisation(context);
+    }
+
+    return reason;
+  }
+
+  private Reason analyzeLibraryInvoke(
+      InvokeMethod invoke,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue,
+      DexMethod singleTargetReference,
+      DexClass targetHolder) {
+    // Calls to java.lang.Enum.
+    if (targetHolder.getType() == factory.enumType) {
+      // TODO(b/147860220): EnumSet and EnumMap may be interesting to model.
+      if (singleTargetReference == factory.enumMembers.compareTo
+          || singleTargetReference == factory.enumMembers.compareToWithObject) {
+        DexProgramClass otherEnumClass =
+            getEnumUnboxingCandidateOrNull(invoke.getLastArgument().getType());
+        if (otherEnumClass == enumClass || invoke.getLastArgument().getType().isNullType()) {
+          return Reason.ELIGIBLE;
+        }
+      } else if (singleTargetReference == factory.enumMembers.equals) {
+        return Reason.ELIGIBLE;
+      } else if (singleTargetReference == factory.enumMembers.nameMethod
+          || singleTargetReference == factory.enumMembers.toString) {
+        assert invoke.asInvokeMethodWithReceiver().getReceiver() == enumValue;
+        addRequiredNameData(enumClass);
+        return Reason.ELIGIBLE;
+      } else if (singleTargetReference == factory.enumMembers.ordinalMethod) {
+        return Reason.ELIGIBLE;
+      } else if (singleTargetReference == factory.enumMembers.hashCode) {
+        return Reason.ELIGIBLE;
+      } else if (singleTargetReference == factory.enumMembers.constructor) {
+        // Enum constructor call is allowed only if called from an enum initializer.
+        if (code.method().isInstanceInitializer() && code.context().getHolder() == enumClass) {
+          return Reason.ELIGIBLE;
+        }
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Calls to java.lang.Object.
+    if (targetHolder.getType() == factory.objectType) {
+      // Object#getClass without outValue is important since R8 rewrites explicit null checks to
+      // such instructions.
+      if (singleTargetReference == factory.objectMembers.getClass && invoke.hasUnusedOutValue()) {
+        // This is a hidden null check.
+        return Reason.ELIGIBLE;
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Calls to java.lang.Objects.
+    if (targetHolder.getType() == factory.objectsType) {
+      // Objects#requireNonNull is important since R8 rewrites explicit null checks to such
+      // instructions.
+      if (singleTargetReference == factory.objectsMethods.requireNonNull
+          || singleTargetReference == factory.objectsMethods.requireNonNullWithMessage) {
+        return Reason.ELIGIBLE;
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Calls to java.lang.String.
+    if (targetHolder.getType() == factory.stringType) {
+      if (singleTargetReference == factory.stringMembers.valueOf) {
+        addRequiredNameData(enumClass);
+        return Reason.ELIGIBLE;
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Calls to java.lang.StringBuilder and java.lang.StringBuffer.
+    if (targetHolder.getType() == factory.stringBuilderType
+        || targetHolder.getType() == factory.stringBufferType) {
+      if (singleTargetReference == factory.stringBuilderMethods.appendObject
+          || singleTargetReference == factory.stringBufferMethods.appendObject) {
+        addRequiredNameData(enumClass);
+        return Reason.ELIGIBLE;
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Calls to java.lang.System.
+    if (targetHolder.getType() == factory.javaLangSystemType) {
+      if (singleTargetReference == factory.javaLangSystemMethods.arraycopy) {
+        // Important for Kotlin 1.5 enums, which use arraycopy to create a copy of $VALUES instead
+        // of int[].clone().
+        return Reason.ELIGIBLE;
+      }
+      if (singleTargetReference == factory.javaLangSystemMethods.identityHashCode) {
+        // Important for proto enum unboxing.
+        return Reason.ELIGIBLE;
+      }
+      return new UnsupportedLibraryInvokeReason(singleTargetReference);
+    }
+
+    // Unsupported holder.
+    return new UnsupportedLibraryInvokeReason(singleTargetReference);
+  }
+
+  // Return is used for valueOf methods.
+  private Reason analyzeReturnUser(
+      Return theReturn,
+      IRCode code,
+      ProgramMethod context,
+      DexProgramClass enumClass,
+      Value enumValue) {
+    DexType returnType = context.getReturnType();
+    if (returnType != enumClass.type && returnType.toBaseType(factory) != enumClass.type) {
+      return Reason.IMPLICIT_UP_CAST_IN_RETURN;
+    }
+    return Reason.ELIGIBLE;
+  }
+
+  private void reportEnumsAnalysis() {
+    assert debugLogEnabled;
+    Reporter reporter = appView.reporter();
+    Set<DexType> candidates = enumUnboxingCandidatesInfo.candidates();
+    reporter.info(
+        new StringDiagnostic(
+            "Unboxed " + candidates.size() + " enums: " + Arrays.toString(candidates.toArray())));
+
+    StringBuilder sb =
+        new StringBuilder("Unable to unbox ")
+            .append(debugLogs.size())
+            .append(" enums.")
+            .append(System.lineSeparator())
+            .append(System.lineSeparator());
+
+    // Sort by the number of reasons that prevent enum unboxing.
+    TreeMap<DexType, List<Reason>> sortedDebugLogs =
+        new TreeMap<>(
+            Comparator.<DexType>comparingInt(x -> debugLogs.get(x).size())
+                .thenComparing(Function.identity()));
+    sortedDebugLogs.putAll(debugLogs);
+
+    // Print the pinned enums and remove them from further reporting.
+    List<DexType> pinned = new ArrayList<>();
+    Iterator<Entry<DexType, List<Reason>>> sortedDebugLogIterator =
+        sortedDebugLogs.entrySet().iterator();
+    while (sortedDebugLogIterator.hasNext()) {
+      Entry<DexType, List<Reason>> entry = sortedDebugLogIterator.next();
+      List<Reason> reasons = entry.getValue();
+      if (reasons.size() > 1) {
+        break;
+      }
+      if (reasons.get(0) == Reason.PINNED) {
+        pinned.add(entry.getKey());
+        sortedDebugLogIterator.remove();
+      }
+    }
+    if (!pinned.isEmpty()) {
+      sb.append("Pinned: ").append(Arrays.toString(pinned.toArray()));
+    }
+
+    // Print the reasons for each unboxable enum.
+    sortedDebugLogs.forEach(
+        (type, reasons) -> {
+          sb.append(type).append(" (").append(reasons.size()).append(" reasons):");
+          HashMultiset.create(reasons)
+              .forEachEntry(
+                  (reason, count) ->
+                      sb.append(System.lineSeparator())
+                          .append(" - ")
+                          .append(reason)
+                          .append(" (")
+                          .append(count)
+                          .append(")"));
+          sb.append(System.lineSeparator());
+        });
+
+    sb.append(System.lineSeparator());
+
+    // Print information about how often a given Reason kind prevents enum unboxing.
+    Object2IntMap<Object> reasonKindCount = new Object2IntOpenHashMap<>();
+    debugLogs.forEach(
+        (type, reasons) ->
+            reasons.forEach(
+                reason ->
+                    reasonKindCount.put(reason.getKind(), reasonKindCount.getInt(reason) + 1)));
+    List<Object> differentReasonKinds = new ArrayList<>(reasonKindCount.keySet());
+    differentReasonKinds.sort(
+        (reasonKind, other) -> {
+          int freq = reasonKindCount.getInt(reasonKind) - reasonKindCount.getInt(other);
+          return freq != 0
+              ? freq
+              : System.identityHashCode(reasonKind) - System.identityHashCode(other);
+        });
+    differentReasonKinds.forEach(
+        reasonKind ->
+            sb.append(reasonKind)
+                .append(" (")
+                .append(reasonKindCount.getInt(reasonKind))
+                .append(")")
+                .append(System.lineSeparator()));
+
+    reporter.info(new StringDiagnostic(sb.toString()));
+  }
+
+  boolean reportFailure(DexProgramClass enumClass, Reason reason) {
+    return reportFailure(enumClass.getType(), reason);
+  }
+
+  /** Returns true if the failure was reported. */
+  boolean reportFailure(DexType enumType, Reason reason) {
+    if (debugLogEnabled) {
+      debugLogs
+          .computeIfAbsent(enumType, ignore -> Collections.synchronizedList(new ArrayList<>()))
+          .add(reason);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public Set<Phi> rewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    // This has no effect during primary processing since the enumUnboxerRewriter is set
+    // in between primary and post processing.
+    if (enumUnboxerRewriter != null) {
+      return enumUnboxerRewriter.rewriteCode(code, methodProcessor);
+    }
+    return Sets.newIdentityHashSet();
+  }
+
+  @Override
+  public void unsetRewriter() {
+    enumUnboxerRewriter = null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 4225447..51e4191 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -25,12 +25,12 @@
   private static final int MAX_INSTANCE_FIELDS_FOR_UNBOXING = 7;
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final EnumUnboxer enumUnboxer;
+  private final EnumUnboxerImpl enumUnboxer;
   private final DexItemFactory factory;
   private EnumUnboxingCandidateInfoCollection enumToUnboxCandidates =
       new EnumUnboxingCandidateInfoCollection();
 
-  EnumUnboxingCandidateAnalysis(AppView<AppInfoWithLiveness> appView, EnumUnboxer enumUnboxer) {
+  EnumUnboxingCandidateAnalysis(AppView<AppInfoWithLiveness> appView, EnumUnboxerImpl enumUnboxer) {
     this.appView = appView;
     this.enumUnboxer = enumUnboxer;
     factory = appView.dexItemFactory();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 287a4d6..5fc743e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -288,7 +288,7 @@
           assert unboxedEnumsData.isUnboxedEnum(newUnboxedEnumInstance.getType());
           iterator.replaceCurrentInstruction(
               code.createIntConstant(
-                  EnumUnboxer.ordinalToUnboxedInt(newUnboxedEnumInstance.getOrdinal())));
+                  EnumUnboxerImpl.ordinalToUnboxedInt(newUnboxedEnumInstance.getOrdinal())));
         }
       }
     }
@@ -398,26 +398,38 @@
       return;
     }
 
-    if (singleTarget.isProgramMethod()) {
-      EnumUnboxerMethodClassification classification =
-          singleTarget.getOptimizationInfo().getEnumUnboxerMethodClassification();
-      if (classification.isCheckNotNullClassification()) {
-        CheckNotNullEnumUnboxerMethodClassification checkNotNullClassification =
-            classification.asCheckNotNullClassification();
-        Value argument = invoke.getArgument(checkNotNullClassification.getArgumentIndex());
-        DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
-        if (enumType != null) {
-          InvokeStatic replacement =
-              InvokeStatic.builder()
-                  .setMethod(checkNotNullToCheckNotZeroMapping.get(singleTarget.getReference()))
-                  .setArguments(invoke.arguments())
-                  .setPosition(invoke.getPosition())
-                  .build();
-          instructionIterator.replaceCurrentInstruction(replacement);
-          convertedEnums.put(replacement, enumType);
+    if (singleTarget.isProgramMethod()
+        && checkNotNullToCheckNotZeroMapping.containsKey(singleTarget.getReference())) {
+      DexMethod checkNotZeroMethodReference =
+          checkNotNullToCheckNotZeroMapping.get(singleTarget.getReference());
+      ProgramMethod checkNotZeroMethod =
+          appView
+              .appInfo()
+              .resolveMethodOnClass(checkNotZeroMethodReference)
+              .getResolvedProgramMethod();
+      if (checkNotZeroMethod != null) {
+        EnumUnboxerMethodClassification classification =
+            checkNotZeroMethod.getOptimizationInfo().getEnumUnboxerMethodClassification();
+        if (classification.isCheckNotNullClassification()) {
+          CheckNotNullEnumUnboxerMethodClassification checkNotNullClassification =
+              classification.asCheckNotNullClassification();
+          Value argument = invoke.getArgument(checkNotNullClassification.getArgumentIndex());
+          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+          if (enumType != null) {
+            InvokeStatic replacement =
+                InvokeStatic.builder()
+                    .setMethod(checkNotZeroMethod)
+                    .setArguments(invoke.arguments())
+                    .setPosition(invoke.getPosition())
+                    .build();
+            instructionIterator.replaceCurrentInstruction(replacement);
+            convertedEnums.put(replacement, enumType);
+          }
+        } else {
+          assert false;
         }
       } else {
-        assert !checkNotNullToCheckNotZeroMapping.containsKey(singleTarget.getReference());
+        assert false;
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index c7bcc37..8a97aef 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -54,10 +54,11 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.ListIterator;
@@ -130,16 +131,16 @@
     }
 
     // Create mapping from checkNotNull() to checkNotZero() methods.
-    Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping =
+    BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping =
         duplicateCheckNotNullMethods(converter, executorService);
 
     return new Result(
         checkNotNullToCheckNotZeroMapping, lensBuilder.build(appView), prunedItemsBuilder.build());
   }
 
-  private Map<DexMethod, DexMethod> duplicateCheckNotNullMethods(
+  private BiMap<DexMethod, DexMethod> duplicateCheckNotNullMethods(
       IRConverter converter, ExecutorService executorService) throws ExecutionException {
-    Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping = new IdentityHashMap<>();
+    BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping = HashBiMap.create();
     ProcessorContext processorContext = appView.createProcessorContext();
     OneTimeMethodProcessor.Builder methodProcessorBuilder =
         OneTimeMethodProcessor.builder(processorContext);
@@ -186,6 +187,11 @@
                               .setApiLevelForDefinition(minApiLevelIfEnabledOrUnknown(appView))
                               .setApiLevelForCode(minApiLevelIfEnabledOrUnknown(appView))
                               .setCode(method -> new CheckNotZeroCode(checkNotNullMethod))
+                              .setOptimizationInfo(
+                                  checkNotNullMethod
+                                      .getOptimizationInfo()
+                                      .asMutableMethodOptimizationInfo()
+                                      .mutableCopy())
                               .setProto(newProto));
           checkNotNullToCheckNotZeroMapping.put(
               checkNotNullMethod.getReference(), checkNotZeroMethod.getReference());
@@ -603,12 +609,12 @@
 
   public static class Result {
 
-    private final Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping;
+    private final BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping;
     private final EnumUnboxingLens lens;
     private final PrunedItems prunedItems;
 
     Result(
-        Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping,
+        BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping,
         EnumUnboxingLens lens,
         PrunedItems prunedItems) {
       this.checkNotNullToCheckNotZeroMapping = checkNotNullToCheckNotZeroMapping;
@@ -616,7 +622,7 @@
       this.prunedItems = prunedItems;
     }
 
-    Map<DexMethod, DexMethod> getCheckNotNullToCheckNotZeroMapping() {
+    BiMap<DexMethod, DexMethod> getCheckNotNullToCheckNotZeroMapping() {
       return checkNotNullToCheckNotZeroMapping;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/classification/EnumUnboxerMethodClassificationAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/classification/EnumUnboxerMethodClassificationAnalysis.java
index f01abfc..3d8bccc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/classification/EnumUnboxerMethodClassificationAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/classification/EnumUnboxerMethodClassificationAnalysis.java
@@ -46,13 +46,25 @@
     }
 
     // Look for an argument with a single if-zero user.
+    EnumUnboxerMethodClassification currentClassification =
+        method.getOptimizationInfo().getEnumUnboxerMethodClassification();
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     InstructionIterator entryIterator = code.entryBlock().iterator();
     for (int index = 0; index < method.getParameters().size(); index++) {
       Argument argument = entryIterator.next().asArgument();
       DexType parameter = method.getParameter(index);
-      if (parameter != dexItemFactory.objectType) {
-        continue;
+      // Before enum unboxing, we classify methods with `object != null` as check-not-null methods.
+      // After enum unboxing, we check correctness of the classification for check-not-zero methods.
+      if (appView.hasUnboxedEnums()) {
+        if (parameter != dexItemFactory.intType
+            || !currentClassification.isCheckNotNullClassification()
+            || currentClassification.asCheckNotNullClassification().getArgumentIndex() != index) {
+          continue;
+        }
+      } else {
+        if (parameter != dexItemFactory.objectType) {
+          continue;
+        }
       }
 
       if (onlyHasCheckNotNullUsers(argument, methodProcessor)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/code/CheckNotZeroCode.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/code/CheckNotZeroCode.java
index c2c5427..c2cbec9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/code/CheckNotZeroCode.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/code/CheckNotZeroCode.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.optimize.enums.EnumUnboxerImpl;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -28,8 +29,7 @@
  *
  * <p>Instances of {@link CheckNotZeroCode} are converted to {@link
  * com.android.tools.r8.graph.CfCode} or {@link com.android.tools.r8.graph.DexCode} immediately, and
- * thus should never be seen outside of the {@link
- * com.android.tools.r8.ir.optimize.enums.EnumUnboxer}.
+ * thus should never be seen outside of the {@link EnumUnboxerImpl}.
  */
 public class CheckNotZeroCode extends Code {
 
@@ -49,7 +49,7 @@
     // Start iterating at the argument instruction for the checked argument.
     IteratorUtils.skip(
         instructionIterator,
-        checkNotNullMethod
+        checkNotZeroMethod
             .getOptimizationInfo()
             .getEnumUnboxerMethodClassification()
             .asCheckNotNullClassification()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 24aeb3d..c085286 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -150,6 +150,11 @@
   }
 
   @Override
+  public BitSet getUnusedArguments() {
+    return null;
+  }
+
+  @Override
   public boolean isInitializerEnablingJavaVmAssertions() {
     return UNKNOWN_INITIALIZER_ENABLING_JAVA_ASSERTIONS;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index aab9aa6..aff19ef 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -83,6 +83,13 @@
 
   public abstract SimpleInliningConstraint getSimpleInliningConstraint();
 
+  public final boolean hasUnusedArguments() {
+    assert getUnusedArguments() == null || !getUnusedArguments().isEmpty();
+    return getUnusedArguments() != null;
+  }
+
+  public abstract BitSet getUnusedArguments();
+
   public abstract boolean forceInline();
 
   public abstract boolean neverInline();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 6abfb55..9836791 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -65,6 +65,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.AliasedValueConfiguration;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.DominatorTree;
@@ -155,6 +156,7 @@
     computeReturnValueOnlyDependsOnArguments(feedback, definition, code, timing);
     BitSet nonNullParamOrThrow = computeNonNullParamOrThrow(feedback, definition, code, timing);
     computeNonNullParamOnNormalExits(feedback, code, nonNullParamOrThrow, timing);
+    computeUnusedArguments(method, code, feedback, timing);
   }
 
   private void identifyBridgeInfo(
@@ -808,9 +810,33 @@
       IRCode code,
       OptimizationFeedback feedback,
       MethodProcessor methodProcessor) {
-    EnumUnboxerMethodClassification enumUnboxerMethodClassification =
-        EnumUnboxerMethodClassificationAnalysis.analyze(appView, method, code, methodProcessor);
-    feedback.setEnumUnboxerMethodClassification(method, enumUnboxerMethodClassification);
+    if (appView.hasUnboxedEnums()) {
+      if (appView.unboxedEnums().isEmpty()) {
+        feedback.unsetEnumUnboxerMethodClassification(method);
+      } else {
+        assert verifyEnumUnboxerMethodClassificationCorrect(method, code, methodProcessor);
+      }
+    } else {
+      EnumUnboxerMethodClassification enumUnboxerMethodClassification =
+          EnumUnboxerMethodClassificationAnalysis.analyze(appView, method, code, methodProcessor);
+      feedback.setEnumUnboxerMethodClassification(method, enumUnboxerMethodClassification);
+    }
+  }
+
+  private boolean verifyEnumUnboxerMethodClassificationCorrect(
+      ProgramMethod method, IRCode code, MethodProcessor methodProcessor) {
+    EnumUnboxerMethodClassification existingClassification =
+        method.getOptimizationInfo().getEnumUnboxerMethodClassification();
+    if (existingClassification.isCheckNotNullClassification()) {
+      EnumUnboxerMethodClassification computedClassification =
+          EnumUnboxerMethodClassificationAnalysis.analyze(appView, method, code, methodProcessor);
+      assert computedClassification.isCheckNotNullClassification();
+      assert computedClassification.asCheckNotNullClassification().getArgumentIndex()
+          == existingClassification.asCheckNotNullClassification().getArgumentIndex();
+    } else {
+      assert existingClassification.isUnknownClassification();
+    }
+    return true;
   }
 
   private void computeSimpleInliningConstraint(
@@ -1195,4 +1221,25 @@
     assert uncoveredPaths.isEmpty();
     return true;
   }
+
+  private void computeUnusedArguments(
+      ProgramMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
+    timing.begin("Compute unused arguments");
+    computeUnusedArguments(method, code, feedback);
+    timing.end();
+  }
+
+  private void computeUnusedArguments(
+      ProgramMethod method, IRCode code, OptimizationFeedback feedback) {
+    BitSet unusedArguments = new BitSet(method.getDefinition().getNumberOfArguments());
+    InstructionIterator instructionIterator = code.entryBlock().iterator();
+    Argument argument = instructionIterator.next().asArgument();
+    while (argument != null) {
+      if (!argument.outValue().hasAnyUsers()) {
+        unusedArguments.set(argument.getIndex());
+      }
+      argument = instructionIterator.next().asArgument();
+    }
+    feedback.setUnusedArguments(method, unusedArguments);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
index 58c0f9b..12c5b37 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
@@ -43,4 +43,6 @@
       AppView<AppInfoWithLiveness> appView,
       SimpleInliningConstraint constraint,
       SimpleInliningConstraintFactory factory);
+
+  public abstract BitSet fixupUnusedArguments(BitSet unusedArguments);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index c4a08f4..dd6f917 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.BitSetUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.BitSet;
@@ -73,6 +74,8 @@
   private SimpleInliningConstraint simpleInliningConstraint =
       NeverSimpleInliningConstraint.getInstance();
 
+  private BitSet unusedArguments = null;
+
   // To reduce the memory footprint of UpdatableMethodOptimizationInfo, all the boolean fields are
   // merged into a flag int field. The various static final FLAG fields indicate which bit is
   // used by each boolean. DEFAULT_FLAGS encodes the default value for efficient instantiation and
@@ -165,7 +168,8 @@
         .fixupNonNullParamOnNormalExits(fixer)
         .fixupNonNullParamOrThrow(fixer)
         .fixupReturnedArgumentIndex(fixer)
-        .fixupSimpleInliningConstraint(appView, fixer);
+        .fixupSimpleInliningConstraint(appView, fixer)
+        .fixupUnusedArguments(fixer);
   }
 
   public MutableMethodOptimizationInfo fixupClassTypeReferences(
@@ -280,6 +284,10 @@
     this.enumUnboxerMethodClassification = enumUnboxerMethodClassification;
   }
 
+  public void unsetEnumUnboxerMethodClassification() {
+    this.enumUnboxerMethodClassification = EnumUnboxerMethodClassification.unknown();
+  }
+
   public MutableMethodOptimizationInfo fixupEnumUnboxerMethodClassification(
       MethodOptimizationInfoFixer fixer) {
     enumUnboxerMethodClassification =
@@ -419,6 +427,25 @@
   }
 
   @Override
+  public BitSet getUnusedArguments() {
+    return unusedArguments;
+  }
+
+  public MutableMethodOptimizationInfo fixupUnusedArguments(MethodOptimizationInfoFixer fixer) {
+    unusedArguments = fixer.fixupUnusedArguments(unusedArguments);
+    return this;
+  }
+
+  void setUnusedArguments(BitSet unusedArguments) {
+    // Verify monotonicity (i.e., unused arguments should never become used).
+    assert !hasUnusedArguments() || unusedArguments != null;
+    assert !hasUnusedArguments()
+        || BitSetUtils.verifyLessThanOrEqualTo(getUnusedArguments(), unusedArguments);
+    this.unusedArguments =
+        unusedArguments != null && !unusedArguments.isEmpty() ? unusedArguments : null;
+  }
+
+  @Override
   public boolean isInitializerEnablingJavaVmAssertions() {
     return isFlagSet(INITIALIZER_ENABLING_JAVA_ASSERTIONS_FLAG);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index c9390d7..472a65b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -272,6 +272,11 @@
   }
 
   @Override
+  public void unsetEnumUnboxerMethodClassification(ProgramMethod method) {
+    getMethodOptimizationInfoForUpdating(method).unsetEnumUnboxerMethodClassification();
+  }
+
+  @Override
   public synchronized void setInstanceInitializerInfoCollection(
       DexEncodedMethod method,
       InstanceInitializerInfoCollection instanceInitializerInfoCollection) {
@@ -304,4 +309,9 @@
   public synchronized void classInitializerMayBePostponed(DexEncodedMethod method) {
     getMethodOptimizationInfoForUpdating(method).markClassInitializerMayBePostponed();
   }
+
+  @Override
+  public synchronized void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {
+    getMethodOptimizationInfoForUpdating(method).setUnusedArguments(unusedArguments);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index 623d8d2..366a1ac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -122,6 +122,9 @@
       ProgramMethod method, EnumUnboxerMethodClassification enumUnboxerMethodClassification) {}
 
   @Override
+  public void unsetEnumUnboxerMethodClassification(ProgramMethod method) {}
+
+  @Override
   public void setInstanceInitializerInfoCollection(
       DexEncodedMethod method,
       InstanceInitializerInfoCollection instanceInitializerInfoCollection) {}
@@ -141,4 +144,7 @@
 
   @Override
   public void classInitializerMayBePostponed(DexEncodedMethod method) {}
+
+  @Override
+  public void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {}
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 47f1b8e..f33bf52 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -194,6 +194,16 @@
   }
 
   @Override
+  public void unsetEnumUnboxerMethodClassification(ProgramMethod method) {
+    if (method.getOptimizationInfo().isMutableOptimizationInfo()) {
+      method
+          .getOptimizationInfo()
+          .asMutableMethodOptimizationInfo()
+          .unsetEnumUnboxerMethodClassification();
+    }
+  }
+
+  @Override
   public void setInstanceInitializerInfoCollection(
       DexEncodedMethod method,
       InstanceInitializerInfoCollection instanceInitializerInfoCollection) {
@@ -227,4 +237,9 @@
   public void classInitializerMayBePostponed(DexEncodedMethod method) {
     method.getMutableOptimizationInfo().markClassInitializerMayBePostponed();
   }
+
+  @Override
+  public void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {
+    method.getDefinition().getMutableOptimizationInfo().setUnusedArguments(unusedArguments);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 36719be..cabc28f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
-import static com.android.tools.r8.optimize.argumentpropagation.utils.StronglyConnectedProgramClasses.computeStronglyConnectedProgramClasses;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
@@ -18,6 +16,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ArgumentPropagatorReprocessingCriteriaCollection;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -74,7 +73,8 @@
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
     List<Set<DexProgramClass>> stronglyConnectedProgramClasses =
-        computeStronglyConnectedProgramClasses(appView, immediateSubtypingInfo);
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
     ThreadUtils.processItems(
         stronglyConnectedProgramClasses,
         classes -> {
@@ -132,7 +132,8 @@
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
     List<Set<DexProgramClass>> stronglyConnectedProgramComponents =
-        computeStronglyConnectedProgramClasses(appView, immediateSubtypingInfo);
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
     timing.end();
 
     // Set the optimization info on each method.
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 6288a63..a61d0b4 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -35,6 +35,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteReceiverParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
@@ -63,6 +64,8 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
+  private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
+
   private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
 
   /**
@@ -99,12 +102,34 @@
     return virtualRootMethods.get(method.getReference());
   }
 
+  boolean isMethodParameterAlreadyUnknown(MethodParameter methodParameter, ProgramMethod method) {
+    MethodState methodState =
+        methodStates.get(
+            method.getDefinition().belongsToDirectPool() || isMonomorphicVirtualMethod(method)
+                ? method.getReference()
+                : getVirtualRootMethod(method));
+    if (methodState.isPolymorphic()) {
+      methodState = methodState.asPolymorphic().getMethodStateForBounds(DynamicType.unknown());
+    }
+    if (methodState.isMonomorphic()) {
+      ParameterState parameterState =
+          methodState.asMonomorphic().getParameterState(methodParameter.getIndex());
+      return parameterState.isUnknown();
+    }
+    assert methodState.isBottom() || methodState.isUnknown();
+    return methodState.isUnknown();
+  }
+
   boolean isMonomorphicVirtualMethod(ProgramMethod method) {
-    boolean isMonomorphicVirtualMethod = monomorphicVirtualMethods.contains(method.getReference());
+    boolean isMonomorphicVirtualMethod = isMonomorphicVirtualMethod(method.getReference());
     assert method.getDefinition().belongsToVirtualPool() || !isMonomorphicVirtualMethod;
     return isMonomorphicVirtualMethod;
   }
 
+  boolean isMonomorphicVirtualMethod(DexMethod method) {
+    return monomorphicVirtualMethods.contains(method);
+  }
+
   void scan(ProgramMethod method, IRCode code, Timing timing) {
     timing.begin("Argument propagation scanner");
     for (Invoke invoke : code.<Invoke>instructions(Instruction::isInvoke)) {
@@ -197,14 +222,11 @@
     // possible dispatch targets and propagate the information to these methods (this is expensive).
     // Instead we record the information in one place and then later propagate the information to
     // all dispatch targets.
-    DexMethod representativeMethodReference =
-        getRepresentativeForPolymorphicInvokeOrElse(
-            invoke, resolvedMethod, resolvedMethod.getReference());
     ProgramMethod finalResolvedMethod = resolvedMethod;
     timing.begin("Add method state");
     methodStates.addTemporaryMethodState(
         appView,
-        representativeMethodReference,
+        getRepresentative(invoke, resolvedMethod),
         existingMethodState ->
             computeMethodState(invoke, finalResolvedMethod, context, existingMethodState, timing),
         timing);
@@ -224,10 +246,8 @@
     // compute a polymorphic method state, which includes information about the receiver's dynamic
     // type bounds.
     timing.begin("Compute method state for invoke");
-    boolean isPolymorphicInvoke =
-        getRepresentativeForPolymorphicInvokeOrElse(invoke, resolvedMethod, null) != null;
     MethodState result;
-    if (isPolymorphicInvoke) {
+    if (shouldUsePolymorphicMethodState(invoke, resolvedMethod)) {
       assert existingMethodState.isBottom() || existingMethodState.isPolymorphic();
       result =
           computePolymorphicMethodState(
@@ -420,8 +440,11 @@
     // potentially called from this invoke instruction.
     if (argumentRoot.isArgument()) {
       MethodParameter forwardedParameter =
-          new MethodParameter(
-              context.getReference(), argumentRoot.getDefinition().asArgument().getIndex());
+          methodParameterFactory.create(
+              context, argumentRoot.getDefinition().asArgument().getIndex());
+      if (isMethodParameterAlreadyUnknown(forwardedParameter, context)) {
+        return ParameterState.unknown();
+      }
       if (parameterTypeElement.isClassType()) {
         return new ConcreteClassTypeParameterState(forwardedParameter);
       } else if (parameterTypeElement.isArrayType()) {
@@ -461,10 +484,9 @@
         : new ConcretePrimitiveTypeParameterState(abstractValue);
   }
 
-  private DexMethod getRepresentativeForPolymorphicInvokeOrElse(
-      InvokeMethod invoke, ProgramMethod resolvedMethod, DexMethod defaultValue) {
+  private DexMethod getRepresentative(InvokeMethod invoke, ProgramMethod resolvedMethod) {
     if (resolvedMethod.getDefinition().belongsToDirectPool()) {
-      return defaultValue;
+      return resolvedMethod.getReference();
     }
 
     if (invoke.isInvokeInterface()) {
@@ -475,14 +497,22 @@
     assert invoke.isInvokeSuper() || invoke.isInvokeVirtual();
 
     if (isMonomorphicVirtualMethod(resolvedMethod)) {
-      return defaultValue;
+      return resolvedMethod.getReference();
     }
 
     DexMethod rootMethod = getVirtualRootMethod(resolvedMethod);
     assert rootMethod != null;
+    assert !isMonomorphicVirtualMethod(resolvedMethod)
+        || rootMethod == resolvedMethod.getReference();
     return rootMethod;
   }
 
+  private boolean shouldUsePolymorphicMethodState(
+      InvokeMethod invoke, ProgramMethod resolvedMethod) {
+    return !resolvedMethod.getDefinition().belongsToDirectPool()
+        && !isMonomorphicVirtualMethod(getRepresentative(invoke, resolvedMethod));
+  }
+
   private void scan(InvokeCustom invoke, ProgramMethod context) {
     // If the bootstrap method is program declared it will be called. The call is with runtime
     // provided arguments so ensure that the argument information is unknown.
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index 69f9e86..6f97143 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -12,10 +12,14 @@
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeParameterState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteParameterState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
@@ -26,8 +30,11 @@
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.MethodReprocessingCriteria;
 import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Iterables;
+import java.util.BitSet;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -152,6 +159,8 @@
       return;
     }
 
+    methodState = getMethodStateAfterUnusedParameterRemoval(method, methodState);
+
     if (methodState.isUnknown()) {
       // Nothing is known about the arguments to this method.
       return;
@@ -214,4 +223,51 @@
             ConcreteCallSiteOptimizationInfo.fromMethodState(
                 appView, method, monomorphicMethodState));
   }
+
+  private MethodState getMethodStateAfterUnusedParameterRemoval(
+      ProgramMethod method, MethodState methodState) {
+    assert methodState.isMonomorphic() || methodState.isUnknown();
+    if (!method.getOptimizationInfo().hasUnusedArguments()
+        || appView.appInfo().isKeepUnusedArgumentsMethod(method)) {
+      return methodState;
+    }
+
+    int numberOfArguments = method.getDefinition().getNumberOfArguments();
+    List<ParameterState> parameterStates =
+        methodState.isMonomorphic()
+            ? methodState.asMonomorphic().getParameterStates()
+            : ListUtils.newInitializedArrayList(numberOfArguments, ParameterState.unknown());
+
+    BitSet unusedArguments = method.getOptimizationInfo().getUnusedArguments();
+    for (int argumentIndex = method.getDefinition().getFirstNonReceiverArgumentIndex();
+        argumentIndex < numberOfArguments;
+        argumentIndex++) {
+      boolean isUnused = unusedArguments.get(argumentIndex);
+      if (isUnused) {
+        DexType argumentType = method.getArgumentType(argumentIndex);
+        parameterStates.set(argumentIndex, getUnusedParameterState(argumentType));
+      }
+    }
+
+    if (methodState.isUnknown()) {
+      if (!unusedArguments.get(0) || Iterables.any(parameterStates, ParameterState::isConcrete)) {
+        assert parameterStates.stream().anyMatch(ParameterState::isConcrete);
+        return new ConcreteMonomorphicMethodState(parameterStates);
+      }
+    }
+    return methodState;
+  }
+
+  private ParameterState getUnusedParameterState(DexType argumentType) {
+    if (argumentType.isArrayType()) {
+      return new ConcreteArrayTypeParameterState(Nullability.definitelyNull());
+    } else if (argumentType.isClassType()) {
+      return new ConcreteClassTypeParameterState(
+          appView.abstractValueFactory().createNullValue(), DynamicType.definitelyNull());
+    } else {
+      assert argumentType.isPrimitiveType();
+      return new ConcretePrimitiveTypeParameterState(
+          appView.abstractValueFactory().createZeroValue());
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index 76845cc..5f14c9d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -265,7 +265,8 @@
 
     private boolean isParameterRemovalAllowed(ProgramMethod method) {
       return appView.getKeepInfo(method).isParameterRemovalAllowed(options)
-          && !method.getDefinition().isLibraryMethodOverride().isPossiblyTrue();
+          && !method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
+          && !appView.appInfo().isMethodTargetedByInvokeDynamic(method);
     }
 
     private boolean canRemoveParameterFromVirtualMethods(
@@ -357,8 +358,7 @@
 
       // We need to find a new name for this method, since the signature is already occupied.
       // TODO(b/190154391): Instead of generating a new name, we could also try permuting the order
-      // of
-      //  parameters.
+      // of parameters.
       DexMethod newMethod =
           dexItemFactory.createFreshMethodNameWithoutHolder(
               method.getName().toString(),
@@ -390,8 +390,14 @@
       assert method.getDefinition().belongsToDirectPool();
       // TODO(b/190154391): Allow parameter removal from initializers. We need to guarantee absence
       //  of collisions since initializers can't be renamed.
-      if (!appView.getKeepInfo(method).isParameterRemovalAllowed(options)
-          || method.getDefinition().isInstanceInitializer()) {
+      if (!isParameterRemovalAllowed(method) || method.getDefinition().isInstanceInitializer()) {
+        return ArgumentInfoCollection.empty();
+      }
+      // TODO(b/199864962): Allow parameter removal from check-not-null classified methods.
+      if (method
+          .getOptimizationInfo()
+          .getEnumUnboxerMethodClassification()
+          .isCheckNotNullClassification()) {
         return ArgumentInfoCollection.empty();
       }
       return computeRemovableParametersFromMethod(method);
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java
index 666c87b..3ba87ce 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java
@@ -25,6 +25,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     if (parameterState.isBottom()) {
       return this;
@@ -36,6 +37,9 @@
     assert parameterState.asConcrete().isReferenceParameter();
     ConcreteReferenceTypeParameterState concreteParameterState =
         parameterState.asConcrete().asReferenceParameter();
+    if (concreteParameterState.isArrayParameter()) {
+      return cloner.mutableCopy(concreteParameterState);
+    }
     Nullability nullability = concreteParameterState.getNullability();
     if (nullability.isMaybeNull()) {
       return unknown();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java
index d1df0ac..2ae85ed 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java
@@ -27,6 +27,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     if (parameterState.isBottom()) {
       return this;
@@ -42,6 +43,9 @@
     DynamicType dynamicType = concreteParameterState.getDynamicType();
     DynamicType widenedDynamicType =
         WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
+    if (concreteParameterState.isClassParameter() && !widenedDynamicType.isUnknown()) {
+      return cloner.mutableCopy(concreteParameterState);
+    }
     return abstractValue.isUnknown() && widenedDynamicType.isUnknown()
         ? unknown()
         : new ConcreteClassTypeParameterState(
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
index f5f117e..b7564eb 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
@@ -44,7 +44,8 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      MethodState methodState) {
+      MethodState methodState,
+      StateCloner cloner) {
     return methodState.mutableCopy();
   }
 
@@ -52,7 +53,8 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      Function<MethodState, MethodState> methodStateSupplier) {
-    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this));
+      Function<MethodState, MethodState> methodStateSupplier,
+      StateCloner cloner) {
+    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this), cloner);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java
index fc88145..729938d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java
@@ -25,6 +25,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     if (parameterState.isBottom()) {
       assert parameterState == bottomPrimitiveTypeParameter();
@@ -35,6 +36,6 @@
     }
     assert parameterState.isConcrete();
     assert parameterState.asConcrete().isPrimitiveParameter();
-    return parameterState.mutableCopy();
+    return cloner.mutableCopy(parameterState);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java
index 3a94858..aa331f8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java
@@ -25,6 +25,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     if (parameterState.isBottom()) {
       return this;
@@ -36,6 +37,9 @@
     assert parameterState.asConcrete().isReferenceParameter();
     ConcreteReferenceTypeParameterState concreteParameterState =
         parameterState.asConcrete().asReferenceParameter();
+    if (concreteParameterState.isReceiverParameter()) {
+      return cloner.mutableCopy(concreteParameterState);
+    }
     DynamicType dynamicType = concreteParameterState.getDynamicType();
     if (dynamicType.isUnknown()) {
       return unknown();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
index eb75ac9..9a9de09 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
@@ -106,7 +106,7 @@
       return unknown();
     }
     boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
+    if (widenInParameters(appView)) {
       return unknown();
     }
     if (inParametersChanged) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
index 02db24d..9af5fb3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
@@ -123,7 +123,7 @@
       return unknown();
     }
     boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
+    if (widenInParameters(appView)) {
       return unknown();
     }
     if (abstractValue != oldAbstractValue
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
index ecd3e08..613d864 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
@@ -25,33 +25,38 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      MethodState methodState) {
+      MethodState methodState,
+      StateCloner cloner) {
     if (methodState.isBottom()) {
       return this;
     }
     if (methodState.isUnknown()) {
       return methodState;
     }
-    return mutableJoin(appView, methodSignature, methodState.asConcrete());
+    return mutableJoin(appView, methodSignature, methodState.asConcrete(), cloner);
   }
 
   @Override
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      Function<MethodState, MethodState> methodStateSupplier) {
-    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this));
+      Function<MethodState, MethodState> methodStateSupplier,
+      StateCloner cloner) {
+    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this), cloner);
   }
 
   private MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      ConcreteMethodState methodState) {
+      ConcreteMethodState methodState,
+      StateCloner cloner) {
     if (isMonomorphic() && methodState.isMonomorphic()) {
-      return asMonomorphic().mutableJoin(appView, methodSignature, methodState.asMonomorphic());
+      return asMonomorphic()
+          .mutableJoin(appView, methodSignature, methodState.asMonomorphic(), cloner);
     }
     if (isPolymorphic() && methodState.isPolymorphic()) {
-      return asPolymorphic().mutableJoin(appView, methodSignature, methodState.asPolymorphic());
+      return asPolymorphic()
+          .mutableJoin(appView, methodSignature, methodState.asPolymorphic(), cloner);
     }
     assert false;
     return unknown();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
index b49e48f..c98c383 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
@@ -46,7 +46,8 @@
   public ConcreteMonomorphicMethodStateOrUnknown mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      ConcreteMonomorphicMethodState methodState) {
+      ConcreteMonomorphicMethodState methodState,
+      StateCloner cloner) {
     if (size() != methodState.size()) {
       assert false;
       return unknown();
@@ -59,7 +60,7 @@
       ParameterState otherParameterState = methodState.parameterStates.get(0);
       DexType parameterType = null;
       parameterStates.set(
-          0, parameterState.mutableJoin(appView, otherParameterState, parameterType));
+          0, parameterState.mutableJoin(appView, otherParameterState, parameterType, cloner));
       argumentIndex++;
     }
 
@@ -68,7 +69,8 @@
       ParameterState otherParameterState = methodState.parameterStates.get(argumentIndex);
       DexType parameterType = methodSignature.getParameter(parameterIndex);
       parameterStates.set(
-          argumentIndex, parameterState.mutableJoin(appView, otherParameterState, parameterType));
+          argumentIndex,
+          parameterState.mutableJoin(appView, otherParameterState, parameterType, cloner));
       assert !parameterStates.get(argumentIndex).isConcrete()
           || !parameterStates.get(argumentIndex).asConcrete().isReceiverParameter();
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
index 3b9da36..ed3873f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
@@ -107,6 +107,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     if (parameterState.isBottom()) {
       return this;
@@ -140,8 +141,13 @@
     return inParameters.addAll(parameterState.inParameters);
   }
 
-  boolean widenInParameters() {
-    // TODO(b/190154391): Widen to unknown when the size of the collection exceeds a threshold.
-    return false;
+  /**
+   * Returns true if the in-parameters set should be widened to unknown, in which case the entire
+   * parameter state must be widened to unknown.
+   */
+  boolean widenInParameters(AppView<AppInfoWithLiveness> appView) {
+    return inParameters != null
+        && inParameters.size()
+            > appView.options().callSiteOptimizationOptions().getMaxNumberOfInParameters();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
index c4a1feb..d17ff72 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
@@ -45,7 +45,8 @@
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
       DynamicType bounds,
-      ConcreteMonomorphicMethodStateOrUnknown methodState) {
+      ConcreteMonomorphicMethodStateOrUnknown methodState,
+      StateCloner cloner) {
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
     if (methodState.isUnknown()) {
@@ -58,7 +59,8 @@
     } else {
       assert methodState.isMonomorphic();
       ConcreteMonomorphicMethodStateOrUnknown newMethodStateForBounds =
-          joinInner(appView, methodSignature, receiverBoundsToState.get(bounds), methodState);
+          joinInner(
+              appView, methodSignature, receiverBoundsToState.get(bounds), methodState, cloner);
       if (bounds.isUnknown() && newMethodStateForBounds.isUnknown()) {
         return unknown();
       } else {
@@ -68,19 +70,23 @@
     }
   }
 
+  @SuppressWarnings("unchecked")
   private static ConcreteMonomorphicMethodStateOrUnknown joinInner(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
       ConcreteMonomorphicMethodStateOrUnknown methodState,
-      ConcreteMonomorphicMethodStateOrUnknown other) {
+      ConcreteMonomorphicMethodStateOrUnknown other,
+      StateCloner cloner) {
     if (methodState == null) {
-      return other.mutableCopy();
+      return (ConcreteMonomorphicMethodStateOrUnknown) cloner.mutableCopy(other);
     }
     if (methodState.isUnknown() || other.isUnknown()) {
       return unknown();
     }
     assert methodState.isMonomorphic();
-    return methodState.asMonomorphic().mutableJoin(appView, methodSignature, other.asMonomorphic());
+    return methodState
+        .asMonomorphic()
+        .mutableJoin(appView, methodSignature, other.asMonomorphic(), cloner);
   }
 
   public void forEach(
@@ -118,7 +124,8 @@
   public MethodState mutableCopyWithRewrittenBounds(
       AppView<AppInfoWithLiveness> appView,
       Function<DynamicType, DynamicType> boundsRewriter,
-      DexMethodSignature methodSignature) {
+      DexMethodSignature methodSignature,
+      StateCloner cloner) {
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
     Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> rewrittenReceiverBoundsToState =
@@ -132,7 +139,8 @@
       ConcreteMonomorphicMethodStateOrUnknown existingMethodStateForBounds =
           rewrittenReceiverBoundsToState.get(rewrittenBounds);
       ConcreteMonomorphicMethodStateOrUnknown newMethodStateForBounds =
-          joinInner(appView, methodSignature, existingMethodStateForBounds, entry.getValue());
+          joinInner(
+              appView, methodSignature, existingMethodStateForBounds, entry.getValue(), cloner);
       if (rewrittenBounds.isUnknown() && newMethodStateForBounds.isUnknown()) {
         return unknown();
       }
@@ -146,7 +154,8 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      ConcretePolymorphicMethodState methodState) {
+      ConcretePolymorphicMethodState methodState,
+      StateCloner cloner) {
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
     assert !methodState.isEffectivelyBottom();
@@ -154,7 +163,7 @@
     for (Entry<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> entry :
         methodState.receiverBoundsToState.entrySet()) {
       ConcretePolymorphicMethodStateOrUnknown result =
-          add(appView, methodSignature, entry.getKey(), entry.getValue());
+          add(appView, methodSignature, entry.getKey(), entry.getValue(), cloner);
       if (result.isUnknown()) {
         return result;
       }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
index 3f1baff..429e514 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
@@ -69,7 +69,7 @@
       return unknown();
     }
     boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
+    if (widenInParameters(appView)) {
       return unknown();
     }
     if (abstractValue != oldAbstractValue || inParametersChanged) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
index d7f7bbc..74c9107 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
@@ -100,7 +100,7 @@
       return unknown();
     }
     boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
+    if (widenInParameters(appView)) {
       return unknown();
     }
     if (!dynamicType.equals(oldDynamicType) || inParametersChanged) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameterFactory.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameterFactory.java
new file mode 100644
index 0000000..d1501e6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameterFactory.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class MethodParameterFactory {
+
+  private final Map<MethodParameter, MethodParameter> methodParameters = new ConcurrentHashMap<>();
+
+  public MethodParameter create(ProgramMethod method, int argumentIndex) {
+    return methodParameters.computeIfAbsent(
+        new MethodParameter(method.getReference(), argumentIndex), Function.identity());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
index 0de2d3e..b9df609 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
@@ -44,10 +44,12 @@
   MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      MethodState methodState);
+      MethodState methodState,
+      StateCloner cloner);
 
   MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      Function<MethodState, MethodState> methodStateSupplier);
+      Function<MethodState, MethodState> methodStateSupplier,
+      StateCloner cloner);
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
index 50f02c9..856ff9d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
@@ -45,7 +45,8 @@
               newMethodState = methodState.mutableCopy();
             } else {
               newMethodState =
-                  existingMethodState.mutableJoin(appView, getSignature(method), methodState);
+                  existingMethodState.mutableJoin(
+                      appView, getSignature(method), methodState, StateCloner.getCloner());
             }
             assert !newMethodState.isBottom();
             return newMethodState;
@@ -73,7 +74,8 @@
           assert !existingMethodState.isBottom();
           timing.begin("Join temporary method state");
           MethodState joinResult =
-              existingMethodState.mutableJoin(appView, getSignature(method), methodStateSupplier);
+              existingMethodState.mutableJoin(
+                  appView, getSignature(method), methodStateSupplier, StateCloner.getIdentity());
           assert !joinResult.isBottom();
           timing.end();
           return joinResult;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
index ad7b97c..d70fdfd 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
@@ -57,13 +57,17 @@
   public abstract ParameterState mutableCopy();
 
   public final ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ParameterState parameterState, DexType parameterType) {
-    return mutableJoin(appView, parameterState, parameterType, Action.empty());
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      StateCloner cloner) {
+    return mutableJoin(appView, parameterState, parameterType, cloner, Action.empty());
   }
 
   public abstract ParameterState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction);
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/StateCloner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/StateCloner.java
new file mode 100644
index 0000000..6a2e941
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/StateCloner.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+/**
+ * A strategy for cloning method and parameter states.
+ *
+ * <p>During the primary optimization pass, for each invoke we compute a fresh method state and join
+ * the state into the existing method state for the resolved method. Since the added method state is
+ * completely fresh and not stored anywhere else, we can avoid copying the method state when we join
+ * it into the existing method state. This is achieved by using the {@link #getIdentity()} cloner
+ * below.
+ *
+ * <p>When we later propagate argument information for virtual methods to their overrides, we join
+ * method states from one virtual method into the state for another virtual method. Therefore, it is
+ * important to copy the method state during the join, which is achieved using the {@link
+ * #getCloner()} cloner.
+ */
+public abstract class StateCloner {
+
+  private static StateCloner CLONER =
+      new StateCloner() {
+        @Override
+        public MethodState mutableCopy(MethodState methodState) {
+          return methodState.mutableCopy();
+        }
+
+        @Override
+        public ParameterState mutableCopy(ParameterState parameterState) {
+          return parameterState.mutableCopy();
+        }
+      };
+
+  private static StateCloner IDENTITY =
+      new StateCloner() {
+        @Override
+        public MethodState mutableCopy(MethodState methodState) {
+          return methodState;
+        }
+
+        @Override
+        public ParameterState mutableCopy(ParameterState parameterState) {
+          return parameterState;
+        }
+      };
+
+  public static StateCloner getCloner() {
+    return CLONER;
+  }
+
+  public static StateCloner getIdentity() {
+    return IDENTITY;
+  }
+
+  public abstract MethodState mutableCopy(MethodState methodState);
+
+  public abstract ParameterState mutableCopy(ParameterState parameterState);
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
index b4bb41f..dd822b8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
@@ -35,7 +35,8 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      MethodState methodState) {
+      MethodState methodState,
+      StateCloner cloner) {
     return this;
   }
 
@@ -43,7 +44,8 @@
   public MethodState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      Function<MethodState, MethodState> methodStateSupplier) {
+      Function<MethodState, MethodState> methodStateSupplier,
+      StateCloner cloner) {
     return this;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
index bcb5317..f70ae3d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
@@ -40,6 +40,7 @@
       AppView<AppInfoWithLiveness> appView,
       ParameterState parameterState,
       DexType parameterType,
+      StateCloner cloner,
       Action onChangedAction) {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
index 5d5399d..29a5e40 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -45,6 +45,22 @@
       return root;
     }
 
+    ProgramMethod getSingleNonAbstractMethod() {
+      ProgramMethod singleNonAbstractMethod = root.getAccessFlags().isAbstract() ? null : root;
+      for (ProgramMethod override : overrides) {
+        if (!override.getAccessFlags().isAbstract()) {
+          if (singleNonAbstractMethod != null) {
+            // Not a single non-abstract method.
+            return null;
+          }
+          singleNonAbstractMethod = override;
+        }
+      }
+      assert singleNonAbstractMethod == null
+          || !singleNonAbstractMethod.getAccessFlags().isAbstract();
+      return singleNonAbstractMethod;
+    }
+
     void forEach(Consumer<ProgramMethod> consumer) {
       consumer.accept(root);
       overrides.forEach(consumer);
@@ -53,6 +69,12 @@
     boolean hasOverrides() {
       return !overrides.isEmpty();
     }
+
+    boolean isInterfaceMethodWithSiblings() {
+      // TODO(b/190154391): Conservatively returns true for all interface methods, but should only
+      //  return true for those with siblings.
+      return root.getHolder().isInterface();
+    }
   }
 
   private final Map<DexProgramClass, Map<DexMethodSignature, VirtualRootMethod>>
@@ -125,9 +147,25 @@
           if (isMonomorphicVirtualMethod) {
             monomorphicVirtualMethods.add(rootCandidate.getReference());
           } else {
-            virtualRootMethod.forEach(
-                method ->
-                    virtualRootMethods.put(method.getReference(), rootCandidate.getReference()));
+            ProgramMethod singleNonAbstractMethod = virtualRootMethod.getSingleNonAbstractMethod();
+            if (singleNonAbstractMethod != null
+                && !virtualRootMethod.isInterfaceMethodWithSiblings()) {
+              virtualRootMethod.forEach(
+                  method -> {
+                    // Interface methods can have siblings and can therefore not be mapped to their
+                    // unique non-abstract implementation, unless the interface method does not have
+                    // any siblings.
+                    virtualRootMethods.put(
+                        method.getReference(), singleNonAbstractMethod.getReference());
+                  });
+              if (!singleNonAbstractMethod.getHolder().isInterface()) {
+                monomorphicVirtualMethods.add(singleNonAbstractMethod.getReference());
+              }
+            } else {
+              virtualRootMethod.forEach(
+                  method ->
+                      virtualRootMethods.put(method.getReference(), rootCandidate.getReference()));
+            }
           }
         });
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
index b551b68..9b583c7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
@@ -20,6 +20,8 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
+import com.android.tools.r8.optimize.argumentpropagation.utils.BidirectedGraph;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -27,6 +29,7 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -52,23 +55,26 @@
     // must be included in the argument information for p'.
     FlowGraph flowGraph = new FlowGraph(appView.appInfo().classes());
 
+    List<Set<ParameterNode>> stronglyConnectedComponents =
+        flowGraph.computeStronglyConnectedComponents();
+    ThreadUtils.processItems(stronglyConnectedComponents, this::process, executorService);
+
+    // The algorithm only changes the parameter states of each monomorphic method state. In case any
+    // of these method states have effectively become unknown, we replace them by the canonicalized
+    // unknown method state.
+    postProcessMethodStates(executorService);
+  }
+
+  private void process(Set<ParameterNode> stronglyConnectedComponent) {
     // Build a worklist containing all the parameter nodes.
-    Deque<ParameterNode> worklist = new ArrayDeque<>();
-    flowGraph.forEachNode(worklist::add);
+    Deque<ParameterNode> worklist = new ArrayDeque<>(stronglyConnectedComponent);
 
     // Repeatedly propagate argument information through edges in the flow graph until there are no
     // more changes.
-    // TODO(b/190154391): Consider parallelizing the flow propagation. There are a few scenarios
-    //  that need to be covered, such as (i) two threads could race to update the same parameter
-    //  state, (ii) a thread may try to propagate a parameter state to its successors while
-    //  another thread is trying to update the state of the parameter itself.
     // TODO(b/190154391): Consider a path p1 -> p2 -> p3 in the graph. If we process p2 first, then
     //  p3, and then p1, then the processing of p1 could cause p2 to change, which means that we
     //  need to reprocess p2 and then p3. If we always process leaves in the graph first, we would
     //  process p1, then p2, then p3, and then be done.
-    // TODO(b/190154391): Prune the graph on-the-fly. If the argument information for a parameter
-    //  becomes unknown, we could consider clearing its predecessors since none of the predecessors
-    //  could contribute any information even if they change.
     while (!worklist.isEmpty()) {
       ParameterNode parameterNode = worklist.removeLast();
       parameterNode.unsetPending();
@@ -83,11 +89,6 @@
             }
           });
     }
-
-    // The algorithm only changes the parameter states of each monomorphic method state. In case any
-    // of these method states have effectively become unknown, we replace them by the canonicalized
-    // unknown method state.
-    postProcessMethodStates(executorService);
   }
 
   private void propagate(
@@ -96,9 +97,19 @@
     if (parameterState.isBottom()) {
       return;
     }
+    List<ParameterNode> newlyUnknownParameterNodes = new ArrayList<>();
     for (ParameterNode successorNode : parameterNode.getSuccessors()) {
-      successorNode.addState(
-          appView, parameterState.asNonEmpty(), () -> affectedNodeConsumer.accept(successorNode));
+      ParameterState newParameterState =
+          successorNode.addState(
+              appView,
+              parameterState.asNonEmpty(),
+              () -> affectedNodeConsumer.accept(successorNode));
+      if (newParameterState.isUnknown()) {
+        newlyUnknownParameterNodes.add(successorNode);
+      }
+    }
+    for (ParameterNode newlyUnknownParameterNode : newlyUnknownParameterNodes) {
+      newlyUnknownParameterNode.clearPredecessors();
     }
   }
 
@@ -133,7 +144,7 @@
     }
   }
 
-  private class FlowGraph {
+  public class FlowGraph extends BidirectedGraph<ParameterNode> {
 
     private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> nodes = new IdentityHashMap<>();
 
@@ -141,7 +152,14 @@
       classes.forEach(this::add);
     }
 
-    void forEachNode(Consumer<? super ParameterNode> consumer) {
+    @Override
+    public void forEachNeighbor(ParameterNode node, Consumer<? super ParameterNode> consumer) {
+      node.getPredecessors().forEach(consumer);
+      node.getSuccessors().forEach(consumer);
+    }
+
+    @Override
+    public void forEachNode(Consumer<? super ParameterNode> consumer) {
       nodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
     }
 
@@ -278,6 +296,10 @@
       predecessors.clear();
     }
 
+    Set<ParameterNode> getPredecessors() {
+      return predecessors;
+    }
+
     ParameterState getState() {
       return methodState.getParameterState(parameterIndex);
     }
@@ -294,18 +316,23 @@
       return pending;
     }
 
-    void addState(
+    ParameterState addState(
         AppView<AppInfoWithLiveness> appView,
         NonEmptyParameterState parameterStateToAdd,
         Action onChangedAction) {
       ParameterState oldParameterState = getState();
       ParameterState newParameterState =
           oldParameterState.mutableJoin(
-              appView, parameterStateToAdd, parameterType, onChangedAction);
+              appView,
+              parameterStateToAdd,
+              parameterType,
+              StateCloner.getCloner(),
+              onChangedAction);
       if (newParameterState != oldParameterState) {
         setState(newParameterState);
         onChangedAction.execute();
       }
+      return newParameterState;
     }
 
     void setPending() {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
index 72990cb..e1e5f63 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionBySignature;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -130,7 +131,7 @@
                   if (resolutionResult.isFailedResolution()) {
                     // TODO(b/190154391): Do we need to propagate argument information to the first
                     //  virtual method above the inaccessible method in the class hierarchy?
-                    assert resolutionResult.isIllegalAccessErrorResult(subclass, appView.appInfo());
+                    assert resolutionResult.asFailedResolution().hasMethodsCausingError();
                     return;
                   }
 
@@ -185,7 +186,8 @@
               }
               return null;
             },
-            resolvedMethod.getMethodSignature());
+            resolvedMethod.getMethodSignature(),
+            StateCloner.getCloner());
 
     // If the resolved method is a virtual method that does not override any methods and are not
     // overridden by any methods, then we use a monomorphic method state for it. Therefore, we
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
index 41cb0af..36525f5 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionBySignature;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
@@ -150,7 +151,9 @@
       if (!activeUntilLowerBound.isEmpty()) {
         DexMethodSignature methodSignature = method.getMethodSignature();
         for (MethodStateCollectionBySignature methodStates : activeUntilLowerBound.values()) {
-          methodState = methodState.mutableJoin(appView, methodSignature, methodStates.get(method));
+          methodState =
+              methodState.mutableJoin(
+                  appView, methodSignature, methodStates.get(method), StateCloner.getCloner());
         }
       }
       return methodState;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/BidirectedGraph.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/BidirectedGraph.java
new file mode 100644
index 0000000..8e76917
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/BidirectedGraph.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.utils;
+
+import com.android.tools.r8.utils.WorkList;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public abstract class BidirectedGraph<T> {
+
+  public abstract void forEachNeighbor(T node, Consumer<? super T> consumer);
+
+  public abstract void forEachNode(Consumer<? super T> consumer);
+
+  /**
+   * Computes the strongly connected components in the current bidirectional graph (i.e., each
+   * strongly connected component can be found using a breadth first search).
+   */
+  public List<Set<T>> computeStronglyConnectedComponents() {
+    Set<T> seen = new HashSet<>();
+    List<Set<T>> stronglyConnectedComponents = new ArrayList<>();
+    forEachNode(
+        node -> {
+          if (seen.contains(node)) {
+            return;
+          }
+          Set<T> stronglyConnectedComponent = internalComputeStronglyConnectedProgramClasses(node);
+          stronglyConnectedComponents.add(stronglyConnectedComponent);
+          seen.addAll(stronglyConnectedComponent);
+        });
+    return stronglyConnectedComponents;
+  }
+
+  private Set<T> internalComputeStronglyConnectedProgramClasses(T node) {
+    WorkList<T> worklist = WorkList.newEqualityWorkList(node);
+    while (worklist.hasNext()) {
+      T current = worklist.next();
+      forEachNeighbor(current, worklist::addIfNotSeen);
+    }
+    return worklist.getSeenSet();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ProgramClassesBidirectedGraph.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ProgramClassesBidirectedGraph.java
new file mode 100644
index 0000000..474c445
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ProgramClassesBidirectedGraph.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.utils;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.function.Consumer;
+
+public class ProgramClassesBidirectedGraph extends BidirectedGraph<DexProgramClass> {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+
+  public ProgramClassesBidirectedGraph(
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    this.appView = appView;
+    this.immediateSubtypingInfo = immediateSubtypingInfo;
+  }
+
+  @Override
+  public void forEachNeighbor(DexProgramClass node, Consumer<? super DexProgramClass> consumer) {
+    immediateSubtypingInfo.forEachImmediateProgramSuperClass(node, consumer);
+    immediateSubtypingInfo.getSubclasses(node).forEach(consumer);
+  }
+
+  @Override
+  public void forEachNode(Consumer<? super DexProgramClass> consumer) {
+    appView.appInfo().classes().forEach(consumer);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java
deleted file mode 100644
index 25d28cb..0000000
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.optimize.argumentpropagation.utils;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-public class StronglyConnectedProgramClasses {
-
-  /**
-   * Computes the strongly connected components in the program class hierarchy (where extends and
-   * implements edges are treated as bidirectional).
-   */
-  public static List<Set<DexProgramClass>> computeStronglyConnectedProgramClasses(
-      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    Set<DexProgramClass> seen = Sets.newIdentityHashSet();
-    List<Set<DexProgramClass>> stronglyConnectedComponents = new ArrayList<>();
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (seen.contains(clazz)) {
-        continue;
-      }
-      Set<DexProgramClass> stronglyConnectedComponent =
-          internalComputeStronglyConnectedProgramClasses(clazz, immediateSubtypingInfo);
-      stronglyConnectedComponents.add(stronglyConnectedComponent);
-      seen.addAll(stronglyConnectedComponent);
-    }
-    return stronglyConnectedComponents;
-  }
-
-  private static Set<DexProgramClass> internalComputeStronglyConnectedProgramClasses(
-      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList(clazz);
-    while (worklist.hasNext()) {
-      DexProgramClass current = worklist.next();
-      immediateSubtypingInfo.forEachImmediateProgramSuperClass(current, worklist::addIfNotSeen);
-      worklist.addIfNotSeen(immediateSubtypingInfo.getSubclasses(current));
-    }
-    return worklist.getSeenSet();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index 62c04bb..11dcc13 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.OptionsParsing;
 import com.android.tools.r8.utils.OptionsParsing.ParseContext;
+import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
@@ -36,7 +37,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
-import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 /**
@@ -139,7 +139,7 @@
   private final StackTraceLineParser<T, ST> stackTraceLineParser;
   private final StackTraceElementProxyRetracer<T, ST> proxyRetracer;
   private final DiagnosticsHandler diagnosticsHandler;
-  private final boolean isVerbose;
+  protected final boolean isVerbose;
 
   Retrace(
       StackTraceLineParser<T, ST> stackTraceLineParser,
@@ -153,12 +153,12 @@
   }
 
   /**
-   * Retraces a stack frame and calls the consumer for each retraced line
+   * Retraces a complete stack frame and returns a list of retraced stack traces.
    *
    * @param stackTrace the stack trace to be retrace
-   * @param retracedFrameConsumer the consumer to accept the retraced stack trace.
+   * @return list of potentially ambiguous stack traces.
    */
-  public void retraceStackTrace(List<T> stackTrace, Consumer<List<List<T>>> retracedFrameConsumer) {
+  public List<List<T>> retraceStackTrace(List<T> stackTrace) {
     ListUtils.forEachWithIndex(
         stackTrace,
         (line, lineNumber) -> {
@@ -168,7 +168,57 @@
             throw new RetraceAbortException();
           }
         });
-    stackTrace.forEach(line -> retracedFrameConsumer.accept(retraceFrame(line)));
+    List<Pair<List<T>, RetraceStackTraceContext>> retracedStackTraces = new ArrayList<>();
+    retracedStackTraces.add(
+        new Pair<>(new ArrayList<>(), RetraceStackTraceContext.getInitialContext()));
+    retracedStackTraces =
+        ListUtils.fold(
+            stackTrace,
+            retracedStackTraces,
+            (acc, stackTraceLine) -> {
+              ST parsedLine = stackTraceLineParser.parse(stackTraceLine);
+              List<Pair<List<T>, RetraceStackTraceContext>> newRetracedStackTraces =
+                  new ArrayList<>();
+              for (Pair<List<T>, RetraceStackTraceContext> retracedStackTrace : acc) {
+                Map<
+                        RetraceStackTraceElementProxy<T, ST>,
+                        List<RetraceStackTraceElementProxy<T, ST>>>
+                    ambiguousBlocks = new HashMap<>();
+                List<RetraceStackTraceElementProxy<T, ST>> ambiguousKeys = new ArrayList<>();
+                proxyRetracer
+                    .retrace(parsedLine, retracedStackTrace.getSecond())
+                    .forEach(
+                        retracedElement -> {
+                          if (retracedElement.isTopFrame() || !retracedElement.hasRetracedClass()) {
+                            ambiguousKeys.add(retracedElement);
+                            ambiguousBlocks.put(retracedElement, new ArrayList<>());
+                          }
+                          ambiguousBlocks.get(ListUtils.last(ambiguousKeys)).add(retracedElement);
+                        });
+                if (ambiguousKeys.isEmpty()) {
+                  // This happens when there is nothing to report.
+                  newRetracedStackTraces.add(
+                      new Pair<>(
+                          retracedStackTrace.getFirst(),
+                          RetraceStackTraceContext.getInitialContext()));
+                  continue;
+                }
+                Collections.sort(ambiguousKeys);
+                ambiguousKeys.forEach(
+                    key -> {
+                      List<T> resultList = new ArrayList<>();
+                      resultList.addAll(retracedStackTrace.getFirst());
+                      resultList.addAll(
+                          ListUtils.map(
+                              ambiguousBlocks.get(key),
+                              retracedElement ->
+                                  parsedLine.toRetracedItem(retracedElement, isVerbose)));
+                      newRetracedStackTraces.add(new Pair<>(resultList, key.getContext()));
+                    });
+              }
+              return newRetracedStackTraces;
+            });
+    return ListUtils.map(retracedStackTraces, Pair::getFirst);
   }
 
   /**
@@ -178,11 +228,11 @@
    * @return A collection of retraced frame where each entry in the outer list is ambiguous
    */
   public List<List<T>> retraceFrame(T stackTraceFrame) {
-    Map<RetraceStackTraceProxy<T, ST>, List<T>> ambiguousBlocks = new HashMap<>();
-    List<RetraceStackTraceProxy<T, ST>> ambiguousKeys = new ArrayList<>();
+    Map<RetraceStackTraceElementProxy<T, ST>, List<T>> ambiguousBlocks = new HashMap<>();
+    List<RetraceStackTraceElementProxy<T, ST>> ambiguousKeys = new ArrayList<>();
     ST parsedLine = stackTraceLineParser.parse(stackTraceFrame);
     proxyRetracer
-        .retrace(parsedLine)
+        .retrace(parsedLine, RetraceStackTraceContext.getInitialContext())
         .forEach(
             retracedElement -> {
               if (retracedElement.isTopFrame() || !retracedElement.hasRetracedClass()) {
@@ -209,7 +259,7 @@
   public List<T> retraceLine(T stackTraceLine) {
     ST parsedLine = stackTraceLineParser.parse(stackTraceLine);
     return proxyRetracer
-        .retrace(parsedLine)
+        .retrace(parsedLine, RetraceStackTraceContext.getInitialContext())
         .map(
             retraceFrame -> {
               retraceFrame.getOriginalItem().toRetracedItem(retraceFrame, isVerbose);
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceClassElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceClassElement.java
index 35380e4..43bf109 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceClassElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceClassElement.java
@@ -4,26 +4,31 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import java.util.List;
+import java.util.Optional;
 
 @Keep
 public interface RetraceClassElement extends RetraceElement<RetraceClassResult> {
 
   RetracedClassReference getRetracedClass();
 
-  RetraceSourceFileResult getSourceFile();
+  RetracedSourceFile getSourceFile();
 
   RetraceFieldResult lookupField(String fieldName);
 
   RetraceMethodResult lookupMethod(String methodName);
 
-  RetraceFrameResult lookupFrame(String methodName);
-
-  RetraceFrameResult lookupFrame(String methodName, int position);
+  RetraceFrameResult lookupFrame(Optional<Integer> position, String methodName);
 
   RetraceFrameResult lookupFrame(
-      String methodName, int position, List<TypeReference> formalTypes, TypeReference returnType);
+      Optional<Integer> position,
+      String methodName,
+      List<TypeReference> formalTypes,
+      TypeReference returnType);
+
+  RetraceFrameResult lookupFrame(Optional<Integer> position, MethodReference methodReference);
 
   RetraceUnknownJsonMappingInformationResult getUnknownJsonMappingInformation();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java
index 29f4703..e2b59fa 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.Keep;
 import com.android.tools.r8.references.TypeReference;
 import java.util.List;
+import java.util.Optional;
 
 @Keep
 public interface RetraceClassResult extends RetraceResult<RetraceClassElement> {
@@ -22,11 +23,13 @@
   RetraceMethodResult lookupMethod(
       String methodName, List<TypeReference> formalTypes, TypeReference returnType);
 
-  RetraceFrameResult lookupFrame(String methodName);
-
-  RetraceFrameResult lookupFrame(String methodName, int position);
+  RetraceFrameResult lookupFrame(
+      RetraceStackTraceContext context, Optional<Integer> position, String methodName);
 
   RetraceFrameResult lookupFrame(
-      String methodName, int position, List<TypeReference> formalTypes, TypeReference returnType);
-
+      RetraceStackTraceContext context,
+      Optional<Integer> position,
+      String methodName,
+      List<TypeReference> formalTypes,
+      TypeReference returnType);
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
index 55de558..63d40db 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
@@ -16,4 +16,6 @@
   R getRetraceResultContext();
 
   boolean isCompilerSynthesized();
+
+  RetraceStackTraceContext getContext();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceFieldElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceFieldElement.java
index 5982550..d51e175 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceFieldElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceFieldElement.java
@@ -14,5 +14,5 @@
 
   RetraceClassElement getClassElement();
 
-  RetraceSourceFileResult getSourceFile();
+  RetracedSourceFile getSourceFile();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceFrameElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceFrameElement.java
index 3295c44..b114240 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceFrameElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceFrameElement.java
@@ -20,7 +20,7 @@
 
   void visitNonCompilerSynthesizedFrames(BiConsumer<RetracedMethodReference, Integer> consumer);
 
-  RetraceSourceFileResult getSourceFile(RetracedClassMemberReference frame);
+  RetracedSourceFile getSourceFile(RetracedClassMemberReference frame);
 
   List<? extends RetracedMethodReference> getOuterFrames();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceMethodElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceMethodElement.java
index 887bff5..132a200 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceMethodElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceMethodElement.java
@@ -14,5 +14,5 @@
 
   RetraceClassElement getClassElement();
 
-  RetraceSourceFileResult getSourceFile();
+  RetracedSourceFile getSourceFile();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceContext.java b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceContext.java
new file mode 100644
index 0000000..9004cc1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceContext.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2021, 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.retrace;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.retrace.internal.RetraceStackTraceContextImpl;
+
+@Keep
+public interface RetraceStackTraceContext {
+
+  static RetraceStackTraceContext getInitialContext() {
+    return new RetraceStackTraceContextImpl();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceElementProxy.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java
rename to src/main/java/com/android/tools/r8/retrace/RetraceStackTraceElementProxy.java
index d326609d..c148bc0 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceElementProxy.java
@@ -8,8 +8,8 @@
 import java.util.List;
 
 @Keep
-public interface RetraceStackTraceProxy<T, ST extends StackTraceElementProxy<T, ST>>
-    extends Comparable<RetraceStackTraceProxy<T, ST>> {
+public interface RetraceStackTraceElementProxy<T, ST extends StackTraceElementProxy<T, ST>>
+    extends Comparable<RetraceStackTraceElementProxy<T, ST>> {
 
   boolean isAmbiguous();
 
@@ -44,4 +44,6 @@
   String getSourceFile();
 
   int getLineNumber();
+
+  RetraceStackTraceContext getContext();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceSourceFileResult.java b/src/main/java/com/android/tools/r8/retrace/RetracedSourceFile.java
similarity index 66%
rename from src/main/java/com/android/tools/r8/retrace/RetraceSourceFileResult.java
rename to src/main/java/com/android/tools/r8/retrace/RetracedSourceFile.java
index 9941d9c..c0e0a7d 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceSourceFileResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetracedSourceFile.java
@@ -6,11 +6,10 @@
 
 import com.android.tools.r8.Keep;
 
-// TODO: This does not seem to be a "result" per say, should this rather be a RetracedSourceFile?
 @Keep
-public interface RetraceSourceFileResult {
+public interface RetracedSourceFile {
 
   boolean hasRetraceResult();
 
-  String getFilename();
+  String getSourceFile();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/Retracer.java b/src/main/java/com/android/tools/r8/retrace/Retracer.java
index b99f20c..c609e44 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retracer.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retracer.java
@@ -22,6 +22,9 @@
 
   RetraceFrameResult retraceFrame(MethodReference methodReference, int position);
 
+  RetraceFrameResult retraceFrame(
+      MethodReference methodReference, int position, RetraceStackTraceContext context);
+
   RetraceFieldResult retraceField(FieldReference fieldReference);
 
   RetraceTypeResult retraceType(TypeReference typeReference);
diff --git a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
index 7cffce0..46c7eff 100644
--- a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
@@ -14,7 +14,7 @@
 
   public abstract boolean hasMethodName();
 
-  public abstract boolean hasFileName();
+  public abstract boolean hasSourceFile();
 
   public abstract boolean hasLineNumber();
 
@@ -28,7 +28,7 @@
 
   public abstract String getMethodName();
 
-  public abstract String getFileName();
+  public abstract String getSourceFile();
 
   public abstract int getLineNumber();
 
@@ -38,5 +38,6 @@
 
   public abstract String getMethodArguments();
 
-  public abstract T toRetracedItem(RetraceStackTraceProxy<T, ST> retracedProxy, boolean verbose);
+  public abstract T toRetracedItem(
+      RetraceStackTraceElementProxy<T, ST> retracedProxy, boolean verbose);
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxyRetracer.java b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxyRetracer.java
index 18be7e7..851bbc2 100644
--- a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxyRetracer.java
+++ b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxyRetracer.java
@@ -11,7 +11,8 @@
 @Keep
 public interface StackTraceElementProxyRetracer<T, ST extends StackTraceElementProxy<T, ST>> {
 
-  Stream<RetraceStackTraceProxy<T, ST>> retrace(ST element);
+  Stream<? extends RetraceStackTraceElementProxy<T, ST>> retrace(
+      ST element, RetraceStackTraceContext context);
 
   static <T, ST extends StackTraceElementProxy<T, ST>>
       StackTraceElementProxyRetracer<T, ST> createDefault(Retracer retracer) {
diff --git a/src/main/java/com/android/tools/r8/retrace/StringRetrace.java b/src/main/java/com/android/tools/r8/retrace/StringRetrace.java
index 4bdb42c..0296d71 100644
--- a/src/main/java/com/android/tools/r8/retrace/StringRetrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/StringRetrace.java
@@ -76,10 +76,42 @@
    */
   public List<String> retrace(List<String> stackTrace) {
     List<String> retracedStrings = new ArrayList<>();
-    retraceStackTrace(stackTrace, result -> joinAmbiguousLines(result, retracedStrings::add));
+    List<List<String>> retracedStackTraces =
+        removeDuplicateStackTraces(retraceStackTrace(stackTrace));
+    if (retracedStackTraces.size() > 1) {
+      retracedStrings.add(
+          "There are "
+              + retracedStackTraces.size()
+              + " ambiguous stack traces."
+              + (isVerbose ? "" : " Use --verbose to have all listed."));
+    }
+    for (int i = 0; i < retracedStackTraces.size(); i++) {
+      if (i > 0) {
+        retracedStrings.add("< OR >");
+      }
+      retracedStrings.addAll(retracedStackTraces.get(i));
+      if (!isVerbose) {
+        break;
+      }
+    }
     return retracedStrings;
   }
 
+  private List<List<String>> removeDuplicateStackTraces(List<List<String>> stackTraces) {
+    if (stackTraces.size() == 1) {
+      return stackTraces;
+    }
+    Set<List<String>> seenStackTraces = new HashSet<>();
+    List<List<String>> nonDuplicateStackTraces = new ArrayList<>();
+    stackTraces.forEach(
+        stackTrace -> {
+          if (seenStackTraces.add(stackTrace)) {
+            nonDuplicateStackTraces.add(stackTrace);
+          }
+        });
+    return nonDuplicateStackTraces;
+  }
+
   /**
    * Retraces a single stack trace line and returns the potential list of original frames
    *
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceClassResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceClassResultImpl.java
index 55ed4db..54c90e6 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceClassResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceClassResultImpl.java
@@ -11,19 +11,22 @@
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.mappinginformation.MappingInformation;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.retrace.RetraceClassElement;
 import com.android.tools.r8.retrace.RetraceClassResult;
 import com.android.tools.r8.retrace.RetraceFrameResult;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
 import com.android.tools.r8.retrace.RetraceUnknownJsonMappingInformationResult;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Pair;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.stream.Stream;
 
@@ -121,25 +124,26 @@
   }
 
   @Override
-  public RetraceFrameResultImpl lookupFrame(String methodName) {
-    return lookupFrame(MethodDefinition.create(obfuscatedReference, methodName), -1);
-  }
-
-  @Override
-  public RetraceFrameResultImpl lookupFrame(String methodName, int position) {
+  public RetraceFrameResultImpl lookupFrame(
+      RetraceStackTraceContext context, Optional<Integer> position, String methodName) {
     return lookupFrame(MethodDefinition.create(obfuscatedReference, methodName), position);
   }
 
   @Override
   public RetraceFrameResultImpl lookupFrame(
-      String methodName, int position, List<TypeReference> formalTypes, TypeReference returnType) {
+      RetraceStackTraceContext context,
+      Optional<Integer> position,
+      String methodName,
+      List<TypeReference> formalTypes,
+      TypeReference returnType) {
     return lookupFrame(
         MethodDefinition.create(
             Reference.method(obfuscatedReference, methodName, formalTypes, returnType)),
         position);
   }
 
-  private RetraceFrameResultImpl lookupFrame(MethodDefinition definition, int position) {
+  private RetraceFrameResultImpl lookupFrame(
+      MethodDefinition definition, Optional<Integer> position) {
     List<Pair<RetraceClassElementImpl, List<MappedRange>>> mappings = new ArrayList<>();
     internalStream()
         .forEach(
@@ -154,7 +158,7 @@
   }
 
   private List<List<MappedRange>> getMappedRangesForFrame(
-      RetraceClassElementImpl element, MethodDefinition definition, int position) {
+      RetraceClassElementImpl element, MethodDefinition definition, Optional<Integer> position) {
     List<List<MappedRange>> overloadedRanges = new ArrayList<>();
     if (mapper == null) {
       overloadedRanges.add(null);
@@ -167,8 +171,8 @@
       return overloadedRanges;
     }
     List<MappedRange> mappedRangesForPosition = null;
-    if (position >= 0) {
-      mappedRangesForPosition = mappedRanges.allRangesForLine(position, false);
+    if (position.isPresent() && position.get() >= 0) {
+      mappedRangesForPosition = mappedRanges.allRangesForLine(position.get(), false);
     }
     if (mappedRangesForPosition == null || mappedRangesForPosition.isEmpty()) {
       mappedRangesForPosition = mappedRanges.getMappedRanges();
@@ -240,15 +244,15 @@
     }
 
     @Override
-    public RetraceSourceFileResult getSourceFile() {
+    public RetracedSourceFile getSourceFile() {
       if (classResult.mapper != null) {
         for (MappingInformation info : classResult.mapper.getAdditionalMappingInfo()) {
           if (info.isFileNameInformation()) {
-            return new RetraceSourceFileResultImpl(info.asFileNameInformation().getFileName());
+            return new RetracedSourceFileImpl(info.asFileNameInformation().getFileName());
           }
         }
       }
-      return new RetraceSourceFileResultImpl(null);
+      return new RetracedSourceFileImpl(null);
     }
 
     @Override
@@ -269,6 +273,12 @@
     }
 
     @Override
+    public RetraceStackTraceContext getContext() {
+      // TODO(b/197936862): Extend the context to enable tracking information.
+      return RetraceStackTraceContext.getInitialContext();
+    }
+
+    @Override
     public RetraceFieldResultImpl lookupField(String fieldName) {
       return lookupField(FieldDefinition.create(classReference.getClassReference(), fieldName));
     }
@@ -322,27 +332,28 @@
     }
 
     @Override
-    public RetraceFrameResultImpl lookupFrame(String methodName) {
-      return lookupFrame(methodName, -1);
-    }
-
-    @Override
-    public RetraceFrameResultImpl lookupFrame(String methodName, int position) {
+    public RetraceFrameResultImpl lookupFrame(Optional<Integer> position, String methodName) {
       return lookupFrame(
-          MethodDefinition.create(classReference.getClassReference(), methodName), position);
+          position, MethodDefinition.create(classReference.getClassReference(), methodName));
     }
 
     @Override
     public RetraceFrameResult lookupFrame(
+        Optional<Integer> position,
         String methodName,
-        int position,
         List<TypeReference> formalTypes,
         TypeReference returnType) {
       return lookupFrame(
+          position,
           MethodDefinition.create(
               Reference.method(
-                  classReference.getClassReference(), methodName, formalTypes, returnType)),
-          position);
+                  classReference.getClassReference(), methodName, formalTypes, returnType)));
+    }
+
+    @Override
+    public RetraceFrameResult lookupFrame(
+        Optional<Integer> position, MethodReference methodReference) {
+      return lookupFrame(position, MethodDefinition.create(methodReference));
     }
 
     @Override
@@ -351,7 +362,8 @@
           mapper.getAdditionalMappingInfo());
     }
 
-    private RetraceFrameResultImpl lookupFrame(MethodDefinition definition, int position) {
+    private RetraceFrameResultImpl lookupFrame(
+        Optional<Integer> position, MethodDefinition definition) {
       MethodDefinition methodDefinition =
           MethodDefinition.create(classReference.getClassReference(), definition.getName());
       ImmutableList.Builder<Pair<RetraceClassElementImpl, List<MappedRange>>> builder =
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
index 6f5e557..b536bea 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
@@ -10,7 +10,8 @@
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.RetraceFieldElement;
 import com.android.tools.r8.retrace.RetraceFieldResult;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.retrace.internal.RetraceClassResultImpl.RetraceClassElementImpl;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -113,6 +114,11 @@
     }
 
     @Override
+    public RetraceStackTraceContext getContext() {
+      return RetraceStackTraceContext.getInitialContext();
+    }
+
+    @Override
     public boolean isUnknown() {
       return fieldReference.isUnknown();
     }
@@ -133,7 +139,7 @@
     }
 
     @Override
-    public RetraceSourceFileResult getSourceFile() {
+    public RetracedSourceFile getSourceFile() {
       return classElement.getSourceFile();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFrameResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFrameResultImpl.java
index edde5ab..2f79d8e 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFrameResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFrameResultImpl.java
@@ -11,9 +11,10 @@
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.retrace.RetraceFrameElement;
 import com.android.tools.r8.retrace.RetraceFrameResult;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
 import com.android.tools.r8.retrace.RetracedClassMemberReference;
 import com.android.tools.r8.retrace.RetracedMethodReference;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.retrace.internal.RetraceClassResultImpl.RetraceClassElementImpl;
 import com.android.tools.r8.utils.ListUtils;
@@ -24,14 +25,15 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.BiConsumer;
 import java.util.stream.Stream;
 
-public class RetraceFrameResultImpl implements RetraceFrameResult {
+class RetraceFrameResultImpl implements RetraceFrameResult {
 
   private final RetraceClassResultImpl classResult;
   private final MethodDefinition methodDefinition;
-  private final int obfuscatedPosition;
+  private final Optional<Integer> obfuscatedPosition;
   private final List<Pair<RetraceClassElementImpl, List<MappedRange>>> mappedRanges;
   private final Retracer retracer;
 
@@ -41,7 +43,7 @@
       RetraceClassResultImpl classResult,
       List<Pair<RetraceClassElementImpl, List<MappedRange>>> mappedRanges,
       MethodDefinition methodDefinition,
-      int obfuscatedPosition,
+      Optional<Integer> obfuscatedPosition,
       Retracer retracer) {
     this.classResult = classResult;
     this.methodDefinition = methodDefinition;
@@ -127,17 +129,24 @@
   }
 
   private RetracedMethodReferenceImpl getRetracedMethod(
-      MethodReference methodReference, MappedRange mappedRange, int obfuscatedPosition) {
-    if (mappedRange.minifiedRange == null || (obfuscatedPosition == -1 && !isAmbiguous())) {
+      MethodReference methodReference,
+      MappedRange mappedRange,
+      Optional<Integer> obfuscatedPosition) {
+    if (mappedRange.minifiedRange == null
+        || (obfuscatedPosition.orElse(-1) == -1 && !isAmbiguous())) {
       int originalLineNumber = mappedRange.getFirstLineNumberOfOriginalRange();
-      return RetracedMethodReferenceImpl.create(
-          methodReference, originalLineNumber > 0 ? originalLineNumber : obfuscatedPosition);
+      if (originalLineNumber > 0) {
+        return RetracedMethodReferenceImpl.create(methodReference, originalLineNumber);
+      } else {
+        return RetracedMethodReferenceImpl.create(methodReference);
+      }
     }
-    if (!mappedRange.minifiedRange.contains(obfuscatedPosition)) {
+    if (!obfuscatedPosition.isPresent()
+        || !mappedRange.minifiedRange.contains(obfuscatedPosition.get())) {
       return RetracedMethodReferenceImpl.create(methodReference);
     }
     return RetracedMethodReferenceImpl.create(
-        methodReference, mappedRange.getOriginalLineNumber(obfuscatedPosition));
+        methodReference, mappedRange.getOriginalLineNumber(obfuscatedPosition.get()));
   }
 
   public static class ElementImpl implements RetraceFrameElement {
@@ -146,14 +155,14 @@
     private final RetraceFrameResultImpl retraceFrameResult;
     private final RetraceClassElementImpl classElement;
     private final List<MappedRange> mappedRanges;
-    private final int obfuscatedPosition;
+    private final Optional<Integer> obfuscatedPosition;
 
-    public ElementImpl(
+    private ElementImpl(
         RetraceFrameResultImpl retraceFrameResult,
         RetraceClassElementImpl classElement,
         RetracedMethodReferenceImpl methodReference,
         List<MappedRange> mappedRanges,
-        int obfuscatedPosition) {
+        Optional<Integer> obfuscatedPosition) {
       this.methodReference = methodReference;
       this.retraceFrameResult = retraceFrameResult;
       this.classElement = classElement;
@@ -179,6 +188,11 @@
     }
 
     @Override
+    public RetraceStackTraceContext getContext() {
+      return RetraceStackTraceContext.getInitialContext();
+    }
+
+    @Override
     public RetraceFrameResult getRetraceResultContext() {
       return retraceFrameResult;
     }
@@ -224,7 +238,7 @@
     }
 
     @Override
-    public RetraceSourceFileResult getSourceFile(RetracedClassMemberReference frame) {
+    public RetracedSourceFile getSourceFile(RetracedClassMemberReference frame) {
       return RetraceUtils.getSourceFileOrLookup(
           frame.getHolderClass(), classElement, retraceFrameResult.retracer);
     }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
index 37b8ffa..8ccdbdb 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
@@ -10,13 +10,16 @@
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.retrace.RetraceMethodElement;
 import com.android.tools.r8.retrace.RetraceMethodResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
 import com.android.tools.r8.retrace.RetracedMethodReference;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.retrace.internal.RetraceClassResultImpl.RetraceClassElementImpl;
 import com.android.tools.r8.utils.Pair;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 public class RetraceMethodResultImpl implements RetraceMethodResult {
@@ -91,7 +94,7 @@
         classResult,
         narrowedRanges.isEmpty() ? noMappingRanges : narrowedRanges,
         methodDefinition,
-        position,
+        Optional.of(position),
         retracer);
   }
 
@@ -146,6 +149,11 @@
     }
 
     @Override
+    public RetraceStackTraceContext getContext() {
+      return RetraceStackTraceContext.getInitialContext();
+    }
+
+    @Override
     public boolean isUnknown() {
       return methodReference.isUnknown();
     }
@@ -166,7 +174,7 @@
     }
 
     @Override
-    public com.android.tools.r8.retrace.RetraceSourceFileResult getSourceFile() {
+    public RetracedSourceFile getSourceFile() {
       return RetraceUtils.getSourceFileOrLookup(
           methodReference.getHolderClass(), classElement, retraceMethodResult.retracer);
     }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceStackTraceContextImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceStackTraceContextImpl.java
new file mode 100644
index 0000000..b33e06d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceStackTraceContextImpl.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, 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.retrace.internal;
+
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
+
+public class RetraceStackTraceContextImpl implements RetraceStackTraceContext {}
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceUtils.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceUtils.java
index d1a6b10..920393b 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceUtils.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceUtils.java
@@ -14,10 +14,10 @@
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.retrace.RetraceClassElement;
 import com.android.tools.r8.retrace.RetraceClassResult;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
 import com.android.tools.r8.retrace.RetracedClassReference;
 import com.android.tools.r8.retrace.RetracedMethodReference;
 import com.android.tools.r8.retrace.RetracedMethodReference.KnownRetracedMethodReference;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -75,18 +75,18 @@
     return clazz.substring(lastIndexOfPeriod + 1, endIndex);
   }
 
-  public static RetraceSourceFileResult getSourceFileOrLookup(
+  public static RetracedSourceFile getSourceFileOrLookup(
       RetracedClassReference holder, RetraceClassElement context, Retracer retracer) {
     if (holder.equals(context.getRetracedClass())) {
       return context.getSourceFile();
     }
     RetraceClassResult contextClassResult = retracer.retraceClass(holder.getClassReference());
-    Box<RetraceSourceFileResult> retraceSourceFile = new Box<>();
+    Box<RetracedSourceFile> retraceSourceFile = new Box<>();
     contextClassResult.forEach(element -> retraceSourceFile.set(element.getSourceFile()));
     return retraceSourceFile.get();
   }
 
-  public static String inferFileName(
+  public static String inferSourceFile(
       String retracedClassName, String sourceFile, boolean hasRetraceResult) {
     if (!hasRetraceResult || KEEP_SOURCEFILE_NAMES.contains(sourceFile)) {
       return sourceFile;
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceSourceFileResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetracedSourceFileImpl.java
similarity index 67%
rename from src/main/java/com/android/tools/r8/retrace/internal/RetraceSourceFileResultImpl.java
rename to src/main/java/com/android/tools/r8/retrace/internal/RetracedSourceFileImpl.java
index d3753e3..ef19bb3 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceSourceFileResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetracedSourceFileImpl.java
@@ -4,13 +4,13 @@
 
 package com.android.tools.r8.retrace.internal;
 
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 
-public class RetraceSourceFileResultImpl implements RetraceSourceFileResult {
+public class RetracedSourceFileImpl implements RetracedSourceFile {
 
   private final String filename;
 
-  RetraceSourceFileResultImpl(String filename) {
+  RetracedSourceFileImpl(String filename) {
     this.filename = filename;
   }
 
@@ -20,7 +20,7 @@
   }
 
   @Override
-  public String getFilename() {
+  public String getSourceFile() {
     return filename;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetracerImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetracerImpl.java
index d1c5874..0758be3 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetracerImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetracerImpl.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.retrace.InvalidMappingFileException;
 import com.android.tools.r8.retrace.ProguardMapProducer;
+import com.android.tools.r8.retrace.RetraceFrameResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
 import com.android.tools.r8.retrace.Retracer;
 import java.io.BufferedReader;
 
@@ -53,7 +55,13 @@
   }
 
   @Override
-  public RetraceFrameResultImpl retraceFrame(MethodReference methodReference, int position) {
+  public RetraceFrameResult retraceFrame(MethodReference methodReference, int position) {
+    return retraceFrame(methodReference, position, RetraceStackTraceContext.getInitialContext());
+  }
+
+  @Override
+  public RetraceFrameResult retraceFrame(
+      MethodReference methodReference, int position, RetraceStackTraceContext context) {
     return retraceClass(methodReference.getHolderClass())
         .lookupMethod(methodReference.getMethodName())
         .narrowByPosition(position);
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
index 06143aa..9fe783b 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
@@ -9,23 +9,24 @@
 import com.android.tools.r8.retrace.RetraceClassResult;
 import com.android.tools.r8.retrace.RetraceFieldResult;
 import com.android.tools.r8.retrace.RetraceFrameResult;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
-import com.android.tools.r8.retrace.RetraceStackTraceProxy;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
+import com.android.tools.r8.retrace.RetraceStackTraceElementProxy;
 import com.android.tools.r8.retrace.RetraceTypeResult;
+import com.android.tools.r8.retrace.RetraceTypeResult.Element;
 import com.android.tools.r8.retrace.RetracedClassReference;
 import com.android.tools.r8.retrace.RetracedFieldReference;
 import com.android.tools.r8.retrace.RetracedMethodReference;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.RetracedTypeReference;
 import com.android.tools.r8.retrace.Retracer;
 import com.android.tools.r8.retrace.StackTraceElementProxy;
 import com.android.tools.r8.retrace.StackTraceElementProxyRetracer;
-import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.ListUtils;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Consumer;
-import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -39,178 +40,181 @@
   }
 
   @Override
-  public Stream<RetraceStackTraceProxy<T, ST>> retrace(ST element) {
-    if (!element.hasClassName()) {
-      RetraceStackTraceProxyImpl.Builder<T, ST> builder =
-          RetraceStackTraceProxyImpl.builder(element);
-      return Stream.of(builder.build());
+  public Stream<? extends RetraceStackTraceElementProxy<T, ST>> retrace(
+      ST element, RetraceStackTraceContext context) {
+    Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults =
+        Stream.of(RetraceStackTraceElementProxyImpl.create(element, context));
+    if (!element.hasClassName()
+        && !element.hasFieldOrReturnType()
+        && !element.hasMethodArguments()) {
+      return currentResults;
     }
-    RetraceClassResult classResult = retracer.retraceClass(element.getClassReference());
-    if (element.hasMethodName()) {
-      return retraceMethod(element, classResult);
-    } else if (element.hasFieldName()) {
-      return retraceField(element, classResult);
-    } else {
-      return retraceClassOrType(element, classResult);
+    currentResults = retraceFieldOrReturnType(currentResults, element);
+    currentResults = retracedMethodArguments(currentResults, element);
+    if (element.hasClassName()) {
+      RetraceClassResult classResult = retracer.retraceClass(element.getClassReference());
+      if (element.hasMethodName()) {
+        currentResults = retraceMethod(currentResults, element, classResult);
+      } else if (element.hasFieldName()) {
+        currentResults = retraceField(currentResults, element, classResult);
+      } else {
+        currentResults = retraceClassOrType(currentResults, element, classResult);
+      }
     }
+    return currentResults;
   }
 
-  private Stream<RetraceStackTraceProxy<T, ST>> retraceClassOrType(
-      ST element, RetraceClassResult classResult) {
-    return classResult.stream()
-        .flatMap(
-            classElement ->
-                retraceFieldOrReturnType(element)
-                    .flatMap(
-                        fieldOrReturnTypeConsumer ->
-                            retracedMethodArguments(element)
-                                .map(
-                                    argumentsConsumer -> {
-                                      RetraceStackTraceProxyImpl.Builder<T, ST> proxy =
-                                          RetraceStackTraceProxyImpl.builder(element)
-                                              .setRetracedClass(classElement.getRetracedClass())
-                                              .setAmbiguous(classResult.isAmbiguous())
-                                              .setTopFrame(true);
-                                      argumentsConsumer.accept(proxy);
-                                      fieldOrReturnTypeConsumer.accept(proxy);
-                                      if (element.hasFileName()) {
-                                        proxy.setSourceFile(
-                                            getSourceFile(
-                                                classElement::getSourceFile,
-                                                classElement.getRetracedClass(),
-                                                element.getFileName(),
-                                                classResult.hasRetraceResult()));
-                                      }
-                                      return proxy.build();
-                                    })));
-  }
-
-  private Stream<RetraceStackTraceProxy<T, ST>> retraceMethod(
-      ST element, RetraceClassResult classResult) {
-    return retraceFieldOrReturnType(element)
-        .flatMap(
-            fieldOrReturnTypeConsumer ->
-                retracedMethodArguments(element)
-                    .flatMap(
-                        argumentsConsumer -> {
-                          RetraceFrameResult frameResult =
-                              element.hasLineNumber()
-                                  ? classResult.lookupFrame(
-                                      element.getMethodName(), element.getLineNumber())
-                                  : classResult.lookupFrame(element.getMethodName());
-                          return frameResult.stream()
-                              .flatMap(
-                                  frameElement -> {
-                                    List<RetraceStackTraceProxy<T, ST>> retracedProxies =
-                                        new ArrayList<>();
-                                    frameElement.visitNonCompilerSynthesizedFrames(
-                                        (frame, index) -> {
-                                          boolean isTopFrame = retracedProxies.isEmpty();
-                                          RetraceStackTraceProxyImpl.Builder<T, ST> proxy =
-                                              RetraceStackTraceProxyImpl.builder(element)
-                                                  .setRetracedClass(frame.getHolderClass())
-                                                  .setRetracedMethod(frame)
-                                                  .setAmbiguous(
-                                                      frameResult.isAmbiguous() && isTopFrame)
-                                                  .setTopFrame(isTopFrame);
-                                          if (element.hasLineNumber()) {
-                                            proxy.setLineNumber(
-                                                frame.getOriginalPositionOrDefault(
-                                                    element.getLineNumber()));
-                                          }
-                                          if (element.hasFileName()) {
-                                            proxy.setSourceFile(
-                                                getSourceFile(
-                                                    () -> frameElement.getSourceFile(frame),
-                                                    frame.getHolderClass(),
-                                                    element.getFileName(),
-                                                    classResult.hasRetraceResult()));
-                                          }
-                                          fieldOrReturnTypeConsumer.accept(proxy);
-                                          argumentsConsumer.accept(proxy);
-                                          retracedProxies.add(proxy.build());
-                                        });
-                                    return retracedProxies.stream();
-                                  });
-                        }));
-  }
-
-  private Stream<RetraceStackTraceProxy<T, ST>> retraceField(
-      ST element, RetraceClassResult classResult) {
-    return retraceFieldOrReturnType(element)
-        .flatMap(
-            fieldOrReturnTypeConsumer ->
-                retracedMethodArguments(element)
-                    .flatMap(
-                        argumentsConsumer -> {
-                          RetraceFieldResult retraceFieldResult =
-                              classResult.lookupField(element.getFieldName());
-                          return retraceFieldResult.stream()
-                              .map(
-                                  fieldElement -> {
-                                    RetraceStackTraceProxyImpl.Builder<T, ST> proxy =
-                                        RetraceStackTraceProxyImpl.builder(element)
-                                            .setRetracedClass(
-                                                fieldElement.getField().getHolderClass())
-                                            .setRetracedField(fieldElement.getField())
-                                            .setAmbiguous(retraceFieldResult.isAmbiguous())
-                                            .setTopFrame(true);
-                                    if (element.hasFileName()) {
-                                      proxy.setSourceFile(
-                                          getSourceFile(
-                                              // May not be fieldElement::getSourceFile since this
-                                              // throws off the jdk11 compiler,
-                                              () -> fieldElement.getSourceFile(),
-                                              fieldElement.getField().getHolderClass(),
-                                              element.getFileName(),
+  private Stream<RetraceStackTraceElementProxyImpl<T, ST>> retraceClassOrType(
+      Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults,
+      ST element,
+      RetraceClassResult classResult) {
+    return currentResults.flatMap(
+        proxy ->
+            classResult.stream()
+                .map(
+                    classElement ->
+                        proxy
+                            .builder()
+                            .setRetracedClass(classElement.getRetracedClass())
+                            .joinAmbiguous(classResult.isAmbiguous())
+                            .setTopFrame(true)
+                            .setContext(classElement.getContext())
+                            .applyIf(
+                                element.hasSourceFile(),
+                                builder -> {
+                                  RetracedSourceFile sourceFile = classElement.getSourceFile();
+                                  builder.setSourceFile(
+                                      sourceFile.hasRetraceResult()
+                                          ? sourceFile.getSourceFile()
+                                          : RetraceUtils.inferSourceFile(
+                                              classElement.getRetracedClass().getTypeName(),
+                                              element.getSourceFile(),
                                               classResult.hasRetraceResult()));
-                                    }
-                                    fieldOrReturnTypeConsumer.accept(proxy);
-                                    argumentsConsumer.accept(proxy);
-                                    return proxy.build();
-                                  });
-                        }));
+                                })
+                            .build()));
   }
 
-  private String getSourceFile(
-      Supplier<RetraceSourceFileResult> sourceFile,
-      RetracedClassReference classReference,
-      String fileName,
-      boolean hasRetraceResult) {
-    RetraceSourceFileResult sourceFileResult = sourceFile.get();
-    return sourceFileResult.hasRetraceResult()
-        ? sourceFileResult.getFilename()
-        : RetraceUtils.inferFileName(classReference.getTypeName(), fileName, hasRetraceResult);
+  private Stream<RetraceStackTraceElementProxyImpl<T, ST>> retraceMethod(
+      Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults,
+      ST element,
+      RetraceClassResult classResult) {
+    return currentResults.flatMap(
+        proxy -> {
+          RetraceFrameResult frameResult =
+              classResult.lookupFrame(
+                  proxy.context,
+                  element.hasLineNumber() ? Optional.of(element.getLineNumber()) : Optional.empty(),
+                  element.getMethodName());
+          return frameResult.stream()
+              .flatMap(
+                  frameElement -> {
+                    List<RetraceStackTraceElementProxyImpl<T, ST>> retracedProxies =
+                        new ArrayList<>();
+                    frameElement.visitNonCompilerSynthesizedFrames(
+                        (frame, index) -> {
+                          boolean isTopFrame = index == 0;
+                          retracedProxies.add(
+                              proxy
+                                  .builder()
+                                  .setRetracedClass(frame.getHolderClass())
+                                  .setRetracedMethod(frame)
+                                  .joinAmbiguous(frameResult.isAmbiguous() && isTopFrame)
+                                  .setTopFrame(isTopFrame)
+                                  .setContext(frameElement.getContext())
+                                  .applyIf(
+                                      element.hasLineNumber(),
+                                      builder -> {
+                                        builder.setLineNumber(
+                                            frame.getOriginalPositionOrDefault(
+                                                element.getLineNumber()));
+                                      })
+                                  .applyIf(
+                                      element.hasSourceFile(),
+                                      builder -> {
+                                        RetracedSourceFile sourceFileResult =
+                                            frameElement.getSourceFile(frame);
+                                        builder.setSourceFile(
+                                            sourceFileResult.hasRetraceResult()
+                                                ? sourceFileResult.getSourceFile()
+                                                : RetraceUtils.inferSourceFile(
+                                                    frame.getHolderClass().getTypeName(),
+                                                    element.getSourceFile(),
+                                                    classResult.hasRetraceResult()));
+                                      })
+                                  .build());
+                        });
+                    return retracedProxies.stream();
+                  });
+        });
   }
 
-  private Stream<Consumer<RetraceStackTraceProxyImpl.Builder<T, ST>>> retraceFieldOrReturnType(
-      ST element) {
+  private Stream<RetraceStackTraceElementProxyImpl<T, ST>> retraceField(
+      Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults,
+      ST element,
+      RetraceClassResult classResult) {
+    return currentResults.flatMap(
+        proxy -> {
+          RetraceFieldResult retraceFieldResult = classResult.lookupField(element.getFieldName());
+          return retraceFieldResult.stream()
+              .map(
+                  fieldElement ->
+                      proxy
+                          .builder()
+                          .setRetracedClass(fieldElement.getField().getHolderClass())
+                          .setRetracedField(fieldElement.getField())
+                          .joinAmbiguous(retraceFieldResult.isAmbiguous())
+                          .setTopFrame(true)
+                          .setContext(fieldElement.getContext())
+                          .applyIf(
+                              element.hasSourceFile(),
+                              builder -> {
+                                RetracedSourceFile sourceFile = fieldElement.getSourceFile();
+                                builder.setSourceFile(
+                                    sourceFile.hasRetraceResult()
+                                        ? sourceFile.getSourceFile()
+                                        : RetraceUtils.inferSourceFile(
+                                            fieldElement.getField().getHolderClass().getTypeName(),
+                                            element.getSourceFile(),
+                                            classResult.hasRetraceResult()));
+                              })
+                          .build());
+        });
+  }
+
+  private Stream<RetraceStackTraceElementProxyImpl<T, ST>> retraceFieldOrReturnType(
+      Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults, ST element) {
     if (!element.hasFieldOrReturnType()) {
-      return Stream.of(noEffect -> {});
+      return currentResults;
     }
     String elementOrReturnType = element.getFieldOrReturnType();
     if (elementOrReturnType.equals("void")) {
-      return Stream.of(
-          proxy -> proxy.setRetracedFieldOrReturnType(RetracedTypeReferenceImpl.createVoid()));
+      return currentResults.map(
+          proxy ->
+              proxy
+                  .builder()
+                  .setRetracedFieldOrReturnType(RetracedTypeReferenceImpl.createVoid())
+                  .build());
     } else {
       TypeReference typeReference = Reference.typeFromTypeName(elementOrReturnType);
       RetraceTypeResult retraceTypeResult = retracer.retraceType(typeReference);
-      return retraceTypeResult.stream()
-          .map(
-              type ->
-                  (proxy -> {
-                    proxy.setRetracedFieldOrReturnType(type.getType());
-                    if (retraceTypeResult.isAmbiguous()) {
-                      proxy.setAmbiguous(true);
-                    }
-                  }));
+      List<Element> retracedElements = retraceTypeResult.stream().collect(Collectors.toList());
+      return currentResults.flatMap(
+          proxy ->
+              retracedElements.stream()
+                  .map(
+                      retracedResult ->
+                          proxy
+                              .builder()
+                              .setRetracedFieldOrReturnType(retracedResult.getType())
+                              .joinAmbiguous(retraceTypeResult.isAmbiguous())
+                              .build()));
     }
   }
 
-  private Stream<Consumer<RetraceStackTraceProxyImpl.Builder<T, ST>>> retracedMethodArguments(
-      ST element) {
+  private Stream<RetraceStackTraceElementProxyImpl<T, ST>> retracedMethodArguments(
+      Stream<RetraceStackTraceElementProxyImpl<T, ST>> currentResults, ST element) {
     if (!element.hasMethodArguments()) {
-      return Stream.of(noEffect -> {});
+      return currentResults;
     }
     List<RetraceTypeResult> retracedResults =
         Arrays.stream(element.getMethodArguments().split(","))
@@ -218,40 +222,37 @@
             .collect(Collectors.toList());
     List<List<RetracedTypeReference>> initial = new ArrayList<>();
     initial.add(new ArrayList<>());
-    Box<Boolean> isAmbiguous = new Box<>(false);
-    List<List<RetracedTypeReference>> retracedArguments =
+    List<List<RetracedTypeReference>> allRetracedArguments =
         ListUtils.fold(
             retracedResults,
             initial,
             (acc, retracedTypeResult) -> {
-              if (retracedTypeResult.isAmbiguous()) {
-                isAmbiguous.set(true);
-              }
               List<List<RetracedTypeReference>> newResult = new ArrayList<>();
               retracedTypeResult.forEach(
-                  retracedElement -> {
-                    acc.forEach(
-                        oldResult -> {
-                          List<RetracedTypeReference> newList = new ArrayList<>(oldResult);
-                          newList.add(retracedElement.getType());
-                          newResult.add(newList);
-                        });
-                  });
+                  retracedElement ->
+                      acc.forEach(
+                          oldResult -> {
+                            List<RetracedTypeReference> newList = new ArrayList<>(oldResult);
+                            newList.add(retracedElement.getType());
+                            newResult.add(newList);
+                          }));
               return newResult;
             });
-    return retracedArguments.stream()
-        .map(
-            arguments ->
-                proxy -> {
-                  proxy.setRetracedMethodArguments(arguments);
-                  if (isAmbiguous.get()) {
-                    proxy.setAmbiguous(true);
-                  }
-                });
+    boolean isAmbiguous = allRetracedArguments.size() > 1;
+    return currentResults.flatMap(
+        proxy ->
+            allRetracedArguments.stream()
+                .map(
+                    retracedArguments ->
+                        proxy
+                            .builder()
+                            .setRetracedMethodArguments(retracedArguments)
+                            .joinAmbiguous(isAmbiguous)
+                            .build()));
   }
 
-  public static class RetraceStackTraceProxyImpl<T, ST extends StackTraceElementProxy<T, ST>>
-      implements RetraceStackTraceProxy<T, ST> {
+  public static class RetraceStackTraceElementProxyImpl<T, ST extends StackTraceElementProxy<T, ST>>
+      implements RetraceStackTraceElementProxy<T, ST> {
 
     private final ST originalItem;
     private final RetracedClassReference retracedClass;
@@ -263,8 +264,9 @@
     private final int lineNumber;
     private final boolean isAmbiguous;
     private final boolean isTopFrame;
+    private final RetraceStackTraceContext context;
 
-    private RetraceStackTraceProxyImpl(
+    private RetraceStackTraceElementProxyImpl(
         ST originalItem,
         RetracedClassReference retracedClass,
         RetracedMethodReference retracedMethod,
@@ -274,7 +276,8 @@
         String sourceFile,
         int lineNumber,
         boolean isAmbiguous,
-        boolean isTopFrame) {
+        boolean isTopFrame,
+        RetraceStackTraceContext context) {
       assert originalItem != null;
       this.originalItem = originalItem;
       this.retracedClass = retracedClass;
@@ -286,6 +289,7 @@
       this.lineNumber = lineNumber;
       this.isAmbiguous = isAmbiguous;
       this.isTopFrame = isTopFrame;
+      this.context = context;
     }
 
     @Override
@@ -368,9 +372,26 @@
       return sourceFile;
     }
 
-    private static <T, ST extends StackTraceElementProxy<T, ST>> Builder<T, ST> builder(
-        ST originalElement) {
-      return new Builder<>(originalElement);
+    private static <T, ST extends StackTraceElementProxy<T, ST>>
+        RetraceStackTraceElementProxyImpl<T, ST> create(
+            ST originalItem, RetraceStackTraceContext context) {
+      return new RetraceStackTraceElementProxyImpl<T, ST>(
+          originalItem, null, null, null, null, null, null, -1, false, false, context);
+    }
+
+    private Builder<T, ST> builder() {
+      Builder<T, ST> builder = new Builder<>(originalItem);
+      builder.classContext = retracedClass;
+      builder.methodContext = retracedMethod;
+      builder.retracedField = retracedField;
+      builder.fieldOrReturnType = fieldOrReturnType;
+      builder.methodArguments = methodArguments;
+      builder.sourceFile = sourceFile;
+      builder.lineNumber = lineNumber;
+      builder.isAmbiguous = isAmbiguous;
+      builder.isTopFrame = isTopFrame;
+      builder.context = context;
+      return builder;
     }
 
     @Override
@@ -379,7 +400,12 @@
     }
 
     @Override
-    public int compareTo(RetraceStackTraceProxy<T, ST> other) {
+    public RetraceStackTraceContext getContext() {
+      return context;
+    }
+
+    @Override
+    public int compareTo(RetraceStackTraceElementProxy<T, ST> other) {
       int classCompare = Boolean.compare(hasRetracedClass(), other.hasRetracedClass());
       if (classCompare != 0) {
         return classCompare;
@@ -433,6 +459,7 @@
       private int lineNumber = -1;
       private boolean isAmbiguous;
       private boolean isTopFrame;
+      private RetraceStackTraceContext context;
 
       private Builder(ST originalElement) {
         this.originalElement = originalElement;
@@ -473,8 +500,8 @@
         return this;
       }
 
-      private Builder<T, ST> setAmbiguous(boolean ambiguous) {
-        this.isAmbiguous = ambiguous;
+      private Builder<T, ST> joinAmbiguous(boolean ambiguous) {
+        this.isAmbiguous = ambiguous || this.isAmbiguous;
         return this;
       }
 
@@ -483,12 +510,24 @@
         return this;
       }
 
-      private RetraceStackTraceProxy<T, ST> build() {
+      private Builder<T, ST> setContext(RetraceStackTraceContext context) {
+        this.context = context;
+        return this;
+      }
+
+      private Builder<T, ST> applyIf(boolean condition, Consumer<Builder<T, ST>> consumer) {
+        if (condition) {
+          consumer.accept(this);
+        }
+        return this;
+      }
+
+      private RetraceStackTraceElementProxyImpl<T, ST> build() {
         RetracedClassReference retracedClass = classContext;
         if (methodContext != null) {
           retracedClass = methodContext.getHolderClass();
         }
-        return new RetraceStackTraceProxyImpl<>(
+        return new RetraceStackTraceElementProxyImpl<>(
             originalElement,
             retracedClass,
             methodContext,
@@ -498,7 +537,8 @@
             sourceFile,
             lineNumber,
             isAmbiguous,
-            isTopFrame);
+            isTopFrame,
+            context);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
index 5f512f4..7afc10b 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
@@ -10,7 +10,7 @@
 
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.retrace.RetraceStackTraceProxy;
+import com.android.tools.r8.retrace.RetraceStackTraceElementProxy;
 import com.android.tools.r8.retrace.RetracedClassReference;
 import com.android.tools.r8.retrace.RetracedFieldReference;
 import com.android.tools.r8.retrace.RetracedTypeReference;
@@ -69,7 +69,7 @@
   }
 
   @Override
-  public boolean hasFileName() {
+  public boolean hasSourceFile() {
     return sourceFile.hasIndex();
   }
 
@@ -104,8 +104,8 @@
   }
 
   @Override
-  public String getFileName() {
-    return hasFileName() ? getEntryInLine(sourceFile) : null;
+  public String getSourceFile() {
+    return hasSourceFile() ? getEntryInLine(sourceFile) : null;
   }
 
   @Override
@@ -141,7 +141,8 @@
 
   @Override
   public String toRetracedItem(
-      RetraceStackTraceProxy<String, StackTraceElementStringProxy> retracedProxy, boolean verbose) {
+      RetraceStackTraceElementProxy<String, StackTraceElementStringProxy> retracedProxy,
+      boolean verbose) {
     StringBuilder sb = new StringBuilder();
     int lastSeenIndex = 0;
     for (StringIndex index : orderedIndices) {
@@ -225,7 +226,7 @@
               startIndex,
               endIndex,
               (retraced, original, verbose) ->
-                  retraced.hasSourceFile() ? retraced.getSourceFile() : original.getFileName());
+                  retraced.hasSourceFile() ? retraced.getSourceFile() : original.getSourceFile());
       orderedIndices.add(sourceFile);
       return this;
     }
@@ -333,14 +334,17 @@
     protected final int startIndex;
     protected final int endIndex;
     private final TriFunction<
-            RetraceStackTraceProxy<String, ?>, StackTraceElementStringProxy, Boolean, String>
+            RetraceStackTraceElementProxy<String, ?>, StackTraceElementStringProxy, Boolean, String>
         retracedString;
 
     private StringIndex(
         int startIndex,
         int endIndex,
         TriFunction<
-                RetraceStackTraceProxy<String, ?>, StackTraceElementStringProxy, Boolean, String>
+                RetraceStackTraceElementProxy<String, ?>,
+                StackTraceElementStringProxy,
+                Boolean,
+                String>
             retracedString) {
       this.startIndex = startIndex;
       this.endIndex = endIndex;
@@ -363,7 +367,10 @@
         int startIndex,
         int endIndex,
         TriFunction<
-                RetraceStackTraceProxy<String, ?>, StackTraceElementStringProxy, Boolean, String>
+                RetraceStackTraceElementProxy<String, ?>,
+                StackTraceElementStringProxy,
+                Boolean,
+                String>
             retracedString,
         ClassNameType classNameType) {
       super(startIndex, endIndex, retracedString);
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 242a08c..5ef1b35 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -659,6 +659,10 @@
     return keepConstantArguments.contains(method);
   }
 
+  public boolean isKeepUnusedArgumentsMethod(ProgramMethod method) {
+    return isKeepUnusedArgumentsMethod(method.getReference());
+  }
+
   public boolean isKeepUnusedArgumentsMethod(DexMethod method) {
     return keepUnusedArguments.contains(method);
   }
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 c4e0518..52d59bb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -10,6 +10,7 @@
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentifier;
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod;
 import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 import static java.util.Collections.emptySet;
 
 import com.android.tools.r8.Diagnostic;
@@ -63,6 +64,7 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.InvalidCode;
 import com.android.tools.r8.graph.LookupLambdaTarget;
+import com.android.tools.r8.graph.LookupResult;
 import com.android.tools.r8.graph.LookupTarget;
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.MethodResolutionResult;
@@ -378,7 +380,7 @@
   private final ProgramMethodSet pendingReflectiveUses = ProgramMethodSet.createLinked();
 
   /** Mapping of types to the resolved methods for that type along with the context. */
-  private final Map<DexProgramClass, Map<ResolutionSearchKey, Set<DexProgramClass>>>
+  private final Map<DexProgramClass, Map<ResolutionSearchKey, ProgramMethodSet>>
       reachableVirtualTargets = new IdentityHashMap<>();
 
   /** Collection of keep requirements for the program. */
@@ -2460,7 +2462,7 @@
     }
   }
 
-  private Map<ResolutionSearchKey, Set<DexProgramClass>> getReachableVirtualTargets(
+  private Map<ResolutionSearchKey, ProgramMethodSet> getReachableVirtualTargets(
       DexProgramClass clazz) {
     return reachableVirtualTargets.getOrDefault(clazz, Collections.emptyMap());
   }
@@ -2480,26 +2482,46 @@
                 assert false : "Should not be null";
                 return;
               }
-              contexts.forEach(
-                  context ->
-                      singleResolution
-                          .lookupVirtualDispatchTargets(
-                              context,
-                              appInfo,
-                              (type, subTypeConsumer, lambdaConsumer) -> {
-                                assert appInfo.isSubtype(currentClass.type, type);
-                                instantiation.apply(subTypeConsumer, lambdaConsumer);
-                              },
-                              definition ->
-                                  keepInfo.isPinned(definition.getReference(), appInfo, options))
-                          .forEach(
-                              target ->
-                                  markVirtualDispatchTargetAsLive(
-                                      target,
-                                      programMethod ->
-                                          graphReporter.reportReachableMethodAsLive(
-                                              singleResolution.getResolvedMethod().getReference(),
-                                              programMethod))));
+              Map<DexProgramClass, List<ProgramMethod>> contextsByClass = new IdentityHashMap<>();
+              for (ProgramMethod context : contexts) {
+                contextsByClass
+                    .computeIfAbsent(context.getHolder(), ignoreKey(ArrayList::new))
+                    .add(context);
+              }
+              contextsByClass.forEach(
+                  (contextHolder, contextsInHolder) -> {
+                    LookupResult lookupResult =
+                        singleResolution.lookupVirtualDispatchTargets(
+                            contextHolder,
+                            appInfo,
+                            (type, subTypeConsumer, lambdaConsumer) -> {
+                              assert appInfo.isSubtype(currentClass.type, type);
+                              instantiation.apply(subTypeConsumer, lambdaConsumer);
+                            },
+                            definition ->
+                                keepInfo.isPinned(definition.getReference(), appInfo, options));
+                    lookupResult.forEach(
+                        target ->
+                            markVirtualDispatchTargetAsLive(
+                                target,
+                                programMethod ->
+                                    graphReporter.reportReachableMethodAsLive(
+                                        singleResolution.getResolvedMethod().getReference(),
+                                        programMethod)));
+                    lookupResult.forEachFailureDependency(
+                        method -> {
+                          DexProgramClass clazz =
+                              getProgramClassOrNull(method.getHolderType(), contextHolder);
+                          if (clazz != null) {
+                            failedMethodResolutionTargets.add(method.getReference());
+                            for (ProgramMethod context : contextsInHolder) {
+                              markMethodAsTargeted(
+                                  new ProgramMethod(clazz, method),
+                                  KeepReason.invokedFrom(context));
+                            }
+                          }
+                        });
+                  });
             });
   }
 
@@ -2886,7 +2908,7 @@
   }
 
   private void markVirtualMethodAsReachable(
-      DexMethod method, boolean interfaceInvoke, ProgramDefinition context, KeepReason reason) {
+      DexMethod method, boolean interfaceInvoke, ProgramMethod context, KeepReason reason) {
     if (method.holder.isArrayType()) {
       // This is an array type, so the actual class will be generated at runtime. We treat this
       // like an invoke on a direct subtype of java.lang.Object that has no further subtypes.
@@ -2924,9 +2946,9 @@
     // If the method has already been marked, just report the new reason for the resolved target and
     // save the context to ensure correct lookup of virtual dispatch targets.
     ResolutionSearchKey resolutionSearchKey = new ResolutionSearchKey(method, interfaceInvoke);
-    Set<DexProgramClass> seenContexts = getReachableVirtualTargets(holder).get(resolutionSearchKey);
+    ProgramMethodSet seenContexts = getReachableVirtualTargets(holder).get(resolutionSearchKey);
     if (seenContexts != null) {
-      seenContexts.add(contextHolder);
+      seenContexts.add(context);
       graphReporter.registerMethod(resolution.getResolvedMethod(), reason);
       return;
     }
@@ -2948,8 +2970,8 @@
     // The method resolved and is accessible, so currently live overrides become live.
     reachableVirtualTargets
         .computeIfAbsent(holder, ignoreArgument(HashMap::new))
-        .computeIfAbsent(resolutionSearchKey, ignoreArgument(Sets::newIdentityHashSet))
-        .add(contextHolder);
+        .computeIfAbsent(resolutionSearchKey, ignoreArgument(ProgramMethodSet::create))
+        .add(context);
 
     resolution
         .lookupVirtualDispatchTargets(
@@ -4603,7 +4625,7 @@
             virtualMethod -> {
               keepInfo.joinMethod(
                   virtualMethod, joiner -> joiner.disallowOptimization().disallowShrinking());
-              markVirtualMethodAsReachable(virtualMethod.getReference(), true, clazz, reason);
+              markVirtualMethodAsReachable(virtualMethod.getReference(), true, method, reason);
             });
       }
     }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
index d81a03f..56a9d20 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -17,6 +17,8 @@
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.AndroidApiLevel;
 
@@ -39,6 +41,7 @@
   private ParameterAnnotationsList parameterAnnotationsList = ParameterAnnotationsList.empty();
   private AndroidApiLevel apiLevelForDefinition = NOT_SET;
   private AndroidApiLevel apiLevelForCode = NOT_SET;
+  private MethodOptimizationInfo optimizationInfo = DefaultMethodOptimizationInfo.getInstance();
 
   private boolean checkAndroidApiLevels = true;
 
@@ -65,6 +68,11 @@
     return this;
   }
 
+  public SyntheticMethodBuilder setOptimizationInfo(MethodOptimizationInfo optimizationInfo) {
+    this.optimizationInfo = optimizationInfo;
+    return this;
+  }
+
   public SyntheticMethodBuilder setProto(DexProto proto) {
     this.proto = proto;
     return this;
@@ -132,6 +140,7 @@
             .setClassFileVersion(classFileVersion)
             .setApiLevelForDefinition(apiLevelForDefinition)
             .setApiLevelForCode(apiLevelForCode)
+            .setOptimizationInfo(optimizationInfo)
             .applyIf(!checkAndroidApiLevels, DexEncodedMethod.Builder::disableAndroidApiLevelCheck)
             .build();
     assert isValidSyntheticMethod(method, syntheticKind);
diff --git a/src/main/java/com/android/tools/r8/utils/BitSetUtils.java b/src/main/java/com/android/tools/r8/utils/BitSetUtils.java
new file mode 100644
index 0000000..483eb67
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/BitSetUtils.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import java.util.BitSet;
+
+public class BitSetUtils {
+
+  @SuppressWarnings("unchecked")
+  public static BitSet or(BitSet bitSet, BitSet other) {
+    BitSet newBitSet = (BitSet) bitSet.clone();
+    newBitSet.or(other);
+    return newBitSet;
+  }
+
+  public static boolean verifyLessThanOrEqualTo(BitSet bitSet, BitSet other) {
+    assert other.equals(or(bitSet, other));
+    return true;
+  }
+}
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 860860b..3718fcf 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -124,7 +124,7 @@
     }
   }
 
-  public static final CfVersion SUPPORTED_CF_VERSION = CfVersion.V16_PREVIEW;
+  public static final CfVersion SUPPORTED_CF_VERSION = CfVersion.V17;
   public static final CfVersion EXPERIMENTAL_CF_VERSION = CfVersion.V12;
 
   public static final int SUPPORTED_DEX_VERSION =
@@ -1219,7 +1219,7 @@
 
     // TODO(b/69963623): enable if everything is ready, including signature rewriting at call sites.
     private boolean enableLegacyConstantPropagation = false;
-    private boolean enableExperimentalArgumentPropagation = false;
+    private boolean enableExperimentalArgumentPropagation = true;
     private boolean enableDynamicTypePropagation = true;
 
     public void disableOptimization() {
@@ -1235,6 +1235,10 @@
       return maxNumberOfDispatchTargetsBeforeAbandoning;
     }
 
+    public int getMaxNumberOfInParameters() {
+      return 10;
+    }
+
     public boolean isEnabled() {
       if (!isOptimizing() || !isShrinking()) {
         return false;
@@ -1255,7 +1259,7 @@
     }
 
     public CallSiteOptimizationOptions setEnableLegacyConstantPropagation() {
-      assert !isConstantPropagationEnabled();
+      assert !enableLegacyConstantPropagation;
       enableLegacyConstantPropagation = true;
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 8fd84fc..1ff7079 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -186,6 +186,14 @@
     return list;
   }
 
+  public static <T> ArrayList<T> newInitializedArrayList(int size, T element) {
+    ArrayList<T> list = new ArrayList<>(size);
+    for (int i = 0; i < size; i++) {
+      list.add(element);
+    }
+    return list;
+  }
+
   public static <T> ImmutableList<T> newImmutableList(ForEachable<T> forEachable) {
     ImmutableList.Builder<T> builder = ImmutableList.builder();
     forEachable.forEach(builder::add);
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index 04b399d..a7305a2 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -89,6 +89,17 @@
     return builder.build();
   }
 
+  @SafeVarargs
+  public static <T> ImmutableSet<T> newImmutableSetExcludingNullItems(T... items) {
+    ImmutableSet.Builder<T> builder = ImmutableSet.builder();
+    for (T item : items) {
+      if (item != null) {
+        builder.add(item);
+      }
+    }
+    return builder.build();
+  }
+
   public static <T, S> Set<T> mapIdentityHashSet(Set<S> set, Function<S, T> fn) {
     Set<T> out = newIdentityHashSet(set.size());
     for (S element : set) {
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index 67e4148..3177689 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -17,11 +17,17 @@
   private final Set<T> seen;
 
   public static <T> WorkList<T> newEqualityWorkList() {
-    return new WorkList<T>(EqualityTest.HASH);
+    return new WorkList<T>(EqualityTest.EQUALS);
+  }
+
+  public static <T> WorkList<T> newEqualityWorkList(T item) {
+    WorkList<T> workList = new WorkList<>(EqualityTest.EQUALS);
+    workList.addIfNotSeen(item);
+    return workList;
   }
 
   public static <T> WorkList<T> newEqualityWorkList(Iterable<T> items) {
-    WorkList<T> workList = new WorkList<>(EqualityTest.HASH);
+    WorkList<T> workList = new WorkList<>(EqualityTest.EQUALS);
     workList.addIfNotSeen(items);
     return workList;
   }
@@ -53,7 +59,7 @@
   }
 
   private WorkList(EqualityTest equalityTest) {
-    this(equalityTest == EqualityTest.HASH ? new HashSet<>() : Sets.newIdentityHashSet());
+    this(equalityTest == EqualityTest.EQUALS ? new HashSet<>() : Sets.newIdentityHashSet());
   }
 
   private WorkList(Set<T> seen) {
@@ -120,7 +126,7 @@
   }
 
   public enum EqualityTest {
-    HASH,
+    EQUALS,
     IDENTITY
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/LongLivedClassSetBuilder.java b/src/main/java/com/android/tools/r8/utils/collections/LongLivedClassSetBuilder.java
new file mode 100644
index 0000000..fe3361e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/LongLivedClassSetBuilder.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2021, 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.collections;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Set;
+import java.util.function.IntFunction;
+
+public class LongLivedClassSetBuilder<T extends DexClass>
+    extends LongLivedCollectionBuilder<Set<DexType>, Set<T>> {
+
+  private LongLivedClassSetBuilder(
+      GraphLens currentGraphLens,
+      IntFunction<Set<T>> factory,
+      IntFunction<Set<DexType>> factoryForBuilder) {
+    super(currentGraphLens, factory, factoryForBuilder);
+  }
+
+  public static <T extends DexClass>
+      LongLivedClassSetBuilder<T> createConcurrentBuilderForIdentitySet(
+          GraphLens currentGraphLens) {
+    return new LongLivedClassSetBuilder<>(
+        currentGraphLens, SetUtils::newIdentityHashSet, SetUtils::newConcurrentHashSet);
+  }
+
+  public void add(T clazz, GraphLens currentGraphLens) {
+    // All classes in a long lived class set should be rewritten up until the same graph lens.
+    assert verifyIsRewrittenWithLens(currentGraphLens);
+    backing.add(clazz.getType());
+  }
+
+  public LongLivedClassSetBuilder<T> rewrittenWithLens(AppView<AppInfoWithLiveness> appView) {
+    return rewrittenWithLens(appView.graphLens());
+  }
+
+  public LongLivedClassSetBuilder<T> rewrittenWithLens(GraphLens newGraphLens) {
+    // Check if the graph lens has changed (otherwise lens rewriting is not needed).
+    if (newGraphLens == appliedGraphLens) {
+      return this;
+    }
+
+    // Rewrite the backing.
+    Set<DexType> rewrittenBacking = factoryForBuilder.apply(backing.size());
+    for (DexType type : backing) {
+      rewrittenBacking.add(newGraphLens.lookupType(type, appliedGraphLens));
+    }
+    backing = rewrittenBacking;
+
+    // Record that this collection is now rewritten up until the given graph lens.
+    appliedGraphLens = newGraphLens;
+    return this;
+  }
+
+  @SuppressWarnings("unchecked")
+  public Set<T> build(AppView<AppInfoWithLiveness> appView) {
+    Set<T> result = factory.apply(backing.size());
+    for (DexType type : backing) {
+      DexType rewrittenType = appView.graphLens().lookupType(type, appliedGraphLens);
+      T clazz = (T) appView.definitionFor(rewrittenType);
+      if (clazz != null) {
+        result.add(clazz);
+      } else {
+        assert false : "Unable to find definition for: " + rewrittenType.getTypeName();
+      }
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/LongLivedCollectionBuilder.java b/src/main/java/com/android/tools/r8/utils/collections/LongLivedCollectionBuilder.java
new file mode 100644
index 0000000..6c7fdc2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/LongLivedCollectionBuilder.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2021, 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.collections;
+
+import com.android.tools.r8.graph.GraphLens;
+import java.util.function.IntFunction;
+
+public abstract class LongLivedCollectionBuilder<BuilderCollection, ResultCollection> {
+
+  // Factory for creating the final result collection.
+  protected final IntFunction<ResultCollection> factory;
+
+  // Factory for creating a builder collection.
+  protected final IntFunction<BuilderCollection> factoryForBuilder;
+
+  // The graph lens that this collection has been rewritten up until.
+  protected GraphLens appliedGraphLens;
+
+  // The underlying backing.
+  protected BuilderCollection backing;
+
+  protected LongLivedCollectionBuilder(
+      GraphLens currentGraphLens,
+      IntFunction<ResultCollection> factory,
+      IntFunction<BuilderCollection> factoryForBuilder) {
+    this.appliedGraphLens = currentGraphLens;
+    this.factory = factory;
+    this.factoryForBuilder = factoryForBuilder;
+    this.backing = factoryForBuilder.apply(2);
+  }
+
+  public boolean isRewrittenWithLens(GraphLens graphLens) {
+    return appliedGraphLens == graphLens;
+  }
+
+  public boolean verifyIsRewrittenWithLens(GraphLens graphLens) {
+    assert isRewrittenWithLens(graphLens);
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodMapBuilder.java b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodMapBuilder.java
new file mode 100644
index 0000000..1591350
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodMapBuilder.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2021, 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.collections;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+
+public class LongLivedProgramMethodMapBuilder<V>
+    extends LongLivedCollectionBuilder<Map<DexMethod, V>, ProgramMethodMap<?>> {
+
+  private LongLivedProgramMethodMapBuilder(
+      GraphLens currentGraphLens,
+      IntFunction<ProgramMethodMap<?>> factory,
+      IntFunction<Map<DexMethod, V>> factoryForBuilder) {
+    super(currentGraphLens, factory, factoryForBuilder);
+  }
+
+  public static <V> LongLivedProgramMethodMapBuilder<V> create(GraphLens currentGraphLens) {
+    return new LongLivedProgramMethodMapBuilder<>(
+        currentGraphLens, ProgramMethodMap::create, IdentityHashMap::new);
+  }
+
+  public static <V> LongLivedProgramMethodMapBuilder<V> createConcurrentBuilderForNonConcurrentMap(
+      GraphLens currentGraphLens) {
+    return new LongLivedProgramMethodMapBuilder<>(
+        currentGraphLens, ProgramMethodMap::create, ConcurrentHashMap::new);
+  }
+
+  public V computeIfAbsent(
+      ProgramMethod key, Function<ProgramMethod, V> fn, GraphLens currentGraphLens) {
+    assert verifyIsRewrittenWithLens(currentGraphLens);
+    return backing.computeIfAbsent(key.getReference(), ignoreKey(() -> fn.apply(key)));
+  }
+
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  public void put(ProgramMethod key, V value, GraphLens currentGraphLens) {
+    // All methods in a long lived program method set should be rewritten up until the same graph
+    // lens.
+    assert verifyIsRewrittenWithLens(currentGraphLens);
+    backing.put(key.getReference(), value);
+  }
+
+  public LongLivedProgramMethodMapBuilder<V> rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, BiFunction<V, GraphLens, V> valueRewriter) {
+    return rewrittenWithLens(valueRewriter, appView.graphLens());
+  }
+
+  public LongLivedProgramMethodMapBuilder<V> rewrittenWithLens(
+      BiFunction<V, GraphLens, V> valueRewriter, GraphLens newGraphLens) {
+    // Check if the graph lens has changed (otherwise lens rewriting is not needed).
+    if (newGraphLens == appliedGraphLens) {
+      return this;
+    }
+
+    // Rewrite the backing.
+    Map<DexMethod, V> rewrittenBacking = factoryForBuilder.apply(backing.size());
+    backing.forEach(
+        (key, value) -> {
+          DexMethod rewrittenKey = newGraphLens.getRenamedMethodSignature(key, appliedGraphLens);
+          V rewrittenValue = valueRewriter.apply(value, appliedGraphLens);
+          assert !rewrittenBacking.containsKey(rewrittenKey);
+          rewrittenBacking.put(rewrittenKey, rewrittenValue);
+        });
+    backing = rewrittenBacking;
+
+    // Record that this collection is now rewritten up until the given graph lens.
+    appliedGraphLens = newGraphLens;
+    return this;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <U> ProgramMethodMap<U> build(
+      AppView<AppInfoWithLiveness> appView, Function<V, U> valueTransformer) {
+    assert verifyIsRewrittenWithLens(appView.graphLens());
+
+    ProgramMethodMap<U> result = (ProgramMethodMap<U>) factory.apply(backing.size());
+    backing.forEach(
+        (key, value) -> {
+          DexProgramClass holder = asProgramClassOrNull(appView.definitionFor(key.getHolderType()));
+          ProgramMethod method = key.lookupOnProgramClass(holder);
+          if (method != null) {
+            result.put(method, valueTransformer.apply(value));
+          } else {
+            assert false;
+          }
+        });
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMemberMap.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMemberMap.java
index 43daf74..ef58a2d 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMemberMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMemberMap.java
@@ -17,7 +17,11 @@
   private final Map<Wrapper<K>, V> backing;
 
   ProgramMemberMap(Supplier<Map<Wrapper<K>, V>> backingFactory) {
-    backing = backingFactory.get();
+    this.backing = backingFactory.get();
+  }
+
+  ProgramMemberMap(Map<Wrapper<K>, V> backing) {
+    this.backing = backing;
   }
 
   public void clear() {
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
index 8fdf054..d4dcca6 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
@@ -21,10 +21,18 @@
     super(backingFactory);
   }
 
+  private ProgramMethodMap(Map<Wrapper<ProgramMethod>, V> backing) {
+    super(backing);
+  }
+
   public static <V> ProgramMethodMap<V> create() {
     return new ProgramMethodMap<>(HashMap::new);
   }
 
+  public static <V> ProgramMethodMap<V> create(int capacity) {
+    return new ProgramMethodMap<>(new HashMap<>(capacity));
+  }
+
   public static <V> ProgramMethodMap<V> createConcurrent() {
     return new ProgramMethodMap<>(ConcurrentHashMap::new);
   }
diff --git a/src/test/examples/shaking14/KeepConstantArguments.java b/src/test/examples/shaking14/KeepConstantArguments.java
new file mode 100644
index 0000000..0ff3910
--- /dev/null
+++ b/src/test/examples/shaking14/KeepConstantArguments.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, 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 shaking14;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD})
+public @interface KeepConstantArguments {}
diff --git a/src/test/examples/shaking14/Subclass.java b/src/test/examples/shaking14/Subclass.java
index 31dd650..2cb520a 100644
--- a/src/test/examples/shaking14/Subclass.java
+++ b/src/test/examples/shaking14/Subclass.java
@@ -4,10 +4,13 @@
 package shaking14;
 
 public class Subclass extends Superclass {
+
+  @KeepConstantArguments
   static int aMethod(int value) {
     return value + 42;
   }
 
+  @KeepConstantArguments
   static double anotherMethod(double value) {
     return value + 42;
   }
diff --git a/src/test/examples/shaking14/keep-rules.txt b/src/test/examples/shaking14/keep-rules.txt
index 94682a3..594e244 100644
--- a/src/test/examples/shaking14/keep-rules.txt
+++ b/src/test/examples/shaking14/keep-rules.txt
@@ -10,3 +10,4 @@
 
 # allow access modification to enable minifcation
 -allowaccessmodification
+-keepconstantarguments class * { @shaking14.KeepConstantArguments *; }
diff --git a/src/test/examplesJava16/pattern_matching_for_instanceof/Main.java b/src/test/examplesJava17/pattern_matching_for_instanceof/Main.java
similarity index 100%
rename from src/test/examplesJava16/pattern_matching_for_instanceof/Main.java
rename to src/test/examplesJava17/pattern_matching_for_instanceof/Main.java
diff --git a/src/test/examplesJava16/records/EmptyRecord.java b/src/test/examplesJava17/records/EmptyRecord.java
similarity index 100%
rename from src/test/examplesJava16/records/EmptyRecord.java
rename to src/test/examplesJava17/records/EmptyRecord.java
diff --git a/src/test/examplesJava17/records/EmptyRecordAnnotation.java b/src/test/examplesJava17/records/EmptyRecordAnnotation.java
new file mode 100644
index 0000000..350e8ce
--- /dev/null
+++ b/src/test/examplesJava17/records/EmptyRecordAnnotation.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2021, 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 records;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class EmptyRecordAnnotation {
+
+  record Empty() {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface ClassAnnotation {
+    Class<? extends Record> theClass();
+  }
+
+  @ClassAnnotation(theClass = Record.class)
+  public static void annotatedMethod1() {}
+
+  @ClassAnnotation(theClass = Empty.class)
+  public static void annotatedMethod2() {}
+
+  public static void main(String[] args) throws Exception {
+    Class<?> annotatedMethod1Content =
+        EmptyRecordAnnotation.class
+            .getDeclaredMethod("annotatedMethod1")
+            .getAnnotation(ClassAnnotation.class)
+            .theClass();
+    System.out.println(annotatedMethod1Content);
+    Class<?> annotatedMethod2Content =
+        EmptyRecordAnnotation.class
+            .getDeclaredMethod("annotatedMethod2")
+            .getAnnotation(ClassAnnotation.class)
+            .theClass();
+    System.out.println(annotatedMethod2Content);
+  }
+}
diff --git a/src/test/examplesJava16/records/RecordInstanceOf.java b/src/test/examplesJava17/records/RecordInstanceOf.java
similarity index 100%
rename from src/test/examplesJava16/records/RecordInstanceOf.java
rename to src/test/examplesJava17/records/RecordInstanceOf.java
diff --git a/src/test/examplesJava16/records/RecordInvokeCustom.java b/src/test/examplesJava17/records/RecordInvokeCustom.java
similarity index 100%
rename from src/test/examplesJava16/records/RecordInvokeCustom.java
rename to src/test/examplesJava17/records/RecordInvokeCustom.java
diff --git a/src/test/examplesJava16/records/RecordReflection.java b/src/test/examplesJava17/records/RecordReflection.java
similarity index 100%
rename from src/test/examplesJava16/records/RecordReflection.java
rename to src/test/examplesJava17/records/RecordReflection.java
diff --git a/src/test/examplesJava16/records/RecordWithMembers.java b/src/test/examplesJava17/records/RecordWithMembers.java
similarity index 100%
rename from src/test/examplesJava16/records/RecordWithMembers.java
rename to src/test/examplesJava17/records/RecordWithMembers.java
diff --git a/src/test/examplesJava16/records/SimpleRecord.java b/src/test/examplesJava17/records/SimpleRecord.java
similarity index 100%
rename from src/test/examplesJava16/records/SimpleRecord.java
rename to src/test/examplesJava17/records/SimpleRecord.java
diff --git a/src/test/examplesJava17/records/UnusedRecordField.java b/src/test/examplesJava17/records/UnusedRecordField.java
new file mode 100644
index 0000000..1d184cd
--- /dev/null
+++ b/src/test/examplesJava17/records/UnusedRecordField.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2021, 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 records;
+
+public class UnusedRecordField {
+
+  Record unusedInstanceField;
+
+  void printHello() {
+    System.out.println("Hello!");
+  }
+
+  public static void main(String[] args) {
+    new UnusedRecordField().printHello();
+  }
+}
diff --git a/src/test/examplesJava17/records/UnusedRecordMethod.java b/src/test/examplesJava17/records/UnusedRecordMethod.java
new file mode 100644
index 0000000..342b178
--- /dev/null
+++ b/src/test/examplesJava17/records/UnusedRecordMethod.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2021, 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 records;
+
+public class UnusedRecordMethod {
+
+  Record unusedInstanceMethod(Record unused) {
+    return null;
+  }
+
+  void printHello() {
+    System.out.println("Hello!");
+  }
+
+  public static void main(String[] args) {
+    new UnusedRecordMethod().printHello();
+  }
+}
diff --git a/src/test/examplesJava17/records/UnusedRecordReflection.java b/src/test/examplesJava17/records/UnusedRecordReflection.java
new file mode 100644
index 0000000..1a4891b
--- /dev/null
+++ b/src/test/examplesJava17/records/UnusedRecordReflection.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2021, 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 records;
+
+import java.lang.reflect.Method;
+
+public class UnusedRecordReflection {
+
+  Record instanceField;
+
+  Record method(int i, Record unused, int j) {
+    return null;
+  }
+
+  Object reflectiveGetField() {
+    try {
+      return this.getClass().getDeclaredField("instanceField").get(this);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  Object reflectiveCallMethod() {
+    try {
+      for (Method declaredMethod : this.getClass().getDeclaredMethods()) {
+        if (declaredMethod.getName().equals("method")) {
+          return declaredMethod.invoke(this, 0, null, 1);
+        }
+      }
+      throw new RuntimeException("Unreachable");
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.println(new UnusedRecordReflection().reflectiveGetField());
+    System.out.println(new UnusedRecordReflection().reflectiveCallMethod());
+  }
+}
diff --git a/src/test/examplesJava16/sealed/Compiler.java b/src/test/examplesJava17/sealed/Compiler.java
similarity index 100%
rename from src/test/examplesJava16/sealed/Compiler.java
rename to src/test/examplesJava17/sealed/Compiler.java
diff --git a/src/test/examplesJava16/sealed/D8Compiler.java b/src/test/examplesJava17/sealed/D8Compiler.java
similarity index 100%
rename from src/test/examplesJava16/sealed/D8Compiler.java
rename to src/test/examplesJava17/sealed/D8Compiler.java
diff --git a/src/test/examplesJava16/sealed/Main.java b/src/test/examplesJava17/sealed/Main.java
similarity index 100%
rename from src/test/examplesJava16/sealed/Main.java
rename to src/test/examplesJava17/sealed/Main.java
diff --git a/src/test/examplesJava16/sealed/R8Compiler.java b/src/test/examplesJava17/sealed/R8Compiler.java
similarity index 100%
rename from src/test/examplesJava16/sealed/R8Compiler.java
rename to src/test/examplesJava17/sealed/R8Compiler.java
diff --git a/src/test/java/com/android/tools/r8/KeepConstantArguments.java b/src/test/java/com/android/tools/r8/KeepConstantArguments.java
index 87edb4f..132b92d 100644
--- a/src/test/java/com/android/tools/r8/KeepConstantArguments.java
+++ b/src/test/java/com/android/tools/r8/KeepConstantArguments.java
@@ -4,7 +4,10 @@
 package com.android.tools.r8;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
 public @interface KeepConstantArguments {}
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index c81cdfc..188f23f 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -1287,8 +1287,16 @@
 
   private static Map<String, List<String>> keepRules =
       ImmutableMap.of(
-          "021-string2", ImmutableList.of("-dontwarn junit.framework.**"),
-          "082-inline-execute", ImmutableList.of("-dontwarn junit.framework.**"));
+          "021-string2",
+          ImmutableList.of("-dontwarn junit.framework.**"),
+          "082-inline-execute",
+          ImmutableList.of("-dontwarn junit.framework.**"),
+          // Constructor MakeBoundType.<init>(int) is called using reflection.
+          "476-checker-ctor-fence-redun-elim",
+          ImmutableList.of(
+              "-keep class TestDontOptimizeAcrossEscape$MakeBoundTypeTest$MakeBoundType {",
+              "  void <init>(int);",
+              "}"));
 
   private static Map<String, Consumer<InternalOptions>> configurations =
       ImmutableMap.of(
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index fb37493..08dd682 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -120,7 +120,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 10, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 9, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
@@ -159,7 +159,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 10, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 9, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
@@ -204,7 +204,7 @@
             b ->
                 b.addProguardConfiguration(
                     getProguardOptionsNPlus(enableProguardCompatibilityMode), Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaringnplus"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaringnplus"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 05f9e16..20d71c6 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -29,7 +29,7 @@
     JDK9("jdk9", 53),
     JDK10("jdk10", 54),
     JDK11("jdk11", 55),
-    JDK16("jdk16", 60),
+    JDK17("jdk17", 61),
     ;
 
     private final String name;
@@ -70,13 +70,13 @@
   private static final Path JDK9_PATH =
       Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "openjdk-9.0.4");
   private static final Path JDK11_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-11");
-  private static final Path JDK16_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-16");
+  private static final Path JDK17_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-17");
   private static final Map<CfVm, Path> jdkPaths =
       ImmutableMap.of(
           CfVm.JDK8, JDK8_PATH,
           CfVm.JDK9, JDK9_PATH,
           CfVm.JDK11, JDK11_PATH,
-          CfVm.JDK16, JDK16_PATH);
+          CfVm.JDK17, JDK17_PATH);
 
   public static CfRuntime getCheckedInJdk(CfVm vm) {
     if (vm == CfVm.JDK8) {
@@ -121,9 +121,9 @@
     return new CfRuntime(CfVm.JDK11, getCheckedInJdkHome(CfVm.JDK11));
   }
 
-  // TODO(b/169692487): Add this to 'getCheckedInCfRuntimes' when we start having support for JDK16.
-  public static CfRuntime getCheckedInJdk16() {
-    return new CfRuntime(CfVm.JDK16, getCheckedInJdkHome(CfVm.JDK16));
+  // TODO(b/169692487): Add this to 'getCheckedInCfRuntimes' when we start having support for JDK17.
+  public static CfRuntime getCheckedInJdk17() {
+    return new CfRuntime(CfVm.JDK17, getCheckedInJdkHome(CfVm.JDK17));
   }
 
   public static List<CfRuntime> getCheckedInCfRuntimes() {
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index 8cd7860..19c8015 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -32,7 +32,7 @@
 
   private static final String STRING = "java.lang.String";
 
-  private boolean enableArgumentRemoval;
+  private boolean enableUnusedArgumentRemoval;
 
   @Parameterized.Parameters(name = "{0}, argument removal: {1}")
   public static List<Object[]> data() {
@@ -40,9 +40,10 @@
         getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
-  public NonConstructorRelaxationTest(TestParameters parameters, boolean enableArgumentRemoval) {
+  public NonConstructorRelaxationTest(
+      TestParameters parameters, boolean enableUnusedArgumentRemoval) {
     super(parameters);
-    this.enableArgumentRemoval = enableArgumentRemoval;
+    this.enableUnusedArgumentRemoval = enableUnusedArgumentRemoval;
   }
 
   @Test
@@ -78,11 +79,13 @@
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
+            .addUnusedArgumentAnnotations()
+            .enableConstantArgumentAnnotations()
             .enableInliningAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
             .enableMemberValuePropagationAnnotations()
+            .enableUnusedArgumentAnnotations(!enableUnusedArgumentRemoval)
             .addKeepMainRule(mainClass)
-            .addOptionsModification(o -> o.enableArgumentRemoval = enableArgumentRemoval)
             .noMinification()
             .addKeepRules(
                 // Note: we use '-checkdiscard' to indirectly check that the access relaxation is
@@ -104,23 +107,25 @@
             .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), mainClass);
 
-    assertEquals(
-        expectedOutput,
-        result
-            .getStdOut()
-            .replace("java.lang.IncompatibleClassChangeError", "java.lang.IllegalAccessError"));
+    assertEquals(expectedOutput, result.getStdOut());
 
     CodeInspector inspector = result.inspector();
+
+    MethodSignature barMethodSignatureAfterArgumentRemoval =
+        enableUnusedArgumentRemoval
+            ? new MethodSignature("bara", STRING, ImmutableList.of())
+            : new MethodSignature("bar", STRING, ImmutableList.of("int"));
     assertPublic(inspector, A.class, new MethodSignature("baz", STRING, ImmutableList.of()));
     assertPublic(inspector, A.class, new MethodSignature("bar", STRING, ImmutableList.of()));
-    assertPublic(inspector, A.class, new MethodSignature("bar", STRING, ImmutableList.of("int")));
+    assertPublic(inspector, A.class, barMethodSignatureAfterArgumentRemoval);
 
-    MethodSignature blahMethodSignature =
+    MethodSignature blahMethodSignatureAfterArgumentRemoval =
         new MethodSignature(
-            "blah", STRING, enableArgumentRemoval ? ImmutableList.of() : ImmutableList.of("int"));
-    assertPublic(inspector, A.class, blahMethodSignature);
-    assertPublic(inspector, B.class, blahMethodSignature);
-    assertPublic(inspector, BB.class, blahMethodSignature);
+            "blah",
+            STRING,
+            enableUnusedArgumentRemoval ? ImmutableList.of() : ImmutableList.of("int"));
+    assertPublic(inspector, A.class, blahMethodSignatureAfterArgumentRemoval);
+    assertPublic(inspector, BB.class, blahMethodSignatureAfterArgumentRemoval);
   }
 
   @Test
@@ -162,6 +167,7 @@
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
             .addKeepMainRule(mainClass)
             .addOptionsModification(o -> o.enableVerticalClassMerging = enableVerticalClassMerging)
+            .enableConstantArgumentAnnotations()
             .enableNeverClassInliningAnnotations()
             .enableInliningAnnotations()
             .enableMemberValuePropagationAnnotations()
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
index c832fe7..7974306 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.accessrelaxation.privateinstance;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
@@ -16,6 +17,7 @@
     return "Sub1::foo1()";
   }
 
+  @KeepConstantArguments
   @NeverInline
   private String bar1(int i) {
     return "Sub1::bar1(" + i + ")";
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
index ffdeec6..6e8d5f7 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.accessrelaxation.privateinstance;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
@@ -16,6 +17,7 @@
     return "Sub2::foo2()";
   }
 
+  @KeepConstantArguments
   @NeverInline
   private String bar1(int i) {
     return "Sub2::bar1(" + i + ")";
@@ -25,6 +27,7 @@
     return bar1(1);
   }
 
+  @KeepConstantArguments
   @NeverInline
   private String bar2(int i) {
     return "Sub2::bar2(" + i + ")";
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/A.java b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/A.java
index a05f41f..e5981b3 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/A.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/A.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.accessrelaxation.privatestatic;
 
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.KeepUnusedArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 
@@ -34,6 +36,7 @@
 
   @NeverInline
   @NeverPropagateValue
+  @KeepUnusedArguments
   private static String bar(int i) {
     return "A::bar(int)";
   }
@@ -42,6 +45,8 @@
     return bar(1);
   }
 
+  @KeepConstantArguments
+  @KeepUnusedArguments
   @NeverInline
   @NeverPropagateValue
   private static String blah(int i) {
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
index a6c40ff..4ac5250 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
@@ -4,12 +4,17 @@
 
 package com.android.tools.r8.accessrelaxation.privatestatic;
 
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.KeepUnusedArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 import com.android.tools.r8.NoHorizontalClassMerging;
 
 @NoHorizontalClassMerging
 public class BB extends A {
+
+  @KeepUnusedArguments
+  @KeepConstantArguments
   @NeverInline
   @NeverPropagateValue
   private static String blah(int i) {
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java
index 602f4a1..bdaf09d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
@@ -44,6 +45,7 @@
         .addDefaultRuntimeLibrary(parameters)
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
@@ -63,6 +65,7 @@
   @NoHorizontalClassMerging
   public static class ApiCaller {
 
+    @KeepConstantArguments
     public static void callInterfaceMethod(Api api) {
       System.out.println("ApiCaller::callInterfaceMethod");
       if (api != null) {
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
index 07ad8f0..1790724 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -93,6 +94,7 @@
     ClassBuilder cls1 = jasminBuilder.addClass("Cls1", absCls.name, itf1.name);
     // Mimic Kotlin's "internal" class
     cls1.setAccess("");
+    cls1.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
     cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
         ".limit stack 2",
         ".limit locals 2",
@@ -143,6 +145,7 @@
         .addProgramClassFileData(jasminBuilder.buildClasses())
         .addKeepMainRule(mainClass.name)
         .addOptionsModification(this::configure)
+        .enableConstantArgumentAnnotations()
         .noHorizontalClassMerging(cls2Class.name)
         .noMinification()
         .setMinApi(parameters.getApiLevel())
@@ -239,6 +242,7 @@
         "return");
 
     ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
+    cls2.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
     cls2.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
         ".limit stack 2",
         ".limit locals 2",
@@ -272,6 +276,7 @@
         .addProgramClassFileData(jasminBuilder.buildClasses())
         .addKeepMainRule(mainClass.name)
         .addOptionsModification(this::configure)
+        .enableConstantArgumentAnnotations()
         .noHorizontalClassMerging(derivedIntegerClass.name)
         .noMinification()
         .setMinApi(parameters.getApiLevel())
@@ -347,6 +352,7 @@
         "return");
 
     ClassBuilder subCls = jasminBuilder.addClass("DerivedString", baseCls.name);
+    subCls.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
     subCls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
         ".limit stack 2",
         ".limit locals 2",
@@ -381,6 +387,7 @@
         .addProgramClassFileData(jasminBuilder.buildClasses())
         .addKeepMainRule(mainClass.name)
         .addOptionsModification(this::configure)
+        .enableConstantArgumentAnnotations()
         .noMinification()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -433,6 +440,7 @@
         "aload_1",
         "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
         "return");
+    cls.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
     cls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
         ".limit stack 2",
         ".limit locals 2",
@@ -467,6 +475,7 @@
         .addProgramClassFileData(jasminBuilder.buildClasses())
         .addKeepMainRule(mainClass.name)
         .addOptionsModification(this::configure)
+        .enableConstantArgumentAnnotations()
         .noMinification()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -495,7 +504,6 @@
     // Callees are invoked with a simple constant, e.g., "Bar". Propagating it into the callees
     // bothers what the tests want to check, such as exact instructions in the body that include
     // invocation kinds, like virtual call to a bridge.
-    assert !options.callSiteOptimizationOptions().isConstantPropagationEnabled();
     // Disable inlining to avoid the (short) tested method from being inlined and then removed.
     options.enableInlining = false;
   }
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
index 81c5c04..45855d8 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -71,6 +72,7 @@
                         int.class))
                 .transform())
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
@@ -119,6 +121,7 @@
 
     // This bridge cannot be hoisted to A, since it would then become inaccessible to the call site
     // in TestClass.main().
+    @KeepConstantArguments
     @NeverInline
     /*bridge*/ String bridgeB(Object o) {
       return (String) m((String) o);
@@ -131,6 +134,7 @@
     // This bridge is invoked from another package. However, this does not prevent us from hoisting
     // the bridge to B, although B is not public, since users from outside this package can still
     // access bridgeC() via class C. From B, the bridge can be hoisted again to A.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String bridgeC(Object o) {
       return (String) m((String) o);
@@ -142,6 +146,7 @@
 
     // This bridge cannot be hoisted to A, since it would then become inaccessible to the call site
     // in TestClass.main().
+    @KeepConstantArguments
     @NeverInline
     /*bridge*/ String bridgeB(Object o, int a, int b, int c, int d, int e) {
       return (String) m((String) o, a, b, c, d, e);
@@ -154,6 +159,7 @@
     // This bridge is invoked from another package. However, this does not prevent us from hoisting
     // the bridge to B, although B is not public, since users from outside this package can still
     // access bridgeC() via class C. From B, the bridge can be hoisted again to A.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String bridgeC(Object o, int a, int b, int c, int d, int e) {
       return (String) m((String) o, a, b, c, d, e);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/FinalBridgeHoistingTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/FinalBridgeHoistingTest.java
index de2e041..bf958f8 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/FinalBridgeHoistingTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/FinalBridgeHoistingTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -42,6 +43,7 @@
                 .transform())
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(B1.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -85,6 +87,7 @@
   @NeverClassInline
   static class B1 extends A {
 
+    @KeepConstantArguments
     public String virtualBridge(Object o) {
       return (String) m((String) o);
     }
@@ -93,6 +96,7 @@
   @NeverClassInline
   static class B2 extends A {
 
+    @KeepConstantArguments
     @NeverInline
     public final /*bridge*/ String virtualBridge(Object o) {
       return (String) m((String) o);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonSuperclassBridgeHoistingTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonSuperclassBridgeHoistingTest.java
index f2a91a8..e987a43 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonSuperclassBridgeHoistingTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonSuperclassBridgeHoistingTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertFalse;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -42,6 +43,7 @@
                 .setBridge(B.class.getDeclaredMethod("bridge", Object.class))
                 .transform())
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
@@ -76,6 +78,7 @@
   @NeverClassInline
   static class B extends A {
 
+    @KeepConstantArguments
     @NeverInline
     public Object m(String arg) {
       return System.currentTimeMillis() >= 0 ? arg : null;
@@ -83,6 +86,7 @@
 
     // This bridge cannot be hoisted to A, since it targets a method on the enclosing class.
     // Hoisting the bridge to A would lead to a NoSuchMethodError.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String bridge(Object o) {
       return (String) m((String) o);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
index dbdb906..ae77a45 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
@@ -51,6 +52,7 @@
                 .setBridge(B5.class.getDeclaredMethod("virtualBridge", Object.class))
                 .transform())
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -106,6 +108,7 @@
 
   static class A {
 
+    @KeepConstantArguments
     @NeverInline
     public Object m(String arg) {
       return System.currentTimeMillis() >= 0 ? arg : null;
@@ -122,12 +125,14 @@
 
     // This bridge can be hoisted to A if the invoke-super instruction is rewritten to an
     // invoke-virtual instruction.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String superBridge(Object o) {
       return (String) super.m((String) o);
     }
 
     // This bridge can be hoisted to A.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String virtualBridge(Object o) {
       return (String) m((String) o);
@@ -139,12 +144,14 @@
   static class B2 extends A {
 
     // By hoisting B1.superBridge() to A this method bridge redundant.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String superBridge(Object o) {
       return (String) super.m((String) o);
     }
 
     // By hoisting B1.virtualBridge() to A this method bridge redundant.
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String virtualBridge(Object o) {
       return (String) m((String) o);
@@ -171,11 +178,13 @@
   @NoHorizontalClassMerging
   static class B4 extends A {
 
+    @KeepConstantArguments
     @NeverInline
     public String superBridge(Object o) {
       return System.currentTimeMillis() >= 0 ? ((String) o) : null;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public String virtualBridge(Object o) {
       return System.currentTimeMillis() >= 0 ? ((String) o) : null;
@@ -188,11 +197,13 @@
   @NoHorizontalClassMerging
   static class B5 extends A {
 
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String superBridge(Object o) {
       return (String) super.m2((String) o);
     }
 
+    @KeepConstantArguments
     @NeverInline
     public /*bridge*/ String virtualBridge(Object o) {
       return (String) m2((String) o);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
index e7d430f..64c19e5 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.bridgeremoval.hoisting.testclasses;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -33,6 +34,7 @@
   @NoVerticalClassMerging
   public static class AWithRangedInvoke {
 
+    @KeepConstantArguments
     @NeverInline
     public Object m(String arg, int a, int b, int c, int d, int e) {
       return System.currentTimeMillis() > 0 ? arg + a + b + c + d + e : null;
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
index fadf934..3f887af 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.classmerging;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
@@ -35,6 +36,7 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class))
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
         .enableNeverClassInliningAnnotations()
@@ -58,6 +60,9 @@
 
     @NeverPropagateValue private final String data;
 
+    // TODO(b/198758663): With argument propagation the constructors end up not being equivalent,
+    //  which prevents merging in the final round of horizontal class merging.
+    @KeepConstantArguments
     A(String data) {
       this.data = data;
     }
@@ -75,6 +80,9 @@
 
     @NeverPropagateValue private final String data;
 
+    // TODO(b/198758663): With argument propagation the constructors end up not being equivalent,
+    //  which prevents merging in the final round of horizontal class merging.
+    @KeepConstantArguments
     B(String data) {
       this.data = data;
     }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
index 3858a1e..667c261 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
@@ -9,6 +9,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
@@ -52,18 +53,13 @@
               } else {
                 inspector.assertIsCompleteMergeGroup(A.class, B.class, C.class);
               }
+              inspector.assertNoOtherClassesMerged();
             })
+        .enableConstantArgumentAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
-        .run(parameters.getRuntime(), Main.class)
-        .applyIf(
-            enableProguardCompatibilityMode,
-            result ->
-                result.assertSuccessWithOutputLines(
-                    "a", "b", "c", "foo", "null", "annotation 2", "annotation 1", "annotation 2"),
-            result ->
-                result.assertSuccessWithOutputLines("a", "b", "c", "foo", "null", "annotation 2"))
+        .compile()
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(TypeAnnotation.class), isPresent());
@@ -73,7 +69,15 @@
                   codeInspector.clazz(B.class),
                   onlyIf(enableProguardCompatibilityMode, isPresent()));
               assertThat(codeInspector.clazz(C.class), isAbsent());
-            });
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            enableProguardCompatibilityMode,
+            result ->
+                result.assertSuccessWithOutputLines(
+                    "a", "b", "c", "foo", "null", "annotation 2", "annotation 1", "annotation 2"),
+            result ->
+                result.assertSuccessWithOutputLines("a", "b", "c", "foo", "null", "annotation 2"));
   }
 
   @Retention(RetentionPolicy.RUNTIME)
@@ -114,6 +118,7 @@
   }
 
   static class Main {
+    @KeepConstantArguments
     @NeverInline
     public static void foo(TypeAnnotation annotation) {
       System.out.println(annotation);
@@ -124,7 +129,7 @@
       System.out.println(annotation.toString().replaceFirst(".*@.*", "annotation 2"));
     }
 
-    public static void main(String[] args) throws NoSuchMethodException {
+    public static void main(String[] args) {
       A a = new A();
       B b = new B("b");
       C c = new C("c");
diff --git a/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java b/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java
index aab82f0..352186b 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
@@ -156,6 +157,7 @@
     nonConstArraySize(0);
   }
 
+  @KeepConstantArguments
   @NeverInline
   static void nonConstArraySize(int argumentTypesSize) {
     try {
@@ -485,28 +487,23 @@
 
   @Test
   public void testNonConstArraySize() throws Exception {
-    Class<?> mainClass = MainNonConstArraySize.class;
-    R8Command.Builder builder =
-        ToolHelper.prepareR8CommandBuilder(
-                readClasses(A.class, mainClass, NeverInline.class), emptyConsumer(backend))
-            .addLibraryFiles(runtimeJar(backend));
-    builder.addProguardConfiguration(
-        ImmutableList.of(keepMainProguardConfigurationWithInliningAnnotation(mainClass)),
-        Origin.unknown());
-    ToolHelper.allowTestProguardOptions(builder);
-    AndroidApp output = ToolHelper.runR8(builder.build());
-    CodeInspector inspector = new CodeInspector(output);
-
-    assertThat(
-        inspector.clazz(A.class).method("void", "method0", ImmutableList.of()),
-        isPresentAndRenamed());
-
-    // The reference run on the Java VM will succeed, whereas the run on the R8 output will fail
-    // as in this test we fail to recognize the reflective call. To compare the output of the
-    // successful reference run append "java.lang.NoSuchMethodException" to it.
-    assertEquals(
-        runOnJava(mainClass) + "java.lang.NoSuchMethodException",
-        runOnVM(output, mainClass, backend));
+    testForR8(backend)
+        .addProgramClasses(MainNonConstArraySize.class, A.class)
+        .addKeepMainRule(MainNonConstArraySize.class)
+        .enableConstantArgumentAnnotations()
+        .enableInliningAnnotations()
+        .run(MainNonConstArraySize.class)
+        .inspect(
+            inspector -> {
+              assertThat(
+                  inspector.clazz(A.class).method("void", "method0", ImmutableList.of()),
+                  isPresentAndRenamed());
+            })
+        // The reference run on the Java VM will succeed, whereas the run on the R8 output will fail
+        // as in this test we fail to recognize the reflective call. To compare the output of the
+        // successful reference run append "java.lang.NoSuchMethodException" to it.
+        .assertSuccessWithOutput(
+            runOnJava(MainNonConstArraySize.class) + "java.lang.NoSuchMethodException");
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java
index b137f64..a63c85a 100644
--- a/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java
@@ -51,6 +51,11 @@
   @Test
   // Once R8 does not use expanded frames this can be enabled again.
   public void test() throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     Assume.assumeTrue(ToolHelper.getDexVm().isNewerThan(DexVm.ART_5_1_1_HOST)
         && !ToolHelper.isWindows());
     DebugTestConfig cfConfig = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
diff --git a/src/test/java/com/android/tools/r8/debug/ArraySimplificationLineNumberTestRunner.java b/src/test/java/com/android/tools/r8/debug/ArraySimplificationLineNumberTestRunner.java
index 22bca9f..4f72a65 100644
--- a/src/test/java/com/android/tools/r8/debug/ArraySimplificationLineNumberTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/ArraySimplificationLineNumberTestRunner.java
@@ -4,10 +4,12 @@
 package com.android.tools.r8.debug;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
 import java.util.Collections;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -21,6 +23,11 @@
   @Test
   @IgnoreIfVmOlderThan(Version.V6_0_1)
   public void testHitOnEntryOnly() throws Throwable {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     DebugTestConfig cf = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
     DebugTestConfig d8 = new D8DebugTestConfig().compileAndAdd(
         temp, Collections.singletonList(ToolHelper.getClassFileForTestClass(CLASS)));
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index d92d4ec..baa98a2 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -188,6 +188,11 @@
   protected DebugTestRunner getDebugTestRunner(
       DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
       throws Throwable {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     // Skip test due to unsupported runtime.
     Assume.assumeTrue("Skipping test " + testName.getMethodName() + " because ART is not supported",
         ToolHelper.artSupported());
diff --git a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
index 12c6bc8..349ac60 100644
--- a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
 import com.android.tools.r8.origin.Origin;
@@ -335,6 +336,11 @@
   }
 
   private DebugStreamComparator init(String pkg, String clazz) throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     // See verifyStateLocation in DebugTestBase.
     Assume.assumeTrue(
         "Streaming on Dalvik DEX runtimes has some unknown interference issue",
diff --git a/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
index 3e7367e..8a503e9 100644
--- a/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
@@ -69,6 +70,11 @@
   }
 
   private void stepOutput(byte[] clazz) throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     // See verifyStateLocation in DebugTestBase.
     Assume.assumeTrue(
         "Streaming on Dalvik DEX runtimes has some unknown interference issue",
diff --git a/src/test/java/com/android/tools/r8/debug/NonExitingMethodTestRunner.java b/src/test/java/com/android/tools/r8/debug/NonExitingMethodTestRunner.java
index df358b2..6fb4aa7 100644
--- a/src/test/java/com/android/tools/r8/debug/NonExitingMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/NonExitingMethodTestRunner.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
@@ -56,6 +57,11 @@
   @Test
   @IgnoreIfVmOlderThan(Version.V6_0_1)
   public void test() throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     Assume.assumeTrue(
         "Skipping test "
             + testName.getMethodName()
diff --git a/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java b/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java
index 6ddc9db..8f967e7 100644
--- a/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java
@@ -24,6 +24,11 @@
   @Test
   @IgnoreIfVmOlderOrEqualThan(Version.V5_1_1)
   public void test() throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     Assume.assumeTrue("Older runtimes cause some kind of debug streaming issues",
         ToolHelper.getDexVm().isNewerThan(DexVm.ART_5_1_1_HOST));
     DebugTestConfig cfConfig = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
diff --git a/src/test/java/com/android/tools/r8/debug/ThrowNPETestRunner.java b/src/test/java/com/android/tools/r8/debug/ThrowNPETestRunner.java
index 9271847..1f9eca9 100644
--- a/src/test/java/com/android/tools/r8/debug/ThrowNPETestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/ThrowNPETestRunner.java
@@ -23,6 +23,11 @@
   @Test
   @IgnoreIfVmOlderOrEqualThan(Version.V5_1_1)
   public void test() throws Exception {
+    // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags
+    // fixed.
+    Assume.assumeTrue(
+        "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0",
+        !ToolHelper.getDexVm().isEqualTo(DexVm.ART_12_0_0_HOST));
     Assume.assumeTrue("Older runtimes cause some kind of debug streaming issues",
         ToolHelper.getDexVm().isNewerThan(DexVm.ART_5_1_1_HOST));
     DebugTestConfig cfConfig = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaUtilFunctionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaUtilFunctionTest.java
index 0ac3687..2e0481d 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaUtilFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaUtilFunctionTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
@@ -89,6 +90,7 @@
         .addKeepMainRule(TestClass.class)
         .addInnerClasses(JavaUtilFunctionTest.class)
         .setMinApi(parameters.getApiLevel())
+        .enableConstantArgumentAnnotations()
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
         .compile()
         .inspect(this::checkRewrittenArguments)
@@ -103,6 +105,7 @@
 
   static class TestClass {
 
+    @KeepConstantArguments
     @NeverInline
     private static String applyFunction(Function<String, String> f) {
       return f.apply("Hello, world");
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaPrivateStaticMethodTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaPrivateStaticMethodTest.java
new file mode 100644
index 0000000..448234c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaPrivateStaticMethodTest.java
@@ -0,0 +1,62 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.lambdas;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LambdaPrivateStaticMethodTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public LambdaPrivateStaticMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  interface MyFun {
+    void run();
+  }
+
+  static class TestClass {
+
+    public static void run(MyFun fn) {
+      fn.run();
+    }
+
+    public static void main(String[] args) {
+      run(() -> System.out.println("Hello world"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
new file mode 100644
index 0000000..4ea7cd7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.records;
+
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyRecordAnnotationTest extends TestBase {
+
+  private static final String RECORD_NAME = "EmptyRecordAnnotation";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT_CF =
+      StringUtils.lines("class java.lang.Record", "class records.EmptyRecordAnnotation$Empty");
+  private static final String EXPECTED_RESULT_DEX =
+      StringUtils.lines(
+          "class com.android.tools.r8.RecordTag", "class records.EmptyRecordAnnotation$Empty");
+
+  private final TestParameters parameters;
+
+  public EmptyRecordAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT_CF);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT_DEX);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepRules("-keep class records.EmptyRecordAnnotation { *; }")
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .enableJVMPreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT_CF);
+      return;
+    }
+    builder
+        .addKeepRules("-keepattributes *Annotation*")
+        .addKeepRules("-keep class records.EmptyRecordAnnotation$Empty")
+        .addKeepRules("-keep class java.lang.Record")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT_DEX);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
index 817114e..effcba3 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -32,10 +33,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -46,7 +47,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addProgramClassFileData(PROGRAM_DATA)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
     }
@@ -61,29 +61,22 @@
 
   @Test
   public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-        .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
index 6d0f1a3..d4da59e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -32,10 +33,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -46,7 +47,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addProgramClassFileData(PROGRAM_DATA)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
     }
@@ -61,29 +61,22 @@
 
   @Test
   public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-        .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
index 0383285..2d771cc 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
-
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.InternalOptions.TestingOptions;
@@ -79,7 +77,6 @@
     testForR8(parameters.getBackend())
         .addProgramFiles(desugared)
         .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
         .addKeepMainRule(MAIN_TYPE)
         .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
index 3326483..1fc2f3e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -45,10 +46,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -59,7 +60,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addProgramClassFileData(PROGRAM_DATA)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
     }
@@ -74,29 +74,22 @@
 
   @Test
   public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-        .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
index ef11ef4..7af5f41 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -40,10 +40,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
index cad3c5b..26d259a 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -42,16 +42,15 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
-        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk16()).build());
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk17()).build());
   }
 
   @Test
   public void testJvm() throws Exception {
     testForJvm()
         .addProgramClassFileData(PROGRAM_DATA)
-        .enablePreview()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
@@ -61,8 +60,8 @@
     testForR8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
         .addKeepMainRule(MAIN_TYPE)
+        .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
         .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
         .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
index 224a873..3f8adcc 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
@@ -33,15 +33,18 @@
  */
 public class RecordTestUtils {
 
-  private static final String EXAMPLE_FOLDER = "examplesJava16";
+  private static final String EXAMPLE_FOLDER = "examplesJava17";
   private static final String RECORD_FOLDER = "records";
 
   public static Path jar() {
     return Paths.get(ToolHelper.TESTS_BUILD_DIR, EXAMPLE_FOLDER, RECORD_FOLDER + ".jar");
   }
 
-  // TODO(b/169645628): Consider if that keep rule should be required or not.
-  public static final String RECORD_KEEP_RULE =
+  // TODO(b/197081367): Rediscuss if we want to maintain the toString() method.
+  // In R8 Cf to Cf we need to maintain the record component attributes, the class name and fields
+  // so the mapping between the record components and fields is valid and the toString() prints
+  // the record as specified (including the class name).
+  public static final String RECORD_KEEP_RULE_R8_CF_TO_CF =
       "-keepattributes *\n" + "-keep class * extends java.lang.Record { private final <fields>; }";
 
   public static Path[] getJdk15LibraryFiles(TemporaryFolder temp) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
index 4b2c62a..f1fc5ae 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -34,10 +35,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -48,7 +49,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addProgramClassFileData(PROGRAM_DATA)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
     }
@@ -63,29 +63,22 @@
 
   @Test
   public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-        .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
index ad338ed..68ad95e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.records;
 
-import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE;
+import static com.android.tools.r8.desugar.records.RecordTestUtils.RECORD_KEEP_RULE_R8_CF_TO_CF;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -33,10 +34,10 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -47,7 +48,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addProgramClassFileData(PROGRAM_DATA)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
     }
@@ -65,27 +65,23 @@
 
   @Test
   public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+    builder
         .compile()
         .inspectWithOptions(
             RecordTestUtils::assertNoJavaLangRecord,
@@ -96,29 +92,24 @@
 
   @Test
   public void testR8NoMinification() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .noMinification()
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
     if (parameters.isCfRuntime()) {
-      testForR8(parameters.getBackend())
-          .addProgramClassFileData(PROGRAM_DATA)
-          .noMinification()
-          .setMinApi(parameters.getApiLevel())
-          .addKeepRules(RECORD_KEEP_RULE)
-          .addKeepMainRule(MAIN_TYPE)
+      builder
+          .addKeepRules(RECORD_KEEP_RULE_R8_CF_TO_CF)
           .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .enableJVMPreview()
           .run(parameters.getRuntime(), MAIN_TYPE)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .noMinification()
-        .setMinApi(parameters.getApiLevel())
-        .addKeepRules(RECORD_KEEP_RULE)
-        .addKeepMainRule(MAIN_TYPE)
-        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+    builder
         .compile()
         .inspectWithOptions(
             RecordTestUtils::assertNoJavaLangRecord,
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
new file mode 100644
index 0000000..5c520cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordFieldTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordField";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepRules("-keep class records.UnusedRecordField { *; }")
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
new file mode 100644
index 0000000..4c2ddf5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordMethodTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordMethod";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepRules("-keep class records.UnusedRecordMethod { *; }")
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
new file mode 100644
index 0000000..7317cea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordReflectionTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordReflection";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("null", "null");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordReflectionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepRules("-keep class records.UnusedRecordReflection { *; }")
+            .addKeepMainRule(MAIN_TYPE)
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java
index d311db8..710f6f5 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime;
-import com.android.tools.r8.examples.jdk16.Sealed;
+import com.android.tools.r8.examples.jdk17.Sealed;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.util.List;
 import org.junit.Test;
@@ -28,9 +28,9 @@
 
   @Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
-        getTestParameters().withCustomRuntime(TestRuntime.getCheckedInJdk16()).build(),
+        getTestParameters().withCustomRuntime(TestRuntime.getCheckedInJdk17()).build(),
         Backend.values());
   }
 
@@ -43,8 +43,7 @@
     assumeTrue(backend == Backend.CF);
     testForJvm()
         .addRunClasspathFiles(Sealed.jar())
-        .enablePreview()
-        .run(TestRuntime.getCheckedInJdk16(), Sealed.Main.typeName())
+        .run(TestRuntime.getCheckedInJdk17(), Sealed.Main.typeName())
         .assertSuccessWithOutputLines("R8 compiler", "D8 compiler");
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPrivateStaticResolutionInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPrivateStaticResolutionInvokeVirtualTest.java
index 363e539..3feba89 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPrivateStaticResolutionInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPrivateStaticResolutionInvokeVirtualTest.java
@@ -83,7 +83,8 @@
         testForRuntime(parameters)
             .addProgramClasses(getProgramClasses())
             .addProgramClassFileData(getProgramClassData())
-            .run(parameters.getRuntime(), TestClass.class));
+            .run(parameters.getRuntime(), TestClass.class),
+        false);
   }
 
   @Test
@@ -95,21 +96,24 @@
             .addKeepAllClassesRule()
             .setMinApi(parameters.getApiLevel())
             .compile()
-            .run(parameters.getRuntime(), TestClass.class));
+            .run(parameters.getRuntime(), TestClass.class),
+        true);
   }
 
-  private void checkResult(TestRunResult<?> result) {
+  private void checkResult(TestRunResult<?> result, boolean isR8) {
     // Invalid invoke case is where the invoke-virtual targets C.m.
     if (invalidInvoke) {
-      // Up to 4.4 the exception for targeting a private static was ICCE.
-      if (isDexOlderThanOrEqual(Version.V4_4_4)) {
-        result.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
-        return;
-      }
-      // Then up to 6.0 the runtime just ignores privates leading to incorrectly hitting I.m
-      if (isDexOlderThanOrEqual(Version.V6_0_1)) {
-        result.assertSuccessWithOutput(EXPECTED);
-        return;
+      if (!isR8) {
+        // Up to 4.4 the exception for targeting a private static was ICCE.
+        if (isDexOlderThanOrEqual(Version.V4_4_4)) {
+          result.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+          return;
+        }
+        // Then up to 6.0 the runtime just ignores privates leading to incorrectly hitting I.m
+        if (isDexOlderThanOrEqual(Version.V6_0_1)) {
+          result.assertSuccessWithOutput(EXPECTED);
+          return;
+        }
       }
       // The expected behavior is IAE since the resolved method is private.
       result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPublicStaticResolutionInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPublicStaticResolutionInvokeVirtualTest.java
index c960b33..2639a1f 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPublicStaticResolutionInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/DefaultInterfaceMethodDesugaringWithPublicStaticResolutionInvokeVirtualTest.java
@@ -103,7 +103,8 @@
     // Invalid invoke case is where the invoke-virtual targets C.m.
     if (invalidInvoke) {
       if (parameters.isDexRuntimeVersion(Version.V7_0_0)
-          && parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring()) {
+          && parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring()
+          && !isR8) {
         // The v7 VM incorrectly fails to throw.
         result.assertSuccessWithOutput(EXPECTED);
       } else {
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
index 32cfb18..cbe8874 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
@@ -138,7 +138,7 @@
   }
 
   @Test(expected = CompilationFailedException.class)
-  @IgnoreForRangeOfVmVersions(from = Version.V7_0_0, to = Version.V10_0_0) // No desugaring
+  @IgnoreForRangeOfVmVersions(from = Version.V7_0_0, to = Version.V12_0_0) // No desugaring
   public void testInvokeDefault1() throws Exception {
     ensureSameOutput(
         TestMainDefault1.class.getCanonicalName(),
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodOverrideEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodOverrideEnumUnboxingTest.java
index 4c69310..093f902 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodOverrideEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodOverrideEnumUnboxingTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertFalse;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -45,6 +46,7 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .addKeepRules(enumKeepRules.getKeepRules())
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
@@ -89,11 +91,13 @@
   @NeverClassInline
   static class B extends A {
 
+    @KeepConstantArguments
     @NeverInline
     void m(int x, MyEnum y) {
       System.out.println("B.m(" + x + " : int, " + y.toString() + " : MyEnum)");
     }
 
+    @KeepConstantArguments
     @NeverInline
     @Override
     void m(MyEnum x, int y) {
diff --git a/src/test/java/com/android/tools/r8/examples/jdk16/PatternMatchingForInstenceof.java b/src/test/java/com/android/tools/r8/examples/jdk17/PatternMatchingForInstanceof.java
similarity index 78%
rename from src/test/java/com/android/tools/r8/examples/jdk16/PatternMatchingForInstenceof.java
rename to src/test/java/com/android/tools/r8/examples/jdk17/PatternMatchingForInstanceof.java
index 0cc4846..ec5f4cd 100644
--- a/src/test/java/com/android/tools/r8/examples/jdk16/PatternMatchingForInstenceof.java
+++ b/src/test/java/com/android/tools/r8/examples/jdk17/PatternMatchingForInstanceof.java
@@ -2,14 +2,14 @@
 // 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.examples.jdk16;
+package com.android.tools.r8.examples.jdk17;
 
 import com.android.tools.r8.examples.JavaExampleClassProxy;
 import java.nio.file.Path;
 
-public class PatternMatchingForInstenceof {
+public class PatternMatchingForInstanceof {
 
-  private static final String EXAMPLE_FILE = "examplesJava16/pattern_matching_for_instanceof";
+  private static final String EXAMPLE_FILE = "examplesJava17/pattern_matching_for_instanceof";
 
   public static final JavaExampleClassProxy Main =
       new JavaExampleClassProxy(EXAMPLE_FILE, "pattern_matching_for_instanceof/Main");
diff --git a/src/test/java/com/android/tools/r8/examples/jdk16/Records.java b/src/test/java/com/android/tools/r8/examples/jdk17/Records.java
similarity index 85%
rename from src/test/java/com/android/tools/r8/examples/jdk16/Records.java
rename to src/test/java/com/android/tools/r8/examples/jdk17/Records.java
index a8677b8..aa4581e 100644
--- a/src/test/java/com/android/tools/r8/examples/jdk16/Records.java
+++ b/src/test/java/com/android/tools/r8/examples/jdk17/Records.java
@@ -2,14 +2,14 @@
 // 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.examples.jdk16;
+package com.android.tools.r8.examples.jdk17;
 
 import com.android.tools.r8.examples.JavaExampleClassProxy;
 import java.nio.file.Path;
 
 public class Records {
 
-  private static final String EXAMPLE_FILE = "examplesJava16/records";
+  private static final String EXAMPLE_FILE = "examplesJava17/records";
 
   public static final JavaExampleClassProxy Main =
       new JavaExampleClassProxy(EXAMPLE_FILE, "records/Main");
diff --git a/src/test/java/com/android/tools/r8/examples/jdk16/Sealed.java b/src/test/java/com/android/tools/r8/examples/jdk17/Sealed.java
similarity index 89%
rename from src/test/java/com/android/tools/r8/examples/jdk16/Sealed.java
rename to src/test/java/com/android/tools/r8/examples/jdk17/Sealed.java
index c995d13..8858316 100644
--- a/src/test/java/com/android/tools/r8/examples/jdk16/Sealed.java
+++ b/src/test/java/com/android/tools/r8/examples/jdk17/Sealed.java
@@ -2,14 +2,14 @@
 // 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.examples.jdk16;
+package com.android.tools.r8.examples.jdk17;
 
 import com.android.tools.r8.examples.JavaExampleClassProxy;
 import java.nio.file.Path;
 
 public class Sealed {
 
-  private static final String EXAMPLE_FILE = "examplesJava16/sealed";
+  private static final String EXAMPLE_FILE = "examplesJava17/sealed";
 
   public static final JavaExampleClassProxy Compiler =
       new JavaExampleClassProxy(EXAMPLE_FILE, "sealed/Compiler");
diff --git a/src/test/java/com/android/tools/r8/graph/invokevirtual/InvokeVirtualPrivateBaseWithDefaultTest.java b/src/test/java/com/android/tools/r8/graph/invokevirtual/InvokeVirtualPrivateBaseWithDefaultTest.java
index 9c9926d..698de1b 100644
--- a/src/test/java/com/android/tools/r8/graph/invokevirtual/InvokeVirtualPrivateBaseWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/graph/invokevirtual/InvokeVirtualPrivateBaseWithDefaultTest.java
@@ -63,12 +63,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
-        .applyIf(
-            parameters.isCfRuntime()
-                || parameters.getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.M),
-            r -> r.assertFailureWithErrorThatThrows(IllegalAccessError.class),
-            // TODO(b/152199517): Should be illegal access for DEX.
-            r -> r.assertSuccessWithOutputLines("I::foo"));
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
   }
 
   @NoVerticalClassMerging
diff --git a/src/test/java/com/android/tools/r8/internal/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/internal/retrace/RetraceTests.java
index 441fd68..d283ee6 100644
--- a/src/test/java/com/android/tools/r8/internal/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/internal/retrace/RetraceTests.java
@@ -88,6 +88,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return null;
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -123,6 +128,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return null;
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -162,6 +172,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return null;
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
diff --git a/src/test/java/com/android/tools/r8/internal/retrace/stacktraces/RetraceInternalStackTraceForTest.java b/src/test/java/com/android/tools/r8/internal/retrace/stacktraces/RetraceInternalStackTraceForTest.java
index e4005de..978e33a 100644
--- a/src/test/java/com/android/tools/r8/internal/retrace/stacktraces/RetraceInternalStackTraceForTest.java
+++ b/src/test/java/com/android/tools/r8/internal/retrace/stacktraces/RetraceInternalStackTraceForTest.java
@@ -57,6 +57,11 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    throw new RuntimeException("Do not test");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
index 3c7307c..f392b1d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
@@ -4,9 +4,9 @@
 package com.android.tools.r8.ir.optimize.callsites;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 9cb52da..cb494cb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -240,7 +240,7 @@
     SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
-            .enableProguardTestOptions()
+            .enableConstantArgumentAnnotations()
             .enableInliningAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
             .addKeepMainRule(main)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
index 2b29cea..d9785fe 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.classinliner.invalidroot;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
 
@@ -72,6 +73,7 @@
     }
   }
 
+  @KeepConstantArguments
   private void neverReturnsNormallyExtra(String prefix, NeverReturnsNormally a) {
     throw new RuntimeException("neverReturnsNormallyExtra(" +
         prefix + ", " + (a == null ? "null" : a.foo()) + "): " + next());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress199142666.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress199142666.java
index e92f84a..7f77799 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress199142666.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress199142666.java
@@ -4,13 +4,13 @@
 
 package com.android.tools.r8.ir.optimize.inliner;
 
-import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import java.io.IOException;
@@ -39,24 +39,21 @@
 
   @Test
   public void testInliningWhenInvalidCaller() throws Exception {
-    R8TestRunResult run =
-        testForR8(parameters.getBackend())
-            .addProgramClasses(HasInvalidStaticCall.class)
-            .addProgramClassFileData(getVirtualAAsStaticA())
-            .addKeepMainRule(HasInvalidStaticCall.class)
-            .addKeepMethodRules(StaticA.class, "void foo()")
-            .setMinApi(parameters.getApiLevel())
-            .run(parameters.getRuntime(), HasInvalidStaticCall.class);
-    if (parameters.getRuntime().asDex().getVm().getVersion().isDalvik()) {
-      // TODO(b/199142666): We should not inline to provoke this error.
-      run.assertFailureWithErrorThatMatches(
-          containsString("invoke type does not match method type of"));
-    } else {
-      // TODO(b/199142666): We should consider if we want to inline in this case (there are no
-      // verification errors)
-      run.assertSuccessWithOutputLines("foochanged")
-          .inspect(inspector -> ensureThisNumberOfCalls(inspector, HasInvalidStaticCall.class, 2));
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(HasInvalidStaticCall.class)
+        .addProgramClassFileData(getVirtualAAsStaticA())
+        .addKeepMainRule(HasInvalidStaticCall.class)
+        .addKeepMethodRules(StaticA.class, "void foo()")
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), HasInvalidStaticCall.class)
+        // TODO(b/199142666): We should consider if we want to inline in this case (there are no
+        //  verification errors)
+        .assertSuccessWithOutputLines("foochanged")
+        .inspect(
+            inspector -> {
+              ensureThisNumberOfCalls(inspector, HasInvalidStaticCall.class, 1);
+              ensureThisNumberThrowICCE(inspector, HasInvalidStaticCall.class, 1);
+            });
   }
 
   @Test
@@ -70,10 +67,13 @@
         .run(parameters.getRuntime(), TargetHasInvalidStaticCall.class)
         .assertSuccessWithEmptyOutput()
         .inspect(
-            inspector -> ensureThisNumberOfCalls(inspector, TargetHasInvalidStaticCall.class, 1));
+            inspector -> {
+              ensureThisNumberOfCalls(inspector, TargetHasInvalidStaticCall.class, 0);
+              ensureThisNumberThrowICCE(inspector, TargetHasInvalidStaticCall.class, 2);
+            });
   }
 
-  private void ensureThisNumberOfCalls(CodeInspector inspector, Class clazz, int fooCalls) {
+  private void ensureThisNumberOfCalls(CodeInspector inspector, Class<?> clazz, int fooCalls) {
     long count =
         inspector
             .clazz(clazz)
@@ -85,6 +85,27 @@
     assertEquals(fooCalls, count);
   }
 
+  private void ensureThisNumberThrowICCE(CodeInspector inspector, Class<?> clazz, int expected) {
+    IRCode code = inspector.clazz(clazz).mainMethod().buildIR();
+    long count =
+        code.streamInstructions()
+            .filter(Instruction::isThrow)
+            .filter(
+                instruction ->
+                    instruction
+                        .getFirstOperand()
+                        .isDefinedByInstructionSatisfying(
+                            definition ->
+                                definition.isNewInstance()
+                                    && definition
+                                        .asNewInstance()
+                                        .getType()
+                                        .getTypeName()
+                                        .equals(IncompatibleClassChangeError.class.getTypeName())))
+            .count();
+    assertEquals(expected, count);
+  }
+
   static class StaticA {
     public static void callFoo() {
       StaticA.foo();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/NopInliningConstraintTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/NopInliningConstraintTest.java
index 3295203..dcc572b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/NopInliningConstraintTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/NopInliningConstraintTest.java
@@ -40,6 +40,8 @@
         .addKeepMainRule(Main.class)
         .enableAlwaysInliningAnnotations()
         .enableInliningAnnotations()
+        // TODO(b/173398086): uniqueMethodWithName() does not work with argument removal.
+        .noMinification()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfNullOrNotNullInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfNullOrNotNullInliningTest.java
index c5073c5..f75c4c8 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfNullOrNotNullInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfNullOrNotNullInliningTest.java
@@ -10,6 +10,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverSingleCallerInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -51,6 +52,7 @@
         .addProgramClasses(mainClass, TestMethods.class)
         .addKeepMainRule(mainClass)
         .apply(this::configure)
+        .enableConstantArgumentAnnotations()
         .enableNeverSingleCallerInlineAnnotations()
         .compile()
         .inspect(this::inspect)
@@ -116,6 +118,7 @@
 
   static class TestMethods {
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfNullTest(Object o) {
       if (o == null) {
@@ -135,6 +138,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfBothNullTest(Object o1, Object o2) {
       if (o1 == null && o2 == null) {
@@ -154,6 +158,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfNotNullTest(Object o) {
       if (o != null) {
@@ -173,6 +178,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfBothNotNullTest(Object o1, Object o2) {
       if (o1 != null && o2 != null) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfTrueOrFalseInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfTrueOrFalseInliningTest.java
index b7a7186..180b971 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfTrueOrFalseInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SimpleIfTrueOrFalseInliningTest.java
@@ -10,6 +10,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverSingleCallerInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -51,6 +52,7 @@
         .addProgramClasses(mainClass, TestMethods.class)
         .addKeepMainRule(mainClass)
         .apply(this::configure)
+        .enableConstantArgumentAnnotations()
         .enableNeverSingleCallerInlineAnnotations()
         .compile()
         .inspect(this::inspect)
@@ -114,6 +116,7 @@
 
   static class TestMethods {
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfTrueTest(boolean b) {
       if (b) {
@@ -133,6 +136,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfBothTrueTest(boolean b1, boolean b2) {
       if (b1 && b2) {
@@ -152,6 +156,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfFalseTest(boolean b) {
       if (!b) {
@@ -171,6 +176,7 @@
       System.out.println("!");
     }
 
+    @KeepConstantArguments
     @NeverSingleCallerInline
     static void simpleIfBothFalseTest(boolean b1, boolean b2) {
       if (!b1 && !b2) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/InstanceOfRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/InstanceOfRemovalTest.java
index 791bbae..09aef4c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/InstanceOfRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/InstanceOfRemovalTest.java
@@ -177,6 +177,6 @@
     MethodSubject barMethodSubject = testClass.uniqueMethodWithName("bar");
     Iterator<InstructionSubject> barInstructionIterator =
         barMethodSubject.iterateInstructions(InstructionSubject::isInstanceOf);
-    assertEquals(4, Streams.stream(barInstructionIterator).count());
+    assertEquals(2, Streams.stream(barInstructionIterator).count());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
index 29de662..3a0b304 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -64,6 +65,7 @@
         .enableInliningAnnotations()
         .addInnerClasses(B134462736.class)
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .setMinApi(parameters.getApiLevel())
         .noMinification()
         .addOptionsModification(
@@ -78,11 +80,14 @@
   }
 
   public static class TestClass {
+
+    @KeepConstantArguments
     @NeverInline
     public void consumer(String arg1, String arg2) {
       System.out.println(arg1 + " " + arg2);
     }
 
+    @KeepConstantArguments
     @NeverInline
     public void method1(StringBuilder builder, String arg1, String arg2) {
       builder.append(arg1);
@@ -90,6 +95,7 @@
       consumer(builder.toString(), null);
     }
 
+    @KeepConstantArguments
     @NeverInline
     public void method2(StringBuilder builder, String arg1, String arg2) {
       builder.append(arg1);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
index e384d9b..8cc3b8c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
@@ -5,20 +5,23 @@
 package com.android.tools.r8.ir.optimize.outliner.primitivetypes;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertEquals;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -26,18 +29,30 @@
 @RunWith(Parameterized.class)
 public class PrimitiveTypesTest extends TestBase {
 
+  private final boolean enableArgumentPropagation;
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameterized.Parameters(name = "{1}, argument propagation: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public PrimitiveTypesTest(TestParameters parameters) {
+  public PrimitiveTypesTest(boolean keepConstantArguments, TestParameters parameters) {
+    this.enableArgumentPropagation = keepConstantArguments;
     this.parameters = parameters;
   }
 
   private void validateOutlining(CodeInspector inspector, Class<?> testClass, String argumentType) {
+    boolean isStringBuilderOptimized =
+        enableArgumentPropagation
+            && parameters.isDexRuntime()
+            && (argumentType.equals("char") || argumentType.equals("boolean"));
+    if (isStringBuilderOptimized) {
+      assertEquals(1, inspector.allClasses().size());
+      return;
+    }
+
     ClassSubject outlineClass =
         inspector.clazz(SyntheticItemsTestUtils.syntheticOutlineClass(testClass, 0));
     MethodSubject outline0Method =
@@ -56,6 +71,8 @@
   public void runTest(Class<?> testClass, String argumentType, String expectedOutput)
       throws Exception {
     testForR8(parameters.getBackend())
+        .addConstantArgumentAnnotations()
+        .enableConstantArgumentAnnotations(!enableArgumentPropagation)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .addProgramClasses(testClass)
@@ -120,6 +137,7 @@
 
   static class TestClassBoolean {
 
+    @KeepConstantArguments
     @NeverInline
     public static String method1(boolean b) {
       StringBuilder sb = new StringBuilder();
@@ -128,6 +146,7 @@
       return sb.toString();
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static String method2(boolean b) {
       StringBuilder sb = new StringBuilder();
@@ -144,6 +163,7 @@
 
   static class TestClassByte {
 
+    @KeepConstantArguments
     @NeverInline
     public static String method1(byte b) {
       MyStringBuilder sb = new MyStringBuilder();
@@ -152,6 +172,7 @@
       return sb.toString();
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static String method2(byte b) {
       MyStringBuilder sb = new MyStringBuilder();
@@ -168,6 +189,7 @@
 
   static class TestClassShort {
 
+    @KeepConstantArguments
     @NeverInline
     public static String method1(short s) {
       MyStringBuilder sb = new MyStringBuilder();
@@ -176,6 +198,7 @@
       return sb.toString();
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static String method2(short s) {
       MyStringBuilder sb = new MyStringBuilder();
@@ -192,6 +215,7 @@
 
   static class TestClassChar {
 
+    @KeepConstantArguments
     @NeverInline
     public static String method1(char c) {
       StringBuilder sb = new StringBuilder();
@@ -200,6 +224,7 @@
       return sb.toString();
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static String method2(char c) {
       StringBuilder sb = new StringBuilder();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index 5a60806..a1b6f08 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -410,6 +410,7 @@
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
+            .enableConstantArgumentAnnotations()
             .enableInliningAnnotations()
             .addKeepMainRule(main)
             .allowAccessModification()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/dualcallinline/Candidate.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/dualcallinline/Candidate.java
index 9ccd630..4733ab3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/dualcallinline/Candidate.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/dualcallinline/Candidate.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.staticizer.dualcallinline;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 
 public class Candidate {
@@ -12,6 +13,7 @@
     return bar("Candidate::foo()");
   }
 
+  @KeepConstantArguments
   @NeverInline
   public String bar(String other) {
     return "Candidate::bar(" + other + ")";
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
index 5c9c9a3..6c47b03 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
@@ -58,7 +58,6 @@
     // This test wants to check if compile-time computation is not applied to non-null,
     // non-constant value. In a simple test setting, call-site optimization knows the argument is
     // always a non-null, specific constant, but that is beyond the scope of this test.
-    assert !options.callSiteOptimizationOptions().isConstantPropagationEnabled();
   }
 
   private void test(SingleTestRunResult result, int expectedStringIsEmptyCount) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/ParameterRewritingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/ParameterRewritingTest.java
index 0125189..ee595ce 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/ParameterRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/ParameterRewritingTest.java
@@ -11,7 +11,8 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -25,15 +26,15 @@
 @RunWith(Parameterized.class)
 public class ParameterRewritingTest extends TestBase {
 
-  private final Backend backend;
+  private final TestParameters parameters;
 
-  @Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public ParameterRewritingTest(Backend backend) {
-    this.backend = backend;
+  public ParameterRewritingTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
@@ -49,14 +50,15 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(ParameterRewritingTest.class)
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
             .addOptionsModification(options -> options.enableClassInlining = false)
             .noMinification()
-            .run(TestClass.class)
+            .setMinApi(parameters.getApiLevel())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
             .inspector();
 
@@ -64,7 +66,7 @@
     MethodSubject createStaticMethodSubject =
         factoryClassSubject.uniqueMethodWithName("createStatic");
     assertThat(createStaticMethodSubject, isPresent());
-    assertEquals(1, createStaticMethodSubject.getMethod().getReference().proto.parameters.size());
+    assertEquals(1, createStaticMethodSubject.getMethod().getParameters().size());
 
     for (int i = 1; i <= 3; ++i) {
       String createStaticWithUnusedMethodName = "createStaticWithUnused" + i;
@@ -73,8 +75,8 @@
       assertThat(createStaticWithUnusedMethodSubject, isPresent());
 
       DexMethod method = createStaticWithUnusedMethodSubject.getMethod().getReference();
-      assertEquals(1, method.proto.parameters.size());
-      assertEquals("java.lang.String", method.proto.parameters.toString());
+      assertEquals(1, method.getParameters().size());
+      assertEquals("java.lang.String", method.getParameters().toString());
     }
 
     MethodSubject createStaticWithUnusedMethodSubject =
@@ -82,9 +84,9 @@
     assertThat(createStaticWithUnusedMethodSubject, isPresent());
 
     DexMethod method = createStaticWithUnusedMethodSubject.getMethod().getReference();
-    assertEquals(3, method.proto.parameters.size());
+    assertEquals(3, method.getParameters().size());
     assertEquals(
-        "java.lang.String java.lang.String java.lang.String", method.proto.parameters.toString());
+        "java.lang.String java.lang.String java.lang.String", method.getParameters().toString());
 
     assertThat(inspector.clazz(Uninstantiated.class), not(isPresent()));
   }
@@ -92,26 +94,39 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      Object obj1 = Factory.createStatic(null, "Factory.createStatic()");
+      Object obj1 = Factory.createStatic(null, asNonConstantString("Factory.createStatic()"));
       System.out.println(" -> " + obj1);
 
       Object obj2 =
-          Factory.createStaticWithUnused1(new Object(), null, "Factory.createStaticWithUnused1()");
+          Factory.createStaticWithUnused1(
+              new Object(), null, asNonConstantString("Factory.createStaticWithUnused1()"));
       System.out.println(" -> " + obj2);
 
       Object obj3 =
-          Factory.createStaticWithUnused2(null, new Object(), "Factory.createStaticWithUnused2()");
+          Factory.createStaticWithUnused2(
+              null, new Object(), asNonConstantString("Factory.createStaticWithUnused2()"));
       System.out.println(" -> " + obj3);
 
       Object obj4 =
-          Factory.createStaticWithUnused3(null, "Factory.createStaticWithUnused3()", new Object());
+          Factory.createStaticWithUnused3(
+              null, asNonConstantString("Factory.createStaticWithUnused3()"), new Object());
       System.out.println(" -> " + obj4);
 
       Object obj5 =
           Factory.createStaticWithUnused4(
-              "Factory", new Object(), null, ".", new Object(), null, "createStaticWithUnused4()");
+              asNonConstantString("Factory"),
+              new Object(),
+              null,
+              asNonConstantString("."),
+              new Object(),
+              null,
+              asNonConstantString("createStaticWithUnused4()"));
       System.out.println(" -> " + obj5);
     }
+
+    public static String asNonConstantString(String string) {
+      return System.currentTimeMillis() > 0 ? string : null;
+    }
   }
 
   @NoHorizontalClassMerging
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
index 339bbbc..663bcf6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
@@ -158,12 +158,14 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      testRemoveStaticFromStart(null, "Hello", " world!");
-      testRemoveStaticFromMiddle("Hello", null, " world!");
-      testRemoveStaticFromEnd("Hello", " world!", null);
-      new TestClass().testRemoveVirtualFromStart(null, "Hello", " world!");
-      new TestClass().testRemoveVirtualFromMiddle("Hello", null, " world!");
-      new TestClass().testRemoveVirtualFromEnd("Hello", " world!", null);
+      String hello = System.currentTimeMillis() > 0 ? "Hello" : null;
+      String world = System.currentTimeMillis() > 0 ? " world!" : null;
+      testRemoveStaticFromStart(null, hello, world);
+      testRemoveStaticFromMiddle(hello, null, world);
+      testRemoveStaticFromEnd(hello, world, null);
+      new TestClass().testRemoveVirtualFromStart(null, hello, world);
+      new TestClass().testRemoveVirtualFromMiddle(hello, null, world);
+      new TestClass().testRemoveVirtualFromEnd(hello, world, null);
     }
 
     @KeepConstantArguments
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
index b7ddd9e..ed2d01d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -60,6 +61,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(PrivateInstanceMethodCollisionTest.class)
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .minification(minification)
@@ -109,11 +111,13 @@
 
   @NeverClassInline
   static class A {
+    @KeepConstantArguments
     @NeverInline
     private void foo(String used) {
       System.out.println("A#foo(" + used + ")");
     }
 
+    @KeepConstantArguments
     @NeverInline
     void foo(String used, Object unused) {
       System.out.println("A#foo(" + used + ", Object)");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
index 7cf6193..2f3e14f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.KeepUnusedArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -66,6 +67,7 @@
         .addKeepAttributes("RuntimeVisibleParameterAnnotations")
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
+        .enableConstantArgumentAnnotations()
         .enableUnusedArgumentAnnotations(keepUnusedArguments)
         // TODO(b/123060011): Mapping not working in presence of unused argument removal.
         .minification(keepUnusedArguments)
@@ -152,6 +154,7 @@
       new TestClass().testRemoveVirtualFromEnd("Hello", " world!", null);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     static void testRemoveStaticFromStart(
@@ -159,6 +162,7 @@
       System.out.println(used + otherUsed);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     static void testRemoveStaticFromMiddle(
@@ -166,6 +170,7 @@
       System.out.println(used + otherUsed);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     static void testRemoveStaticFromEnd(
@@ -173,6 +178,7 @@
       System.out.println(used + otherUsed);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     void testRemoveVirtualFromStart(
@@ -180,6 +186,7 @@
       System.out.println(used + otherUsed);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     void testRemoveVirtualFromMiddle(
@@ -187,6 +194,7 @@
       System.out.println(used + otherUsed);
     }
 
+    @KeepConstantArguments
     @KeepUnusedArguments
     @NeverInline
     void testRemoveVirtualFromEnd(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionMappingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionMappingTest.java
index d8a7d6b..7d9a941 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionMappingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionMappingTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
@@ -50,6 +51,7 @@
             .addProgramClasses(Main.class)
             .setMinApi(parameters.getApiLevel())
             .addKeepMainRule(Main.class)
+            .enableConstantArgumentAnnotations()
             .enableInliningAnnotations()
             .addKeepAttributeLineNumberTable()
             .run(parameters.getRuntime(), Main.class)
@@ -92,6 +94,7 @@
       System.out.println("test with unused");
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static void test(String used, String unused) {
       System.out.println("test with used: " + used);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsDoubleTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsDoubleTest.java
index 95d571d..282371b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsDoubleTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsDoubleTest.java
@@ -6,7 +6,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.util.Collection;
 import org.junit.Assert;
@@ -27,16 +29,19 @@
   }
 
   static class TestClass {
+    @KeepConstantArguments
     @NeverInline
     public static double a(double a) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static double a(double a, double b) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static double a(double a, double b, double c) {
       return a;
@@ -50,6 +55,12 @@
   }
 
   @Override
+  public void configure(R8FullTestBuilder builder) {
+    super.configure(builder);
+    builder.enableConstantArgumentAnnotations();
+  }
+
+  @Override
   public Class<?> getTestClass() {
     return TestClass.class;
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsIntTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsIntTest.java
index 06f665d..80309b2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsIntTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsIntTest.java
@@ -6,7 +6,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.google.common.collect.ImmutableSet;
 import java.util.Collection;
@@ -31,22 +33,26 @@
 
   static class TestClass {
 
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, int b) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static int iinc(int a, int b) {
       b++;
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, int b, int c) {
       return a;
@@ -61,6 +67,12 @@
   }
 
   @Override
+  public void configure(R8FullTestBuilder builder) {
+    super.configure(builder);
+    builder.enableConstantArgumentAnnotations();
+  }
+
+  @Override
   public Class<?> getTestClass() {
     return TestClass.class;
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsLongTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsLongTest.java
index 6f44c8d..0dba75b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsLongTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsLongTest.java
@@ -6,7 +6,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.util.Collection;
 import org.junit.Assert;
@@ -27,16 +29,20 @@
   }
 
   static class TestClass {
+
+    @KeepConstantArguments
     @NeverInline
     public static long a(long a) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static long a(long a, long b) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static long a(long a, long b, long c) {
       return a;
@@ -50,6 +56,12 @@
   }
 
   @Override
+  public void configure(R8FullTestBuilder builder) {
+    super.configure(builder);
+    builder.enableConstantArgumentAnnotations();
+  }
+
+  @Override
   public Class<?> getTestClass() {
     return TestClass.class;
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedTest.java
index 717adac..dd63e78 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedTest.java
@@ -6,7 +6,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.util.Collection;
 import org.junit.Assert;
@@ -27,6 +29,8 @@
   }
 
   static class TestClass {
+
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, Object b) {
       return a;
@@ -37,6 +41,7 @@
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, Object b, int c) {
       return c;
@@ -56,6 +61,12 @@
   }
 
   @Override
+  public void configure(R8FullTestBuilder builder) {
+    super.configure(builder);
+    builder.enableConstantArgumentAnnotations();
+  }
+
+  @Override
   public Class<?> getTestClass() {
     return TestClass.class;
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedWidthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedWidthTest.java
index e10793d..7ed9021 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedWidthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsMixedWidthTest.java
@@ -6,7 +6,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.util.Collection;
 import org.junit.Assert;
@@ -27,21 +29,26 @@
   }
 
   static class TestClass {
+
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, long b) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static long a(long a, int b) {
       return a;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static int a(int a, long b, int c) {
       return c;
     }
 
+    @KeepConstantArguments
     @NeverInline
     public static long a(long a, int b, long c) {
       return c;
@@ -56,6 +63,12 @@
   }
 
   @Override
+  public void configure(R8FullTestBuilder builder) {
+    super.configure(builder);
+    builder.enableConstantArgumentAnnotations();
+  }
+
+  @Override
   public Class<?> getTestClass() {
     return TestClass.class;
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index 7f41f05..beb87b8 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -95,6 +95,8 @@
     private boolean isInterface = false;
     private String access = "public";
 
+    private final List<String> pendingAnnotations = new ArrayList<>();
+
     private ClassBuilder(String name) {
       this(name, "java/lang/Object");
     }
@@ -124,6 +126,13 @@
       return addMethod("public abstract", name, argumentTypes, returnType);
     }
 
+    public void addRuntimeInvisibleAnnotation(String typeName) {
+      pendingAnnotations.add(
+          StringUtils.lines(
+              ".annotation invisible " + DescriptorUtils.javaTypeToDescriptor(typeName),
+              ".end annotation"));
+    }
+
     public MethodSignature addFinalMethod(
         String name,
         List<String> argumentTypes,
@@ -221,6 +230,10 @@
           .append(StringUtils.join("", argumentTypes, BraceType.PARENS))
           .append(returnType)
           .append("\n");
+      if (!pendingAnnotations.isEmpty()) {
+        pendingAnnotations.forEach(builder::append);
+        pendingAnnotations.clear();
+      }
       for (String line : lines) {
         builder.append(line).append("\n");
       }
diff --git a/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java b/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
index 345f198..95cf5b2 100644
--- a/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/java_language/pattern_matching_for_instenceof/PattternMatchingForInstanceOfTest.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
-import com.android.tools.r8.examples.jdk16.PatternMatchingForInstenceof;
+import com.android.tools.r8.examples.jdk17.PatternMatchingForInstanceof;
 import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
@@ -26,15 +26,15 @@
 
   private static List<String> EXPECTED = ImmutableList.of("Hello, world!");
 
-  private static final Path JAR = PatternMatchingForInstenceof.jar();
-  private static final String MAIN = PatternMatchingForInstenceof.Main.typeName();
+  private static final Path JAR = PatternMatchingForInstanceof.jar();
+  private static final String MAIN = PatternMatchingForInstanceof.Main.typeName();
 
   @Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
     return buildParameters(
         getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
             .withDexRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
@@ -45,7 +45,6 @@
     if (parameters.isCfRuntime()) {
       testForJvm()
           .addRunClasspathFiles(JAR)
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN)
           .assertSuccessWithOutputLines(EXPECTED);
     }
@@ -71,7 +70,6 @@
     } else {
       testForJvm()
           .addRunClasspathFiles(builder.compile().writeToZip())
-          .enablePreview()
           .run(parameters.getRuntime(), MAIN)
           .assertSuccessWithOutputLines(EXPECTED);
     }
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 495eae6..2516b74 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -262,7 +262,7 @@
     }
   }
 
-  private void checkMethodPresenceInInput(
+  protected void checkMethodPresenceInInput(
       String className, MethodSignature methodSignature, boolean isPresent) {
     boolean foundMethod = AsmUtils.doesMethodExist(classpath, className,
         methodSignature.name, methodSignature.toDescriptor());
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index 3d872c4..46adea0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
@@ -28,7 +29,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -111,7 +111,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsKept(outerClass, setterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
               }
             });
   }
@@ -146,7 +146,7 @@
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
 
                 checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsKept(outerClass, setterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
               }
             });
   }
@@ -180,7 +180,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsKept(outerClass, setterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
               }
             });
   }
@@ -214,7 +214,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsKept(outerClass, setterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
               }
             });
   }
@@ -247,7 +247,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsKept(outerClass, setterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
               }
             });
   }
@@ -340,7 +340,6 @@
   }
 
   @Test
-  @Ignore("b/74103342")
   public void testAccessorFromPrivate() throws Exception {
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("accessors.AccessorKt",
@@ -348,28 +347,8 @@
     runTest("accessors", mainClass)
         .inspect(
             inspector -> {
-              ClassSubject outerClass =
-                  checkClassIsKept(inspector, testedClass.getOuterClassName());
-              ClassSubject companionClass = checkClassIsKept(inspector, testedClass.getClassName());
-              String propertyName = "property";
-              FieldSubject fieldSubject =
-                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-              assertTrue(fieldSubject.getField().accessFlags.isStatic());
-
-              // We cannot inline the getter because we don't know if NPE is preserved.
-              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-              checkMethodIsKept(companionClass, getter);
-
-              // We should always inline the static accessor method.
-              MemberNaming.MethodSignature getterAccessor =
-                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-              checkMethodIsRemoved(companionClass, getterAccessor);
-
-              if (allowAccessModification) {
-                assertTrue(fieldSubject.getField().accessFlags.isPublic());
-              } else {
-                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-              }
+              checkClassIsRemoved(inspector, testedClass.getOuterClassName());
+              checkClassIsRemoved(inspector, testedClass.getClassName());
             });
   }
 
@@ -389,7 +368,6 @@
   }
 
   @Test
-  @Ignore("b/74103342")
   public void testPrivatePropertyAccessorForInnerClassCanBeInlined() throws Exception {
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_INNER_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
@@ -397,6 +375,13 @@
     runTest("accessors", mainClass)
         .inspect(
             inspector -> {
+              if (allowAccessModification
+                  && (testParameters.isCfRuntime()
+                      || !kotlinParameters.is(KOTLINC_1_5_0, KotlinTargetVersion.JAVA_8))) {
+                checkClassIsRemoved(inspector, testedClass.getClassName());
+                return;
+              }
+
               ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
 
               String propertyName = "privateProp";
@@ -415,13 +400,12 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(classSubject, getterAccessor);
-                checkMethodIsKept(classSubject, setterAccessor);
+                checkMethodIsRemoved(classSubject, setterAccessor);
               }
             });
   }
 
   @Test
-  @Ignore("b/74103342")
   public void testPrivateLateInitPropertyAccessorForInnerClassCanBeInlined() throws Exception {
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_INNER_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
@@ -429,31 +413,15 @@
     runTest("accessors", mainClass)
         .inspect(
             inspector -> {
-              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
-
-              String propertyName = "privateLateInitProp";
-              FieldSubject fieldSubject =
-                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
-              assertFalse(fieldSubject.getField().accessFlags.isStatic());
-
-              MemberNaming.MethodSignature getterAccessor =
-                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-              MemberNaming.MethodSignature setterAccessor =
-                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-              if (allowAccessModification) {
-                assertTrue(fieldSubject.getField().accessFlags.isPublic());
-                checkMethodIsRemoved(classSubject, getterAccessor);
-                checkMethodIsRemoved(classSubject, setterAccessor);
+              if (kotlinc.getCompilerVersion() == KOTLINC_1_5_0 && testParameters.isDexRuntime()) {
+                checkClassIsKept(inspector, testedClass.getClassName());
               } else {
-                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-                checkMethodIsKept(classSubject, getterAccessor);
-                checkMethodIsKept(classSubject, setterAccessor);
+                checkClassIsRemoved(inspector, testedClass.getClassName());
               }
             });
   }
 
   @Test
-  @Ignore("b/74103342")
   public void testAccessorForLambdaIsRemovedWhenNotUsed() throws Exception {
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_LAMBDA_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
@@ -461,21 +429,11 @@
     runTest("accessors", mainClass)
         .inspect(
             inspector -> {
-              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
-              String propertyName = "property";
-
-              MemberNaming.MethodSignature getterAccessor =
-                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
-              MemberNaming.MethodSignature setterAccessor =
-                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
-
-              checkMethodIsRemoved(classSubject, getterAccessor);
-              checkMethodIsRemoved(classSubject, setterAccessor);
+              checkClassIsRemoved(inspector, testedClass.getClassName());
             });
   }
 
   @Test
-  @Ignore("b/74103342")
   public void testAccessorForLambdaCanBeInlined() throws Exception {
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_LAMBDA_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
@@ -483,16 +441,25 @@
     runTest("accessors", mainClass)
         .inspect(
             inspector -> {
+              if (allowAccessModification) {
+                checkClassIsRemoved(inspector, testedClass.getClassName());
+                return;
+              }
+
               ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
               String propertyName = "property";
               FieldSubject fieldSubject =
                   checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
               assertFalse(fieldSubject.getField().accessFlags.isStatic());
 
+              AccessorKind accessorKind =
+                  kotlinc.getCompilerVersion() == KOTLINC_1_5_0
+                      ? AccessorKind.FROM_INNER
+                      : AccessorKind.FROM_LAMBDA;
               MemberNaming.MethodSignature getterAccessor =
-                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+                  testedClass.getGetterAccessorForProperty(propertyName, accessorKind);
               MemberNaming.MethodSignature setterAccessor =
-                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+                  testedClass.getSetterAccessorForProperty(propertyName, accessorKind);
               if (allowAccessModification) {
                 assertTrue(fieldSubject.getField().accessFlags.isPublic());
                 checkMethodIsRemoved(classSubject, getterAccessor);
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
index 5de5dbd..4457386 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
@@ -54,9 +54,7 @@
             "intrinsics",
             "intrinsics.IntrinsicsKt",
             testBuilder ->
-                testBuilder
-                    .addKeepRules(extraRules)
-                    .noHorizontalClassMerging(Intrinsics.class))
+                testBuilder.addKeepRules(extraRules).noHorizontalClassMerging(Intrinsics.class))
         .inspect(
             inspector -> {
               ClassSubject intrinsicsClass =
@@ -75,8 +73,7 @@
                               "checkParameterIsNotNull",
                               "void",
                               Lists.newArrayList("java.lang.Object", "java.lang.String")),
-                          // TODO(b/179866251): This is also different on CF
-                          !allowAccessModification || testParameters.isCfRuntime())
+                          !allowAccessModification)
                       .build());
             });
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 65822f7..c605951 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -253,9 +253,7 @@
     runTest(
             PACKAGE_NAME,
             mainClass,
-            testBuilder ->
-                testBuilder
-                    .addOptionsModification(disableAggressiveClassOptimizations))
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
         .inspect(
             inspector -> {
               ClassSubject classSubject =
@@ -272,7 +270,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(classSubject, getter);
-                checkMethodIsKept(classSubject, setter);
+                checkMethodIsRemoved(classSubject, setter);
               }
             });
   }
@@ -650,9 +648,7 @@
     runTest(
             PACKAGE_NAME,
             mainClass,
-            testBuilder ->
-                testBuilder
-                    .addOptionsModification(disableAggressiveClassOptimizations))
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
         .inspect(
             inspector -> {
               ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
@@ -671,7 +667,7 @@
                 checkMethodIsRemoved(objectClass, setter);
               } else {
                 checkMethodIsKept(objectClass, getter);
-                checkMethodIsKept(objectClass, setter);
+                checkMethodIsRemoved(objectClass, setter);
               }
 
               if (allowAccessModification) {
@@ -724,9 +720,7 @@
     runTest(
             PACKAGE_NAME,
             mainClass,
-            testBuilder ->
-                testBuilder
-                    .addOptionsModification(disableAggressiveClassOptimizations))
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
         .inspect(
             inspector -> {
               ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
@@ -746,7 +740,7 @@
                 checkMethodIsRemoved(objectClass, setter);
               } else {
                 checkMethodIsKept(objectClass, getter);
-                checkMethodIsKept(objectClass, setter);
+                checkMethodIsRemoved(objectClass, setter);
               }
 
               if (allowAccessModification) {
@@ -765,9 +759,7 @@
     runTest(
             PACKAGE_NAME,
             mainClass,
-            testBuilder ->
-                testBuilder
-                    .addOptionsModification(disableAggressiveClassOptimizations))
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
         .inspect(
             inspector -> {
               ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
@@ -787,7 +779,7 @@
                 checkMethodIsRemoved(objectClass, setter);
               } else {
                 checkMethodIsKept(objectClass, getter);
-                checkMethodIsKept(objectClass, setter);
+                checkMethodIsRemoved(objectClass, setter);
               }
 
               if (allowAccessModification) {
@@ -896,9 +888,7 @@
     runTest(
             PACKAGE_NAME,
             mainClass,
-            testBuilder ->
-                testBuilder
-                    .addOptionsModification(disableAggressiveClassOptimizations))
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
         .inspect(
             inspector -> {
               ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
@@ -916,7 +906,7 @@
               } else {
                 assertTrue(fieldSubject.getField().accessFlags.isPrivate());
                 checkMethodIsKept(objectClass, getter);
-                checkMethodIsKept(objectClass, setter);
+                checkMethodIsRemoved(objectClass, setter);
               }
             });
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java b/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
index e83626b..091a8b4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
@@ -47,7 +48,13 @@
             ImmutableList.of("java.util.Collection", STRING, STRING, "java.lang.Integer"));
 
     final String mainClassName = ex1.getClassName();
-    final String extraRules = neverInlineMethod(mainClassName, testMethodSignature);
+    final String extraRules =
+        StringUtils.lines(
+            neverInlineMethod(mainClassName, testMethodSignature),
+            // TODO(b/173398086): uniqueMethodWithName() does not work with argument removal.
+            "-keepclassmembers,allowoptimization,allowshrinking class non_null.Example1Kt {",
+            "  *** forMakeAndModel(...);",
+            "}");
     runTest(
             FOLDER,
             mainClassName,
@@ -56,7 +63,11 @@
             inspector -> {
               ClassSubject clazz = checkClassIsKept(inspector, ex1.getClassName());
 
-              MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
+              // Verify forMakeAndModel(...) is present in the input.
+              checkMethodPresenceInInput(clazz.getOriginalName(), testMethodSignature, true);
+
+              // Find forMakeAndModel(...) after parameter removal.
+              MethodSubject testMethod = clazz.uniqueMethodWithName(testMethodSignature.name);
               long ifzCount =
                   testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
               long paramNullCheckCount =
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinification.java b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
index aeb142f..3cf01e0 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinification.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
@@ -102,7 +102,7 @@
 class Main {
 
   public static void main(String[] args) {
-    Enum e = Enum.valueOf("VALUE1");
+    Enum e = Enum.valueOf(System.currentTimeMillis() > 0 ? "VALUE1" : null);
     System.out.print(e);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
index 3c52044..c129553 100644
--- a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
@@ -97,7 +97,12 @@
                 getKotlinAnnotationJar(kotlinc))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(mainClassName)
+            .addKeepRules(
+                "-keepconstantarguments class kotlin.jvm.internal.Intrinsics {",
+                "  *** checkParameterIsNotNull(...);",
+                "}")
             .allowDiagnosticWarningMessages()
+            .enableProguardTestOptions()
             .minification(minification)
             .compile()
             .assertAllWarningMessagesMatch(
@@ -158,7 +163,10 @@
                     "-neverclassinline class **." + targetClassName,
                     "-" + NoVerticalClassMergingRule.RULE_NAME + " class **." + targetClassName,
                     "-" + NoHorizontalClassMergingRule.RULE_NAME + " class **." + targetClassName,
-                    "-neverinline class **." + targetClassName + " { <methods>; }"))
+                    "-neverinline class **." + targetClassName + " { <methods>; }",
+                    "-keepconstantarguments class kotlin.jvm.internal.Intrinsics {",
+                    "  *** checkParameterIsNotNull(...);",
+                    "}"))
             .allowDiagnosticWarningMessages()
             .minification(minification)
             .compile()
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
index 1d188fb..a689715 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/MemberResolutionAsmTest.java
@@ -204,8 +204,7 @@
 
     compileResult
         .run(parameters.getRuntime(), swappingMain)
-        .assertFailureWithErrorThatThrows(IllegalAccessError.class)
-        .assertFailureWithErrorThatMatches(containsString(getMethodSignature("X", "y")));
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
 
     CodeInspector codeInspector = compileResult.inspector();
     ClassSubject base = codeInspector.clazz("A");
diff --git a/src/test/java/com/android/tools/r8/naming/overloadaggressively/ValidNameConflictTest.java b/src/test/java/com/android/tools/r8/naming/overloadaggressively/ValidNameConflictTest.java
index 3d9645d..6c1175c 100644
--- a/src/test/java/com/android/tools/r8/naming/overloadaggressively/ValidNameConflictTest.java
+++ b/src/test/java/com/android/tools/r8/naming/overloadaggressively/ValidNameConflictTest.java
@@ -444,10 +444,12 @@
     ProcessResult javaOutput = runOnJavaNoVerifyRaw(builder, CLASS_NAME);
     assertEquals(0, javaOutput.exitCode);
 
-    List<String> pgConfigs = ImmutableList.of(
-        keepMainProguardConfiguration(CLASS_NAME),
-        "-overloadaggressively",
-        "-dontshrink");
+    List<String> pgConfigs =
+        ImmutableList.of(
+            keepMainProguardConfiguration(CLASS_NAME),
+            "-overloadaggressively",
+            "-dontoptimize",
+            "-dontshrink");
     AndroidApp app = compileWithR8(builder, pgConfigs, null, backend);
 
     CodeInspector codeInspector = new CodeInspector(app);
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CheckNotZeroMethodWithArgumentRemovalTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CheckNotZeroMethodWithArgumentRemovalTest.java
new file mode 100644
index 0000000..be25553
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CheckNotZeroMethodWithArgumentRemovalTest.java
@@ -0,0 +1,79 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CheckNotZeroMethodWithArgumentRemovalTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+        .enableInliningAnnotations()
+        // TODO(b/173398086): uniqueMethodWithName() does not work with argument removal.
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject checkNotNullSubject =
+                  mainClassSubject.uniqueMethodWithName("checkNotNull");
+              assertThat(checkNotNullSubject, isPresent());
+              // TODO(b/199864962): Allow parameter removal from check-not-null classified methods.
+              assertEquals(2, checkNotNullSubject.getProgramMethod().getReference().getArity());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      checkNotNull(System.currentTimeMillis() > 0 ? MyEnum.A : null, "x");
+      checkNotNull(System.currentTimeMillis() > 0 ? new Object() : null, "x");
+    }
+
+    @NeverInline
+    static void checkNotNull(Object o, String name) {
+      if (o == null) {
+        throw new NullPointerException("Expected not null, but " + name + " was null");
+      }
+    }
+  }
+
+  enum MyEnum {
+    A,
+    B
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MixedArgumentRemovalAndEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MixedArgumentRemovalAndEnumUnboxingTest.java
index ad4c08a..58f2423 100644
--- a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MixedArgumentRemovalAndEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MixedArgumentRemovalAndEnumUnboxingTest.java
@@ -84,7 +84,7 @@
     }
 
     @NeverInline
-    static void test(Object alwaysNull, MyEnum alwaysA, Object alsoAlwaysNull, MyEnum alwaysB) {
+    static void test(Main alwaysNull, MyEnum alwaysA, Main alsoAlwaysNull, MyEnum alwaysB) {
       if (alwaysNull == null && alsoAlwaysNull == null) {
         System.out.println(alwaysA.name());
         System.out.println(alwaysB.name());
diff --git a/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java b/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
index bf95231..41d6a49 100644
--- a/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverReprocessMethod;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -35,7 +36,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public Regress165825758Test(TestParameters parameters) {
@@ -57,6 +58,7 @@
         .addInnerClasses(Regress165825758Test.class)
         .addKeepMainRule(TestClass.class)
         .addKeepClassRules(A.class)
+        .enableNeverReprocessMethodAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
@@ -99,6 +101,7 @@
   static class A {
 
     @NeverInline
+    @NeverReprocessMethod
     void synchronizedMethod() {
       synchronized (this) {
         TestClass.throwNpe();
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateMethodParameterAnnotationTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateMethodParameterAnnotationTest.java
index d9833ae..8c754e5 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateMethodParameterAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateMethodParameterAnnotationTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -36,6 +37,7 @@
         .addKeepClassRules(NonPublicKeptAnnotation.class)
         .addKeepRuntimeVisibleParameterAnnotations()
         .apply(this::configureRepackaging)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -67,6 +69,7 @@
 
   public static class IneligibleForRepackaging {
 
+    @KeepConstantArguments
     @NeverInline
     public static void greet(@NonPublicKeptAnnotation String greeting) {
       System.out.println(greeting);
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleResolutionWithFailingDispatchTest.java b/src/test/java/com/android/tools/r8/resolution/SingleResolutionWithFailingDispatchTest.java
new file mode 100644
index 0000000..a5acf15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/SingleResolutionWithFailingDispatchTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.resolution;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestRuntime.CfVm;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SingleResolutionWithFailingDispatchTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, I.class, J.class)
+        .addProgramClassFileData(getProgramClassFileData())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::inspectRunResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, I.class, J.class)
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::inspectRunResult);
+  }
+
+  private void inspectRunResult(TestRunResult<?> runResult) {
+    if (parameters.isCfRuntime(CfVm.JDK11)) {
+      runResult.assertFailureWithErrorThatThrows(AbstractMethodError.class);
+    } else {
+      runResult.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+    }
+  }
+
+  private static byte[] getProgramClassFileData() throws IOException {
+    return transformer(A.class).setImplements(I.class, J.class).transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I i = new A();
+      i.m();
+    }
+  }
+
+  interface I {
+
+    default void m() {
+      System.out.println("I.m()");
+    }
+  }
+
+  interface J {
+
+    default void m() {
+      System.out.println("J.m()");
+    }
+  }
+
+  static class A implements I /*, J*/ {}
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
index 473f349..9a14cad 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
@@ -131,10 +131,6 @@
   }
 
   private Class<? extends Throwable> expectedRuntimeError() {
-    if (parameters.isDexRuntime()
-        && parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
-      return IncompatibleClassChangeError.class;
-    }
     return IllegalAccessError.class;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
index cc2005d..8171081 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
@@ -150,7 +150,8 @@
       // virtual dispatch to C.f. See b/140013075.
       runResult.assertSuccessWithOutputLines("Called C.f");
     } else {
-      runResult.assertFailureWithErrorThatMatches(containsString(expectedRuntimeError()));
+      runResult.assertFailureWithErrorThatMatches(
+          containsString(expectedRuntimeError(isCorrectedByR8)));
     }
   }
 
@@ -161,9 +162,10 @@
         && runtime.asDex().getVm().isOlderThanOrEqual(DexVm.ART_7_0_0_HOST);
   }
 
-  private String expectedRuntimeError() {
+  private String expectedRuntimeError(boolean isCorrectedByR8) {
     if (parameters.isDexRuntime()
-        && parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+        && parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)
+        && !isCorrectedByR8) {
       return "IncompatibleClassChangeError";
     }
     return "IllegalAccessError";
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
index 9c596c9..6289967 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
@@ -186,7 +186,7 @@
         .addProgramClasses(getClasses())
         .addProgramClassFileData(getTransformedClasses())
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, false));
   }
 
   @Test
@@ -197,10 +197,10 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, true));
   }
 
-  private void checkExpectedResult(TestRunResult<?> result) {
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
     // If not in the same nest, the error is always illegal access.
     if (!inSameNest) {
       result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
@@ -209,8 +209,8 @@
 
     // If in the same nest but the reference is not exact, the error is always no such method.
     if (!symbolicReferenceIsDefiningType) {
-      // TODO(b/145775365): D8/R8 does not preserve the thrown error.
-      if (parameters.isDexRuntime()) {
+      // TODO(b/145775365): D8 does not preserve the thrown error.
+      if (parameters.isDexRuntime() && !isR8) {
         result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
         return;
       }
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestStaticMethodAccessWithIntermediateClassTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestStaticMethodAccessWithIntermediateClassTest.java
index 9a549f2..795f109 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestStaticMethodAccessWithIntermediateClassTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestStaticMethodAccessWithIntermediateClassTest.java
@@ -82,7 +82,7 @@
         .addProgramClasses(getClasses())
         .addProgramClassFileData(getTransformedClasses())
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, false));
   }
 
   @Test
@@ -93,11 +93,11 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, true));
   }
 
-  private void checkExpectedResult(TestRunResult<?> result) {
-    if (inSameNest && parameters.isCfRuntime()) {
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    if (inSameNest && (parameters.isCfRuntime() || isR8)) {
       result.assertFailureWithErrorThatMatches(containsString(NoSuchMethodError.class.getName()));
     } else {
       result.assertFailureWithErrorThatMatches(containsString(IllegalAccessError.class.getName()));
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestVirtualMethodAccessWithIntermediateClassTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestVirtualMethodAccessWithIntermediateClassTest.java
index ec1222f..9adae80 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestVirtualMethodAccessWithIntermediateClassTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestVirtualMethodAccessWithIntermediateClassTest.java
@@ -81,7 +81,7 @@
         .addProgramClasses(getClasses())
         .addProgramClassFileData(getTransformedClasses())
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, false));
   }
 
   @Test
@@ -92,11 +92,11 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkExpectedResult);
+        .apply(runResult -> checkExpectedResult(runResult, true));
   }
 
-  private void checkExpectedResult(TestRunResult<?> result) {
-    if (inSameNest && parameters.isCfRuntime()) {
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    if (inSameNest && (parameters.isCfRuntime() || isR8)) {
       result.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
     } else {
       result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
index c1a704e..9872e09 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
@@ -109,7 +109,7 @@
         stackTrace.mapping(),
         StringUtils.joinLines(stackTrace.obfuscatedStackTrace()),
         false,
-        StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR,
+        StringUtils.joinLines(stackTrace.retraceVerboseStackTrace()) + StringUtils.LINE_SEPARATOR,
         "--verbose");
   }
 
@@ -120,7 +120,7 @@
         stackTrace.mapping(),
         StringUtils.joinLines(stackTrace.obfuscatedStackTrace()),
         false,
-        StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR,
+        StringUtils.joinLines(stackTrace.retraceVerboseStackTrace()) + StringUtils.LINE_SEPARATOR,
         "-verbose");
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
index 8cc0883..3a8fd5b 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -19,14 +19,17 @@
 import com.android.tools.r8.retrace.stacktraces.ActualBotStackTraceBase;
 import com.android.tools.r8.retrace.stacktraces.ActualIdentityStackTrace;
 import com.android.tools.r8.retrace.stacktraces.ActualRetraceBotStackTrace;
+import com.android.tools.r8.retrace.stacktraces.AmbiguousMethodVerboseStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousMissingLineStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousWithMultipleLineMappingsStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousWithSignatureNonVerboseStackTrace;
+import com.android.tools.r8.retrace.stacktraces.AmbiguousWithSignatureVerboseStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AutoStackTrace;
 import com.android.tools.r8.retrace.stacktraces.CircularReferenceStackTrace;
 import com.android.tools.r8.retrace.stacktraces.ColonInFileNameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.FileNameExtensionStackTrace;
+import com.android.tools.r8.retrace.stacktraces.FoundMethodVerboseStackTrace;
 import com.android.tools.r8.retrace.stacktraces.InlineFileNameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.InlineFileNameWithInnerClassesStackTrace;
 import com.android.tools.r8.retrace.stacktraces.InlineNoLineNumberStackTrace;
@@ -53,6 +56,7 @@
 import com.android.tools.r8.retrace.stacktraces.SyntheticLambdaMethodWithInliningStackTrace;
 import com.android.tools.r8.retrace.stacktraces.UnicodeInFileNameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.UnknownSourceStackTrace;
+import com.android.tools.r8.retrace.stacktraces.VerboseUnknownStackTrace;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -71,17 +75,20 @@
 @RunWith(Parameterized.class)
 public class RetraceTests extends TestBase {
 
-  @Parameters(name = "{0}, external: {1}")
+  @Parameters(name = "{0}, external: {1}, verbose: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(getTestParameters().withCfRuntimes().build(), BooleanUtils.values());
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(), BooleanUtils.values(), BooleanUtils.values());
   }
 
   private final TestParameters testParameters;
   private final boolean external;
+  private final boolean verbose;
 
-  public RetraceTests(TestParameters parameters, boolean external) {
+  public RetraceTests(TestParameters parameters, boolean external, boolean verbose) {
     this.testParameters = parameters;
     this.external = external;
+    this.verbose = verbose;
   }
 
   @Test
@@ -282,6 +289,26 @@
     runRetraceTest(new MultipleLinesNoLineNumberStackTrace());
   }
 
+  @Test
+  public void testFoundMethod() throws Exception {
+    runRetraceTest(new FoundMethodVerboseStackTrace());
+  }
+
+  @Test
+  public void testUnknownMethod() throws Exception {
+    runRetraceTest(new AmbiguousMethodVerboseStackTrace());
+  }
+
+  @Test
+  public void testVerboseUnknownMethod() throws Exception {
+    runRetraceTest(new VerboseUnknownStackTrace());
+  }
+
+  @Test
+  public void testAmbiguousMissingLineVerbose() throws Exception {
+    runRetraceTest(new AmbiguousWithSignatureVerboseStackTrace());
+  }
+
   private void inspectRetraceTest(
       StackTraceForTest stackTraceForTest, Consumer<Retracer> inspection) {
     inspection.accept(
@@ -302,6 +329,11 @@
 
   private TestDiagnosticMessagesImpl runRetraceTest(
       StackTraceForTest stackTraceForTest, boolean allowExperimentalMapping) throws Exception {
+    String expectedStackTrace =
+        StringUtils.joinLines(
+            verbose
+                ? stackTraceForTest.retraceVerboseStackTrace()
+                : stackTraceForTest.retracedStackTrace());
     if (external) {
       assumeTrue(testParameters.isCfRuntime());
       // The external dependency is built on top of R8Lib. If test.py is run with
@@ -327,13 +359,13 @@
       command.add("com.android.tools.r8.retrace.Retrace");
       command.add(mappingFile.toString());
       command.add(stackTraceFile.toString());
+      if (verbose) {
+        command.add("-verbose");
+      }
       command.add("-quiet");
       ProcessBuilder builder = new ProcessBuilder(command);
       ProcessResult processResult = ToolHelper.runProcess(builder);
-      assertEquals(
-          StringUtils.joinLines(stackTraceForTest.retracedStackTrace())
-              + StringUtils.LINE_SEPARATOR,
-          processResult.stdout);
+      assertEquals(expectedStackTrace + StringUtils.LINE_SEPARATOR, processResult.stdout);
       // TODO(b/177204438): Parse diagnostics from stdErr
       return new TestDiagnosticMessagesImpl();
     } else {
@@ -343,10 +375,8 @@
               .setProguardMapProducer(ProguardMapProducer.fromString(stackTraceForTest.mapping()))
               .setStackTrace(stackTraceForTest.obfuscatedStackTrace())
               .setRetracedStackTraceConsumer(
-                  retraced ->
-                      assertEquals(
-                          StringUtils.joinLines(stackTraceForTest.retracedStackTrace()),
-                          StringUtils.joinLines(retraced)))
+                  retraced -> assertEquals(expectedStackTrace, StringUtils.joinLines(retraced)))
+              .setVerbose(verbose)
               .build();
       Retrace.runForTesting(retraceCommand, allowExperimentalMapping);
       return diagnosticsHandler;
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java
deleted file mode 100644
index 8f8e049..0000000
--- a/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2019, 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.retrace;
-
-import static junit.framework.TestCase.assertEquals;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestDiagnosticMessagesImpl;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.retrace.stacktraces.AmbiguousMethodVerboseStackTrace;
-import com.android.tools.r8.retrace.stacktraces.AmbiguousWithSignatureVerboseStackTrace;
-import com.android.tools.r8.retrace.stacktraces.FoundMethodVerboseStackTrace;
-import com.android.tools.r8.retrace.stacktraces.StackTraceForTest;
-import com.android.tools.r8.retrace.stacktraces.VerboseUnknownStackTrace;
-import java.util.Collection;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class RetraceVerboseTests extends TestBase {
-
-  @Parameters(name = "{0}")
-  public static Collection<Object[]> data() {
-    return buildParameters(getTestParameters().withNoneRuntime().build());
-  }
-
-  public RetraceVerboseTests(TestParameters parameters) {}
-
-  @Test
-  public void testFoundMethod() {
-    runRetraceTest(new FoundMethodVerboseStackTrace());
-  }
-
-  @Test
-  public void testUnknownMethod() {
-    runRetraceTest(new AmbiguousMethodVerboseStackTrace());
-  }
-
-  @Test
-  public void testVerboseUnknownMethod() {
-    runRetraceTest(new VerboseUnknownStackTrace());
-  }
-
-  @Test
-  public void testAmbiguousMissingLineVerbose() {
-    assumeTrue("b/169346455", false);
-    runRetraceTest(new AmbiguousWithSignatureVerboseStackTrace());
-  }
-
-  private void runRetraceTest(StackTraceForTest stackTraceForTest) {
-    TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
-    RetraceCommand retraceCommand =
-        RetraceCommand.builder(diagnosticsHandler)
-            .setProguardMapProducer(ProguardMapProducer.fromString(stackTraceForTest.mapping()))
-            .setStackTrace(stackTraceForTest.obfuscatedStackTrace())
-            .setVerbose(true)
-            .setRetracedStackTraceConsumer(
-                retraced -> assertEquals(stackTraceForTest.retracedStackTrace(), retraced))
-            .build();
-    Retrace.run(retraceCommand);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/retrace/SourceFileTest.java b/src/test/java/com/android/tools/r8/retrace/SourceFileTest.java
index 30dc206..5bc7a26 100644
--- a/src/test/java/com/android/tools/r8/retrace/SourceFileTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/SourceFileTest.java
@@ -63,7 +63,7 @@
                   .clazz(ClassWithCustomFileName.class)
                   .retraceUnique()
                   .getSourceFile()
-                  .getFilename());
+                  .getSourceFile());
         }));
   }
 
@@ -78,7 +78,7 @@
               inspector.clazz(ClassWithoutCustomFileName.class).retraceUnique();
           assertEquals(
               "SourceFileTest.java",
-              RetraceUtils.inferFileName(
+              RetraceUtils.inferSourceFile(
                   retraceClassElement.getRetracedClass().getTypeName(), "nofile.java", true));
         }));
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/StackTraceRegularExpressionParserTests.java b/src/test/java/com/android/tools/r8/retrace/StackTraceRegularExpressionParserTests.java
index f74fcda..9af1378 100644
--- a/src/test/java/com/android/tools/r8/retrace/StackTraceRegularExpressionParserTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/StackTraceRegularExpressionParserTests.java
@@ -10,10 +10,10 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.retrace.stacktraces.InlineFileNameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.RetraceAssertionErrorStackTrace;
 import com.android.tools.r8.retrace.stacktraces.StackTraceForTest;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.Collections;
@@ -27,13 +27,16 @@
 @RunWith(Parameterized.class)
 public class StackTraceRegularExpressionParserTests extends TestBase {
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withNoneRuntime().build();
+  @Parameters(name = "{0}, verbose: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), BooleanUtils.values());
   }
 
-  public StackTraceRegularExpressionParserTests(TestParameters parameters) {
+  private final boolean verbose;
+
+  public StackTraceRegularExpressionParserTests(TestParameters parameters, boolean verbose) {
     parameters.assertNoneRuntime();
+    this.verbose = verbose;
   }
 
   @Test
@@ -57,6 +60,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("foocom.android.tools.r8.a");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -86,6 +94,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("AA.AA.AA b.b.b c.c.c");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -114,6 +127,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("AA/AA b/b/b c/c/c");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -141,6 +159,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -168,6 +191,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.void foo()");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -195,6 +223,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.a");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -227,6 +260,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -254,6 +292,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.int foo");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -281,6 +324,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.a");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -303,6 +351,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return Collections.singletonList("foo.Bar$Baz.int baz(Bar.java)");
+          }
+
+          @Override
           public String mapping() {
             return StringUtils.lines(
                 "com.android.tools.r8.naming.retrace.Main -> a.b.c:",
@@ -337,6 +390,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8(R8.java)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -364,6 +422,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("a.b.d(SourceFile)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -392,6 +455,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.boolean foo()(7)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -420,6 +488,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.boolean foo()(7)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -448,6 +521,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.boolean foo()(42)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -475,6 +553,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.boolean foo()(4)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -505,6 +588,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.boolean foo()(7)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -532,6 +620,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("void", "a.a.a[]", "a.a.a[][][]");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -565,6 +658,15 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            // TODO(b/199919195): Consider not writing full method description.
+            return ImmutableList.of(
+                "void com.android.tools.r8.R8.void foo()",
+                "com.android.tools.r8.D8[] com.android.tools.r8.R8.com.android.tools.r8.D8[]"
+                    + " bar()");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -601,6 +703,13 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of(
+                "void com.android.tools.r8.R8.foo",
+                "com.android.tools.r8.D8[] com.android.tools.r8.R8.bar");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -628,6 +737,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("void");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -659,6 +773,13 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of(
+                "com.android.tools.r8.R8.void foo(int,com.android.tools.r8.D8[],boolean)"
+                    + "(int,com.android.tools.r8.D8[],boolean)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -689,6 +810,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.void foo()()");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -721,6 +847,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.bar(com.android.tools.r8.D8)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -759,6 +890,14 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of(
+                "com.android.tools.r8.R8: foo bar baz",
+                "  at com.android.tools.r8.Bar.void foo()(Bar.java)",
+                "  at com.android.tools.r8.Baz.void bar()(Baz.java)");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -794,6 +933,11 @@
           }
 
           @Override
+          public List<String> retraceVerboseStackTrace() {
+            return ImmutableList.of("%c\\com.android.tools.r8.Bar\\%c.void m()(\\Bar.java:13)\\%S");
+          }
+
+          @Override
           public int expectedWarnings() {
             return 0;
           }
@@ -809,9 +953,14 @@
             .setStackTrace(stackTraceForTest.obfuscatedStackTrace())
             .setRetracedStackTraceConsumer(
                 retraced -> {
-                  assertEquals(stackTraceForTest.retracedStackTrace(), retraced);
+                  assertEquals(
+                      verbose
+                          ? stackTraceForTest.retraceVerboseStackTrace()
+                          : stackTraceForTest.retracedStackTrace(),
+                      retraced);
                 })
             .setRegularExpression(regularExpression)
+            .setVerbose(verbose)
             .build();
     Retrace.run(retraceCommand);
     return diagnosticsHandler;
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiInferSourceFileTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiInferSourceFileTest.java
index 829969d..5563fea 100644
--- a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiInferSourceFileTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiInferSourceFileTest.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.ProguardMapProducer;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import java.util.ArrayList;
 import java.util.List;
@@ -40,7 +40,7 @@
 
     @Test
     public void testRetracingSourceFile() {
-      List<RetraceSourceFileResult> sourceFileResults = new ArrayList<>();
+      List<RetracedSourceFile> sourceFileResults = new ArrayList<>();
       Retracer.createDefault(ProguardMapProducer.fromString(mapping), new DiagnosticsHandler() {})
           .retraceClass(Reference.classFromTypeName("a"))
           .forEach(clazz -> sourceFileResults.add(clazz.getSourceFile()));
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileNotFoundTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileNotFoundTest.java
index a567df1..29a97e9 100644
--- a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileNotFoundTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileNotFoundTest.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.ProguardMapProducer;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import java.util.ArrayList;
 import java.util.List;
@@ -40,7 +40,7 @@
 
     @Test
     public void testRetracingSourceFile() {
-      List<RetraceSourceFileResult> sourceFileResults = new ArrayList<>();
+      List<RetracedSourceFile> sourceFileResults = new ArrayList<>();
       Retracer.createDefault(ProguardMapProducer.fromString(mapping), new DiagnosticsHandler() {})
           .retraceClass(Reference.classFromTypeName("a"))
           .forEach(clazz -> sourceFileResults.add(clazz.getSourceFile()));
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileTest.java
index f94e0e4..e3152e8 100644
--- a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSourceFileTest.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.ProguardMapProducer;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
+import com.android.tools.r8.retrace.RetracedSourceFile;
 import com.android.tools.r8.retrace.Retracer;
 import java.util.ArrayList;
 import java.util.List;
@@ -41,13 +41,13 @@
 
     @Test
     public void testRetracingSourceFile() {
-      List<RetraceSourceFileResult> sourceFileResults = new ArrayList<>();
+      List<RetracedSourceFile> sourceFileResults = new ArrayList<>();
       Retracer.createDefault(ProguardMapProducer.fromString(mapping), new DiagnosticsHandler() {})
           .retraceClass(Reference.classFromTypeName("a"))
           .forEach(clazz -> sourceFileResults.add(clazz.getSourceFile()));
       assertEquals(1, sourceFileResults.size());
       assertTrue(sourceFileResults.get(0).hasRetraceResult());
-      assertEquals("SomeFileName.kt", sourceFileResults.get(0).getFilename());
+      assertEquals("SomeFileName.kt", sourceFileResults.get(0).getSourceFile());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedFrameTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedFrameTest.java
index e7da9a7..e1ad580 100644
--- a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedFrameTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedFrameTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.retrace.Retracer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,7 +49,7 @@
                   ProguardMapProducer.fromString(mapping), new DiagnosticsHandler() {})
               .retraceClass(Reference.classFromTypeName("a"))
               .stream()
-              .flatMap(element -> element.lookupFrame("a", 3).stream())
+              .flatMap(element -> element.lookupFrame(Optional.of(3), "a").stream())
               .collect(Collectors.toList());
       assertEquals(1, frameResults.size());
       RetraceFrameElement retraceFrameElement = frameResults.get(0);
diff --git a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedInnerFrameTest.java b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedInnerFrameTest.java
index 96aa22d..73367a6 100644
--- a/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedInnerFrameTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/api/RetraceApiSynthesizedInnerFrameTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.retrace.Retracer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,7 +49,7 @@
                   ProguardMapProducer.fromString(mapping), new DiagnosticsHandler() {})
               .retraceClass(Reference.classFromTypeName("a"))
               .stream()
-              .flatMap(element -> element.lookupFrame("a", 3).stream())
+              .flatMap(element -> element.lookupFrame(Optional.of(3), "a").stream())
               .collect(Collectors.toList());
       assertEquals(1, frameResults.size());
       RetraceFrameElement retraceFrameElement = frameResults.get(0);
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java
index 9ac7068..1b78928 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java
@@ -55,6 +55,11 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return obfuscatedStackTrace();
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java
index 0e38636..8ffd732 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java
@@ -67,6 +67,38 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "com.android.tools.r8.CompilationFailedException: Compilation failed to complete",
+        "\tat com.android.tools.r8.BaseCommand$Builder."
+            + "com.android.tools.r8.BaseCommand build()(BaseCommand.java:143)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:104)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:29)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.compile(TestCompilerBuilder.java:89)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.run(TestCompilerBuilder.java:113)",
+        "\tat com.android.tools.r8.TestBuilder.run(TestBuilder.java:49)",
+        "\tat com.android.tools.r8.ir.optimize.classinliner.ClassInlinerTest.testCodeSample(ClassInlinerTest.java:289)",
+        "",
+        "Caused by:",
+        "com.android.tools.r8.utils.AbortException: Error: offset: 158, line: 2, column: 33,"
+            + " Unexpected attribute at <no file>:2:33",
+        "-keepattributes -keepattributes LineNumberTable",
+        "                                ^",
+        "\tat com.android.tools.r8.utils.Reporter.void failIfPendingErrors()(Reporter.java:101)",
+        "\tat com.android.tools.r8.shaking.ProguardConfigurationParser."
+            + "void parse(java.util.List)(ProguardConfigurationParser.java:187)",
+        "\tat com.android.tools.r8.R8Command$Builder."
+            + "com.android.tools.r8.R8Command makeR8Command()(R8Command.java:432)",
+        "\tat com.android.tools.r8.R8Command$Builder."
+            + "com.android.tools.r8.R8Command makeCommand()(R8Command.java:413)",
+        "\tat com.android.tools.r8.R8Command$Builder."
+            + "com.android.tools.r8.BaseCommand makeCommand()(R8Command.java:61)",
+        "\tat com.android.tools.r8.BaseCommand$Builder."
+            + "com.android.tools.r8.BaseCommand build()(BaseCommand.java:139)",
+        "\t... 6 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTraceWithInfo.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTraceWithInfo.java
index d57b024..ce0441d 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTraceWithInfo.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTraceWithInfo.java
@@ -69,6 +69,32 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "com.android.tools.r8.CompilationFailedException: Compilation failed to complete",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(BaseCommand.java:143)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:104)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:29)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.compile(TestCompilerBuilder.java:89)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.run(TestCompilerBuilder.java:113)",
+        "\tat com.android.tools.r8.TestBuilder.run(TestBuilder.java:49)",
+        "\tat com.android.tools.r8.ir.optimize.classinliner.ClassInlinerTest.testCodeSample(ClassInlinerTest.java:289)",
+        "",
+        "Caused by:",
+        "com.android.tools.r8.utils.AbortException: Error: offset: 158, line: 2, column: 33,"
+            + " Unexpected attribute at <no file>:2:33",
+        "-keepattributes -keepattributes LineNumberTable",
+        "                                ^",
+        "\tat com.android.tools.r8.utils.Reporter.failIfPendingErrors(Reporter.java:101)",
+        "\tat com.android.tools.r8.shaking.ProguardConfigurationParser.parse(ProguardConfigurationParser.java:187)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeR8Command(R8Command.java:432)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeCommand(R8Command.java:413)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeCommand(R8Command.java:61)",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(BaseCommand.java:139)",
+        "\t... 6 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 1;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMethodVerboseStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMethodVerboseStackTrace.java
index 3d3a1ee..5ce9e89 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMethodVerboseStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMethodVerboseStackTrace.java
@@ -33,12 +33,26 @@
     return Arrays.asList(
         "Exception in thread \"main\" java.lang.NullPointerException",
         "\tat com.android.tools.r8.naming.retrace.Main.c(Main.java)",
-        "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo main("
-            + "java.lang.String[])(Main.java)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.void main("
-            + "com.android.Bar)(Main.java)",
-        "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo main("
-            + "java.lang.String[],com.android.Bar)(Main.java)");
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 2 ambiguous stack traces.",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.c(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo"
+            + " main(java.lang.String[])(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo"
+            + " main(java.lang.String[],com.android.Bar)(Main.java)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.c(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(com.android.Bar)(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo"
+            + " main(java.lang.String[],com.android.Bar)(Main.java)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMissingLineStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMissingLineStackTrace.java
index 310390c..3442711 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMissingLineStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousMissingLineStackTrace.java
@@ -25,15 +25,82 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 8 ambiguous stack traces. Use --verbose to have all listed.",
         "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java:7)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java:7)",
         "    at com.android.tools.r8.R8.bar(R8.java:8)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java:8)",
         "    at com.android.tools.r8.R8.main(Unknown Source)",
         "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java:9)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java:9)",
+        "    ... 42 more");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 8 ambiguous stack traces.",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java:9)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:7)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:8)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java:9)",
         "    ... 42 more");
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousStackTrace.java
index 38fbb08..989868f 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousStackTrace.java
@@ -24,43 +24,83 @@
 
   @Override
   public List<String> retracedStackTrace() {
-    // ProGuard version shows ambiguous traces differently:
-    //
-    // Proguard Retrace:
-    // com.android.tools.r8.CompilationException: foo[parens](Source:3)
-    //   at com.android.tools.r8.R8.foo(R8.java)
-    //                              bar(R8.java)
-    //   at com.android.tools.r8.R8.foo(R8.java)
-    //                              bar(R8.java)
-    //   at com.android.tools.r8.R8.main(r8.java)
-    // Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)
-    //   at com.android.tools.r8.R8.foo(R8.java)
-    //                              bar(R8.java)
-    //   ... 42 more
-    //
-    // Other Retrace:
-    // com.android.tools.r8.CompilationException: foo[parens](Source:3)
-    //   at com.android.tools.r8.R8.foo(R8.java)
-    //   at <OR> com.android.tools.r8.R8.bar(R8.java)
-    //   at com.android.tools.r8.R8.foo(R8.java)
-    //   at <OR> com.android.tools.r8.R8.bar(R8.java)
-    //   at com.android.tools.r8.R8.main(r8.java)
-    // Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)
-    //   at <OR> com.android.tools.r8.R8.bar(R8.java)
-    //   at com.android.tools.r8.R8(r8.java)
-    //   ... 42 more
-    //
-    // We have decided on the format below.
     return Arrays.asList(
+        "There are 8 ambiguous stack traces. Use --verbose to have all listed.",
         "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
         "    at com.android.tools.r8.R8.main(Unknown Source)",
         "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
+        "    ... 42 more");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 8 ambiguous stack traces.",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
         "    ... 42 more");
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithMultipleLineMappingsStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithMultipleLineMappingsStackTrace.java
index 4543fab..1041ace 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithMultipleLineMappingsStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithMultipleLineMappingsStackTrace.java
@@ -36,6 +36,14 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int)(Internal.java)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureNonVerboseStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureNonVerboseStackTrace.java
index e4dc987..97f2162 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureNonVerboseStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureNonVerboseStackTrace.java
@@ -37,6 +37,27 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 4 ambiguous stack traces.",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.boolean foo(int,int)(Internal.java)",
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int)(Internal.java)",
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int,boolean)(Internal.java)",
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int,int)(Internal.java)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
index 45c103f..23991fb 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
@@ -33,10 +33,28 @@
     return Arrays.asList(
         "java.lang.IndexOutOfBoundsException",
         "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.foo(Internal.java)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 4 ambiguous stack traces.",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
         "\tat com.android.tools.r8.Internal.boolean foo(int,int)(Internal.java)",
-        "\t<OR> at com.android.tools.r8.Internal.void foo(int)(Internal.java)",
-        "\t<OR> at com.android.tools.r8.Internal.void foo(int,boolean)(Internal.java)",
-        "\t<OR> at com.android.tools.r8.Internal.void foo(int,int)(Internal.java)");
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int)(Internal.java)",
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int,boolean)(Internal.java)",
+        "< OR >",
+        "java.lang.IndexOutOfBoundsException",
+        "\tat java.util.ArrayList.get(ArrayList.java:411)",
+        "\tat com.android.tools.r8.Internal.void foo(int,int)(Internal.java)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AutoStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AutoStackTrace.java
index 0504b58..ab7f261 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AutoStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AutoStackTrace.java
@@ -35,6 +35,14 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return ImmutableList.of(
+        "java.io.IOException: INVALID_SENDER",
+        "\tat com.android.tools.r8.AutoTest.void foo(int)(AutoTest.java:200)",
+        "\tat com.android.tools.r8.AutoTest.void foo(int,int)(AutoTest.java:24)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java
index 5df37f8..c0b1977 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java
@@ -43,6 +43,19 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "        [CIRCULAR REFERENCE:foo.bar.Baz]",
+        " [CIRCULAR REFERENCE:foo.bar.Qux]",
+        "        [CIRCULAR REFERENCE:None.existing.class]",
+        "        [CIRCULAR REFERENCE:foo.bar.Baz] ",
+        "        [CIRCU:AA]",
+        "        [CIRCULAR REFERENCE:A.A",
+        "        [CIRCULAR REFERENCE:]",
+        "        [CIRCULAR REFERENCE:None existing class]");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 5;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ColonInFileNameStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ColonInFileNameStackTrace.java
index 0bdb870..618b83f 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ColonInFileNameStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ColonInFileNameStackTrace.java
@@ -29,6 +29,11 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return ImmutableList.of("  at some.Class.int strawberry(int)(Class.kt:99)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java
index dc77ca6..06cc043 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java
@@ -52,6 +52,24 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at R8.main(R8.java:800)",
+        "    at R8.main(Native Method)",
+        "    at R8.main(R8.java:)",
+        "    at R8.main(R8.kt:1)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java:1)",
+        "Suppressed: foo.bar.baz: You have to write the program first",
+        "    at R8.retrace(R8.java:184)",
+        "    ... 7 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/FoundMethodVerboseStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/FoundMethodVerboseStackTrace.java
index ce7a8f5..b4812a5 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/FoundMethodVerboseStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/FoundMethodVerboseStackTrace.java
@@ -27,6 +27,13 @@
   public List<String> retracedStackTrace() {
     return Arrays.asList(
         "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java:102)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
         "\tat com.android.tools.r8.naming.retrace.Main.com.android.Foo main(java.lang.String[],"
             + "com.android.Bar)(Main.java:102)");
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameStackTrace.java
index ef36965..048b941 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameStackTrace.java
@@ -28,6 +28,17 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat foo.Bar$Baz.void baz(long)(Bar.java:0)",
+        "\tat Foo$Bar.void bar(int)(Foo.java:2)",
+        "\tat com.android.tools.r8.naming.retrace.Main$Foo"
+            + ".void method1(java.lang.String)(Main.java:8)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:7)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main:",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameWithInnerClassesStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameWithInnerClassesStackTrace.java
index 8d7551a..177eb8a 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameWithInnerClassesStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineFileNameWithInnerClassesStackTrace.java
@@ -26,6 +26,14 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat foo.Bar$Baz$Qux.void baz(long)(Bar.java:0)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:7)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main:",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineNoLineNumberStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineNoLineNumberStackTrace.java
index dda09dd..763ec80 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineNoLineNumberStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineNoLineNumberStackTrace.java
@@ -28,6 +28,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method3(long)(Main.java:0)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method2(int)(Main.java:0)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method1(java.lang.String)(Main.java:0)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:0)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main:",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineSourceFileContextStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineSourceFileContextStackTrace.java
index 3c22097..f5943b5 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineSourceFileContextStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineSourceFileContextStackTrace.java
@@ -48,6 +48,17 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "  at com.google.appreduce.remapper.KotlinJavaSourceFileTestLibrary"
+            + ".void throwsException()(KotlinJavaSourceFileTestLibrary.kt:22)",
+        "  at com.google.appreduce.remapper.KotlinJavaSourceFileTestLibrary"
+            + ".void callsThrowsException()(KotlinJavaSourceFileTestLibrary.kt:19)",
+        "  at com.google.appreduce.remapper.KotlinJavaSourceFileTestObject"
+            + ".void main(java.lang.String[])(KotlinJavaSourceFileTestObject.java:32)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineWithLineNumbersStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineWithLineNumbersStackTrace.java
index 59c9693..eb1144e 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineWithLineNumbersStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InlineWithLineNumbersStackTrace.java
@@ -44,6 +44,18 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method3(long)(Main.java:81)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method2(int)(Main.java:88)",
+        "\tat com.android.tools.r8.naming.retrace.Main."
+            + "void method1(java.lang.String)(Main.java:96)",
+        "\tat com.android.tools.r8.naming.retrace.Main."
+            + "void main(java.lang.String[])(Main.java:102)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java
index 87d30d5..737a32a 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java
@@ -35,6 +35,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "  hulubulu",
+        "  XXX, where are you",
+        "foo.bar.baz: Problem when compiling program",
+        " . . . 7 more",
+        "  ... 7 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 1;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/MemberFieldOverlapStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/MemberFieldOverlapStackTrace.java
index 9b5ba56..ca6b1c9 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/MemberFieldOverlapStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/MemberFieldOverlapStackTrace.java
@@ -38,6 +38,13 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat foo.Bar.int method()(Bar.java:5)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 1;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleDotsInFileNameStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleDotsInFileNameStackTrace.java
index a67335f..43ac3ab 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleDotsInFileNameStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleDotsInFileNameStackTrace.java
@@ -29,6 +29,11 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return ImmutableList.of("  at some.Class.int strawberry(int)(Class.kt:99)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleLinesNoLineNumberStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleLinesNoLineNumberStackTrace.java
index 54ef0bf..004b7d5 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleLinesNoLineNumberStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/MultipleLinesNoLineNumberStackTrace.java
@@ -29,10 +29,21 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 2 ambiguous stack traces. Use --verbose to have all listed.",
         "Exception in thread \"main\" java.lang.NullPointerException",
-        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.method1(Main.java)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.main(Main.java)");
+        "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 2 ambiguous stack traces.",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void method1(java.lang.String)(Main.java)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/NamedModuleStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/NamedModuleStackTrace.java
index 8bd402f..1866585 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/NamedModuleStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/NamedModuleStackTrace.java
@@ -50,6 +50,17 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "SomeFakeException: this is a fake exception",
+        "\tat com.android.tools.r8.Classloader/named_module@9.0/com.android.tools.r8.Main.main(Main.java:1)",
+        "\tat com.android.tools.r8.Classloader//com.android.tools.r8.Main.foo(Main.java:2)",
+        "\tat named_module@2.1/com.android.tools.r8.Main.bar(Main.java:3)",
+        "\tat named_module/com.android.tools.r8.Main.baz(Main.java:4)",
+        "\tat com.android.tools.r8.Main.qux(Main.java:5)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscatedLineNumberWithOverrideTest.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscatedLineNumberWithOverrideTest.java
index 23c5652..858c06b 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscatedLineNumberWithOverrideTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscatedLineNumberWithOverrideTest.java
@@ -35,16 +35,52 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 4 ambiguous stack traces. Use --verbose to have all listed.",
         "Exception in thread \"main\" java.lang.NullPointerException",
         "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java:3)",
         "\tat com.android.tools.r8.naming.retrace.Main.overload1(Main.java:7)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.overload2(Main.java:11)",
         "\tat com.android.tools.r8.naming.retrace.Main.definedOverload(Main.java:7)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.definedOverload(Main.java:11)",
         "\tat com.android.tools.r8.naming.retrace.Main.mainPC(Main.java:42)");
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 4 ambiguous stack traces.",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String)(Main.java:3)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void overload1()(Main.java:7)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void definedOverload()(Main.java:7)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " mainPC(java.lang.String[])(Main.java:42)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String)(Main.java:3)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void overload1()(Main.java:7)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " definedOverload(java.lang.String)(Main.java:11)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " mainPC(java.lang.String[])(Main.java:42)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String)(Main.java:3)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " overload2(java.lang.String)(Main.java:11)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void definedOverload()(Main.java:7)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " mainPC(java.lang.String[])(Main.java:42)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String)(Main.java:3)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " overload2(java.lang.String)(Main.java:11)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " definedOverload(java.lang.String)(Main.java:11)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " mainPC(java.lang.String[])(Main.java:42)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscationRangeMappingWithStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscationRangeMappingWithStackTrace.java
index 3f944e2..579541f 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscationRangeMappingWithStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/NoObfuscationRangeMappingWithStackTrace.java
@@ -31,6 +31,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void foo(long)(Main.java:1)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void bar(int)(Main.java:3)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void baz()(Main.java:8)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:7)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "com.android.tools.r8.naming.retrace.Main -> foo:",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java
index 99aa584..9cd0c45 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java
@@ -34,6 +34,12 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    fail();
+    return null;
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java
index 707874d..de7673b 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java
@@ -35,6 +35,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Caused by: foo.bar.baz: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfuscatedRangeToSingleLineStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfuscatedRangeToSingleLineStackTrace.java
index 1579756..e87126f 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfuscatedRangeToSingleLineStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfuscatedRangeToSingleLineStackTrace.java
@@ -40,6 +40,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "UnknownException: This is just a fake exception",
+        "  at foo.bar.Baz.void qux()(Baz.java:27)",
+        "  at foo.bar.Baz.void qux()(Baz.java:42)",
+        "  at foo.bar.Baz.void quux()(Baz.java:113)",
+        "  at foo.bar.Baz.void quuz()(Baz.java:72)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/OverloadSameLineTest.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/OverloadSameLineTest.java
index f1fbedb..c5fe164 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/OverloadSameLineTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/OverloadSameLineTest.java
@@ -29,11 +29,24 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 3 ambiguous stack traces. Use --verbose to have all listed.",
         "Exception in thread \"main\" java.lang.NullPointerException",
-        // TODO(b/199058242): Should be ambiguous and not inline frames
-        "\tat com.android.tools.r8.naming.retrace.Main.overload(Main.java:7)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.overload(Main.java:15)",
-        "\t<OR #2> at com.android.tools.r8.naming.retrace.Main.overload(Main.java:13)");
+        "\tat com.android.tools.r8.naming.retrace.Main.overload(Main.java:7)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 3 ambiguous stack traces.",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void overload()(Main.java:7)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void overload(int)(Main.java:15)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " overload(java.lang.String)(Main.java:13)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/PGStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/PGStackTrace.java
index 8698ac9..2a3a63a 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/PGStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/PGStackTrace.java
@@ -45,6 +45,19 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "09-16 15:43:01.249 23316 23316 E AndroidRuntime: java.lang.NullPointerException: Attempt"
+            + " to invoke virtual method 'boolean"
+            + " com.google.android.foo(com.google.android.foo.Data$Key)' on a null object"
+            + " reference",
+        "09-16 15:43:01.249 23316 23316 E AndroidRuntime:        at"
+            + " com.google.apps.sectionheader.SectionHeaderListController.onToolbarStateChanged(SectionHeaderListController.java:586)",
+        "09-16 15:43:01.249 23316 23316 E AndroidRuntime:        at"
+            + " com.google.apps.Controller.onToolbarStateChanged(Controller.java:1087)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/RetraceAssertionErrorStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/RetraceAssertionErrorStackTrace.java
index d6bcacc..11d2669 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/RetraceAssertionErrorStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/RetraceAssertionErrorStackTrace.java
@@ -36,6 +36,23 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "java.lang.AssertionError",
+        "        at com.android.tools.r8.retrace.RetraceCore$StackTraceNode."
+            + "void <init>(java.util.List)(RetraceCore.java:31)",
+        "        at com.android.tools.r8.retrace.RetraceCore."
+            + "void retraceLine(java.util.List,int,java.util.List)(RetraceCore.java:117)",
+        "        at com.android.tools.r8.retrace.RetraceCore."
+            + "com.android.tools.r8.retrace."
+            + "RetraceCore$RetraceResult retrace()(RetraceCore.java:107)",
+        "        at com.android.tools.r8.retrace.Retrace."
+            + "void run(com.android.tools.r8.retrace.RetraceCommand)(Retrace.java:116)",
+        "        at com.android.tools.r8.retrace.RetraceTests."
+            + "testNullLineTrace(RetraceTests.java:73)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "com.android.tools.r8.retrace.Retrace -> com.android.tools.r8.retrace.Retrace:",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SingleLineNoLineNumberStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SingleLineNoLineNumberStackTrace.java
index 0b0c7ef..120cd8e 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SingleLineNoLineNumberStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SingleLineNoLineNumberStackTrace.java
@@ -36,17 +36,48 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 2 ambiguous stack traces. Use --verbose to have all listed.",
         "Exception in thread \"main\" java.lang.NullPointerException",
         "\tat com.android.tools.r8.naming.retrace.Main.method1(Main.java:42)",
         "\tat com.android.tools.r8.naming.retrace.Main.main(Main.java:28)",
         "\tat com.android.tools.r8.naming.retrace.Main.method2(Main.java:42)",
         "\tat com.android.tools.r8.naming.retrace.Main.main2(Main.java:29)",
         "\tat com.android.tools.r8.naming.retrace.Main.main3(Main.java:30)",
-        "\t<OR #1> at com.android.tools.r8.naming.retrace.Main.method3(Main.java:72)",
         "\tat com.android.tools.r8.naming.retrace.Main.main4(Main.java:153)");
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 2 ambiguous stack traces.",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " method1(java.lang.String)(Main.java:42)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:28)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " method2(java.lang.String)(Main.java:42)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " main2(java.lang.String[])(Main.java:29)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " main3(java.lang.String[])(Main.java:30)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " main4(java.lang.String[])(Main.java:153)",
+        "< OR >",
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " method1(java.lang.String)(Main.java:42)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void main(java.lang.String[])(Main.java:28)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " method2(java.lang.String)(Main.java:42)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " main2(java.lang.String[])(Main.java:29)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " method3(java.lang.String)(Main.java:72)",
+        "\tat com.android.tools.r8.naming.retrace.Main.void"
+            + " main4(java.lang.String[])(Main.java:153)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileNameSynthesizeStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileNameSynthesizeStackTrace.java
index 0315fc0..9e15a2d 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileNameSynthesizeStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileNameSynthesizeStackTrace.java
@@ -36,6 +36,14 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "\tat android.support.v7.widget.ActionMenuView.void invokeItem()(ActionMenuView.java:624)",
+        "\tat noMappingKt.noMapping(AW779999992:21)",
+        "\tat android.support.v7.widget.ActionMenuViewKt.void invokeItem()(ActionMenuView.kt:624)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileWithNumberAndEmptyStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileWithNumberAndEmptyStackTrace.java
index 1522918..fd20517 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileWithNumberAndEmptyStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SourceFileWithNumberAndEmptyStackTrace.java
@@ -39,6 +39,23 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "  at com.android.tools.r8.utils.ExceptionUtils.void withR8CompilationHandler("
+            + "com.android.tools.r8.utils.Reporter,"
+            + "com.android.tools.r8.utils.ExceptionUtils$CompileAction)(ExceptionUtils.java:59)",
+        "  at com.android.tools.r8.R8.void runForTesting("
+            + "com.android.tools.r8.utils.AndroidApp,"
+            + "com.android.tools.r8.utils.InternalOptions)(R8.java:261)",
+        "  at com.android.tools.r8.utils.ExceptionUtils.void withR8CompilationHandler("
+            + "com.android.tools.r8.utils.Reporter,"
+            + "com.android.tools.r8.utils.ExceptionUtils$CompileAction)(ExceptionUtils.java:59)",
+        "  at com.android.tools.r8.R8.void runForTesting("
+            + "com.android.tools.r8.utils.AndroidApp,"
+            + "com.android.tools.r8.utils.InternalOptions)(R8.java:261)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java
index e9783e6..29baca3 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java
@@ -14,5 +14,7 @@
 
   List<String> retracedStackTrace();
 
+  List<String> retraceVerboseStackTrace();
+
   int expectedWarnings();
 }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java
index eb44d96..e0cd74c 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java
@@ -35,6 +35,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Suppressed: foo.bar.baz: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodStackTrace.java
index 793fc0c..6beaba4 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodStackTrace.java
@@ -32,6 +32,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "  at example.Foo.void lambda$main$0()(Foo.java:225)",
+        "  at example.Foo.void runIt()(Foo.java:218)",
+        "  at example.Foo.void main()(Foo.java:223)",
+        "  at example.Main.void main(java.lang.String[])(Main.java:123)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodWithInliningStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodWithInliningStackTrace.java
index c132733..201700d 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodWithInliningStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SyntheticLambdaMethodWithInliningStackTrace.java
@@ -31,6 +31,16 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "Exception in thread \"main\" java.lang.NullPointerException",
+        "  at example.Foo.void lambda$main$0()(Foo.java:225)",
+        "  at example.Foo.void runIt()(Foo.java:218)",
+        "  at example.Foo.void main()(Foo.java:223)",
+        "  at example.Main.void main(java.lang.String[])(Main.java:123)");
+  }
+
+  @Override
   public String mapping() {
     return StringUtils.lines(
         "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/UnicodeInFileNameStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/UnicodeInFileNameStackTrace.java
index 5090c39..1b3c0eb 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/UnicodeInFileNameStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/UnicodeInFileNameStackTrace.java
@@ -29,6 +29,11 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return ImmutableList.of("  at some.Class.int strawberry(int)(Class.kt:99)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/UnknownSourceStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/UnknownSourceStackTrace.java
index 13b8bdd..2f946f8 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/UnknownSourceStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/UnknownSourceStackTrace.java
@@ -25,15 +25,82 @@
   @Override
   public List<String> retracedStackTrace() {
     return Arrays.asList(
+        "There are 8 ambiguous stack traces. Use --verbose to have all listed.",
         "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
         "    at com.android.tools.r8.R8.main(Unknown Source)",
         "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
         "    at com.android.tools.r8.R8.bar(R8.java)",
-        "    <OR #1> at com.android.tools.r8.R8.foo(R8.java)",
+        "    ... 42 more");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "There are 8 ambiguous stack traces.",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void bar(int,int)(R8.java)",
+        "    ... 42 more",
+        "< OR >",
+        "com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
+        "    at com.android.tools.r8.R8.main(Unknown Source)",
+        "Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3)",
+        "    at com.android.tools.r8.R8.void foo(int)(R8.java)",
         "    ... 42 more");
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/VerboseUnknownStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/VerboseUnknownStackTrace.java
index 3cb6808..24e773c 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/VerboseUnknownStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/VerboseUnknownStackTrace.java
@@ -27,6 +27,12 @@
   }
 
   @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Arrays.asList(
+        "java.lang.IndexOutOfBoundsException", "\tat java.util.ArrayList.get(ArrayList.java:411)");
+  }
+
+  @Override
   public int expectedWarnings() {
     return 0;
   }
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
index 7733b23..a9574e8 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
@@ -57,6 +57,7 @@
     testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(Ordinals.class)
         .addKeepMainRule(Ordinals.class)
+        .enableConstantArgumentAnnotations()
         .enableForceInliningAnnotations()
         .enableInliningAnnotations()
         .enableSideEffectAnnotations()
@@ -106,6 +107,7 @@
     testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(Names.class)
         .addKeepMainRule(Names.class)
+        .enableConstantArgumentAnnotations()
         .enableForceInliningAnnotations()
         .enableInliningAnnotations()
         .enableSideEffectAnnotations()
@@ -152,6 +154,7 @@
     testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(ToStrings.class)
         .addKeepMainRule(ToStrings.class)
+        .enableConstantArgumentAnnotations()
         .enableForceInliningAnnotations()
         .enableInliningAnnotations()
         .enableSideEffectAnnotations()
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/Names.java b/src/test/java/com/android/tools/r8/rewrite/enums/Names.java
index 5dd5ec0..1c2964c 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/Names.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/Names.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.ForceInline;
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import java.util.concurrent.TimeUnit;
 
@@ -69,6 +70,7 @@
     return Number.DEFAULT.name();
   }
 
+  @KeepConstantArguments
   @NeverInline
   private static String phi(boolean value) {
     Number number = Number.ONE;
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/Ordinals.java b/src/test/java/com/android/tools/r8/rewrite/enums/Ordinals.java
index e1eee2c..c3fbffc 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/Ordinals.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/Ordinals.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.ForceInline;
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import java.util.concurrent.TimeUnit;
 
@@ -79,6 +80,7 @@
     return Number.DEFAULT.ordinal();
   }
 
+  @KeepConstantArguments
   @NeverInline
   private static long phi(boolean value) {
     Number number = Number.ONE;
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/ToStrings.java b/src/test/java/com/android/tools/r8/rewrite/enums/ToStrings.java
index 8055344..020f5fd 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/ToStrings.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/ToStrings.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.ForceInline;
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
@@ -106,6 +107,7 @@
     return NoToString.DEFAULT.toString();
   }
 
+  @KeepConstantArguments
   @NeverInline
   private static String phi(boolean value) {
     NoToString number = NoToString.ONE;
diff --git a/src/test/java/com/android/tools/r8/rewrite/switches/MaxIntSwitchTest.java b/src/test/java/com/android/tools/r8/rewrite/switches/MaxIntSwitchTest.java
index b32e476..26dcb7a 100644
--- a/src/test/java/com/android/tools/r8/rewrite/switches/MaxIntSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/switches/MaxIntSwitchTest.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 import com.android.tools.r8.TestBase;
@@ -104,6 +105,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(this.getClass())
         .addKeepMainRule(TestClass.class)
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -125,6 +127,7 @@
           "?");
 
   static class TestClass {
+    @KeepConstantArguments
     @NeverInline
     @NeverPropagateValue
     public static void f(int i) {
@@ -143,6 +146,7 @@
       }
     }
 
+    @KeepConstantArguments
     @NeverInline
     @NeverPropagateValue
     public static void g(int i) {
@@ -162,6 +166,7 @@
       }
     }
 
+    @KeepConstantArguments
     @NeverInline
     @NeverPropagateValue
     public static void h(int i) {
@@ -174,6 +179,7 @@
       }
     }
 
+    @KeepConstantArguments
     @NeverInline
     @NeverPropagateValue
     public static void s(String s) {
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index d98725d..6a95824 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
-import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.proguardConfigurationRuleDoesNotMatch;
 import static com.android.tools.r8.utils.codeinspector.Matchers.typeVariableNotInScope;
@@ -13,7 +12,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
@@ -76,8 +74,7 @@
     assertThat(method, isPresent());
   }
 
-  // TODO(b/159966986): A general keep rule should not cause compiler assertion errors.
-  @Test(expected = CompilationFailedException.class)
+  @Test
   public void testPresentAnnotation() throws Exception {
     testForR8(Backend.CF)
         .addProgramFiles(R8_JAR)
@@ -85,10 +82,7 @@
         .addDontWarnGoogle()
         .addDontWarnJavax()
         .addDontWarn("org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement")
-        .allowDiagnosticInfoMessages()
-        .compileWithExpectedDiagnostics(
-            diagnostics -> diagnostics.assertErrorsMatch(diagnosticException(AssertionError.class)))
-        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation);
+        .compile();
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
index f529432..7c9c834 100644
--- a/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
@@ -105,9 +105,9 @@
           "In C.m3()",
           "In A.m4()",
           "In A.m1()", // With Java: Caught IllegalAccessError when calling B.m1()
-          "In A.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3()
+          "Caught IncompatibleClassChangeError when calling B.m3()",
           "In C.m1()", // With Java: Caught IllegalAccessError when calling B.m1()
-          "In C.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3()
+          "Caught IncompatibleClassChangeError when calling B.m3()",
           "In C.m1()",
           "In C.m3()",
           "");
@@ -179,7 +179,7 @@
       assertThat(classSubject, isPresentAndRenamed());
       assertThat(classSubject.method("void", "m1", ImmutableList.of()), isPresent());
       assertThat(classSubject.method("void", "m2", ImmutableList.of()), isAbsent());
-      assertThat(classSubject.method("void", "m3", ImmutableList.of()), isPresent());
+      assertThat(classSubject.method("void", "m3", ImmutableList.of()), isAbsent());
       assertThat(classSubject.method("void", "m4", ImmutableList.of()), isAbsent());
     }
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java b/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java
index 14259ca..03d5aa6 100644
--- a/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java
@@ -31,7 +31,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public DeadArrayLengthTest(TestParameters parameters) {
@@ -65,7 +65,7 @@
     testForD8()
         .release()
         .addProgramClasses(MAIN)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
         .inspect(codeInspector -> inspect(codeInspector, false));
@@ -79,7 +79,7 @@
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
         .inspect(codeInspector -> inspect(codeInspector, true));
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
index 9dfff6e..fdea6ab 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestBuilder;
@@ -80,6 +81,7 @@
             ProguardKeepAttributes.INNER_CLASSES,
             ProguardKeepAttributes.ENCLOSING_METHOD)
         .setMinApi(parameters.getApiLevel())
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .run(parameters.getRuntime(), KeptClass.class)
@@ -137,6 +139,7 @@
 
     public List<P> notKeptField;
 
+    @KeepConstantArguments
     @NeverInline
     public List<P> notKeptMethod(P p1, P p2) {
       if (notKeptField != null) {
@@ -163,6 +166,7 @@
       return (R) keptField;
     }
 
+    @KeepConstantArguments
     @NeverInline
     @SuppressWarnings("unchecked")
     public <R> R notKeptMethod(T t) {
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking2Test.java b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking2Test.java
index d951404..74232e1 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking2Test.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking2Test.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking.examples;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.shaking.TreeShakingTest;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -10,7 +14,6 @@
 import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.List;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,7 +44,7 @@
   @Test
   public void testKeeprules() throws Exception {
     runTest(
-        TreeShaking2Test::shaking2SuperClassIsAbstract,
+        TreeShaking2Test::shaking2SuperClassIsRemoved,
         null,
         null,
         ImmutableList.of("src/test/examples/shaking2/keep-rules.txt"));
@@ -62,16 +65,15 @@
         null, null, null, ImmutableList.of("src/test/examples/shaking2/keep-rules-printusage.txt"));
   }
 
-  private static void shaking2SuperClassIsAbstract(CodeInspector inspector) {
+  private static void shaking2SuperClassIsRemoved(CodeInspector inspector) {
     ClassSubject clazz = inspector.clazz("shaking2.SuperClass");
-    Assert.assertTrue(clazz.isAbstract());
-    Assert.assertTrue(clazz.method("void", "virtualMethod", Collections.emptyList()).isAbstract());
-    Assert.assertTrue(
-        clazz
-            .method(
-                "void",
-                "virtualMethod2",
-                ImmutableList.of("int", "int", "int", "int", "int", "int", "int", "int"))
-            .isAbstract());
+    assertTrue(clazz.isAbstract());
+    assertTrue(clazz.method("void", "virtualMethod", Collections.emptyList()).isAbstract());
+    assertThat(
+        clazz.method(
+            "void",
+            "virtualMethod2",
+            ImmutableList.of("int", "int", "int", "int", "int", "int", "int", "int")),
+        isAbsent());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
index 4740853..4f8131d 100644
--- a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KeepConstantArguments;
 import com.android.tools.r8.KeepUnusedArguments;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -173,6 +174,7 @@
         .addInnerClasses(KeepParameterNamesTest.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-keep class " + Api.class.getTypeName() + "{ api*(...); }")
+        .enableConstantArgumentAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableUnusedArgumentAnnotations()
@@ -209,6 +211,7 @@
             "In Api.api2",
             "In Api.api3");
     testForR8(parameters.getBackend())
+        .enableConstantArgumentAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .enableUnusedArgumentAnnotations()
@@ -253,6 +256,7 @@
     }
 
     @NeverInline
+    @KeepConstantArguments
     @KeepUnusedArguments
     void api1(int parameter1, String parameter2) {
       try {
@@ -264,12 +268,14 @@
     }
 
     @NeverInline
+    @KeepConstantArguments
     @KeepUnusedArguments
     void api2(long parameter1, double parameter2) {
       System.out.println("In Api.api2");
     }
 
     @NeverInline
+    @KeepConstantArguments
     @KeepUnusedArguments
     void api3(List<String> parameter1, Map<String, Object> parameter2) {
       System.out.println("In Api.api3");
diff --git a/src/test/kotlinR8TestResources/unused_singleton/main.kt b/src/test/kotlinR8TestResources/unused_singleton/main.kt
index 213acb1..6c06b8c 100644
--- a/src/test/kotlinR8TestResources/unused_singleton/main.kt
+++ b/src/test/kotlinR8TestResources/unused_singleton/main.kt
@@ -10,6 +10,6 @@
   fun provideGreeting() = "Hello"
 }
 
-fun main(args: Array<String>) {
+fun main(args: Array<String>?) {
   println(provideGreeting())
 }
diff --git a/third_party/openjdk/jdk-17/linux.tar.gz.sha1 b/third_party/openjdk/jdk-17/linux.tar.gz.sha1
new file mode 100644
index 0000000..27451d3
--- /dev/null
+++ b/third_party/openjdk/jdk-17/linux.tar.gz.sha1
@@ -0,0 +1 @@
+fa768e38f9c28e401174cecdd4e326b5fae120e1
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-17/osx.tar.gz.sha1 b/third_party/openjdk/jdk-17/osx.tar.gz.sha1
new file mode 100644
index 0000000..ff2288f
--- /dev/null
+++ b/third_party/openjdk/jdk-17/osx.tar.gz.sha1
@@ -0,0 +1 @@
+7c62e0a6a3168e138f4bad06439650a9e0869333
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-17/windows.tar.gz.sha1 b/third_party/openjdk/jdk-17/windows.tar.gz.sha1
new file mode 100644
index 0000000..0a02a42
--- /dev/null
+++ b/third_party/openjdk/jdk-17/windows.tar.gz.sha1
@@ -0,0 +1 @@
+07d750b353ebd4f5ef9ec99c916e50b33f47a8a8
\ No newline at end of file
diff --git a/third_party/retrace/binary_compatibility.tar.gz.sha1 b/third_party/retrace/binary_compatibility.tar.gz.sha1
index 0aafcf6..c3fc5b5 100644
--- a/third_party/retrace/binary_compatibility.tar.gz.sha1
+++ b/third_party/retrace/binary_compatibility.tar.gz.sha1
@@ -1 +1 @@
-23a607be2e3644321df25d73e1db663ad06e79cb
\ No newline at end of file
+8a3bfb12c41dc6fc56545c186729f234f2680b55
\ No newline at end of file