Merge commit 'a848fa8fca4e37f7fb6fae4f2475ed559ec1b8f3' into dev-release
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index a329c0f..9b4bb2a 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -5,6 +5,10 @@
 from os import path
 import datetime
 from subprocess import check_output, Popen, PIPE, STDOUT
+import sys
+import inspect
+sys.path.append(path.dirname(inspect.getfile(lambda: None)))
+from tools.utils import EnsureDepFromGoogleCloudStorage
 
 FMT_CMD = path.join(
     'third_party',
@@ -16,7 +20,10 @@
     'google-java-format-diff.py')
 
 FMT_CMD_JDK17 = path.join('tools','google-java-format-diff.py')
-
+FMT_SHA1 = path.join(
+    'third_party', 'google', 'google-java-format', '1.14.0.tar.gz.sha1')
+FMT_TGZ = path.join(
+    'third_party', 'google', 'google-java-format', '1.14.0.tar.gz')
 
 def CheckDoNotMerge(input_api, output_api):
   for l in input_api.change.FullDescriptionText().splitlines():
@@ -26,6 +33,7 @@
   return []
 
 def CheckFormatting(input_api, output_api, branch):
+  EnsureDepFromGoogleCloudStorage(FMT_CMD, FMT_TGZ, FMT_SHA1, 'google-format')
   results = []
   for f in input_api.AffectedFiles():
     path = f.LocalPath()
diff --git a/build.gradle b/build.gradle
index 332b74a..834e3d6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -341,8 +341,6 @@
                 "jsr223-api-1.0",
                 "rhino-1.7.10",
                 "rhino-android-1.1.1",
-                "kotlin/kotlin-compiler-1.3.11",
-                "kotlin/kotlin-compiler-1.3.41",
                 "kotlin/kotlin-compiler-1.3.72",
                 "kotlin/kotlin-compiler-1.4.20",
                 "kotlin/kotlin-compiler-1.5.0",
@@ -1578,28 +1576,26 @@
 task buildKotlinR8TestResources {
     def examplesDir = file("src/test/kotlinR8TestResources")
     examplesDir.eachDir { dir ->
-        kotlin.Kotlinc.KotlinTargetVersion.values().each { kotlinTargetVersion ->
-            def name = dir.getName()
-            def taskName = "jar_kotlinR8TestResources_${name}_${kotlinTargetVersion}"
-            def javaOutput = "build/test/kotlinR8TestResources/${kotlinTargetVersion}/${name}/java"
-            def javaOutputJarName = "${name}.java.jar"
-            def javaOutputJarDir = "build/test/kotlinR8TestResources/${kotlinTargetVersion}"
-            task "${taskName}Java"(type: JavaCompile) {
-                source = fileTree(dir: file("${examplesDir}/${name}"), include: '**/*.java')
-                destinationDir = file(javaOutput)
-                classpath = sourceSets.main.compileClasspath
-                sourceCompatibility = JavaVersion.VERSION_1_6
-                targetCompatibility = JavaVersion.VERSION_1_6
-                options.compilerArgs += ["-g", "-Xlint:-options"]
-            }
-            task "${taskName}JavaJar"(type: Jar, dependsOn: "${taskName}Java") {
-                archiveName = javaOutputJarName
-                destinationDir = file(javaOutputJarDir)
-                from javaOutput
-                include "**/*.class"
-            }
-            dependsOn "${taskName}JavaJar"
+        def name = dir.getName()
+        def taskName = "jar_kotlinR8TestResources_${name}"
+        def javaOutput = "build/test/kotlinR8TestResources/${name}/java"
+        def javaOutputJarName = "${name}.jar"
+        def javaOutputJarDir = "build/test/kotlinR8TestResources"
+        task "${taskName}Java"(type: JavaCompile) {
+            source = fileTree(dir: file("${examplesDir}/${name}"), include: '**/*.java')
+            destinationDir = file(javaOutput)
+            classpath = sourceSets.main.compileClasspath
+            sourceCompatibility = JavaVersion.VERSION_1_8
+            targetCompatibility = JavaVersion.VERSION_1_8
+            options.compilerArgs += ["-g", "-Xlint:-options"]
         }
+        task "${taskName}JavaJar"(type: Jar, dependsOn: "${taskName}Java") {
+            archiveName = javaOutputJarName
+            destinationDir = file(javaOutputJarDir)
+            from javaOutput
+            include "**/*.class"
+        }
+        dependsOn "${taskName}JavaJar"
     }
 }
 
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index ab4a518..8af5714 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -11,6 +11,7 @@
 import org.gradle.api.Task
 import org.gradle.api.file.ConfigurableFileCollection
 import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.tasks.JavaExec
 import org.gradle.api.tasks.SourceSet
 import org.gradle.jvm.tasks.Jar
 import org.gradle.kotlin.dsl.register
@@ -96,7 +97,7 @@
 }
 
 /**
- * Builds a jar for each subfolder in an examples test source set.
+ * Builds a jar for each subfolder in an test source set.
  *
  * <p> As an example, src/test/examplesJava9 contains subfolders: backport, collectionof, ..., .
  * These are compiled to individual jars and placed in <repo-root>/build/test/examplesJava9/ as:
@@ -105,37 +106,58 @@
  * Calling this from a project will amend the task graph with the task named
  * getExamplesJarsTaskName(examplesName) such that it can be referenced from the test runners.
  */
-fun Project.buildJavaExamplesJars(examplesName : String) : Task {
+fun Project.buildExampleJars(name : String) : Task {
   val outputFiles : MutableList<File> = mutableListOf()
   val jarTasks : MutableList<Task> = mutableListOf()
-  var testSourceSet = extensions
+  val testSourceSet = extensions
     .getByType(JavaPluginExtension::class.java)
     .sourceSets
     // The TEST_SOURCE_SET_NAME is the source set defined by writing java { sourcesets.test { ... }}
     .getByName(SourceSet.TEST_SOURCE_SET_NAME)
+  val destinationDir = getRoot().resolveAll("build", "test", name)
+  val classesOutput = destinationDir.resolve("classes")
+  testSourceSet.java.destinationDirectory.set(classesOutput)
+  testSourceSet.resources.destinationDirectory.set(destinationDir)
   testSourceSet
     .java
     .sourceDirectories
     .files
     .forEach { srcDir ->
       srcDir.listFiles(File::isDirectory)?.forEach { exampleDir ->
-        jarTasks.add(tasks.register<Jar>("jar-examples$examplesName-${exampleDir.name}") {
+        var generationTask : Task? = null
+        if (exampleDir.resolve("TestGenerator.java").isFile) {
+          generationTask = tasks.register<JavaExec>(
+            "generate-$name-${exampleDir.name}") {
+            dependsOn("compileTestJava")
+            mainClass.set("${exampleDir.name}.TestGenerator")
+            classpath = files(
+              classesOutput,
+              testSourceSet.compileClasspath)
+            args(classesOutput.toString())
+          }.get()
+        }
+        jarTasks.add(tasks.register<Jar>(
+          "jar-$name-${exampleDir.name}") {
           dependsOn("compileTestJava")
+          if (generationTask != null) {
+            dependsOn(generationTask)
+          }
           archiveFileName.set("${exampleDir.name}.jar")
-          destinationDirectory.set(getRoot().resolveAll("build", "test", "examples$examplesName"))
-          from(testSourceSet.output.classesDirs.files.map{ it.resolve(exampleDir.name) }) {
-            include("**/*.class")
+          destinationDirectory.set(destinationDir)
+          from(classesOutput) {
+            include("${exampleDir.name}/**/*.class")
+            exclude("**/TestGenerator*")
           }
         }.get())
       }
     }
-  return tasks.register(getExamplesJarsTaskName(examplesName)) {
-    dependsOn(jarTasks)
+  return tasks.register(getExampleJarsTaskName(name)) {
+    dependsOn(jarTasks.toTypedArray())
     outputs.files(outputFiles)
   }.get()
 }
 
-fun Project.getExamplesJarsTaskName(name: String) : String {
+fun Project.getExampleJarsTaskName(name: String) : String {
   return "build-example-jars-$name"
 }
 
@@ -275,6 +297,8 @@
 }
 
 object ThirdPartyDeps {
+  val androidJars = getThirdPartyAndroidJars()
+  val androidVMs = getThirdPartyAndroidVms()
   val apiDatabase = ThirdPartyDependency(
     "apiDatabase",
     Paths.get(
@@ -284,26 +308,42 @@
       "resources",
       "new_api_database.ser").toFile(),
     Paths.get("third_party", "api_database", "api_database.tar.gz.sha1").toFile())
+  val compilerApi = ThirdPartyDependency(
+    "compiler-api",
+    Paths.get(
+      "third_party",
+      "binary_compatibility_tests",
+      "compiler_api_tests",
+      "tests.jar").toFile(),
+    Paths.get(
+      "third_party",
+      "binary_compatibility_tests",
+      "compiler_api_tests.tar.gz.sha1").toFile())
   val ddmLib = ThirdPartyDependency(
     "ddmlib",
     Paths.get("third_party", "ddmlib", "ddmlib.jar").toFile(),
     Paths.get("third_party", "ddmlib.tar.gz.sha1").toFile())
+  val jacoco = ThirdPartyDependency(
+    "jacoco",
+    Paths.get("third_party", "jacoco", "0.8.6", "lib", "jacocoagent.jar").toFile(),
+    Paths.get("third_party", "jacoco", "0.8.6.tar.gz.sha1").toFile()
+  )
   val jasmin = ThirdPartyDependency(
     "jasmin",
     Paths.get("third_party", "jasmin", "jasmin-2.4.jar").toFile(),
     Paths.get("third_party", "jasmin.tar.gz.sha1").toFile())
-  val jdwpTests = ThirdPartyDependency(
-    "jdwp-tests",
-    Paths.get("third_party", "jdwp-tests", "apache-harmony-jdwp-tests-host.jar").toFile(),
-    Paths.get("third_party", "jdwp-tests.tar.gz.sha1").toFile())
-  val androidJars : List<ThirdPartyDependency> = getThirdPartyAndroidJars()
   val java8Runtime = ThirdPartyDependency(
     "openjdk-rt-1.8",
     Paths.get("third_party", "openjdk", "openjdk-rt-1.8", "rt.jar").toFile(),
     Paths.get("third_party", "openjdk", "openjdk-rt-1.8.tar.gz.sha1").toFile()
   )
-  val androidVMs : List<ThirdPartyDependency> = getThirdPartyAndroidVms()
-  val jdks : List<ThirdPartyDependency> = getJdks()
+  val jdks = getJdks()
+  val jdwpTests = ThirdPartyDependency(
+    "jdwp-tests",
+    Paths.get("third_party", "jdwp-tests", "apache-harmony-jdwp-tests-host.jar").toFile(),
+    Paths.get("third_party", "jdwp-tests.tar.gz.sha1").toFile())
+  val kotlinCompilers = getThirdPartyKotlinCompilers()
+  val proguards = getThirdPartyProguards()
 }
 
 fun getThirdPartyAndroidJars() : List<ThirdPartyDependency> {
@@ -373,4 +413,39 @@
   } else {
     return Jdk.values().filter{ !it.isJdk8() }.map{ it.getThirdPartyDependency()}
   }
+}
+
+fun getThirdPartyProguards() : List<ThirdPartyDependency> {
+  val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
+  return listOf("proguard5.2.1", "proguard6.0.1", "proguard-7.0.0")
+    .map { ThirdPartyDependency(
+        it,
+        Paths.get(
+          "third_party",
+          "proguard",
+          it,
+          "bin",
+          if (os.isWindows) "proguard.bat" else "proguard.sh").toFile(),
+        Paths.get("third_party", "proguard", "${it}.tar.gz.sha1").toFile())}
+}
+
+fun getThirdPartyKotlinCompilers() : List<ThirdPartyDependency> {
+  return listOf(
+    "kotlin-compiler-1.3.72",
+    "kotlin-compiler-1.4.20",
+    "kotlin-compiler-1.5.0",
+    "kotlin-compiler-1.6.0",
+    "kotlin-compiler-1.7.0",
+    "kotlin-compiler-1.8.0",
+    "kotlin-compiler-dev")
+    .map { ThirdPartyDependency(
+      it,
+      Paths.get(
+        "third_party",
+        "kotlin",
+        it,
+        "kotlinc",
+        "lib",
+        "kotlin-stdlib.jar").toFile(),
+      Paths.get("third_party", "kotlin", "${it}.tar.gz.sha1").toFile())}
 }
\ No newline at end of file
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt
index 24185c6..4d43b40 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt
@@ -40,7 +40,7 @@
     option = "dependency",
     description = "Sets the dependency information for a cloud stored file")
   fun setDependency(
-    dependencyName : String, sha1File: File, outputDir : File, dependencyType: DependencyType) {
+    dependencyName: String, sha1File: File, outputDir: File, dependencyType: DependencyType) {
     _outputDir = outputDir
     _sha1File = sha1File
     _tarGzFile = sha1File.resolveSibling(sha1File.name.replace(".sha1", ""))
@@ -70,12 +70,11 @@
     getWorkerExecutor()!!
       .noIsolation()
       .submit(RunDownload::class.java) {
-        this.type.set(dependencyType)
+        type.set(dependencyType)
         this.sha1File.set(sha1File)
       }
   }
 
-
   interface RunDownloadParameters : WorkParameters {
     val type : Property<DependencyType>
     val sha1File : RegularFileProperty
diff --git a/d8_r8/settings.gradle.kts b/d8_r8/settings.gradle.kts
index 402137e..9907334 100644
--- a/d8_r8/settings.gradle.kts
+++ b/d8_r8/settings.gradle.kts
@@ -23,20 +23,20 @@
     "download_from_google_storage.py",
     "--extract",
     "--bucket",
-    "${dependencies_bucket}",
+    dependencies_bucket,
     "--sha1_file",
     "${sha1File}"
   )
   println("Executing command: ${cmd.joinToString(" ")}")
-  var process = ProcessBuilder().command(cmd).start()
+  val process = ProcessBuilder().command(cmd).start()
   process.waitFor()
   if (process.exitValue() != 0) {
     throw GradleException(
       "Bootstrapping dependencies_new download failed:\n"
         + "${String(process.getErrorStream().readAllBytes(),
                     java.nio.charset.StandardCharsets.UTF_8)}\n"
-        + "${String(process.getInputStream().readAllBytes(),
-                    java.nio.charset.StandardCharsets.UTF_8)}")
+        + String(process.getInputStream().readAllBytes(),
+                 java.nio.charset.StandardCharsets.UTF_8))
   }
 }
 
@@ -44,9 +44,11 @@
 downloadFromGoogleStorage(thirdParty.resolve("dependencies.tar.gz.sha1"))
 downloadFromGoogleStorage(thirdParty.resolve("dependencies_new.tar.gz.sha1"))
 
+pluginManagement {
+  includeBuild(rootProject.projectDir.resolve("commonBuildSrc"))
+}
 // This project is temporarily located in d8_r8. When moved to root, the parent
 // folder should just be removed.
-includeBuild(root.resolve("commonBuildSrc"))
 includeBuild(root.resolve("keepanno"))
 
 // We need to include src/main as a composite-build otherwise our test-modules
diff --git a/d8_r8/test/settings.gradle.kts b/d8_r8/test/settings.gradle.kts
index ed1f4ea..cec4093 100644
--- a/d8_r8/test/settings.gradle.kts
+++ b/d8_r8/test/settings.gradle.kts
@@ -6,13 +6,14 @@
 
 val root = rootProject.projectDir.parentFile
 includeBuild(root.resolve("main"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examples"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidN"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidP"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidO"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_8"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_9"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_10"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_11"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_17"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_20"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examples"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidN"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidP"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidO"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_kotlinR8TestResources"))
diff --git a/d8_r8/test_modules/tests_java_10/build.gradle.kts b/d8_r8/test_modules/tests_java_10/build.gradle.kts
index f24610d..aec1b33 100644
--- a/d8_r8/test_modules/tests_java_10/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_10/build.gradle.kts
@@ -24,7 +24,7 @@
 dependencies { }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("Java10")
+val buildExampleJars = buildExampleJars("examplesJava10")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_11/build.gradle.kts b/d8_r8/test_modules/tests_java_11/build.gradle.kts
index 1ff3ea8..bbf8d9c 100644
--- a/d8_r8/test_modules/tests_java_11/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_11/build.gradle.kts
@@ -24,7 +24,7 @@
 dependencies { }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("Java11")
+val buildExampleJars = buildExampleJars("examplesJava11")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_17/build.gradle.kts b/d8_r8/test_modules/tests_java_17/build.gradle.kts
index 9e0f2b7..e68b356 100644
--- a/d8_r8/test_modules/tests_java_17/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_17/build.gradle.kts
@@ -24,7 +24,7 @@
 dependencies { }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("Java17")
+val buildExampleJars = buildExampleJars("examplesJava17")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_20/build.gradle.kts b/d8_r8/test_modules/tests_java_20/build.gradle.kts
index 85cd393..5c348ed 100644
--- a/d8_r8/test_modules/tests_java_20/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_20/build.gradle.kts
@@ -24,7 +24,7 @@
 dependencies { }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("Java20")
+val buildExampleJars = buildExampleJars("examplesJava20")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_8/build.gradle.kts b/d8_r8/test_modules/tests_java_8/build.gradle.kts
index 3493286..f7e52e5 100644
--- a/d8_r8/test_modules/tests_java_8/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_8/build.gradle.kts
@@ -55,21 +55,25 @@
 
 val thirdPartyRuntimeDependenciesTask = ensureThirdPartyDependencies(
   "runtimeDeps",
-  listOf(ThirdPartyDeps.java8Runtime)
+  listOf(ThirdPartyDeps.compilerApi, ThirdPartyDeps.jacoco, ThirdPartyDeps.java8Runtime)
     + ThirdPartyDeps.androidJars
     + ThirdPartyDeps.androidVMs
-    + ThirdPartyDeps.jdks)
+    + ThirdPartyDeps.jdks
+    + ThirdPartyDeps.kotlinCompilers
+    + ThirdPartyDeps.proguards)
 
 val sourceSetDependenciesTasks = arrayOf(
-  projectTask("tests_java_examples", getExamplesJarsTaskName("")),
-  projectTask("tests_java_examplesAndroidN", getExamplesJarsTaskName("AndroidN")),
-  projectTask("tests_java_examplesAndroidO", getExamplesJarsTaskName("AndroidO")),
-  projectTask("tests_java_examplesAndroidP", getExamplesJarsTaskName("AndroidP")),
-  projectTask("tests_java_9", getExamplesJarsTaskName("Java9")),
-  projectTask("tests_java_10", getExamplesJarsTaskName("Java10")),
-  projectTask("tests_java_11", getExamplesJarsTaskName("Java11")),
-  projectTask("tests_java_17", getExamplesJarsTaskName("Java17")),
-  projectTask("tests_java_20", getExamplesJarsTaskName("Java20")))
+  projectTask("tests_java_examples", getExampleJarsTaskName("examples")),
+  projectTask("tests_java_9", getExampleJarsTaskName("examplesJava9")),
+  projectTask("tests_java_10", getExampleJarsTaskName("examplesJava10")),
+  projectTask("tests_java_11", getExampleJarsTaskName("examplesJava11")),
+  projectTask("tests_java_17", getExampleJarsTaskName("examplesJava17")),
+  projectTask("tests_java_20", getExampleJarsTaskName("examplesJava20")),
+  projectTask("tests_java_examplesAndroidN", getExampleJarsTaskName("examplesAndroidN")),
+  projectTask("tests_java_examplesAndroidO", getExampleJarsTaskName("examplesAndroidO")),
+  projectTask("tests_java_examplesAndroidP", getExampleJarsTaskName("examplesAndroidP")),
+  projectTask("tests_java_kotlinR8TestResources", getExampleJarsTaskName("kotlinR8TestResources")),
+)
 
 fun testDependencies() : FileCollection {
   return sourceSets
diff --git a/d8_r8/test_modules/tests_java_8/settings.gradle.kts b/d8_r8/test_modules/tests_java_8/settings.gradle.kts
index 73f5d73..4935653 100644
--- a/d8_r8/test_modules/tests_java_8/settings.gradle.kts
+++ b/d8_r8/test_modules/tests_java_8/settings.gradle.kts
@@ -11,13 +11,14 @@
 // will compete with the test to compile the source files.
 includeBuild(root.resolve("main"))
 
-includeBuild(root.resolve("test_modules").resolve("tests_java_examples"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidN"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidO"))
-includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidP"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_9"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_10"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_11"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_17"))
 includeBuild(root.resolve("test_modules").resolve("tests_java_20"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examples"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidN"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidO"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_examplesAndroidP"))
+includeBuild(root.resolve("test_modules").resolve("tests_java_kotlinR8TestResources"))
 
diff --git a/d8_r8/test_modules/tests_java_9/build.gradle.kts b/d8_r8/test_modules/tests_java_9/build.gradle.kts
index ac5923f..e7a55e1 100644
--- a/d8_r8/test_modules/tests_java_9/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_9/build.gradle.kts
@@ -24,7 +24,7 @@
 dependencies { }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("Java9")
+val buildExampleJars = buildExampleJars("examplesJava9")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_examples/build.gradle.kts b/d8_r8/test_modules/tests_java_examples/build.gradle.kts
index 05798be..16fec67 100644
--- a/d8_r8/test_modules/tests_java_examples/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_examples/build.gradle.kts
@@ -27,7 +27,7 @@
 }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("")
+val buildExampleJars = buildExampleJars("examples")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_examplesAndroidN/build.gradle.kts b/d8_r8/test_modules/tests_java_examplesAndroidN/build.gradle.kts
index 8cfe7d2..54b5a45 100644
--- a/d8_r8/test_modules/tests_java_examplesAndroidN/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_examplesAndroidN/build.gradle.kts
@@ -27,7 +27,7 @@
 }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("AndroidN")
+val buildExampleJars = buildExampleJars("examplesAndroidN")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_examplesAndroidO/build.gradle.kts b/d8_r8/test_modules/tests_java_examplesAndroidO/build.gradle.kts
index 44ed59d..43020e3 100644
--- a/d8_r8/test_modules/tests_java_examplesAndroidO/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_examplesAndroidO/build.gradle.kts
@@ -27,11 +27,13 @@
 }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("AndroidO")
+val buildExampleJars = buildExampleJars("examplesAndroidO")
 
 tasks {
   withType<JavaCompile> {
     options.setFork(true)
+    options.compilerArgs.add("-Xlint:-options")
+    options.compilerArgs.add("-parameters")
     options.forkOptions.memoryMaximumSize = "3g"
     options.forkOptions.jvmArgs = listOf(
       "-Xss256m",
diff --git a/d8_r8/test_modules/tests_java_examplesAndroidP/build.gradle.kts b/d8_r8/test_modules/tests_java_examplesAndroidP/build.gradle.kts
index 7e58f3b..13c2530 100644
--- a/d8_r8/test_modules/tests_java_examplesAndroidP/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_examplesAndroidP/build.gradle.kts
@@ -27,7 +27,7 @@
 }
 
 // We just need to register the examples jars for it to be referenced by other modules.
-val buildExampleJars = buildJavaExamplesJars("AndroidP")
+val buildExampleJars = buildExampleJars("examplesAndroidP")
 
 tasks {
   withType<JavaCompile> {
diff --git a/d8_r8/test_modules/tests_java_kotlinR8TestResources/build.gradle.kts b/d8_r8/test_modules/tests_java_kotlinR8TestResources/build.gradle.kts
new file mode 100644
index 0000000..353d621
--- /dev/null
+++ b/d8_r8/test_modules/tests_java_kotlinR8TestResources/build.gradle.kts
@@ -0,0 +1,46 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import org.gradle.api.JavaVersion
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  `kotlin-dsl`
+  `java-library`
+  id("dependencies-plugin")
+}
+
+val root = getRoot()
+
+java {
+  sourceSets.test.configure {
+    java.srcDirs.clear()
+    java.srcDir(root.resolveAll("src", "test", "kotlinR8TestResources"))
+  }
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  testCompileOnly(Deps.asm)
+}
+
+// We just need to register the examples jars for it to be referenced by other modules.
+val buildExampleJars = buildExampleJars("kotlinR8TestResources")
+
+tasks {
+  withType<JavaCompile> {
+    options.setFork(true)
+    options.forkOptions.memoryMaximumSize = "3g"
+    options.forkOptions.jvmArgs = listOf(
+      "-Xss256m",
+      // Set the bootclass path so compilation is consistent with 1.8 target compatibility.
+      "-Xbootclasspath/a:third_party/openjdk/openjdk-rt-1.8/rt.jar")
+  }
+  withType<KotlinCompile> {
+    kotlinOptions {
+      jvmTarget = "1.8"
+    }
+  }
+}
diff --git a/d8_r8/test_modules/tests_java_kotlinR8TestResources/gradle.properties b/d8_r8/test_modules/tests_java_kotlinR8TestResources/gradle.properties
new file mode 100644
index 0000000..1de43f9
--- /dev/null
+++ b/d8_r8/test_modules/tests_java_kotlinR8TestResources/gradle.properties
@@ -0,0 +1,17 @@
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
+kotlin.daemon.jvmargs=-Xmx3g -Dkotlin.js.compiler.legacy.force_enabled=true
+systemProp.file.encoding=UTF-8
+
+# Enable new incremental compilation
+kotlin.incremental.useClasspathSnapshot=true
+
+org.gradle.parallel=true
+org.gradle.caching=true
+
+# Do not download any jdks or detect them. We provide them.
+org.gradle.java.installations.auto-detect=false
+org.gradle.java.installations.auto-download=false
diff --git a/d8_r8/test_modules/tests_java_kotlinR8TestResources/settings.gradle.kts b/d8_r8/test_modules/tests_java_kotlinR8TestResources/settings.gradle.kts
new file mode 100644
index 0000000..58a9232
--- /dev/null
+++ b/d8_r8/test_modules/tests_java_kotlinR8TestResources/settings.gradle.kts
@@ -0,0 +1,5 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+rootProject.name = "tests_java_kotlinR8TestResources"
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index 03691dd3..91a12e2 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -41,7 +41,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 1
-    max_batch_size: 1
+    max_batch_size: 42
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 358def3..dd14d77 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -307,7 +307,7 @@
   dimensions = get_dimensions(),
   triggering_policy = scheduler.policy(
       kind = scheduler.GREEDY_BATCHING_KIND,
-      max_batch_size = 1,
+      max_batch_size = 42,
       max_concurrent_invocations = 1
   ),
   priority = 25,
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
index e3a2cbb..be74e08 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
@@ -68,6 +68,7 @@
  * The GlobalSyntheticsGenerator, a tool for generating a dex file for all possible global
  * synthetics.
  */
+@Keep
 public class GlobalSyntheticsGenerator {
 
   private static boolean ensureAllGlobalSyntheticsModeled(SyntheticNaming naming) {
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommand.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommand.java
index 4ae00bd..1d85164 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommand.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommand.java
@@ -25,6 +25,7 @@
 /**
  * Immutable command structure for an invocation of the {@link GlobalSyntheticsGenerator} compiler.
  */
+@Keep
 public final class GlobalSyntheticsGeneratorCommand {
 
   private final ProgramConsumer programConsumer;
@@ -151,6 +152,7 @@
    *
    * <p>A builder is obtained by calling {@link GlobalSyntheticsGeneratorCommand#builder}.
    */
+  @Keep
   public static class Builder {
 
     private ProgramConsumer programConsumer = null;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 86b5b5f..080447c 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -505,6 +505,9 @@
           .appInfo()
           .notifyHorizontalClassMergerFinished(HorizontalClassMerger.Mode.INITIAL);
 
+      // TODO(b/225838009): Horizontal merging currently assumes pre-phase CF conversion.
+      appView.testing().enterLirSupportedPhase();
+
       new ProtoNormalizer(appViewWithLiveness).run(executorService, timing);
 
       // Clear traced methods roots to not hold on to the main dex live method set.
@@ -541,6 +544,9 @@
       appView.setGraphLens(new AppliedGraphLens(appView));
       timing.end();
 
+      // TODO(b/225838009): Support tracing and building LIR in Enqueuer.
+      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+
       if (options.shouldRerunEnqueuer()) {
         timing.begin("Post optimization code stripping");
         try {
diff --git a/src/main/java/com/android/tools/r8/dump/CompilerDump.java b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
index eaf9e53..7655a6e 100644
--- a/src/main/java/com/android/tools/r8/dump/CompilerDump.java
+++ b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
@@ -45,6 +45,10 @@
     return directory.resolve("proguard.config");
   }
 
+  public Path getDesugaredLibraryFile() {
+    return directory.resolve("desugared-library.json");
+  }
+
   public void sanitizeProguardConfig(ProguardConfigSanitizer sanitizer) throws IOException {
     try (BufferedReader reader = Files.newBufferedReader(getProguardConfigFile())) {
       String next = reader.readLine();
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 5a20e3d..06deed3 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -26,6 +26,8 @@
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteShrinker;
 import com.android.tools.r8.ir.analysis.proto.ProtoShrinker;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.AbstractValueJoiner.AbstractValueFieldJoiner;
+import com.android.tools.r8.ir.analysis.value.AbstractValueJoiner.AbstractValueParameterJoiner;
 import com.android.tools.r8.ir.desugar.TypeRewriter;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
@@ -100,6 +102,8 @@
   private KeepInfoCollection keepInfo = null;
 
   private final AbstractValueFactory abstractValueFactory = new AbstractValueFactory();
+  private final AbstractValueFieldJoiner abstractValueFieldJoiner;
+  private final AbstractValueParameterJoiner abstractValueParameterJoiner;
   private final InstanceFieldInitializationInfoFactory instanceFieldInitializationInfoFactory =
       new InstanceFieldInitializationInfoFactory();
   private final SimpleInliningConstraintFactory simpleInliningConstraintFactory =
@@ -170,13 +174,20 @@
     this.context =
         timing.time(
             "Compilation context", () -> CompilationContext.createInitialContext(options()));
+    this.wholeProgramOptimizations = wholeProgramOptimizations;
+    if (enableWholeProgramOptimizations()) {
+      abstractValueFieldJoiner = new AbstractValueFieldJoiner(withClassHierarchy());
+      abstractValueParameterJoiner = new AbstractValueParameterJoiner(withClassHierarchy());
+    } else {
+      abstractValueFieldJoiner = null;
+      abstractValueParameterJoiner = null;
+    }
     this.artProfileCollection = artProfileCollection;
     this.startupProfile = startupProfile;
     this.dontWarnConfiguration =
         timing.time(
             "Dont warn config",
             () -> DontWarnConfiguration.create(options().getProguardConfiguration()));
-    this.wholeProgramOptimizations = wholeProgramOptimizations;
     this.initClassLens = timing.time("Init class lens", InitClassLens::getThrowingInstance);
     this.typeRewriter = mapper;
     timing.begin("Create argument propagator");
@@ -314,6 +325,14 @@
     return abstractValueFactory;
   }
 
+  public AbstractValueFieldJoiner getAbstractValueFieldJoiner() {
+    return abstractValueFieldJoiner;
+  }
+
+  public AbstractValueParameterJoiner getAbstractValueParameterJoiner() {
+    return abstractValueParameterJoiner;
+  }
+
   public InstanceFieldInitializationInfoFactory instanceFieldInitializationInfoFactory() {
     return instanceFieldInitializationInfoFactory;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index cc8e00f..a87febd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -1043,6 +1043,10 @@
     permittedSubclasses.clear();
   }
 
+  public void removePermittedSubclassAttribute(Predicate<PermittedSubclassAttribute> predicate) {
+    permittedSubclasses.removeIf(predicate);
+  }
+
   public boolean isLocalClass() {
     InnerClassAttribute innerClass = getInnerClassAttributeForThisClass();
     // The corresponding enclosing-method attribute might be not available, e.g., CF version 50.
@@ -1139,6 +1143,10 @@
     return permittedSubclasses;
   }
 
+  public void setPermittedSubclassAttributes(List<PermittedSubclassAttribute> permittedSubclasses) {
+    this.permittedSubclasses = permittedSubclasses;
+  }
+
   public List<RecordComponentInfo> getRecordComponents() {
     return recordComponents;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 1aa866f..fb50d0e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -415,6 +415,18 @@
         .traverse((field, value) -> fn.apply(field.asProgramField(), value), initialValue);
   }
 
+  public TraversalContinuation<?, ?> traverseProgramInstanceFields(
+      Function<? super ProgramField, TraversalContinuation<?, ?>> fn) {
+    return getFieldCollection().traverseInstanceFields(field -> fn.apply(field.asProgramField()));
+  }
+
+  public <BT, CT> TraversalContinuation<BT, CT> traverseProgramInstanceFields(
+      BiFunction<? super ProgramField, CT, TraversalContinuation<BT, CT>> fn, CT initialValue) {
+    return getFieldCollection()
+        .traverseInstanceFields(
+            (field, value) -> fn.apply(field.asProgramField(), value), initialValue);
+  }
+
   public TraversalContinuation<?, ?> traverseProgramMethods(
       Function<? super ProgramMethod, TraversalContinuation<?, ?>> fn) {
     return getMethodCollection().traverse(method -> fn.apply(new ProgramMethod(this, method)));
diff --git a/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
index dee7446..ec0758a 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
@@ -72,15 +72,61 @@
   @Override
   <BT, CT> TraversalContinuation<BT, CT> traverse(
       DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
-    TraversalContinuation<BT, CT> traversalContinuation = TraversalContinuation.doContinue();
-    for (DexEncodedField definition : staticFields) {
-      DexClassAndField field = DexClassAndField.create(holder, definition);
-      traversalContinuation = fn.apply(field);
-      if (traversalContinuation.shouldBreak()) {
-        return traversalContinuation;
-      }
+    TraversalContinuation<BT, CT> traversalContinuation = traverseStaticFields(holder, fn);
+    if (traversalContinuation.shouldBreak()) {
+      return traversalContinuation;
     }
-    for (DexEncodedField definition : instanceFields) {
+    return traverseInstanceFields(holder, fn);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverse(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    TraversalContinuation<BT, CT> traversalContinuation =
+        traverseStaticFields(holder, fn, initialValue);
+    if (traversalContinuation.shouldBreak()) {
+      return traversalContinuation;
+    }
+    return traverseInstanceFields(
+        holder, fn, traversalContinuation.asContinue().getValueOrDefault(null));
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return traverseInstanceOrStaticFields(holder, instanceFields, fn);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return traverseInstanceOrStaticFields(holder, instanceFields, fn, initialValue);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return traverseInstanceOrStaticFields(holder, staticFields, fn);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return traverseInstanceOrStaticFields(holder, staticFields, fn, initialValue);
+  }
+
+  private static <BT, CT> TraversalContinuation<BT, CT> traverseInstanceOrStaticFields(
+      DexClass holder,
+      DexEncodedField[] fields,
+      Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    TraversalContinuation<BT, CT> traversalContinuation = TraversalContinuation.doContinue();
+    for (DexEncodedField definition : fields) {
       DexClassAndField field = DexClassAndField.create(holder, definition);
       traversalContinuation = fn.apply(field);
       if (traversalContinuation.shouldBreak()) {
@@ -90,21 +136,14 @@
     return traversalContinuation;
   }
 
-  @Override
-  <BT, CT> TraversalContinuation<BT, CT> traverse(
+  private static <BT, CT> TraversalContinuation<BT, CT> traverseInstanceOrStaticFields(
       DexClass holder,
+      DexEncodedField[] fields,
       BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
       CT initialValue) {
     TraversalContinuation<BT, CT> traversalContinuation =
         TraversalContinuation.doContinue(initialValue);
-    for (DexEncodedField definition : staticFields) {
-      DexClassAndField field = DexClassAndField.create(holder, definition);
-      traversalContinuation = fn.apply(field, traversalContinuation.asContinue().getValue());
-      if (traversalContinuation.shouldBreak()) {
-        return traversalContinuation;
-      }
-    }
-    for (DexEncodedField definition : instanceFields) {
+    for (DexEncodedField definition : fields) {
       DexClassAndField field = DexClassAndField.create(holder, definition);
       traversalContinuation = fn.apply(field, traversalContinuation.asContinue().getValue());
       if (traversalContinuation.shouldBreak()) {
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollection.java b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
index 729b238..9b3c09b 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
@@ -73,6 +73,28 @@
     return backing.traverse(holder, fn, initialValue);
   }
 
+  public <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return backing.traverseInstanceFields(holder, fn);
+  }
+
+  public <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return backing.traverseInstanceFields(holder, fn, initialValue);
+  }
+
+  public <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return backing.traverseStaticFields(holder, fn);
+  }
+
+  public <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return backing.traverseStaticFields(holder, fn, initialValue);
+  }
+
   public boolean verify() {
     forEachField(
         field -> {
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
index 522748e..cce38fd 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
@@ -34,6 +34,22 @@
       BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
       CT initialValue);
 
+  abstract <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn);
+
+  abstract <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue);
+
+  abstract <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn);
+
+  abstract <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue);
+
   // Collection methods.
 
   abstract int size();
diff --git a/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
index 6b74673..a6d9b57 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.google.common.base.Predicates.alwaysTrue;
+
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
@@ -49,15 +51,7 @@
   @Override
   <BT, CT> TraversalContinuation<BT, CT> traverse(
       DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
-    TraversalContinuation<BT, CT> traversalContinuation = TraversalContinuation.doContinue();
-    for (DexEncodedField definition : fieldMap.values()) {
-      DexClassAndField field = DexClassAndField.create(holder, definition);
-      traversalContinuation = fn.apply(field);
-      if (traversalContinuation.shouldBreak()) {
-        return traversalContinuation;
-      }
-    }
-    return traversalContinuation;
+    return traverseFieldsThatMatches(holder, fn, alwaysTrue());
   }
 
   @Override
@@ -65,13 +59,68 @@
       DexClass holder,
       BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
       CT initialValue) {
+    return traverseFieldsThatMatches(holder, fn, alwaysTrue(), initialValue);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return traverseFieldsThatMatches(holder, fn, DexEncodedField::isInstance);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseInstanceFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return traverseFieldsThatMatches(holder, fn, DexEncodedField::isInstance, initialValue);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder, Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn) {
+    return traverseFieldsThatMatches(holder, fn, DexEncodedField::isStatic);
+  }
+
+  @Override
+  <BT, CT> TraversalContinuation<BT, CT> traverseStaticFields(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      CT initialValue) {
+    return traverseFieldsThatMatches(holder, fn, DexEncodedField::isStatic, initialValue);
+  }
+
+  private <BT, CT> TraversalContinuation<BT, CT> traverseFieldsThatMatches(
+      DexClass holder,
+      Function<? super DexClassAndField, TraversalContinuation<BT, CT>> fn,
+      Predicate<? super DexEncodedField> predicate) {
+    TraversalContinuation<BT, CT> traversalContinuation = TraversalContinuation.doContinue();
+    for (DexEncodedField definition : fieldMap.values()) {
+      if (predicate.test(definition)) {
+        DexClassAndField field = DexClassAndField.create(holder, definition);
+        traversalContinuation = fn.apply(field);
+        if (traversalContinuation.shouldBreak()) {
+          return traversalContinuation;
+        }
+      }
+    }
+    return traversalContinuation;
+  }
+
+  private <BT, CT> TraversalContinuation<BT, CT> traverseFieldsThatMatches(
+      DexClass holder,
+      BiFunction<? super DexClassAndField, ? super CT, TraversalContinuation<BT, CT>> fn,
+      Predicate<? super DexEncodedField> predicate,
+      CT initialValue) {
     TraversalContinuation<BT, CT> traversalContinuation =
         TraversalContinuation.doContinue(initialValue);
     for (DexEncodedField definition : fieldMap.values()) {
-      DexClassAndField field = DexClassAndField.create(holder, definition);
-      traversalContinuation = fn.apply(field, traversalContinuation.asContinue().getValue());
-      if (traversalContinuation.shouldBreak()) {
-        return traversalContinuation;
+      if (predicate.test(definition)) {
+        DexClassAndField field = DexClassAndField.create(holder, definition);
+        traversalContinuation = fn.apply(field, traversalContinuation.asContinue().getValue());
+        if (traversalContinuation.shouldBreak()) {
+          return traversalContinuation;
+        }
       }
     }
     return traversalContinuation;
diff --git a/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java b/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
index a84af1a..035ab26 100644
--- a/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
+++ b/src/main/java/com/android/tools/r8/graph/fixup/TreeFixerBase.java
@@ -28,6 +28,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
@@ -297,15 +298,23 @@
       return permittedSubclassAttributes;
     }
     boolean changed = false;
-    List<PermittedSubclassAttribute> newPermittedSubclassAttributes =
-        new ArrayList<>(permittedSubclassAttributes.size());
+    LinkedHashSet<DexType> newPermittedSubclassTypes =
+        new LinkedHashSet<>(permittedSubclassAttributes.size());
     for (PermittedSubclassAttribute permittedSubclassAttribute : permittedSubclassAttributes) {
-      DexType permittedSubclassType = permittedSubclassAttribute.getPermittedSubclass();
-      DexType newPermittedSubclassType = fixupType(permittedSubclassType);
-      newPermittedSubclassAttributes.add(new PermittedSubclassAttribute(newPermittedSubclassType));
-      changed |= newPermittedSubclassType != permittedSubclassType;
+      DexType permittedSubClassType = permittedSubclassAttribute.getPermittedSubclass();
+      DexType newPermittedSubClassType = fixupType(permittedSubClassType);
+      newPermittedSubclassTypes.add(newPermittedSubClassType);
+      changed |= newPermittedSubClassType != permittedSubClassType;
     }
-    return changed ? newPermittedSubclassAttributes : permittedSubclassAttributes;
+    if (!changed) {
+      return permittedSubclassAttributes;
+    }
+    List<PermittedSubclassAttribute> newPermittedSubclassAttributes =
+        new ArrayList<>(newPermittedSubclassTypes.size());
+    for (DexType newPermittedSubclassType : newPermittedSubclassTypes) {
+      newPermittedSubclassAttributes.add(new PermittedSubclassAttribute(newPermittedSubclassType));
+    }
+    return newPermittedSubclassAttributes;
   }
 
   protected List<RecordComponentInfo> fixupRecordComponents(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerUtils.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerUtils.java
index bd83a89..6eecb7a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerUtils.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerUtils.java
@@ -7,9 +7,14 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.ProgramField;
 
 public class HorizontalClassMergerUtils {
 
+  public static boolean isClassIdField(AppView<?> appView, ProgramField field) {
+    return isClassIdField(appView, field.getDefinition());
+  }
+
   public static boolean isClassIdField(AppView<?> appView, DexEncodedField field) {
     DexField classIdField = appView.dexItemFactory().objectMembers.classIdField;
     if (field.isD8R8Synthesized() && field.getType().isIntType()) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
index 804b505..ee56cc7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -157,6 +157,8 @@
     clazz.setInnerClasses(fixupInnerClassAttributes(clazz.getInnerClasses()));
     clazz.setNestHostAttribute(fixupNestHost(clazz.getNestHostClassAttribute()));
     clazz.setNestMemberAttributes(fixupNestMemberAttributes(clazz.getNestMembersClassAttributes()));
+    clazz.setPermittedSubclassAttributes(
+        fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
   }
 
   private void fixupProgramClassSuperTypes(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index ec226d9..9676db7 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.utils.BooleanBox;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.BitSet;
@@ -36,24 +37,8 @@
  */
 public class SparseConditionalConstantPropagation extends CodeRewriterPass<AppInfo> {
 
-  private final IRCode code;
-  private final Map<Value, LatticeElement> mapping = new HashMap<>();
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private final Deque<Value> ssaEdges = new LinkedList<>();
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private final Deque<BasicBlock> flowEdges = new LinkedList<>();
-
-  private final BitSet[] executableFlowEdges;
-  private final BitSet visitedBlocks;
-
-  public SparseConditionalConstantPropagation(AppView<?> appView, IRCode code) {
+  public SparseConditionalConstantPropagation(AppView<?> appView) {
     super(appView);
-    this.code = code;
-    int maxBlockNumber = code.getCurrentBlockNumber() + 1;
-    executableFlowEdges = new BitSet[maxBlockNumber];
-    visitedBlocks = new BitSet(maxBlockNumber);
   }
 
   @Override
@@ -68,220 +53,255 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    BasicBlock firstBlock = code.entryBlock();
-    visitInstructions(firstBlock);
+    return new SparseConditionalConstantPropagationOnCode(code).run();
+  }
 
-    while (!flowEdges.isEmpty() || !ssaEdges.isEmpty()) {
-      while (!flowEdges.isEmpty()) {
-        BasicBlock block = flowEdges.poll();
-        for (Phi phi : block.getPhis()) {
-          visitPhi(phi);
+  private class SparseConditionalConstantPropagationOnCode {
+
+    private final IRCode code;
+    private final Map<Value, LatticeElement> mapping = new HashMap<>();
+
+    // TODO(b/270398965): Replace LinkedList.
+    @SuppressWarnings("JdkObsolete")
+    private final Deque<Value> ssaEdges = new LinkedList<>();
+
+    // TODO(b/270398965): Replace LinkedList.
+    @SuppressWarnings("JdkObsolete")
+    private final Deque<BasicBlock> flowEdges = new LinkedList<>();
+
+    private final BitSet[] executableFlowEdges;
+    private final BitSet visitedBlocks;
+
+    private SparseConditionalConstantPropagationOnCode(IRCode code) {
+      this.code = code;
+      int maxBlockNumber = code.getCurrentBlockNumber() + 1;
+      executableFlowEdges = new BitSet[maxBlockNumber];
+      visitedBlocks = new BitSet(maxBlockNumber);
+    }
+
+    protected CodeRewriterResult run() {
+      BasicBlock firstBlock = code.entryBlock();
+      visitInstructions(firstBlock);
+
+      while (!flowEdges.isEmpty() || !ssaEdges.isEmpty()) {
+        while (!flowEdges.isEmpty()) {
+          BasicBlock block = flowEdges.poll();
+          for (Phi phi : block.getPhis()) {
+            visitPhi(phi);
+          }
+          if (!visitedBlocks.get(block.getNumber())) {
+            visitInstructions(block);
+          }
         }
-        if (!visitedBlocks.get(block.getNumber())) {
-          visitInstructions(block);
-        }
-      }
-      while (!ssaEdges.isEmpty()) {
-        Value value = ssaEdges.poll();
-        for (Phi phi : value.uniquePhiUsers()) {
-          visitPhi(phi);
-        }
-        for (Instruction user : value.uniqueUsers()) {
-          BasicBlock userBlock = user.getBlock();
-          if (visitedBlocks.get(userBlock.getNumber())) {
-            visitInstruction(user);
+        while (!ssaEdges.isEmpty()) {
+          Value value = ssaEdges.poll();
+          for (Phi phi : value.uniquePhiUsers()) {
+            visitPhi(phi);
+          }
+          for (Instruction user : value.uniqueUsers()) {
+            BasicBlock userBlock = user.getBlock();
+            if (visitedBlocks.get(userBlock.getNumber())) {
+              visitInstruction(user);
+            }
           }
         }
       }
+      boolean hasChanged = rewriteConstants();
+      assert code.isConsistentSSA(appView);
+      return CodeRewriterResult.hasChanged(hasChanged);
     }
-    rewriteConstants();
-    assert code.isConsistentSSA(appView);
-    return CodeRewriterResult.NONE;
-  }
 
-  private void rewriteConstants() {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    List<BasicBlock> blockToAnalyze = new ArrayList<>();
-    mapping.entrySet().stream()
-        .filter(entry -> entry.getValue().isConst())
-        .forEach(
-            entry -> {
-              Value value = entry.getKey();
-              ConstNumber evaluatedConst = entry.getValue().asConst().getConstNumber();
-              if (value.definition != evaluatedConst) {
-                value.addAffectedValuesTo(affectedValues);
-                if (value.isPhi()) {
-                  // D8 relies on dead code removal to get rid of the dead phi itself.
-                  if (value.hasAnyUsers()) {
-                    BasicBlock block = value.asPhi().getBlock();
-                    blockToAnalyze.add(block);
-                    // Create a new constant, because it can be an existing constant that flow
-                    // directly
-                    // into the phi.
-                    ConstNumber newConst = ConstNumber.copyOf(code, evaluatedConst);
-                    InstructionListIterator iterator = block.listIterator(code);
-                    Instruction inst = iterator.nextUntil(i -> !i.isMoveException());
-                    newConst.setPosition(inst.getPosition());
-                    if (!inst.isDebugPosition()) {
-                      iterator.previous();
+    private boolean rewriteConstants() {
+      Set<Value> affectedValues = Sets.newIdentityHashSet();
+      List<BasicBlock> blockToAnalyze = new ArrayList<>();
+      BooleanBox hasChanged = new BooleanBox(false);
+      mapping.entrySet().stream()
+          .filter(entry -> entry.getValue().isConst())
+          .forEach(
+              entry -> {
+                Value value = entry.getKey();
+                ConstNumber evaluatedConst = entry.getValue().asConst().getConstNumber();
+                if (value.definition != evaluatedConst) {
+                  value.addAffectedValuesTo(affectedValues);
+                  if (value.isPhi()) {
+                    // D8 relies on dead code removal to get rid of the dead phi itself.
+                    if (value.hasAnyUsers()) {
+                      BasicBlock block = value.asPhi().getBlock();
+                      blockToAnalyze.add(block);
+                      // Create a new constant, because it can be an existing constant that flow
+                      // directly
+                      // into the phi.
+                      ConstNumber newConst = ConstNumber.copyOf(code, evaluatedConst);
+                      InstructionListIterator iterator = block.listIterator(code);
+                      Instruction inst = iterator.nextUntil(i -> !i.isMoveException());
+                      newConst.setPosition(inst.getPosition());
+                      if (!inst.isDebugPosition()) {
+                        iterator.previous();
+                      }
+                      iterator.add(newConst);
+                      value.replaceUsers(newConst.outValue());
+                      hasChanged.set();
                     }
-                    iterator.add(newConst);
-                    value.replaceUsers(newConst.outValue());
+                  } else {
+                    BasicBlock block = value.definition.getBlock();
+                    InstructionListIterator iterator = block.listIterator(code);
+                    iterator.nextUntil(i -> i == value.definition);
+                    iterator.replaceCurrentInstruction(evaluatedConst);
+                    hasChanged.set();
                   }
-                } else {
-                  BasicBlock block = value.definition.getBlock();
-                  InstructionListIterator iterator = block.listIterator(code);
-                  iterator.nextUntil(i -> i == value.definition);
-                  iterator.replaceCurrentInstruction(evaluatedConst);
                 }
-              }
-            });
-    for (BasicBlock block : blockToAnalyze) {
-      block.deduplicatePhis();
+              });
+      for (BasicBlock block : blockToAnalyze) {
+        block.deduplicatePhis();
+      }
+      if (!affectedValues.isEmpty()) {
+        new TypeAnalysis(appView).narrowing(affectedValues);
+      }
+      boolean changed = hasChanged.get();
+      if (changed) {
+        code.removeAllDeadAndTrivialPhis();
+        code.removeRedundantBlocks();
+      }
+      return changed;
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
+
+    private LatticeElement getLatticeElement(Value value) {
+      return mapping.getOrDefault(value, Top.getInstance());
     }
-    code.removeAllDeadAndTrivialPhis();
-    code.removeRedundantBlocks();
-  }
 
-  private LatticeElement getLatticeElement(Value value) {
-    return mapping.getOrDefault(value, Top.getInstance());
-  }
+    private void setLatticeElement(Value value, LatticeElement element) {
+      mapping.put(value, element);
+    }
 
-  private void setLatticeElement(Value value, LatticeElement element) {
-    mapping.put(value, element);
-  }
-
-  private void visitPhi(Phi phi) {
-    BasicBlock phiBlock = phi.getBlock();
-    int phiBlockNumber = phiBlock.getNumber();
-    LatticeElement element = Top.getInstance();
-    List<BasicBlock> predecessors = phiBlock.getPredecessors();
-    int size = predecessors.size();
-    for (int i = 0; i < size; i++) {
-      BasicBlock predecessor = predecessors.get(i);
-      if (isExecutableEdge(predecessor.getNumber(), phiBlockNumber)) {
-        element = element.meet(getLatticeElement(phi.getOperand(i)));
-        // bottom lattice can no longer be changed, thus no need to continue
-        if (element.isBottom()) {
-          break;
+    private void visitPhi(Phi phi) {
+      BasicBlock phiBlock = phi.getBlock();
+      int phiBlockNumber = phiBlock.getNumber();
+      LatticeElement element = Top.getInstance();
+      List<BasicBlock> predecessors = phiBlock.getPredecessors();
+      int size = predecessors.size();
+      for (int i = 0; i < size; i++) {
+        BasicBlock predecessor = predecessors.get(i);
+        if (isExecutableEdge(predecessor.getNumber(), phiBlockNumber)) {
+          element = element.meet(getLatticeElement(phi.getOperand(i)));
+          // bottom lattice can no longer be changed, thus no need to continue
+          if (element.isBottom()) {
+            break;
+          }
+        }
+      }
+      if (!element.isTop()) {
+        LatticeElement currentPhiElement = getLatticeElement(phi);
+        if (currentPhiElement.meet(element) != currentPhiElement) {
+          ssaEdges.add(phi);
+          setLatticeElement(phi, element);
         }
       }
     }
-    if (!element.isTop()) {
-      LatticeElement currentPhiElement = getLatticeElement(phi);
-      if (currentPhiElement.meet(element) != currentPhiElement) {
-        ssaEdges.add(phi);
-        setLatticeElement(phi, element);
+
+    private void visitInstructions(BasicBlock block) {
+      for (Instruction instruction : block.getInstructions()) {
+        visitInstruction(instruction);
+      }
+      visitedBlocks.set(block.getNumber());
+    }
+
+    private void visitInstruction(Instruction instruction) {
+      if (instruction.outValue() != null && !instruction.isDebugLocalUninitialized()) {
+        LatticeElement element = instruction.evaluate(code, this::getLatticeElement);
+        LatticeElement currentLattice = getLatticeElement(instruction.outValue());
+        if (currentLattice.meet(element) != currentLattice) {
+          setLatticeElement(instruction.outValue(), element);
+          ssaEdges.add(instruction.outValue());
+        }
+      }
+      if (instruction.isJumpInstruction()) {
+        addFlowEdgesForJumpInstruction(instruction.asJumpInstruction());
       }
     }
-  }
 
-  private void visitInstructions(BasicBlock block) {
-    for (Instruction instruction : block.getInstructions()) {
-      visitInstruction(instruction);
-    }
-    visitedBlocks.set(block.getNumber());
-  }
-
-  private void visitInstruction(Instruction instruction) {
-    if (instruction.outValue() != null && !instruction.isDebugLocalUninitialized()) {
-      LatticeElement element = instruction.evaluate(code, this::getLatticeElement);
-      LatticeElement currentLattice = getLatticeElement(instruction.outValue());
-      if (currentLattice.meet(element) != currentLattice) {
-        setLatticeElement(instruction.outValue(), element);
-        ssaEdges.add(instruction.outValue());
-      }
-    }
-    if (instruction.isJumpInstruction()) {
-      addFlowEdgesForJumpInstruction(instruction.asJumpInstruction());
-    }
-  }
-
-  private void addFlowEdgesForJumpInstruction(JumpInstruction jumpInstruction) {
-    BasicBlock jumpInstBlock = jumpInstruction.getBlock();
-    int jumpInstBlockNumber = jumpInstBlock.getNumber();
-    if (jumpInstruction.isIf()) {
-      If theIf = jumpInstruction.asIf();
-      if (theIf.isZeroTest()) {
-        LatticeElement element = getLatticeElement(theIf.inValues().get(0));
-        if (element.isConst()) {
-          BasicBlock target = theIf.targetFromCondition(element.asConst().getConstNumber());
-          if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
-            setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-            flowEdges.add(target);
+    private void addFlowEdgesForJumpInstruction(JumpInstruction jumpInstruction) {
+      BasicBlock jumpInstBlock = jumpInstruction.getBlock();
+      int jumpInstBlockNumber = jumpInstBlock.getNumber();
+      if (jumpInstruction.isIf()) {
+        If theIf = jumpInstruction.asIf();
+        if (theIf.isZeroTest()) {
+          LatticeElement element = getLatticeElement(theIf.inValues().get(0));
+          if (element.isConst()) {
+            BasicBlock target = theIf.targetFromCondition(element.asConst().getConstNumber());
+            if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
+              setExecutableEdge(jumpInstBlockNumber, target.getNumber());
+              flowEdges.add(target);
+            }
+            return;
           }
+        } else {
+          LatticeElement leftElement = getLatticeElement(theIf.inValues().get(0));
+          LatticeElement rightElement = getLatticeElement(theIf.inValues().get(1));
+          if (leftElement.isConst() && rightElement.isConst()) {
+            ConstNumber leftNumber = leftElement.asConst().getConstNumber();
+            ConstNumber rightNumber = rightElement.asConst().getConstNumber();
+            BasicBlock target = theIf.targetFromCondition(leftNumber, rightNumber);
+            if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
+              setExecutableEdge(jumpInstBlockNumber, target.getNumber());
+              flowEdges.add(target);
+            }
+            return;
+          }
+          assert !leftElement.isTop();
+          assert !rightElement.isTop();
+        }
+      } else if (jumpInstruction.isIntSwitch()) {
+        IntSwitch switchInst = jumpInstruction.asIntSwitch();
+        LatticeElement switchElement = getLatticeElement(switchInst.value());
+        if (switchElement.isConst()) {
+          BasicBlock target =
+              switchInst.getKeyToTargetMap().get(switchElement.asConst().getIntValue());
+          if (target == null) {
+            target = switchInst.fallthroughBlock();
+          }
+          assert target != null;
+          setExecutableEdge(jumpInstBlockNumber, target.getNumber());
+          flowEdges.add(target);
+          return;
+        }
+      } else if (jumpInstruction.isStringSwitch()) {
+        StringSwitch switchInst = jumpInstruction.asStringSwitch();
+        LatticeElement switchElement = getLatticeElement(switchInst.value());
+        if (switchElement.isConst()) {
+          // There is currently no constant propagation for strings, so it must be null.
+          assert switchElement.asConst().getConstNumber().isZero();
+          BasicBlock target = switchInst.fallthroughBlock();
+          setExecutableEdge(jumpInstBlockNumber, target.getNumber());
+          flowEdges.add(target);
           return;
         }
       } else {
-        LatticeElement leftElement = getLatticeElement(theIf.inValues().get(0));
-        LatticeElement rightElement = getLatticeElement(theIf.inValues().get(1));
-        if (leftElement.isConst() && rightElement.isConst()) {
-          ConstNumber leftNumber = leftElement.asConst().getConstNumber();
-          ConstNumber rightNumber = rightElement.asConst().getConstNumber();
-          BasicBlock target = theIf.targetFromCondition(leftNumber, rightNumber);
-          if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
-            setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-            flowEdges.add(target);
-          }
-          return;
+        assert jumpInstruction.isGoto() || jumpInstruction.isReturn() || jumpInstruction.isThrow();
+      }
+
+      for (BasicBlock dst : jumpInstBlock.getSuccessors()) {
+        if (!isExecutableEdge(jumpInstBlockNumber, dst.getNumber())) {
+          setExecutableEdge(jumpInstBlockNumber, dst.getNumber());
+          flowEdges.add(dst);
         }
-        assert !leftElement.isTop();
-        assert !rightElement.isTop();
       }
-    } else if (jumpInstruction.isIntSwitch()) {
-      IntSwitch switchInst = jumpInstruction.asIntSwitch();
-      LatticeElement switchElement = getLatticeElement(switchInst.value());
-      if (switchElement.isConst()) {
-        BasicBlock target = switchInst.getKeyToTargetMap()
-            .get(switchElement.asConst().getIntValue());
-        if (target == null) {
-          target = switchInst.fallthroughBlock();
-        }
-        assert target != null;
-        setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-        flowEdges.add(target);
-        return;
-      }
-    } else if (jumpInstruction.isStringSwitch()) {
-      StringSwitch switchInst = jumpInstruction.asStringSwitch();
-      LatticeElement switchElement = getLatticeElement(switchInst.value());
-      if (switchElement.isConst()) {
-        // There is currently no constant propagation for strings, so it must be null.
-        assert switchElement.asConst().getConstNumber().isZero();
-        BasicBlock target = switchInst.fallthroughBlock();
-        setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-        flowEdges.add(target);
-        return;
-      }
-    } else {
-      assert jumpInstruction.isGoto() || jumpInstruction.isReturn() || jumpInstruction.isThrow();
     }
 
-    for (BasicBlock dst : jumpInstBlock.getSuccessors()) {
-      if (!isExecutableEdge(jumpInstBlockNumber, dst.getNumber())) {
-        setExecutableEdge(jumpInstBlockNumber, dst.getNumber());
-        flowEdges.add(dst);
+    private void setExecutableEdge(int from, int to) {
+      BitSet previousExecutable = executableFlowEdges[to];
+      if (previousExecutable == null) {
+        previousExecutable = new BitSet(executableFlowEdges.length);
+        executableFlowEdges[to] = previousExecutable;
       }
+      previousExecutable.set(from);
     }
-  }
 
-  private void setExecutableEdge(int from, int to) {
-    BitSet previousExecutable = executableFlowEdges[to];
-    if (previousExecutable == null) {
-      previousExecutable = new BitSet(executableFlowEdges.length);
-      executableFlowEdges[to] = previousExecutable;
+    private boolean isExecutableEdge(int from, int to) {
+      BitSet previousExecutable = executableFlowEdges[to];
+      if (previousExecutable == null) {
+        return false;
+      }
+      return previousExecutable.get(from);
     }
-    previousExecutable.set(from);
-  }
-
-  private boolean isExecutableEdge(int from, int to) {
-    BitSet previousExecutable = executableFlowEdges[to];
-    if (previousExecutable == null) {
-      return false;
-    }
-    return previousExecutable.get(from);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
index d871616..e6ac132 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
@@ -85,7 +85,7 @@
             appView.appInfo().resolveField(fieldInstruction.getField()).getProgramField();
         if (field != null) {
           if (fieldAssignmentTracker != null) {
-            fieldAssignmentTracker.recordFieldAccess(fieldInstruction, field, code.context());
+            fieldAssignmentTracker.recordFieldAccess(fieldInstruction, field);
           }
           if (fieldBitAccessAnalysis != null) {
             fieldBitAccessAnalysis.recordFieldAccess(
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
index 0781787..0f47d7e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
@@ -45,12 +45,13 @@
 import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.ProgramFieldMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -76,7 +77,7 @@
   // has been seen to the field.
   private final Map<DexEncodedField, FieldState> fieldStates = new ConcurrentHashMap<>();
 
-  private final Map<DexProgramClass, Map<DexEncodedField, AbstractValue>>
+  private final Map<DexProgramClass, ProgramFieldMap<AbstractValue>>
       abstractFinalInstanceFieldValues = new ConcurrentHashMap<>();
 
   FieldAssignmentTracker(AppView<AppInfoWithLiveness> appView) {
@@ -116,15 +117,14 @@
             // No instance fields to track.
             return;
           }
-          Map<DexEncodedField, AbstractValue> abstractFinalInstanceFieldValuesForClass =
-              new IdentityHashMap<>();
+          ProgramFieldMap<AbstractValue> abstractFinalInstanceFieldValuesForClass =
+              ProgramFieldMap.create();
           clazz.forEachProgramInstanceField(
               field -> {
                 if (field.isFinalOrEffectivelyFinal(appView)) {
                   FieldAccessInfo fieldAccessInfo = fieldAccessInfos.get(field.getReference());
                   if (fieldAccessInfo != null && !fieldAccessInfo.hasReflectiveAccess()) {
-                    abstractFinalInstanceFieldValuesForClass.put(
-                        field.getDefinition(), BottomValue.getInstance());
+                    abstractFinalInstanceFieldValuesForClass.put(field, BottomValue.getInstance());
                   }
                 }
               });
@@ -185,13 +185,13 @@
         });
   }
 
-  void recordFieldAccess(FieldInstruction instruction, ProgramField field, ProgramMethod context) {
+  void recordFieldAccess(FieldInstruction instruction, ProgramField field) {
     if (instruction.isFieldPut()) {
-      recordFieldPut(field, instruction.value(), context);
+      recordFieldPut(field, instruction.value());
     }
   }
 
-  private void recordFieldPut(ProgramField field, Value value, ProgramMethod context) {
+  private void recordFieldPut(ProgramField field, Value value) {
     // For now only attempt to prove that fields are definitely null. In order to prove a single
     // value for fields that are not definitely null, we need to prove that the given field is never
     // read before it is written.
@@ -225,12 +225,12 @@
 
           if (fieldState.isArray()) {
             ConcreteArrayTypeFieldState arrayFieldState = fieldState.asArray();
-            return arrayFieldState.mutableJoin(appView, abstractValue);
+            return arrayFieldState.mutableJoin(appView, field, abstractValue);
           }
 
           if (fieldState.isPrimitive()) {
             ConcretePrimitiveTypeFieldState primitiveFieldState = fieldState.asPrimitive();
-            return primitiveFieldState.mutableJoin(abstractValue, abstractValueFactory);
+            return primitiveFieldState.mutableJoin(appView, field, abstractValue);
           }
 
           assert fieldState.isClass();
@@ -242,7 +242,7 @@
   }
 
   void recordAllocationSite(NewInstance instruction, DexProgramClass clazz, ProgramMethod context) {
-    Map<DexEncodedField, AbstractValue> abstractInstanceFieldValuesForClass =
+    ProgramFieldMap<AbstractValue> abstractInstanceFieldValuesForClass =
         abstractFinalInstanceFieldValues.get(clazz);
     if (abstractInstanceFieldValuesForClass == null) {
       // We are not tracking the value of any of clazz' instance fields.
@@ -273,75 +273,59 @@
     // Synchronize on the lattice element (abstractInstanceFieldValuesForClass) in case we process
     // another allocation site of `clazz` concurrently.
     synchronized (abstractInstanceFieldValuesForClass) {
-      Iterator<Map.Entry<DexEncodedField, AbstractValue>> iterator =
-          abstractInstanceFieldValuesForClass.entrySet().iterator();
-      while (iterator.hasNext()) {
-        Map.Entry<DexEncodedField, AbstractValue> entry = iterator.next();
-        DexEncodedField field = entry.getKey();
-        AbstractValue abstractValue = entry.getValue();
-
-        // The power set lattice is an expensive abstraction, so use it with caution.
-        boolean isClassIdField = HorizontalClassMergerUtils.isClassIdField(appView, field);
-
-        InstanceFieldInitializationInfo initializationInfo =
-            initializationInfoCollection.get(field);
-        if (initializationInfo.isArgumentInitializationInfo()) {
-          InstanceFieldArgumentInitializationInfo argumentInitializationInfo =
-              initializationInfo.asArgumentInitializationInfo();
-          Value argument = invoke.arguments().get(argumentInitializationInfo.getArgumentIndex());
-          AbstractValue argumentAbstractValue = argument.getAbstractValue(appView, context);
-          abstractValue =
-              abstractValue.join(
-                  argumentAbstractValue,
-                  appView.abstractValueFactory(),
-                  field.getType().isReferenceType(),
-                  isClassIdField);
-          assert !abstractValue.isBottom();
-        } else if (initializationInfo.isSingleValue()) {
-          SingleValue singleValueInitializationInfo = initializationInfo.asSingleValue();
-          abstractValue =
-              abstractValue.join(
-                  singleValueInitializationInfo,
-                  appView.abstractValueFactory(),
-                  field.getType().isReferenceType(),
-                  isClassIdField);
-        } else if (initializationInfo.isTypeInitializationInfo()) {
-          // TODO(b/149732532): Not handled, for now.
-          abstractValue = UnknownValue.getInstance();
-        } else {
-          assert initializationInfo.isUnknown();
-          abstractValue = UnknownValue.getInstance();
-        }
-
-        assert !abstractValue.isBottom();
-
-        // When approximating the possible values for the $r8$classId fields from horizontal class
-        // merging, give up if the set of possible values equals the size of the merge group. In
-        // this case, the information is useless.
-        if (isClassIdField && abstractValue.isNonConstantNumberValue()) {
-          NonConstantNumberValue initialAbstractValue =
-              field.getOptimizationInfo().getAbstractValue().asNonConstantNumberValue();
-          if (initialAbstractValue != null) {
-            if (abstractValue.asNonConstantNumberValue().getAbstractionSize()
-                >= initialAbstractValue.getAbstractionSize()) {
+      abstractInstanceFieldValuesForClass.removeIf(
+          (field, abstractValue, entry) -> {
+            InstanceFieldInitializationInfo initializationInfo =
+                initializationInfoCollection.get(field);
+            if (initializationInfo.isArgumentInitializationInfo()) {
+              InstanceFieldArgumentInitializationInfo argumentInitializationInfo =
+                  initializationInfo.asArgumentInitializationInfo();
+              Value argument =
+                  invoke.arguments().get(argumentInitializationInfo.getArgumentIndex());
+              AbstractValue argumentAbstractValue = argument.getAbstractValue(appView, context);
+              abstractValue =
+                  appView
+                      .getAbstractValueFieldJoiner()
+                      .join(abstractValue, argumentAbstractValue, field);
+            } else if (initializationInfo.isSingleValue()) {
+              SingleValue singleValueInitializationInfo = initializationInfo.asSingleValue();
+              abstractValue =
+                  appView
+                      .getAbstractValueFieldJoiner()
+                      .join(abstractValue, singleValueInitializationInfo, field);
+            } else if (initializationInfo.isTypeInitializationInfo()) {
+              // TODO(b/149732532): Not handled, for now.
+              abstractValue = UnknownValue.getInstance();
+            } else {
+              assert initializationInfo.isUnknown();
               abstractValue = UnknownValue.getInstance();
             }
-          }
-        }
 
-        if (!abstractValue.isUnknown()) {
-          entry.setValue(abstractValue);
-          continue;
-        }
+            assert !abstractValue.isBottom();
 
-        // We just lost track for this field.
-        iterator.remove();
-      }
+            // When approximating the possible values for the $r8$classId fields from horizontal
+            // class
+            // merging, give up if the set of possible values equals the size of the merge group. In
+            // this case, the information is useless.
+            if (abstractValue.isNonConstantNumberValue()) {
+              assert HorizontalClassMergerUtils.isClassIdField(appView, field);
+              NonConstantNumberValue initialAbstractValue =
+                  field.getOptimizationInfo().getAbstractValue().asNonConstantNumberValue();
+              if (initialAbstractValue != null
+                  && abstractValue.asNonConstantNumberValue().getAbstractionSize()
+                      >= initialAbstractValue.getAbstractionSize()) {
+                abstractValue = UnknownValue.getInstance();
+              }
+            }
+
+            entry.setValue(abstractValue);
+            return abstractValue.isUnknown();
+          });
     }
   }
 
   private void recordAllFieldPutsProcessed(
-      ProgramField field, ProgramMethod context, OptimizationFeedbackDelayed feedback) {
+      ProgramField field, OptimizationFeedbackDelayed feedback) {
     FieldState fieldState = fieldStates.getOrDefault(field.getDefinition(), FieldState.bottom());
     AbstractValue abstractValue = fieldState.getAbstractValue(appView.abstractValueFactory());
     if (abstractValue.isNonTrivial()) {
@@ -384,10 +368,9 @@
                 .get(field);
         if (fieldInitializationInfo.isSingleValue()) {
           abstractValue =
-              abstractValue.join(
-                  fieldInitializationInfo.asSingleValue(),
-                  appView.abstractValueFactory(),
-                  field.getType());
+              appView
+                  .getAbstractValueFieldJoiner()
+                  .join(abstractValue, fieldInitializationInfo.asSingleValue(), field);
           if (abstractValue.isUnknown()) {
             break;
           }
@@ -413,24 +396,25 @@
 
   private void recordAllAllocationsSitesProcessed(
       DexProgramClass clazz, OptimizationFeedbackDelayed feedback) {
-    Map<DexEncodedField, AbstractValue> abstractInstanceFieldValuesForClass =
+    ProgramFieldMap<AbstractValue> abstractInstanceFieldValuesForClass =
         abstractFinalInstanceFieldValues.get(clazz);
     if (abstractInstanceFieldValuesForClass == null) {
       return;
     }
 
-    for (DexEncodedField field : clazz.instanceFields()) {
-      AbstractValue abstractValue =
-          abstractInstanceFieldValuesForClass.getOrDefault(field, UnknownValue.getInstance());
-      if (abstractValue.isBottom()) {
-        feedback.modifyAppInfoWithLiveness(modifier -> modifier.removeInstantiatedType(clazz));
-        break;
-      }
-      if (abstractValue.isUnknown()) {
-        continue;
-      }
-      feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
-    }
+    clazz.traverseProgramInstanceFields(
+        field -> {
+          AbstractValue abstractValue =
+              abstractInstanceFieldValuesForClass.getOrDefault(field, UnknownValue.getInstance());
+          if (abstractValue.isBottom()) {
+            feedback.modifyAppInfoWithLiveness(modifier -> modifier.removeInstantiatedType(clazz));
+            return TraversalContinuation.doBreak();
+          }
+          if (abstractValue.isNonTrivial()) {
+            feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
+          }
+          return TraversalContinuation.doContinue();
+        });
   }
 
   public void waveDone(ProgramMethodSet wave, OptimizationFeedbackDelayed feedback) {
@@ -438,8 +422,7 @@
     // therefore important that the optimization info has been flushed in advance.
     assert feedback.noUpdatesLeft();
     for (ProgramMethod method : wave) {
-      fieldAccessGraph.markProcessed(
-          method, field -> recordAllFieldPutsProcessed(field, method, feedback));
+      fieldAccessGraph.markProcessed(method, field -> recordAllFieldPutsProcessed(field, feedback));
       objectAllocationGraph.markProcessed(
           method, clazz -> recordAllAllocationsSitesProcessed(clazz, feedback));
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java
index 1b82558..fef8935 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.analysis.fieldaccess.state;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
@@ -36,9 +37,10 @@
     return this;
   }
 
-  public FieldState mutableJoin(AppView<AppInfoWithLiveness> appView, AbstractValue abstractValue) {
+  public FieldState mutableJoin(
+      AppView<AppInfoWithLiveness> appView, ProgramField field, AbstractValue abstractValue) {
     this.abstractValue =
-        this.abstractValue.joinReference(abstractValue, appView.abstractValueFactory());
+        appView.getAbstractValueFieldJoiner().join(this.abstractValue, abstractValue, field);
     return isEffectivelyUnknown() ? unknown() : this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java
index 3f51c25..a323ba6 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java
@@ -48,7 +48,7 @@
       ProgramField field) {
     assert field.getType().isClassType();
     this.abstractValue =
-        this.abstractValue.joinReference(abstractValue, appView.abstractValueFactory());
+        appView.getAbstractValueFieldJoiner().join(this.abstractValue, abstractValue, field);
     this.dynamicType =
         WideningUtils.widenDynamicNonReceiverType(
             appView, this.dynamicType.join(appView, dynamicType), field.getType());
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java
index c2eb778..1f329f5 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java
@@ -4,8 +4,11 @@
 
 package com.android.tools.r8.ir.analysis.fieldaccess.state;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 /** The information that we track for fields whose type is a primitive type. */
 public class ConcretePrimitiveTypeFieldState extends ConcreteFieldState {
@@ -38,11 +41,12 @@
   }
 
   public FieldState mutableJoin(
-      AbstractValue abstractValue, AbstractValueFactory abstractValueFactory) {
+      AppView<AppInfoWithLiveness> appView, ProgramField field, AbstractValue abstractValue) {
     if (abstractValue.isUnknown()) {
       return FieldState.unknown();
     }
-    this.abstractValue = this.abstractValue.joinPrimitive(abstractValue, abstractValueFactory);
+    this.abstractValue =
+        appView.getAbstractValueFieldJoiner().join(this.abstractValue, abstractValue, field);
     return isEffectivelyUnknown() ? unknown() : this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index c4e0a83..f657c65 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.analysis.value;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -75,6 +74,10 @@
     return null;
   }
 
+  public boolean hasDefinitelySetAndUnsetBitsInformation() {
+    return false;
+  }
+
   public boolean hasKnownArrayLength() {
     return false;
   }
@@ -176,71 +179,12 @@
     return null;
   }
 
-  public AbstractValue join(AbstractValue other, AbstractValueFactory factory, DexType type) {
-    return join(other, factory, type.isReferenceType(), false);
+  public boolean isDefiniteBitsNumberValue() {
+    return false;
   }
 
-  public AbstractValue joinPrimitive(AbstractValue other, AbstractValueFactory factory) {
-    return join(other, factory, false, false);
-  }
-
-  public AbstractValue joinReference(AbstractValue other, AbstractValueFactory factory) {
-    return join(other, factory, true, false);
-  }
-
-  // TODO(b/196321452): Clean this up, in particular, replace the "allow" parameters by a
-  //  configuration object.
-  public AbstractValue join(
-      AbstractValue other,
-      AbstractValueFactory factory,
-      boolean allowNullOrAbstractValue,
-      boolean allowNonConstantNumbers) {
-    if (isBottom() || other.isUnknown()) {
-      return other;
-    }
-    if (isUnknown() || other.isBottom()) {
-      return this;
-    }
-    if (equals(other)) {
-      return this;
-    }
-    if (allowNullOrAbstractValue) {
-      if (isNull()) {
-        return NullOrAbstractValue.create(other);
-      }
-      if (other.isNull()) {
-        return NullOrAbstractValue.create(this);
-      }
-      if (isNullOrAbstractValue() && asNullOrAbstractValue().getNonNullValue().equals(other)) {
-        return this;
-      }
-      if (other.isNullOrAbstractValue()
-          && other.asNullOrAbstractValue().getNonNullValue().equals(this)) {
-        return other;
-      }
-      return unknown();
-    }
-    assert !isNullOrAbstractValue();
-    assert !other.isNullOrAbstractValue();
-    if (allowNonConstantNumbers
-        && isConstantOrNonConstantNumberValue()
-        && other.isConstantOrNonConstantNumberValue()) {
-      NumberFromSetValue.Builder numberFromSetValueBuilder;
-      if (isSingleNumberValue()) {
-        numberFromSetValueBuilder = NumberFromSetValue.builder(asSingleNumberValue());
-      } else {
-        assert isNumberFromSetValue();
-        numberFromSetValueBuilder = asNumberFromSetValue().instanceBuilder();
-      }
-      if (other.isSingleNumberValue()) {
-        numberFromSetValueBuilder.addInt(other.asSingleNumberValue().getIntValue());
-      } else {
-        assert other.isNumberFromSetValue();
-        numberFromSetValueBuilder.addInts(other.asNumberFromSetValue());
-      }
-      return numberFromSetValueBuilder.build(factory);
-    }
-    return unknown();
+  public DefiniteBitsNumberValue asDefiniteBitsNumberValue() {
+    return null;
   }
 
   public abstract AbstractValue rewrittenWithLens(
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 bab5e75..bc50262 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
@@ -23,6 +23,14 @@
   private ConcurrentHashMap<Integer, KnownLengthArrayState> knownArrayLengthStates =
       new ConcurrentHashMap<>();
 
+  public AbstractValue createDefiniteBitsNumberValue(
+      int definitelySetBits, int definitelyUnsetBits) {
+    if (definitelySetBits != 0 && definitelyUnsetBits != 0) {
+      return new DefiniteBitsNumberValue(definitelySetBits, definitelyUnsetBits);
+    }
+    return AbstractValue.unknown();
+  }
+
   public SingleConstClassValue createSingleConstClassValue(DexType type) {
     return singleConstClassValues.computeIfAbsent(type, SingleConstClassValue::new);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueJoiner.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueJoiner.java
new file mode 100644
index 0000000..0c49fca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueJoiner.java
@@ -0,0 +1,217 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.value;
+
+import static com.android.tools.r8.ir.analysis.value.AbstractValue.unknown;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerUtils;
+
+public abstract class AbstractValueJoiner {
+
+  protected final AppView<? extends AppInfoWithClassHierarchy> appView;
+
+  private AbstractValueJoiner(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    this.appView = appView;
+  }
+
+  private AbstractValueFactory factory() {
+    return appView.abstractValueFactory();
+  }
+
+  final AbstractValue internalJoin(
+      AbstractValue abstractValue,
+      AbstractValue otherAbstractValue,
+      AbstractValueJoinerConfig config,
+      DexType type) {
+    if (abstractValue.isBottom() || otherAbstractValue.isUnknown()) {
+      return otherAbstractValue;
+    }
+    if (abstractValue.isUnknown()
+        || otherAbstractValue.isBottom()
+        || abstractValue.equals(otherAbstractValue)) {
+      return abstractValue;
+    }
+    return type.isReferenceType()
+        ? joinReference(abstractValue, otherAbstractValue)
+        : joinPrimitive(abstractValue, otherAbstractValue, config, type);
+  }
+
+  private AbstractValue joinPrimitive(
+      AbstractValue abstractValue,
+      AbstractValue otherAbstractValue,
+      AbstractValueJoinerConfig config,
+      DexType type) {
+    assert !abstractValue.isNullOrAbstractValue();
+    assert !otherAbstractValue.isNullOrAbstractValue();
+
+    if (config.canUseNumberIntervalAndNumberSetAbstraction()
+        && abstractValue.isConstantOrNonConstantNumberValue()
+        && otherAbstractValue.isConstantOrNonConstantNumberValue()) {
+      NumberFromSetValue.Builder numberFromSetValueBuilder;
+      if (abstractValue.isSingleNumberValue()) {
+        numberFromSetValueBuilder = NumberFromSetValue.builder(abstractValue.asSingleNumberValue());
+      } else {
+        assert abstractValue.isNumberFromSetValue();
+        numberFromSetValueBuilder = abstractValue.asNumberFromSetValue().instanceBuilder();
+      }
+      if (otherAbstractValue.isSingleNumberValue()) {
+        numberFromSetValueBuilder.addInt(otherAbstractValue.asSingleNumberValue().getIntValue());
+      } else {
+        assert otherAbstractValue.isNumberFromSetValue();
+        numberFromSetValueBuilder.addInts(otherAbstractValue.asNumberFromSetValue());
+      }
+      return numberFromSetValueBuilder.build(factory());
+    }
+
+    if (config.canUseDefiniteBitsAbstraction()) {
+      return joinPrimitiveToDefiniteBitsNumberValue(abstractValue, otherAbstractValue, type);
+    }
+
+    return unknown();
+  }
+
+  private AbstractValue joinPrimitiveToDefiniteBitsNumberValue(
+      AbstractValue abstractValue, AbstractValue otherAbstractValue, DexType type) {
+    assert type.isIntType();
+    if (!abstractValue.hasDefinitelySetAndUnsetBitsInformation()
+        || !otherAbstractValue.hasDefinitelySetAndUnsetBitsInformation()) {
+      return unknown();
+    }
+    // Normalize order.
+    if (!abstractValue.isSingleNumberValue() && otherAbstractValue.isSingleNumberValue()) {
+      AbstractValue tmp = abstractValue;
+      abstractValue = otherAbstractValue;
+      otherAbstractValue = tmp;
+    }
+    if (abstractValue.isSingleNumberValue()) {
+      SingleNumberValue singleNumberValue = abstractValue.asSingleNumberValue();
+      if (otherAbstractValue.isSingleNumberValue()) {
+        SingleNumberValue otherSingleNumberValue = otherAbstractValue.asSingleNumberValue();
+        return factory()
+            .createDefiniteBitsNumberValue(
+                singleNumberValue.getDefinitelySetIntBits()
+                    & otherSingleNumberValue.getDefinitelySetIntBits(),
+                singleNumberValue.getDefinitelyUnsetIntBits()
+                    & otherSingleNumberValue.getDefinitelyUnsetIntBits());
+      } else {
+        assert otherAbstractValue.isDefiniteBitsNumberValue();
+        DefiniteBitsNumberValue otherDefiniteBitsNumberValue =
+            otherAbstractValue.asDefiniteBitsNumberValue();
+        return otherDefiniteBitsNumberValue.join(factory(), singleNumberValue);
+      }
+    } else {
+      // Both are guaranteed to be non-const due to normalization.
+      assert abstractValue.isDefiniteBitsNumberValue();
+      assert otherAbstractValue.isDefiniteBitsNumberValue();
+      DefiniteBitsNumberValue definiteBitsNumberValue = abstractValue.asDefiniteBitsNumberValue();
+      DefiniteBitsNumberValue otherDefiniteBitsNumberValue =
+          otherAbstractValue.asDefiniteBitsNumberValue();
+      return definiteBitsNumberValue.join(factory(), otherDefiniteBitsNumberValue);
+    }
+  }
+
+  private AbstractValue joinReference(
+      AbstractValue abstractValue, AbstractValue otherAbstractValue) {
+    if (abstractValue.isNull()) {
+      return NullOrAbstractValue.create(otherAbstractValue);
+    }
+    if (otherAbstractValue.isNull()) {
+      return NullOrAbstractValue.create(abstractValue);
+    }
+    if (abstractValue.isNullOrAbstractValue()
+        && abstractValue.asNullOrAbstractValue().getNonNullValue().equals(otherAbstractValue)) {
+      return abstractValue;
+    }
+    if (otherAbstractValue.isNullOrAbstractValue()
+        && otherAbstractValue.asNullOrAbstractValue().getNonNullValue().equals(abstractValue)) {
+      return otherAbstractValue;
+    }
+    return unknown();
+  }
+
+  public static class AbstractValueFieldJoiner extends AbstractValueJoiner {
+
+    public AbstractValueFieldJoiner(AppView<? extends AppInfoWithClassHierarchy> appView) {
+      super(appView);
+    }
+
+    public AbstractValue join(
+        AbstractValue abstractValue, AbstractValue otherAbstractValue, ProgramField field) {
+      AbstractValueJoinerConfig config = getConfig(field);
+      AbstractValue result =
+          internalJoin(abstractValue, otherAbstractValue, config, field.getType());
+      assert result.equals(
+          internalJoin(otherAbstractValue, abstractValue, config, field.getType()));
+      return result;
+    }
+
+    private AbstractValueJoinerConfig getConfig(ProgramField field) {
+      if (HorizontalClassMergerUtils.isClassIdField(appView, field)) {
+        return AbstractValueJoinerConfig.getClassIdFieldConfig();
+      }
+      return AbstractValueJoinerConfig.getDefaultConfig();
+    }
+  }
+
+  public static class AbstractValueParameterJoiner extends AbstractValueJoiner {
+
+    public AbstractValueParameterJoiner(AppView<? extends AppInfoWithClassHierarchy> appView) {
+      super(appView);
+    }
+
+    public AbstractValue join(
+        AbstractValue abstractValue, AbstractValue otherAbstractValue, DexType type) {
+      // TODO(b/196017578): Use a config that allows the definite bits abstraction for parameters
+      //  used in bitwise operations.
+      AbstractValueJoinerConfig config = AbstractValueJoinerConfig.getDefaultConfig();
+      AbstractValue result = internalJoin(abstractValue, otherAbstractValue, config, type);
+      assert result.equals(internalJoin(otherAbstractValue, abstractValue, config, type));
+      return result;
+    }
+  }
+
+  private static class AbstractValueJoinerConfig {
+
+    // The power set lattice is an expensive abstraction, so use it with caution.
+    private static final AbstractValueJoinerConfig CLASS_ID_FIELD_CONFIG =
+        new AbstractValueJoinerConfig().setCanUseNumberIntervalAndNumberSetAbstraction();
+
+    private static final AbstractValueJoinerConfig DEFAULT_CONFIG = new AbstractValueJoinerConfig();
+
+    public static AbstractValueJoinerConfig getClassIdFieldConfig() {
+      return CLASS_ID_FIELD_CONFIG;
+    }
+
+    public static AbstractValueJoinerConfig getDefaultConfig() {
+      return DEFAULT_CONFIG;
+    }
+
+    private boolean canUseDefiniteBitsAbstraction;
+    private boolean canUseNumberIntervalAndNumberSetAbstraction;
+
+    boolean canUseDefiniteBitsAbstraction() {
+      return canUseDefiniteBitsAbstraction;
+    }
+
+    @SuppressWarnings("UnusedMethod")
+    AbstractValueJoinerConfig setCanUseDefiniteBitsAbstraction() {
+      canUseDefiniteBitsAbstraction = true;
+      return this;
+    }
+
+    boolean canUseNumberIntervalAndNumberSetAbstraction() {
+      return canUseNumberIntervalAndNumberSetAbstraction;
+    }
+
+    AbstractValueJoinerConfig setCanUseNumberIntervalAndNumberSetAbstraction() {
+      canUseNumberIntervalAndNumberSetAbstraction = true;
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
new file mode 100644
index 0000000..b081050
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.value;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.OptionalBool;
+import java.util.Objects;
+
+public class DefiniteBitsNumberValue extends NonConstantNumberValue {
+
+  private final int definitelySetBits;
+  private final int definitelyUnsetBits;
+
+  public DefiniteBitsNumberValue(int definitelySetBits, int definitelyUnsetBits) {
+    assert (definitelySetBits & definitelyUnsetBits) == 0;
+    this.definitelySetBits = definitelySetBits;
+    this.definitelyUnsetBits = definitelyUnsetBits;
+  }
+
+  @Override
+  public boolean containsInt(int value) {
+    return false;
+  }
+
+  @Override
+  public long getAbstractionSize() {
+    return Long.MAX_VALUE;
+  }
+
+  @Override
+  public boolean hasDefinitelySetAndUnsetBitsInformation() {
+    return true;
+  }
+
+  @Override
+  public boolean isDefiniteBitsNumberValue() {
+    return true;
+  }
+
+  @Override
+  public DefiniteBitsNumberValue asDefiniteBitsNumberValue() {
+    return this;
+  }
+
+  @Override
+  public boolean isNonTrivial() {
+    return true;
+  }
+
+  @Override
+  public OptionalBool isSubsetOf(int[] values) {
+    return OptionalBool.unknown();
+  }
+
+  public AbstractValue join(
+      AbstractValueFactory abstractValueFactory, DefiniteBitsNumberValue definiteBitsNumberValue) {
+    return join(
+        abstractValueFactory,
+        definiteBitsNumberValue.definitelySetBits,
+        definiteBitsNumberValue.definitelyUnsetBits);
+  }
+
+  public AbstractValue join(
+      AbstractValueFactory abstractValueFactory, SingleNumberValue singleNumberValue) {
+    return join(
+        abstractValueFactory,
+        singleNumberValue.getDefinitelySetIntBits(),
+        singleNumberValue.getDefinitelyUnsetIntBits());
+  }
+
+  public AbstractValue join(
+      AbstractValueFactory abstractValueFactory,
+      int otherDefinitelySetBits,
+      int otherDefinitelyUnsetBits) {
+    if (definitelySetBits == otherDefinitelySetBits
+        && definitelyUnsetBits == otherDefinitelyUnsetBits) {
+      return this;
+    }
+    return abstractValueFactory.createDefiniteBitsNumberValue(
+        definitelySetBits & otherDefinitelySetBits, definitelyUnsetBits & otherDefinitelyUnsetBits);
+  }
+
+  @Override
+  public boolean mayOverlapWith(ConstantOrNonConstantNumberValue other) {
+    return true;
+  }
+
+  @Override
+  public AbstractValue rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens lens, GraphLens codeLens) {
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || o.getClass() != getClass()) {
+      return false;
+    }
+    DefiniteBitsNumberValue definiteBitsNumberValue = (DefiniteBitsNumberValue) o;
+    return definitelySetBits == definiteBitsNumberValue.definitelySetBits
+        && definitelyUnsetBits == definiteBitsNumberValue.definitelyUnsetBits;
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = 31 * (31 * (31 + definitelySetBits) + definitelyUnsetBits);
+    assert hash == Objects.hash(definitelySetBits, definitelyUnsetBits);
+    return hash;
+  }
+
+  @Override
+  public String toString() {
+    return "DefiniteBitsNumberValue(set: "
+        + Integer.toBinaryString(definitelySetBits)
+        + "; unset: "
+        + Integer.toBinaryString(definitelyUnsetBits)
+        + ")";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
index 5327427..e29e468 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
@@ -37,6 +37,11 @@
   }
 
   @Override
+  public boolean hasDefinitelySetAndUnsetBitsInformation() {
+    return true;
+  }
+
+  @Override
   public OptionalBool isSubsetOf(int[] values) {
     return OptionalBool.of(ArrayUtils.containsInt(values, getIntValue()));
   }
@@ -81,6 +86,14 @@
     return value != 0;
   }
 
+  public int getDefinitelySetIntBits() {
+    return getIntValue();
+  }
+
+  public int getDefinitelyUnsetIntBits() {
+    return ~getDefinitelySetIntBits();
+  }
+
   public double getDoubleValue() {
     return Double.longBitsToDouble(value);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 0a5ebca..3afb79b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -439,13 +439,14 @@
 
   public static IRBuilder create(
       ProgramMethod method, AppView<?> appView, SourceCode source, Origin origin) {
+    GraphLens codeLens = method.getDefinition().getCode().getCodeLens(appView);
     return new IRBuilder(
         method,
         appView,
-        method.getDefinition().getCode().getCodeLens(appView),
+        codeLens,
         source,
         origin,
-        lookupPrototypeChanges(appView, method),
+        lookupPrototypeChanges(appView, method, codeLens),
         new NumberGenerator());
   }
 
@@ -462,8 +463,10 @@
   }
 
   public static RewrittenPrototypeDescription lookupPrototypeChanges(
-      AppView<?> appView, ProgramMethod method) {
-    return appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
+      AppView<?> appView, ProgramMethod method, GraphLens codeLens) {
+    return appView
+        .graphLens()
+        .lookupPrototypeChangesForMethodDefinition(method.getReference(), codeLens);
   }
 
   private IRBuilder(
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 5b22d0e..cd7af3b 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
@@ -756,7 +756,7 @@
     new ArrayConstructionSimplifier(appView).run(code, timing);
     new MoveResultRewriter(appView).run(code, timing);
     new StringBuilderAppendOptimizer(appView).run(code, timing);
-    new SparseConditionalConstantPropagation(appView, code).run(code, timing);
+    new SparseConditionalConstantPropagation(appView).run(code, timing);
     new ThrowCatchOptimizer(appView).run(code, timing);
     if (new BranchSimplifier(appView)
         .run(code, timing)
@@ -767,7 +767,7 @@
     }
     new SplitBranch(appView).run(code, timing);
     new RedundantConstNumberRemover(appView).run(code, timing);
-    new RedundantFieldLoadAndStoreElimination(appView, code).run(code, timing);
+    new RedundantFieldLoadAndStoreElimination(appView).run(code, timing);
     new BinopRewriter(appView).run(code, timing);
 
     timing.begin("Optimize class initializers");
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 729a2e5..93ce2a2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
@@ -55,7 +56,6 @@
   private DexApplication internalOptimize(
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService)
       throws ExecutionException {
-    appView.testing().enterLirSupportedPhase();
     // Desugaring happens in the enqueuer.
     assert instructionDesugaring.isEmpty();
 
@@ -217,30 +217,37 @@
 
     // Assure that no more optimization feedback left after post processing.
     assert feedback.noUpdatesLeft();
-    finalizeLirToOutputFormat(timing, executorService);
     return appView.appInfo().app();
   }
 
-  private void finalizeLirToOutputFormat(Timing timing, ExecutorService executorService)
+  public static void finalizeLirToOutputFormat(
+      AppView<?> appView, Timing timing, ExecutorService executorService)
       throws ExecutionException {
     appView.testing().exitLirSupportedPhase();
-    if (!options.testing.canUseLir(appView)) {
+    if (!appView.testing().canUseLir(appView)) {
       return;
     }
-    String output = options.isGeneratingClassFiles() ? "CF" : "DEX";
+    DeadCodeRemover deadCodeRemover = new DeadCodeRemover(appView);
+    String output = appView.options().isGeneratingClassFiles() ? "CF" : "DEX";
     timing.begin("LIR->IR->" + output);
     ThreadUtils.processItems(
         appView.appInfo().classes(),
-        clazz -> clazz.forEachProgramMethod(this::finalizeLirMethodToOutputFormat),
+        clazz ->
+            clazz.forEachProgramMethod(
+                m -> finalizeLirMethodToOutputFormat(m, deadCodeRemover, appView)),
         executorService);
     appView
         .getSyntheticItems()
         .getPendingSyntheticClasses()
-        .forEach(clazz -> clazz.forEachProgramMethod(this::finalizeLirMethodToOutputFormat));
+        .forEach(
+            clazz ->
+                clazz.forEachProgramMethod(
+                    m -> finalizeLirMethodToOutputFormat(m, deadCodeRemover, appView)));
     timing.end();
   }
 
-  void finalizeLirMethodToOutputFormat(ProgramMethod method) {
+  private static void finalizeLirMethodToOutputFormat(
+      ProgramMethod method, DeadCodeRemover deadCodeRemover, AppView<?> appView) {
     Code code = method.getDefinition().getCode();
     if (!(code instanceof LirCode)) {
       return;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index 3176d29..b8a54fb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -94,14 +94,16 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = false;
     WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks);
     while (worklist.hasNext()) {
       BasicBlock block = worklist.next();
-      simplifyArrayConstructionBlock(block, worklist, code, appView.options());
+      hasChanged |= simplifyArrayConstructionBlock(block, worklist, code, appView.options());
     }
-    // Do only when the rewriter pass has changed something.
-    code.removeRedundantBlocks();
-    return CodeRewriterResult.NONE;
+    if (hasChanged) {
+      code.removeRedundantBlocks();
+    }
+    return CodeRewriterResult.hasChanged(hasChanged);
   }
 
   @Override
@@ -109,8 +111,9 @@
     return appView.options().isGeneratingDex();
   }
 
-  private void simplifyArrayConstructionBlock(
+  private boolean simplifyArrayConstructionBlock(
       BasicBlock block, WorkList<BasicBlock> worklist, IRCode code, InternalOptions options) {
+    boolean hasChanged = false;
     RewriteArrayOptions rewriteOptions = options.rewriteArrayOptions();
     InstructionListIterator it = block.listIterator(code);
     while (it.hasNext()) {
@@ -199,7 +202,9 @@
 
       // The above has invalidated the block iterator so reset it and continue.
       it = block.listIterator(code, instructionAfterCandidate);
+      hasChanged = true;
     }
+    return hasChanged;
   }
 
   private short[] computeArrayFilledData(Value[] values, int size, int elementSize) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
index 70c64f1..aed22c2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
@@ -252,6 +252,7 @@
 
   @Override
   public CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = false;
     InstructionListIterator iterator = code.instructionListIterator();
     while (iterator.hasNext()) {
       Instruction next = iterator.next();
@@ -262,39 +263,42 @@
           BinopDescriptor binopDescriptor = descriptors.get(binop.getClass());
           assert binopDescriptor != null;
           if (identityAbsorbingSimplification(iterator, binop, binopDescriptor)) {
+            hasChanged = true;
             continue;
           }
-          successiveSimplification(iterator, binop, binopDescriptor, code);
+          hasChanged |= successiveSimplification(iterator, binop, binopDescriptor, code);
         }
       }
     }
-    code.removeAllDeadAndTrivialPhis();
-    code.removeRedundantBlocks();
+    if (hasChanged) {
+      code.removeAllDeadAndTrivialPhis();
+      code.removeRedundantBlocks();
+    }
     assert code.isConsistentSSA(appView);
-    return CodeRewriterResult.NONE;
+    return CodeRewriterResult.hasChanged(hasChanged);
   }
 
-  private void successiveSimplification(
+  private boolean successiveSimplification(
       InstructionListIterator iterator, Binop binop, BinopDescriptor binopDescriptor, IRCode code) {
     if (binop.outValue().hasDebugUsers()) {
-      return;
+      return false;
     }
     ConstNumber constBLeft = getConstNumber(binop.leftValue());
     ConstNumber constBRight = getConstNumber(binop.rightValue());
     if ((constBLeft != null && constBRight != null)
         || (constBLeft == null && constBRight == null)) {
-      return;
+      return false;
     }
     Value otherValue = constBLeft == null ? binop.leftValue() : binop.rightValue();
     if (otherValue.isPhi() || !otherValue.getDefinition().isBinop()) {
-      return;
+      return false;
     }
     Binop prevBinop = otherValue.getDefinition().asBinop();
     ConstNumber constALeft = getConstNumber(prevBinop.leftValue());
     ConstNumber constARight = getConstNumber(prevBinop.rightValue());
     if ((constALeft != null && constARight != null)
         || (constALeft == null && constARight == null)) {
-      return;
+      return false;
     }
     ConstNumber constB = constBLeft == null ? constBRight : constBLeft;
     ConstNumber constA = constALeft == null ? constARight : constALeft;
@@ -306,11 +310,13 @@
         assert binop.isCommutative();
         Value newConst = addNewConstNumber(code, iterator, constB, constA, binopDescriptor);
         replaceBinop(iterator, code, input, newConst, binopDescriptor);
+        return true;
       } else if (binopDescriptor.isShift()) {
         // x shift: a shift: b => x shift: (a + b) where a + b is a constant.
         if (constBRight != null && constARight != null) {
           Value newConst = addNewConstNumber(code, iterator, constB, constA, BinopDescriptor.ADD);
           replaceBinop(iterator, code, input, newConst, binopDescriptor);
+          return true;
         }
       } else if (binop.isSub() && constBRight != null) {
         // a - x - b => (a - b) - x where (a - b) is a constant.
@@ -319,9 +325,11 @@
         if (constARight == null) {
           Value newConst = addNewConstNumber(code, iterator, constA, constB, BinopDescriptor.SUB);
           replaceBinop(iterator, code, newConst, input, BinopDescriptor.SUB);
+          return true;
         } else {
           Value newConst = addNewConstNumber(code, iterator, constB, constA, BinopDescriptor.ADD);
           replaceBinop(iterator, code, input, newConst, BinopDescriptor.SUB);
+          return true;
         }
       }
     } else {
@@ -331,18 +339,22 @@
         // We ignore b - (x + a) and b - (a + x) with constBRight != null.
         Value newConst = addNewConstNumber(code, iterator, constA, constB, BinopDescriptor.SUB);
         replaceBinop(iterator, code, newConst, input, BinopDescriptor.ADD);
+        return true;
       } else if (binop.isAdd() && prevBinop.isSub()) {
         // x - a + b => x - (a - b) where (a - b) is a constant.
         // a - x + b => (a + b) - x where (a + b) is a constant.
         if (constALeft == null) {
           Value newConst = addNewConstNumber(code, iterator, constA, constB, BinopDescriptor.SUB);
           replaceBinop(iterator, code, input, newConst, BinopDescriptor.SUB);
+          return true;
         } else {
           Value newConst = addNewConstNumber(code, iterator, constB, constA, BinopDescriptor.ADD);
           replaceBinop(iterator, code, newConst, input, BinopDescriptor.SUB);
+          return true;
         }
       }
     }
+    return false;
   }
 
   private void replaceBinop(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
index 7057abc..33ec433 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
@@ -41,6 +41,7 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = false;
     int noCandidate = code.reserveMarkingColor();
     if (hasCSECandidate(code, noCandidate)) {
       final ListMultimap<Wrapper<Instruction>, Value> instructionToValue =
@@ -65,6 +66,7 @@
                   instruction.outValue().replaceUsers(candidate);
                   candidate.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
                   eliminated = true;
+                  hasChanged = true;
                   iterator.removeOrReplaceByDebugLocalRead();
                   break; // Don't try any more candidates.
                 }
@@ -78,9 +80,11 @@
       }
     }
     code.returnMarkingColor(noCandidate);
-    code.removeRedundantBlocks();
+    if (hasChanged) {
+      code.removeRedundantBlocks();
+    }
     assert code.isConsistentSSA(appView);
-    return CodeRewriterResult.NONE;
+    return CodeRewriterResult.hasChanged(hasChanged);
   }
 
   private static class CSEExpressionEquivalence extends Equivalence<Instruction> {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 96b52dc..2666be2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -53,7 +53,7 @@
       code.removeRedundantBlocks();
       assert code.isConsistentSSA(appView);
     }
-    return CodeRewriterResult.NONE;
+    return CodeRewriterResult.hasChanged(loopRemoved);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
index 70402cc..99caca6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
@@ -52,22 +52,16 @@
 
   @Override
   protected boolean shouldRewriteCode(IRCode code) {
+    if (appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()
+        && !appView.options().testing.forceRedundantConstNumberRemoval) {
+      // See also b/124152497.
+      return false;
+    }
     return options.enableRedundantConstNumberOptimization && code.metadata().mayHaveConstNumber();
   }
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    redundantConstNumberRemoval(code);
-    return CodeRewriterResult.NONE;
-  }
-
-  public void redundantConstNumberRemoval(IRCode code) {
-    if (appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()
-        && !appView.options().testing.forceRedundantConstNumberRemoval) {
-      // See also b/124152497.
-      return;
-    }
-
     LazyBox<Long2ReferenceMap<List<ConstNumber>>> constantsByValue =
         new LazyBox<>(() -> getConstantsByValue(code));
     LazyBox<DominatorTree> dominatorTree = new LazyBox<>(() -> new DominatorTree(code));
@@ -169,6 +163,7 @@
       code.removeAllDeadAndTrivialPhis();
     }
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.hasChanged(changed);
   }
 
   private static Long2ReferenceMap<List<ConstNumber>> getConstantsByValue(IRCode code) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 2defd76..6ceb56d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -56,11 +56,11 @@
   protected CodeRewriterResult rewriteCode(IRCode code) {
     List<BasicBlock> candidates = computeCandidates(code);
     if (candidates.isEmpty()) {
-      return CodeRewriterResult.NONE;
+      return CodeRewriterResult.NO_CHANGE;
     }
     Map<Goto, BasicBlock> newTargets = findGotosToRetarget(candidates);
     if (newTargets.isEmpty()) {
-      return CodeRewriterResult.NONE;
+      return CodeRewriterResult.NO_CHANGE;
     }
     retargetGotos(newTargets);
     Set<Value> affectedValues = Sets.newIdentityHashSet();
@@ -74,7 +74,7 @@
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
-    return CodeRewriterResult.NONE;
+    return CodeRewriterResult.HAS_CHANGED;
   }
 
   private void retargetGotos(Map<Goto, BasicBlock> newTargets) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
index fd79613..920417a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
@@ -59,15 +59,15 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    optimizeAlwaysThrowingInstructions(code);
+    boolean hasChanged = optimizeAlwaysThrowingInstructions(code);
     if (!isDebugMode(code.context())) {
-      rewriteThrowNullPointerException(code);
+      hasChanged |= rewriteThrowNullPointerException(code);
     }
-    return CodeRewriterResult.NONE;
+    return CodeRewriterResult.hasChanged(hasChanged);
   }
 
   // Rewrite 'throw new NullPointerException()' to 'throw null'.
-  private void rewriteThrowNullPointerException(IRCode code) {
+  private boolean rewriteThrowNullPointerException(IRCode code) {
     boolean hasChanged = false;
     boolean shouldRemoveUnreachableBlocks = false;
     for (BasicBlock block : code.blocks) {
@@ -209,17 +209,19 @@
       code.removeRedundantBlocks();
     }
     assert code.isConsistentSSA(appView);
+    return hasChanged;
   }
 
   // Find all instructions that always throw, split the block after each such instruction and follow
   // it with a block throwing a null value (which should result in NPE). Note that this throw is not
   // expected to be ever reached, but is intended to satisfy verifier.
-  private void optimizeAlwaysThrowingInstructions(IRCode code) {
+  private boolean optimizeAlwaysThrowingInstructions(IRCode code) {
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     ProgramMethod context = code.context();
     boolean hasUnlinkedCatchHandlers = false;
+    boolean hasChanged = false;
     // For cyclic phis we sometimes do not propagate the dynamic upper type after rewritings.
     // The inValue.isAlwaysNull(appView) check below will not recompute the dynamic type of phi's
     // so we recompute all phis here if they are always null.
@@ -287,6 +289,7 @@
             }
             instructionIterator.replaceCurrentInstructionWithThrowNull(
                 appView, code, blockIterator, blocksToRemove, affectedValues);
+            hasChanged = true;
             continue;
           }
         }
@@ -324,6 +327,7 @@
           instructionIterator.replaceCurrentInstructionWithThrowNull(
               appView, code, blockIterator, blocksToRemove, affectedValues);
           instructionIterator.unsetInsertionPosition();
+          hasChanged = true;
         }
       }
     }
@@ -335,8 +339,11 @@
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
-    code.removeRedundantBlocks();
+    if (hasChanged) {
+      code.removeRedundantBlocks();
+    }
     assert code.isConsistentSSA(appView);
+    return hasChanged;
   }
 
   // Find any case where we have a catch followed immediately and only by a rethrow. This is extra
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index 5b45fd5..b2b0782 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -44,6 +44,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.RedundantFieldLoadAndStoreElimination.RedundantFieldLoadAndStoreEliminationOnCode.ExistingValue;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -71,29 +72,8 @@
   private static final int MAX_CAPACITY = 10000;
   private static final int MIN_CAPACITY_PER_BLOCK = 50;
 
-  private final ProgramMethod method;
-  private final IRCode code;
-  private final int maxCapacityPerBlock;
-  private final boolean release;
-
-  // Values that may require type propagation.
-  private final Set<Value> affectedValues = Sets.newIdentityHashSet();
-
-  // Maps keeping track of fields that have an already loaded value at basic block entry.
-  private final BlockStates activeStates = new BlockStates();
-
-  // Maps keeping track of fields with already loaded values for the current block during
-  // elimination.
-  private BlockState activeState;
-
-  private final Map<BasicBlock, Set<Instruction>> instructionsToRemove = new IdentityHashMap<>();
-
-  public RedundantFieldLoadAndStoreElimination(AppView<?> appView, IRCode code) {
+  public RedundantFieldLoadAndStoreElimination(AppView<?> appView) {
     super(appView);
-    this.method = code.context();
-    this.code = code;
-    this.maxCapacityPerBlock = Math.max(MIN_CAPACITY_PER_BLOCK, MAX_CAPACITY / code.blocks.size());
-    this.release = !appView.options().debug;
   }
 
   @Override
@@ -111,8 +91,7 @@
 
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    run();
-    return CodeRewriterResult.NONE;
+    return new RedundantFieldLoadAndStoreEliminationOnCode(code).run();
   }
 
   private interface FieldValue {
@@ -126,80 +105,6 @@
     TypeElement getType(AppView<?> appView, TypeElement outType);
   }
 
-  private class ExistingValue implements FieldValue {
-
-    private final Value value;
-
-    private ExistingValue(Value value) {
-      this.value = value;
-    }
-
-    @Override
-    public ExistingValue asExistingValue() {
-      return this;
-    }
-
-    @Override
-    public void eliminateRedundantRead(InstructionListIterator it, Instruction redundant) {
-      affectedValues.addAll(redundant.outValue().affectedValues());
-      redundant.outValue().replaceUsers(value);
-      it.removeOrReplaceByDebugLocalRead();
-      value.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
-    }
-
-    @Override
-    public TypeElement getType(AppView<?> appView, TypeElement outType) {
-      return value.getType();
-    }
-
-    public Value getValue() {
-      return value;
-    }
-
-    @Override
-    public String toString() {
-      return "ExistingValue(v" + value.getNumber() + ")";
-    }
-  }
-
-  private class MaterializableValue implements FieldValue {
-
-    private final SingleValue value;
-
-    private MaterializableValue(SingleValue value) {
-      assert value.isMaterializableInContext(appView.withLiveness(), method);
-      this.value = value;
-    }
-
-    @Override
-    public void eliminateRedundantRead(InstructionListIterator it, Instruction redundant) {
-      affectedValues.addAll(redundant.outValue().affectedValues());
-      it.replaceCurrentInstruction(
-          value.createMaterializingInstruction(appView.withClassHierarchy(), code, redundant));
-    }
-
-    @Override
-    public TypeElement getType(AppView<?> appView, TypeElement outType) {
-      DexItemFactory dexItemFactory = appView.dexItemFactory();
-      if (value.isSingleStringValue() || value.isSingleDexItemBasedStringValue()) {
-        return dexItemFactory.stringType.toTypeElement(
-            RedundantFieldLoadAndStoreElimination.this.appView, Nullability.definitelyNotNull());
-      }
-      if (value.isSingleFieldValue()) {
-        return value.asSingleFieldValue().getField().getTypeElement(appView);
-      }
-      // For numbers (and null), we don't encode the type along with the value. Therefore, we
-      // fallback to the existing out type in this case.
-      assert value.isSingleNumberValue();
-      if (outType.isReferenceType()) {
-        assert value.isNull();
-        return TypeElement.getNull();
-      }
-      assert outType.isPrimitiveType();
-      return outType;
-    }
-  }
-
   private abstract static class ArraySlot {
 
     protected final Value array;
@@ -294,6 +199,7 @@
   }
 
   private static class FieldAndObject {
+
     private final DexField field;
     private final Value object;
 
@@ -318,578 +224,693 @@
     }
   }
 
-  public boolean isFinal(DexClassAndField field) {
-    if (field.isProgramField()) {
-      // Treat this field as being final if it is declared final or we have determined a constant
-      // value for it.
-      return field.getDefinition().isFinal()
-          || field.getDefinition().getOptimizationInfo().getAbstractValue().isSingleValue();
-    }
-    return appView.libraryMethodOptimizer().isFinalLibraryField(field.getDefinition());
-  }
+  class RedundantFieldLoadAndStoreEliminationOnCode {
 
-  private DexClassAndField resolveField(DexField field) {
-    if (appView.enableWholeProgramOptimizations()) {
-      SingleFieldResolutionResult resolutionResult =
-          appView.appInfo().withLiveness().resolveField(field).asSingleFieldResolutionResult();
-      return resolutionResult != null ? resolutionResult.getResolutionPair() : null;
-    }
-    if (field.getHolderType() == method.getHolderType()) {
-      return method.getHolder().lookupProgramField(field);
-    }
-    return null;
-  }
+    private final ProgramMethod method;
+    private final IRCode code;
+    private final int maxCapacityPerBlock;
+    private final boolean release;
 
-  public void run() {
-    Reference2IntMap<BasicBlock> pendingNormalSuccessors = new Reference2IntOpenHashMap<>();
-    for (BasicBlock block : code.blocks) {
-      if (!block.hasUniqueNormalSuccessor()) {
-        pendingNormalSuccessors.put(block, block.numberOfNormalSuccessors());
+    // Values that may require type propagation.
+    private final Set<Value> affectedValues = Sets.newIdentityHashSet();
+
+    // Maps keeping track of fields that have an already loaded value at basic block entry.
+    private final BlockStates activeStates = new BlockStates();
+
+    // Maps keeping track of fields with already loaded values for the current block during
+    // elimination.
+    private BlockState activeState;
+
+    private final Map<BasicBlock, Set<Instruction>> instructionsToRemove = new IdentityHashMap<>();
+
+    private boolean hasChanged = false;
+
+    private RedundantFieldLoadAndStoreEliminationOnCode(IRCode code) {
+      this.method = code.context();
+      this.code = code;
+      this.maxCapacityPerBlock =
+          Math.max(MIN_CAPACITY_PER_BLOCK, MAX_CAPACITY / code.blocks.size());
+      this.release = !appView.options().debug;
+    }
+
+    class ExistingValue implements FieldValue {
+
+      private final Value value;
+
+      private ExistingValue(Value value) {
+        this.value = value;
+      }
+
+      @Override
+      public ExistingValue asExistingValue() {
+        return this;
+      }
+
+      @Override
+      public void eliminateRedundantRead(InstructionListIterator it, Instruction redundant) {
+        affectedValues.addAll(redundant.outValue().affectedValues());
+        redundant.outValue().replaceUsers(value);
+        it.removeOrReplaceByDebugLocalRead();
+        value.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+        hasChanged = true;
+      }
+
+      @Override
+      public TypeElement getType(AppView<?> appView, TypeElement outType) {
+        return value.getType();
+      }
+
+      public Value getValue() {
+        return value;
+      }
+
+      @Override
+      public String toString() {
+        return "ExistingValue(v" + value.getNumber() + ")";
       }
     }
 
-    AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
-    for (BasicBlock head : code.topologicallySortedBlocks()) {
-      if (head.hasUniquePredecessor() && head.getUniquePredecessor().hasUniqueNormalSuccessor()) {
-        // Already visited.
-        continue;
+    private class MaterializableValue implements FieldValue {
+
+      private final SingleValue value;
+
+      private MaterializableValue(SingleValue value) {
+        assert value.isMaterializableInContext(appView.withLiveness(), method);
+        this.value = value;
       }
-      activeState = activeStates.computeActiveStateOnBlockEntry(head, maxCapacityPerBlock);
-      activeStates.removeDeadBlockExitStates(head, pendingNormalSuccessors);
-      BasicBlock block = head;
-      BasicBlock end = null;
-      do {
-        InstructionListIterator it = block.listIterator(code);
-        while (it.hasNext()) {
-          Instruction instruction = it.next();
-          if (instruction.isArrayAccess()) {
-            if (instruction.isArrayGet()) {
-              handleArrayGet(it, instruction.asArrayGet());
+
+      @Override
+      public void eliminateRedundantRead(InstructionListIterator it, Instruction redundant) {
+        affectedValues.addAll(redundant.outValue().affectedValues());
+        it.replaceCurrentInstruction(
+            value.createMaterializingInstruction(appView.withClassHierarchy(), code, redundant));
+        hasChanged = true;
+      }
+
+      @Override
+      public TypeElement getType(AppView<?> appView, TypeElement outType) {
+        DexItemFactory dexItemFactory = appView.dexItemFactory();
+        if (value.isSingleStringValue() || value.isSingleDexItemBasedStringValue()) {
+          return dexItemFactory.stringType.toTypeElement(
+              RedundantFieldLoadAndStoreElimination.this.appView, Nullability.definitelyNotNull());
+        }
+        if (value.isSingleFieldValue()) {
+          return value.asSingleFieldValue().getField().getTypeElement(appView);
+        }
+        // For numbers (and null), we don't encode the type along with the value. Therefore, we
+        // fallback to the existing out type in this case.
+        assert value.isSingleNumberValue();
+        if (outType.isReferenceType()) {
+          assert value.isNull();
+          return TypeElement.getNull();
+        }
+        assert outType.isPrimitiveType();
+        return outType;
+      }
+    }
+
+    public boolean isFinal(DexClassAndField field) {
+      if (field.isProgramField()) {
+        // Treat this field as being final if it is declared final or we have determined a constant
+        // value for it.
+        return field.getDefinition().isFinal()
+            || field.getDefinition().getOptimizationInfo().getAbstractValue().isSingleValue();
+      }
+      return appView.libraryMethodOptimizer().isFinalLibraryField(field.getDefinition());
+    }
+
+    private DexClassAndField resolveField(DexField field) {
+      if (appView.enableWholeProgramOptimizations()) {
+        SingleFieldResolutionResult resolutionResult =
+            appView.appInfo().withLiveness().resolveField(field).asSingleFieldResolutionResult();
+        return resolutionResult != null ? resolutionResult.getResolutionPair() : null;
+      }
+      if (field.getHolderType() == method.getHolderType()) {
+        return method.getHolder().lookupProgramField(field);
+      }
+      return null;
+    }
+
+    public CodeRewriterResult run() {
+      Reference2IntMap<BasicBlock> pendingNormalSuccessors = new Reference2IntOpenHashMap<>();
+      for (BasicBlock block : code.blocks) {
+        if (!block.hasUniqueNormalSuccessor()) {
+          pendingNormalSuccessors.put(block, block.numberOfNormalSuccessors());
+        }
+      }
+
+      AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
+      for (BasicBlock head : code.topologicallySortedBlocks()) {
+        if (head.hasUniquePredecessor() && head.getUniquePredecessor().hasUniqueNormalSuccessor()) {
+          // Already visited.
+          continue;
+        }
+        activeState = activeStates.computeActiveStateOnBlockEntry(head, maxCapacityPerBlock);
+        activeStates.removeDeadBlockExitStates(head, pendingNormalSuccessors);
+        BasicBlock block = head;
+        BasicBlock end = null;
+        do {
+          InstructionListIterator it = block.listIterator(code);
+          while (it.hasNext()) {
+            Instruction instruction = it.next();
+            if (instruction.isArrayAccess()) {
+              if (instruction.isArrayGet()) {
+                handleArrayGet(it, instruction.asArrayGet());
+              } else {
+                assert instruction.isArrayPut();
+                handleArrayPut(instruction.asArrayPut());
+              }
+            } else if (instruction.isFieldInstruction()) {
+              DexField reference = instruction.asFieldInstruction().getField();
+              DexClassAndField field = resolveField(reference);
+              if (field == null || field.getDefinition().isVolatile()) {
+                killAllNonFinalActiveFields();
+                continue;
+              }
+
+              if (instruction.isInstanceGet()) {
+                handleInstanceGet(it, instruction.asInstanceGet(), field, assumeRemover);
+              } else if (instruction.isInstancePut()) {
+                handleInstancePut(instruction.asInstancePut(), field);
+              } else if (instruction.isStaticGet()) {
+                handleStaticGet(it, instruction.asStaticGet(), field, assumeRemover);
+              } else if (instruction.isStaticPut()) {
+                handleStaticPut(instruction.asStaticPut(), field);
+              }
+            } else if (instruction.isAssume()) {
+              assumeRemover.removeIfMarked(instruction.asAssume(), it);
+            } else if (instruction.isInitClass()) {
+              handleInitClass(it, instruction.asInitClass());
+            } else if (instruction.isMonitor()) {
+              if (instruction.asMonitor().isEnter()) {
+                killAllNonFinalActiveFields();
+              }
+            } else if (instruction.isInvokeDirect()) {
+              handleInvokeDirect(instruction.asInvokeDirect());
+            } else if (instruction.isInvokeStatic()) {
+              handleInvokeStatic(instruction.asInvokeStatic());
+            } else if (instruction.isInvokeMethod() || instruction.isInvokeCustom()) {
+              killAllNonFinalActiveFields();
+            } else if (instruction.isNewInstance()) {
+              handleNewInstance(instruction.asNewInstance());
             } else {
-              assert instruction.isArrayPut();
-              handleArrayPut(instruction.asArrayPut());
-            }
-          } else if (instruction.isFieldInstruction()) {
-            DexField reference = instruction.asFieldInstruction().getField();
-            DexClassAndField field = resolveField(reference);
-            if (field == null || field.getDefinition().isVolatile()) {
-              killAllNonFinalActiveFields();
-              continue;
-            }
+              // If the current instruction could trigger a method invocation, it could also cause
+              // field values to change. In that case, it must be handled above.
+              assert !instruction.instructionMayTriggerMethodInvocation(appView, method);
 
-            if (instruction.isInstanceGet()) {
-              handleInstanceGet(it, instruction.asInstanceGet(), field, assumeRemover);
-            } else if (instruction.isInstancePut()) {
-              handleInstancePut(instruction.asInstancePut(), field);
-            } else if (instruction.isStaticGet()) {
-              handleStaticGet(it, instruction.asStaticGet(), field, assumeRemover);
-            } else if (instruction.isStaticPut()) {
-              handleStaticPut(instruction.asStaticPut(), field);
-            }
-          } else if (instruction.isAssume()) {
-            assumeRemover.removeIfMarked(instruction.asAssume(), it);
-          } else if (instruction.isInitClass()) {
-            handleInitClass(it, instruction.asInitClass());
-          } else if (instruction.isMonitor()) {
-            if (instruction.asMonitor().isEnter()) {
-              killAllNonFinalActiveFields();
-            }
-          } else if (instruction.isInvokeDirect()) {
-            handleInvokeDirect(instruction.asInvokeDirect());
-          } else if (instruction.isInvokeStatic()) {
-            handleInvokeStatic(instruction.asInvokeStatic());
-          } else if (instruction.isInvokeMethod() || instruction.isInvokeCustom()) {
-            killAllNonFinalActiveFields();
-          } else if (instruction.isNewInstance()) {
-            handleNewInstance(instruction.asNewInstance());
-          } else {
-            // If the current instruction could trigger a method invocation, it could also cause
-            // field values to change. In that case, it must be handled above.
-            assert !instruction.instructionMayTriggerMethodInvocation(appView, method);
+              // Clear the field writes.
+              if (instruction.instructionInstanceCanThrow(appView, method)) {
+                activeState.clearMostRecentFieldWrites();
+                activeState.clearMostRecentInitClass();
+              }
 
-            // Clear the field writes.
-            if (instruction.instructionInstanceCanThrow(appView, method)) {
-              activeState.clearMostRecentFieldWrites();
-              activeState.clearMostRecentInitClass();
+              // If this assertion fails for a new instruction we need to determine if that
+              // instruction has side-effects that can change the value of fields. If so, it must be
+              // handled above. If not, it can be safely added to the assert.
+              assert instruction.isArgument()
+                      || instruction.isArrayGet()
+                      || instruction.isArrayLength()
+                      || instruction.isArrayPut()
+                      || instruction.isAssume()
+                      || instruction.isBinop()
+                      || instruction.isCheckCast()
+                      || instruction.isConstClass()
+                      || instruction.isConstMethodHandle()
+                      || instruction.isConstMethodType()
+                      || instruction.isConstNumber()
+                      || instruction.isConstString()
+                      || instruction.isDebugInstruction()
+                      || instruction.isDexItemBasedConstString()
+                      || instruction.isGoto()
+                      || instruction.isIf()
+                      || instruction.isInstanceOf()
+                      || instruction.isInvokeMultiNewArray()
+                      || instruction.isInvokeNewArray()
+                      || instruction.isMoveException()
+                      || instruction.isNewArrayEmpty()
+                      || instruction.isNewArrayFilledData()
+                      || instruction.isReturn()
+                      || instruction.isSwitch()
+                      || instruction.isThrow()
+                      || instruction.isUnop()
+                      || instruction.isRecordFieldValues()
+                  : "Unexpected instruction of type " + instruction.getClass().getTypeName();
             }
-
-            // If this assertion fails for a new instruction we need to determine if that
-            // instruction has side-effects that can change the value of fields. If so, it must be
-            // handled above. If not, it can be safely added to the assert.
-            assert instruction.isArgument()
-                    || instruction.isArrayGet()
-                    || instruction.isArrayLength()
-                    || instruction.isArrayPut()
-                    || instruction.isAssume()
-                    || instruction.isBinop()
-                    || instruction.isCheckCast()
-                    || instruction.isConstClass()
-                    || instruction.isConstMethodHandle()
-                    || instruction.isConstMethodType()
-                    || instruction.isConstNumber()
-                    || instruction.isConstString()
-                    || instruction.isDebugInstruction()
-                    || instruction.isDexItemBasedConstString()
-                    || instruction.isGoto()
-                    || instruction.isIf()
-                    || instruction.isInstanceOf()
-                    || instruction.isInvokeMultiNewArray()
-                    || instruction.isInvokeNewArray()
-                    || instruction.isMoveException()
-                    || instruction.isNewArrayEmpty()
-                    || instruction.isNewArrayFilledData()
-                    || instruction.isReturn()
-                    || instruction.isSwitch()
-                    || instruction.isThrow()
-                    || instruction.isUnop()
-                    || instruction.isRecordFieldValues()
-                : "Unexpected instruction of type " + instruction.getClass().getTypeName();
           }
-        }
-        if (block.hasUniqueNormalSuccessorWithUniquePredecessor()) {
-          block = block.getUniqueNormalSuccessor();
-        } else {
-          end = block;
-          block = null;
-        }
-      } while (block != null);
-      assert end != null;
-      activeStates.recordActiveStateOnBlockExit(end, activeState);
+          if (block.hasUniqueNormalSuccessorWithUniquePredecessor()) {
+            block = block.getUniqueNormalSuccessor();
+          } else {
+            end = block;
+            block = null;
+          }
+        } while (block != null);
+        assert end != null;
+        activeStates.recordActiveStateOnBlockExit(end, activeState);
+      }
+      processInstructionsToRemove();
+      assumeRemover.removeMarkedInstructions().finish();
+      if (hasChanged) {
+        code.removeRedundantBlocks();
+      }
+      assert code.isConsistentSSA(appView);
+      return CodeRewriterResult.hasChanged(hasChanged);
     }
-    processInstructionsToRemove();
-    assumeRemover.removeMarkedInstructions().finish();
-    code.removeRedundantBlocks();
-    assert code.isConsistentSSA(appView);
-  }
 
-  private void processInstructionsToRemove() {
-    instructionsToRemove.forEach(
-        (block, instructionsToRemoveInBlock) -> {
-          assert instructionsToRemoveInBlock.stream()
-              .allMatch(instruction -> instruction.getBlock() == block);
-          InstructionListIterator instructionIterator = block.listIterator(code);
-          while (instructionIterator.hasNext()) {
-            Instruction instruction = instructionIterator.next();
-            assert !instruction.isJumpInstruction();
-            if (instructionsToRemoveInBlock.contains(instruction)) {
-              instructionIterator.removeOrReplaceByDebugLocalRead();
-              instructionsToRemoveInBlock.remove(instruction);
-              if (instructionsToRemoveInBlock.isEmpty()) {
-                return;
+    private void processInstructionsToRemove() {
+      instructionsToRemove.forEach(
+          (block, instructionsToRemoveInBlock) -> {
+            assert instructionsToRemoveInBlock.stream()
+                .allMatch(instruction -> instruction.getBlock() == block);
+            InstructionListIterator instructionIterator = block.listIterator(code);
+            while (instructionIterator.hasNext()) {
+              Instruction instruction = instructionIterator.next();
+              assert !instruction.isJumpInstruction();
+              if (instructionsToRemoveInBlock.contains(instruction)) {
+                instructionIterator.removeOrReplaceByDebugLocalRead();
+                hasChanged = true;
+                instructionsToRemoveInBlock.remove(instruction);
+                if (instructionsToRemoveInBlock.isEmpty()) {
+                  return;
+                }
               }
             }
-          }
-        });
-  }
-
-  private boolean verifyWasInstanceInitializer() {
-    VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
-    assert verticallyMergedClasses != null;
-    assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
-        || appView.horizontallyMergedClasses().isMergeTarget(method.getHolderType());
-    assert appView
-        .dexItemFactory()
-        .isConstructor(appView.graphLens().getOriginalMethodSignature(method.getReference()));
-    assert method.getDefinition().getOptimizationInfo().forceInline();
-    return true;
-  }
-
-  private void handleInvokeDirect(InvokeDirect invoke) {
-    if (!appView.hasLiveness()) {
-      killAllNonFinalActiveFields();
-      return;
+          });
     }
 
-    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-
-    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
-    if (singleTarget == null || !singleTarget.getDefinition().isInstanceInitializer()) {
-      killAllNonFinalActiveFields();
-      return;
+    private boolean verifyWasInstanceInitializer() {
+      VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
+      assert verticallyMergedClasses != null;
+      assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
+          || appView.horizontallyMergedClasses().isMergeTarget(method.getHolderType());
+      assert appView
+          .dexItemFactory()
+          .isConstructor(appView.graphLens().getOriginalMethodSignature(method.getReference()));
+      assert method.getDefinition().getOptimizationInfo().forceInline();
+      return true;
     }
 
-    InstanceInitializerInfo instanceInitializerInfo =
-        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
-    if (instanceInitializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
-      killAllNonFinalActiveFields();
-    }
+    private void handleInvokeDirect(InvokeDirect invoke) {
+      if (!appView.hasLiveness()) {
+        killAllNonFinalActiveFields();
+        return;
+      }
 
-    InstanceFieldInitializationInfoCollection fieldInitializationInfos =
-        instanceInitializerInfo.fieldInitializationInfos();
-    fieldInitializationInfos.forEachWithDeterministicOrder(
-        appView,
-        (field, info) -> {
-          if (!appViewWithLiveness
-              .appInfo()
-              .mayPropagateValueFor(appViewWithLiveness, field.getReference())) {
-            return;
-          }
-          if (info.isArgumentInitializationInfo()) {
-            Value value =
-                invoke.getArgument(info.asArgumentInitializationInfo().getArgumentIndex());
-            Value object = invoke.getReceiver().getAliasedValue();
-            FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
-            if (field.isFinalOrEffectivelyFinal(appViewWithLiveness)) {
-              activeState.putFinalOrEffectivelyFinalInstanceField(
-                  fieldAndObject, new ExistingValue(value));
-            } else {
-              activeState.putNonFinalInstanceField(fieldAndObject, new ExistingValue(value));
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+
+      DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
+      if (singleTarget == null || !singleTarget.getDefinition().isInstanceInitializer()) {
+        killAllNonFinalActiveFields();
+        return;
+      }
+
+      InstanceInitializerInfo instanceInitializerInfo =
+          singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
+      if (instanceInitializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
+        killAllNonFinalActiveFields();
+      }
+
+      InstanceFieldInitializationInfoCollection fieldInitializationInfos =
+          instanceInitializerInfo.fieldInitializationInfos();
+      fieldInitializationInfos.forEachWithDeterministicOrder(
+          appView,
+          (field, info) -> {
+            if (!appViewWithLiveness
+                .appInfo()
+                .mayPropagateValueFor(appViewWithLiveness, field.getReference())) {
+              return;
             }
-          } else if (info.isSingleValue()) {
-            SingleValue value = info.asSingleValue();
-            if (value.isMaterializableInContext(appViewWithLiveness, method)) {
+            if (info.isArgumentInitializationInfo()) {
+              Value value =
+                  invoke.getArgument(info.asArgumentInitializationInfo().getArgumentIndex());
               Value object = invoke.getReceiver().getAliasedValue();
               FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
               if (field.isFinalOrEffectivelyFinal(appViewWithLiveness)) {
                 activeState.putFinalOrEffectivelyFinalInstanceField(
-                    fieldAndObject, new MaterializableValue(value));
+                    fieldAndObject, new ExistingValue(value));
               } else {
-                activeState.putNonFinalInstanceField(
-                    fieldAndObject, new MaterializableValue(value));
+                activeState.putNonFinalInstanceField(fieldAndObject, new ExistingValue(value));
               }
+            } else if (info.isSingleValue()) {
+              SingleValue value = info.asSingleValue();
+              if (value.isMaterializableInContext(appViewWithLiveness, method)) {
+                Value object = invoke.getReceiver().getAliasedValue();
+                FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
+                if (field.isFinalOrEffectivelyFinal(appViewWithLiveness)) {
+                  activeState.putFinalOrEffectivelyFinalInstanceField(
+                      fieldAndObject, new MaterializableValue(value));
+                } else {
+                  activeState.putNonFinalInstanceField(
+                      fieldAndObject, new MaterializableValue(value));
+                }
+              }
+            } else {
+              assert info.isTypeInitializationInfo();
             }
-          } else {
-            assert info.isTypeInitializationInfo();
-          }
-        });
-  }
-
-  private void handleInvokeStatic(InvokeStatic invoke) {
-    if (appView.hasClassHierarchy()) {
-      ProgramMethod resolvedMethod =
-          appView
-              .appInfo()
-              .withClassHierarchy()
-              .unsafeResolveMethodDueToDexFormatLegacy(invoke.getInvokedMethod())
-              .getResolvedProgramMethod();
-      if (resolvedMethod != null) {
-        markClassAsInitialized(resolvedMethod.getHolderType());
-        markMostRecentInitClassForRemoval(resolvedMethod.getHolderType());
-      }
+          });
     }
 
-    killAllNonFinalActiveFields();
-  }
-
-  private void handleInitClass(InstructionListIterator instructionIterator, InitClass initClass) {
-    assert !initClass.outValue().hasAnyUsers();
-
-    killNonFinalActiveFields(initClass);
-
-    // If the instruction can throw, we can't use any previous field stores for store-after-store
-    // elimination.
-    if (initClass.instructionInstanceCanThrow(appView, method)) {
-      activeState.clearMostRecentFieldWrites();
-    }
-
-    DexType clazz = initClass.getClassValue();
-    if (markClassAsInitialized(clazz)) {
-      if (release) {
-        activeState.setMostRecentInitClass(initClass);
-      }
-    } else {
-      instructionIterator.removeOrReplaceByDebugLocalRead();
-    }
-  }
-
-  private boolean markClassAsInitialized(DexType type) {
-    return activeState.markClassAsInitialized(type);
-  }
-
-  private void markMostRecentInitClassForRemoval(DexType initializedType) {
-    InitClass mostRecentInitClass = activeState.getMostRecentInitClass();
-    if (mostRecentInitClass != null && mostRecentInitClass.getClassValue() == initializedType) {
-      instructionsToRemove
-          .computeIfAbsent(mostRecentInitClass.getBlock(), ignoreKey(Sets::newIdentityHashSet))
-          .add(mostRecentInitClass);
-    }
-  }
-
-  private void handleArrayGet(InstructionListIterator it, ArrayGet arrayGet) {
-    if (arrayGet.array().hasLocalInfo()) {
-      // The array may be modified through the debugger. Therefore subsequent reads of the same
-      // array slot may not read this local.
-      return;
-    }
-    if (arrayGet.outValue().hasLocalInfo()) {
-      // This local may be modified through the debugger. Therefore subsequent reads of the same
-      // array slot may not read this local.
-      return;
-    }
-
-    Value array = arrayGet.array().getAliasedValue();
-    Value index = arrayGet.index().getAliasedValue();
-    ArraySlot arraySlot = ArraySlot.create(array, index, arrayGet.getMemberType());
-    FieldValue replacement = activeState.getArraySlotValue(arraySlot);
-    if (replacement != null) {
-      TypeElement outType = arrayGet.outValue().getType();
-      if (replacement.getType(appView, outType).lessThanOrEqual(outType, appView)) {
-        replacement.eliminateRedundantRead(it, arrayGet);
-      }
-      return;
-    }
-    activeState.putArraySlotValue(arraySlot, new ExistingValue(arrayGet.outValue()));
-  }
-
-  private void handleArrayPut(ArrayPut arrayPut) {
-    int index = arrayPut.getIndexOrDefault(-1);
-    MemberType memberType = arrayPut.getMemberType();
-
-    // An array-put instruction can potentially write the given array slot on all arrays because of
-    // aliases.
-    if (index < 0) {
-      activeState.removeArraySlotValues(memberType);
-    } else {
-      activeState.removeArraySlotValues(memberType, index);
-    }
-
-    // Update the value of the field to allow redundant load elimination.
-    Value array = arrayPut.array().getAliasedValue();
-    Value indexValue = arrayPut.index().getAliasedValue();
-    ArraySlot arraySlot = ArraySlot.create(array, indexValue, memberType);
-    ExistingValue value = new ExistingValue(arrayPut.value());
-    activeState.putArraySlotValue(arraySlot, value);
-  }
-
-  private void handleInstanceGet(
-      InstructionListIterator it,
-      InstanceGet instanceGet,
-      DexClassAndField field,
-      AssumeRemover assumeRemover) {
-    if (instanceGet.outValue().hasLocalInfo()) {
-      clearMostRecentInstanceFieldWrite(instanceGet, field);
-      return;
-    }
-
-    Value object = instanceGet.object().getAliasedValue();
-    FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
-    FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
-    if (replacement != null) {
-      if (isRedundantFieldLoadEliminationAllowed(field)) {
-        markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
-        replacement.eliminateRedundantRead(it, instanceGet);
-      }
-      return;
-    }
-
-    activeState.putNonFinalInstanceField(fieldAndObject, new ExistingValue(instanceGet.value()));
-    activeState.clearMostRecentInitClass();
-    clearMostRecentInstanceFieldWrite(instanceGet, field);
-  }
-
-  private boolean isRedundantFieldLoadEliminationAllowed(DexClassAndField field) {
-    // Always allowed in D8 since D8 does not support @NoRedundantFieldLoadElimination.
-    return !appView.enableWholeProgramOptimizations()
-        || !field.isProgramField()
-        || appView
-            .getKeepInfo(field.asProgramField())
-            .isRedundantFieldLoadEliminationAllowed(appView.options());
-  }
-
-  private void handleNewInstance(NewInstance newInstance) {
-    markClassAsInitialized(newInstance.getType());
-    markMostRecentInitClassForRemoval(newInstance.getType());
-    if (newInstance.getType().classInitializationMayHaveSideEffectsInContext(appView, method)) {
-      killAllNonFinalActiveFields();
-    }
-  }
-
-  private void clearMostRecentInstanceFieldWrite(InstanceGet instanceGet, DexClassAndField field) {
-    // If the instruction can throw, we need to clear all most-recent-writes, since subsequent field
-    // writes (if any) are not guaranteed to be executed.
-    if (instanceGet.instructionInstanceCanThrow(appView, method)) {
-      activeState.clearMostRecentFieldWrites();
-    } else {
-      activeState.clearMostRecentInstanceFieldWrite(field.getReference());
-    }
-  }
-
-  private void markAssumeDynamicTypeUsersForRemoval(
-      FieldGet fieldGet, FieldValue replacement, AssumeRemover assumeRemover) {
-    ExistingValue existingValue = replacement.asExistingValue();
-    if (existingValue == null
-        || !existingValue
-            .getValue()
-            .isDefinedByInstructionSatisfying(
-                definition ->
-                    definition.isFieldGet()
-                        && definition.asFieldGet().getField().getType()
-                            == fieldGet.getField().getType())) {
-      assumeRemover.markAssumeDynamicTypeUsersForRemoval(fieldGet.outValue());
-    }
-  }
-
-  private void handleInstancePut(InstancePut instancePut, DexClassAndField field) {
-    // An instance-put instruction can potentially write the given field on all objects because of
-    // aliases.
-    activeState.removeNonFinalInstanceFields(field.getReference());
-
-    // If the instruction can throw, we can't use any previous field stores for store-after-store
-    // elimination.
-    if (instancePut.instructionInstanceCanThrow(appView, method)) {
-      activeState.clearMostRecentFieldWrites();
-    }
-
-    // Update the value of the field to allow redundant load elimination.
-    Value object = instancePut.object().getAliasedValue();
-    FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
-    ExistingValue value = new ExistingValue(instancePut.value());
-    if (field.isFinalOrEffectivelyFinal(appView)) {
-      assert !field.getDefinition().isFinal()
-          || method.getDefinition().isInstanceInitializer()
-          || verifyWasInstanceInitializer();
-      activeState.putFinalOrEffectivelyFinalInstanceField(fieldAndObject, value);
-    } else {
-      activeState.putNonFinalInstanceField(fieldAndObject, value);
-    }
-
-    // Record that this field is now most recently written by the current instruction.
-    if (release) {
-      InstancePut mostRecentInstanceFieldWrite =
-          activeState.putMostRecentInstanceFieldWrite(fieldAndObject, instancePut);
-      if (mostRecentInstanceFieldWrite != null) {
-        instructionsToRemove
-            .computeIfAbsent(
-                mostRecentInstanceFieldWrite.getBlock(), ignoreKey(Sets::newIdentityHashSet))
-            .add(mostRecentInstanceFieldWrite);
-      }
-    }
-
-    activeState.clearMostRecentInitClass();
-  }
-
-  private void handleStaticGet(
-      InstructionListIterator instructionIterator,
-      StaticGet staticGet,
-      DexClassAndField field,
-      AssumeRemover assumeRemover) {
-    markClassAsInitialized(field.getHolderType());
-
-    if (staticGet.outValue().hasLocalInfo()) {
-      killNonFinalActiveFields(staticGet);
-      clearMostRecentStaticFieldWrite(staticGet, field);
-      return;
-    }
-
-    FieldValue replacement = activeState.getStaticFieldValue(field.getReference());
-    if (replacement != null) {
-      markAssumeDynamicTypeUsersForRemoval(staticGet, replacement, assumeRemover);
-      replacement.eliminateRedundantRead(instructionIterator, staticGet);
-      return;
-    }
-
-    // A field get on a different class can cause <clinit> to run and change static field values.
-    killNonFinalActiveFields(staticGet);
-    clearMostRecentStaticFieldWrite(staticGet, field);
-
-    FieldValue value = new ExistingValue(staticGet.value());
-    if (field.isFinalOrEffectivelyFinal(appView)) {
-      activeState.putFinalStaticField(field.getReference(), value);
-    } else {
-      activeState.putNonFinalStaticField(field.getReference(), value);
-    }
-
-    if (appView.hasLiveness()) {
-      SingleFieldValue singleFieldValue =
-          field.getDefinition().getOptimizationInfo().getAbstractValue().asSingleFieldValue();
-      if (singleFieldValue != null) {
-        applyObjectState(staticGet.outValue(), singleFieldValue.getObjectState());
-      }
-    }
-
-    markMostRecentInitClassForRemoval(field.getHolderType());
-    activeState.clearMostRecentInitClass();
-  }
-
-  private void clearMostRecentStaticFieldWrite(StaticGet staticGet, DexClassAndField field) {
-    // If the instruction can throw, we need to clear all most-recent-writes, since subsequent field
-    // writes (if any) are not guaranteed to be executed.
-    if (staticGet.instructionInstanceCanThrow(appView, method)) {
-      activeState.clearMostRecentFieldWrites();
-    } else {
-      activeState.clearMostRecentStaticFieldWrite(field.getReference());
-    }
-  }
-
-  private void handleStaticPut(StaticPut staticPut, DexClassAndField field) {
-    markClassAsInitialized(field.getHolderType());
-
-    // A field put on a different class can cause <clinit> to run and change static field values.
-    killNonFinalActiveFields(staticPut);
-
-    // If the instruction can throw, we can't use any previous field stores for store-after-store
-    // elimination.
-    if (staticPut.instructionInstanceCanThrow(appView, method)) {
-      activeState.clearMostRecentFieldWrites();
-    }
-
-    ExistingValue value = new ExistingValue(staticPut.value());
-    if (field.isFinalOrEffectivelyFinal(appView)) {
-      assert appView.checkForTesting(
-          () -> !field.getDefinition().isFinal() || method.getDefinition().isClassInitializer());
-      activeState.putFinalStaticField(field.getReference(), value);
-    } else {
-      activeState.putNonFinalStaticField(field.getReference(), value);
-
-      if (release) {
-        StaticPut mostRecentStaticFieldWrite =
-            activeState.putMostRecentStaticFieldWrite(field.getReference(), staticPut);
-        if (mostRecentStaticFieldWrite != null) {
-          instructionsToRemove
-              .computeIfAbsent(
-                  mostRecentStaticFieldWrite.getBlock(), ignoreKey(Sets::newIdentityHashSet))
-              .add(mostRecentStaticFieldWrite);
+    private void handleInvokeStatic(InvokeStatic invoke) {
+      if (appView.hasClassHierarchy()) {
+        ProgramMethod resolvedMethod =
+            appView
+                .appInfo()
+                .withClassHierarchy()
+                .unsafeResolveMethodDueToDexFormatLegacy(invoke.getInvokedMethod())
+                .getResolvedProgramMethod();
+        if (resolvedMethod != null) {
+          markClassAsInitialized(resolvedMethod.getHolderType());
+          markMostRecentInitClassForRemoval(resolvedMethod.getHolderType());
         }
       }
+
+      killAllNonFinalActiveFields();
     }
 
-    markMostRecentInitClassForRemoval(field.getHolderType());
-    activeState.clearMostRecentInitClass();
-  }
+    private void handleInitClass(InstructionListIterator instructionIterator, InitClass initClass) {
+      assert !initClass.outValue().hasAnyUsers();
 
-  private void applyObjectState(Value value, ObjectState objectState) {
-    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-    objectState.forEachAbstractFieldValue(
-        (field, fieldValue) -> {
-          if (appViewWithLiveness.appInfo().mayPropagateValueFor(appViewWithLiveness, field)
-              && fieldValue.isSingleValue()) {
-            SingleValue singleFieldValue = fieldValue.asSingleValue();
-            if (singleFieldValue.isMaterializableInContext(appViewWithLiveness, method)) {
-              activeState.putFinalOrEffectivelyFinalInstanceField(
-                  new FieldAndObject(field, value), new MaterializableValue(singleFieldValue));
-            }
-          }
-        });
-  }
+      killNonFinalActiveFields(initClass);
 
-  private void killAllNonFinalActiveFields() {
-    activeState.clearArraySlotValues();
-    activeState.clearNonFinalInstanceFields();
-    activeState.clearNonFinalStaticFields();
-    activeState.clearMostRecentFieldWrites();
-    activeState.clearMostRecentInitClass();
-  }
+      // If the instruction can throw, we can't use any previous field stores for store-after-store
+      // elimination.
+      if (initClass.instructionInstanceCanThrow(appView, method)) {
+        activeState.clearMostRecentFieldWrites();
+      }
 
-  private void killNonFinalActiveFields(Instruction instruction) {
-    assert instruction.isInitClass() || instruction.isStaticFieldInstruction();
-    if (instruction.isStaticPut()) {
-      if (instruction.instructionMayTriggerMethodInvocation(appView, method)) {
-        // Accessing a static field on a different object could cause <clinit> to run which
-        // could modify any static field on any other object.
-        activeState.clearNonFinalStaticFields();
+      DexType clazz = initClass.getClassValue();
+      if (markClassAsInitialized(clazz)) {
+        if (release) {
+          activeState.setMostRecentInitClass(initClass);
+        }
+      } else {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+        hasChanged = true;
+      }
+    }
+
+    private boolean markClassAsInitialized(DexType type) {
+      return activeState.markClassAsInitialized(type);
+    }
+
+    private void markMostRecentInitClassForRemoval(DexType initializedType) {
+      InitClass mostRecentInitClass = activeState.getMostRecentInitClass();
+      if (mostRecentInitClass != null && mostRecentInitClass.getClassValue() == initializedType) {
+        instructionsToRemove
+            .computeIfAbsent(mostRecentInitClass.getBlock(), ignoreKey(Sets::newIdentityHashSet))
+            .add(mostRecentInitClass);
+      }
+    }
+
+    private void handleArrayGet(InstructionListIterator it, ArrayGet arrayGet) {
+      if (arrayGet.array().hasLocalInfo()) {
+        // The array may be modified through the debugger. Therefore subsequent reads of the same
+        // array slot may not read this local.
+        return;
+      }
+      if (arrayGet.outValue().hasLocalInfo()) {
+        // This local may be modified through the debugger. Therefore subsequent reads of the same
+        // array slot may not read this local.
+        return;
+      }
+
+      Value array = arrayGet.array().getAliasedValue();
+      Value index = arrayGet.index().getAliasedValue();
+      ArraySlot arraySlot = ArraySlot.create(array, index, arrayGet.getMemberType());
+      FieldValue replacement = activeState.getArraySlotValue(arraySlot);
+      if (replacement != null) {
+        TypeElement outType = arrayGet.outValue().getType();
+        if (replacement.getType(appView, outType).lessThanOrEqual(outType, appView)) {
+          replacement.eliminateRedundantRead(it, arrayGet);
+        }
+        return;
+      }
+      activeState.putArraySlotValue(arraySlot, new ExistingValue(arrayGet.outValue()));
+    }
+
+    private void handleArrayPut(ArrayPut arrayPut) {
+      int index = arrayPut.getIndexOrDefault(-1);
+      MemberType memberType = arrayPut.getMemberType();
+
+      // An array-put instruction can potentially write the given array slot on all arrays because
+      // of
+      // aliases.
+      if (index < 0) {
+        activeState.removeArraySlotValues(memberType);
+      } else {
+        activeState.removeArraySlotValues(memberType, index);
+      }
+
+      // Update the value of the field to allow redundant load elimination.
+      Value array = arrayPut.array().getAliasedValue();
+      Value indexValue = arrayPut.index().getAliasedValue();
+      ArraySlot arraySlot = ArraySlot.create(array, indexValue, memberType);
+      ExistingValue value = new ExistingValue(arrayPut.value());
+      activeState.putArraySlotValue(arraySlot, value);
+    }
+
+    private void handleInstanceGet(
+        InstructionListIterator it,
+        InstanceGet instanceGet,
+        DexClassAndField field,
+        AssumeRemover assumeRemover) {
+      if (instanceGet.outValue().hasLocalInfo()) {
+        clearMostRecentInstanceFieldWrite(instanceGet, field);
+        return;
+      }
+
+      Value object = instanceGet.object().getAliasedValue();
+      FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
+      FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
+      if (replacement != null) {
+        if (isRedundantFieldLoadEliminationAllowed(field)) {
+          markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
+          replacement.eliminateRedundantRead(it, instanceGet);
+        }
+        return;
+      }
+
+      activeState.putNonFinalInstanceField(fieldAndObject, new ExistingValue(instanceGet.value()));
+      activeState.clearMostRecentInitClass();
+      clearMostRecentInstanceFieldWrite(instanceGet, field);
+    }
+
+    private boolean isRedundantFieldLoadEliminationAllowed(DexClassAndField field) {
+      // Always allowed in D8 since D8 does not support @NoRedundantFieldLoadElimination.
+      return !appView.enableWholeProgramOptimizations()
+          || !field.isProgramField()
+          || appView
+              .getKeepInfo(field.asProgramField())
+              .isRedundantFieldLoadEliminationAllowed(appView.options());
+    }
+
+    private void handleNewInstance(NewInstance newInstance) {
+      markClassAsInitialized(newInstance.getType());
+      markMostRecentInitClassForRemoval(newInstance.getType());
+      if (newInstance.getType().classInitializationMayHaveSideEffectsInContext(appView, method)) {
+        killAllNonFinalActiveFields();
+      }
+    }
+
+    private void clearMostRecentInstanceFieldWrite(
+        InstanceGet instanceGet, DexClassAndField field) {
+      // If the instruction can throw, we need to clear all most-recent-writes, since subsequent
+      // field
+      // writes (if any) are not guaranteed to be executed.
+      if (instanceGet.instructionInstanceCanThrow(appView, method)) {
         activeState.clearMostRecentFieldWrites();
       } else {
-        activeState.removeNonFinalStaticField(instruction.asStaticPut().getField());
+        activeState.clearMostRecentInstanceFieldWrite(field.getReference());
       }
-    } else if (instruction.isInitClass() || instruction.isStaticGet()) {
-      if (instruction.instructionMayTriggerMethodInvocation(appView, method)) {
-        // Accessing a static field on a different object could cause <clinit> to run which
-        // could modify any static field on any other object.
-        activeState.clearNonFinalStaticFields();
+    }
+
+    private void markAssumeDynamicTypeUsersForRemoval(
+        FieldGet fieldGet, FieldValue replacement, AssumeRemover assumeRemover) {
+      ExistingValue existingValue = replacement.asExistingValue();
+      if (existingValue == null
+          || !existingValue
+              .getValue()
+              .isDefinedByInstructionSatisfying(
+                  definition ->
+                      definition.isFieldGet()
+                          && definition.asFieldGet().getField().getType()
+                              == fieldGet.getField().getType())) {
+        assumeRemover.markAssumeDynamicTypeUsersForRemoval(fieldGet.outValue());
+      }
+    }
+
+    private void handleInstancePut(InstancePut instancePut, DexClassAndField field) {
+      // An instance-put instruction can potentially write the given field on all objects because of
+      // aliases.
+      activeState.removeNonFinalInstanceFields(field.getReference());
+
+      // If the instruction can throw, we can't use any previous field stores for store-after-store
+      // elimination.
+      if (instancePut.instructionInstanceCanThrow(appView, method)) {
         activeState.clearMostRecentFieldWrites();
       }
-    } else if (instruction.isInstanceGet()) {
-      throw new Unreachable();
+
+      // Update the value of the field to allow redundant load elimination.
+      Value object = instancePut.object().getAliasedValue();
+      FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
+      ExistingValue value = new ExistingValue(instancePut.value());
+      if (field.isFinalOrEffectivelyFinal(appView)) {
+        assert !field.getDefinition().isFinal()
+            || method.getDefinition().isInstanceInitializer()
+            || verifyWasInstanceInitializer();
+        activeState.putFinalOrEffectivelyFinalInstanceField(fieldAndObject, value);
+      } else {
+        activeState.putNonFinalInstanceField(fieldAndObject, value);
+      }
+
+      // Record that this field is now most recently written by the current instruction.
+      if (release) {
+        InstancePut mostRecentInstanceFieldWrite =
+            activeState.putMostRecentInstanceFieldWrite(fieldAndObject, instancePut);
+        if (mostRecentInstanceFieldWrite != null) {
+          instructionsToRemove
+              .computeIfAbsent(
+                  mostRecentInstanceFieldWrite.getBlock(), ignoreKey(Sets::newIdentityHashSet))
+              .add(mostRecentInstanceFieldWrite);
+        }
+      }
+
+      activeState.clearMostRecentInitClass();
+    }
+
+    private void handleStaticGet(
+        InstructionListIterator instructionIterator,
+        StaticGet staticGet,
+        DexClassAndField field,
+        AssumeRemover assumeRemover) {
+      markClassAsInitialized(field.getHolderType());
+
+      if (staticGet.outValue().hasLocalInfo()) {
+        killNonFinalActiveFields(staticGet);
+        clearMostRecentStaticFieldWrite(staticGet, field);
+        return;
+      }
+
+      FieldValue replacement = activeState.getStaticFieldValue(field.getReference());
+      if (replacement != null) {
+        markAssumeDynamicTypeUsersForRemoval(staticGet, replacement, assumeRemover);
+        replacement.eliminateRedundantRead(instructionIterator, staticGet);
+        return;
+      }
+
+      // A field get on a different class can cause <clinit> to run and change static field values.
+      killNonFinalActiveFields(staticGet);
+      clearMostRecentStaticFieldWrite(staticGet, field);
+
+      FieldValue value = new ExistingValue(staticGet.value());
+      if (field.isFinalOrEffectivelyFinal(appView)) {
+        activeState.putFinalStaticField(field.getReference(), value);
+      } else {
+        activeState.putNonFinalStaticField(field.getReference(), value);
+      }
+
+      if (appView.hasLiveness()) {
+        SingleFieldValue singleFieldValue =
+            field.getDefinition().getOptimizationInfo().getAbstractValue().asSingleFieldValue();
+        if (singleFieldValue != null) {
+          applyObjectState(staticGet.outValue(), singleFieldValue.getObjectState());
+        }
+      }
+
+      markMostRecentInitClassForRemoval(field.getHolderType());
+      activeState.clearMostRecentInitClass();
+    }
+
+    private void clearMostRecentStaticFieldWrite(StaticGet staticGet, DexClassAndField field) {
+      // If the instruction can throw, we need to clear all most-recent-writes, since subsequent
+      // field
+      // writes (if any) are not guaranteed to be executed.
+      if (staticGet.instructionInstanceCanThrow(appView, method)) {
+        activeState.clearMostRecentFieldWrites();
+      } else {
+        activeState.clearMostRecentStaticFieldWrite(field.getReference());
+      }
+    }
+
+    private void handleStaticPut(StaticPut staticPut, DexClassAndField field) {
+      markClassAsInitialized(field.getHolderType());
+
+      // A field put on a different class can cause <clinit> to run and change static field values.
+      killNonFinalActiveFields(staticPut);
+
+      // If the instruction can throw, we can't use any previous field stores for store-after-store
+      // elimination.
+      if (staticPut.instructionInstanceCanThrow(appView, method)) {
+        activeState.clearMostRecentFieldWrites();
+      }
+
+      ExistingValue value = new ExistingValue(staticPut.value());
+      if (field.isFinalOrEffectivelyFinal(appView)) {
+        assert appView.checkForTesting(
+            () -> !field.getDefinition().isFinal() || method.getDefinition().isClassInitializer());
+        activeState.putFinalStaticField(field.getReference(), value);
+      } else {
+        activeState.putNonFinalStaticField(field.getReference(), value);
+
+        if (release) {
+          StaticPut mostRecentStaticFieldWrite =
+              activeState.putMostRecentStaticFieldWrite(field.getReference(), staticPut);
+          if (mostRecentStaticFieldWrite != null) {
+            instructionsToRemove
+                .computeIfAbsent(
+                    mostRecentStaticFieldWrite.getBlock(), ignoreKey(Sets::newIdentityHashSet))
+                .add(mostRecentStaticFieldWrite);
+          }
+        }
+      }
+
+      markMostRecentInitClassForRemoval(field.getHolderType());
+      activeState.clearMostRecentInitClass();
+    }
+
+    private void applyObjectState(Value value, ObjectState objectState) {
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+      objectState.forEachAbstractFieldValue(
+          (field, fieldValue) -> {
+            if (appViewWithLiveness.appInfo().mayPropagateValueFor(appViewWithLiveness, field)
+                && fieldValue.isSingleValue()) {
+              SingleValue singleFieldValue = fieldValue.asSingleValue();
+              if (singleFieldValue.isMaterializableInContext(appViewWithLiveness, method)) {
+                activeState.putFinalOrEffectivelyFinalInstanceField(
+                    new FieldAndObject(field, value), new MaterializableValue(singleFieldValue));
+              }
+            }
+          });
+    }
+
+    private void killAllNonFinalActiveFields() {
+      activeState.clearArraySlotValues();
+      activeState.clearNonFinalInstanceFields();
+      activeState.clearNonFinalStaticFields();
+      activeState.clearMostRecentFieldWrites();
+      activeState.clearMostRecentInitClass();
+    }
+
+    private void killNonFinalActiveFields(Instruction instruction) {
+      assert instruction.isInitClass() || instruction.isStaticFieldInstruction();
+      if (instruction.isStaticPut()) {
+        if (instruction.instructionMayTriggerMethodInvocation(appView, method)) {
+          // Accessing a static field on a different object could cause <clinit> to run which
+          // could modify any static field on any other object.
+          activeState.clearNonFinalStaticFields();
+          activeState.clearMostRecentFieldWrites();
+        } else {
+          activeState.removeNonFinalStaticField(instruction.asStaticPut().getField());
+        }
+      } else if (instruction.isInitClass() || instruction.isStaticGet()) {
+        if (instruction.instructionMayTriggerMethodInvocation(appView, method)) {
+          // Accessing a static field on a different object could cause <clinit> to run which
+          // could modify any static field on any other object.
+          activeState.clearNonFinalStaticFields();
+          activeState.clearMostRecentFieldWrites();
+        }
+      } else if (instruction.isInstanceGet()) {
+        throw new Unreachable();
+      }
     }
   }
 
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 867c2dc..bf25b2d 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
@@ -46,6 +46,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -150,7 +151,6 @@
       return Sets.newIdentityHashSet();
     }
     assert code.isConsistentSSABeforeTypesAreCorrect(appView);
-    ProgramMethod context = code.context();
     EnumUnboxerMethodProcessorEventConsumer eventConsumer = methodProcessor.getEventConsumer();
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
     Map<Instruction, DexType> convertedEnums =
@@ -177,256 +177,42 @@
           if (enumType != null) {
             iterator.removeOrReplaceByDebugLocalRead();
           }
-          continue;
-        }
-
-        if (instruction.isIf()) {
-          If ifInstruction = instruction.asIf();
-          if (!ifInstruction.isZeroTest()) {
-            for (int operandIndex = 0; operandIndex < 2; operandIndex++) {
-              Value operand = ifInstruction.getOperand(operandIndex);
-              DexType enumType = getEnumClassTypeOrNull(operand, convertedEnums);
-              if (enumType != null) {
-                int otherOperandIndex = 1 - operandIndex;
-                Value otherOperand = ifInstruction.getOperand(otherOperandIndex);
-                if (otherOperand.getType().isNullType()) {
-                  iterator.previous();
-                  ifInstruction.replaceValue(
-                      otherOperandIndex, iterator.insertConstIntInstruction(code, options, 0));
-                  iterator.next();
-                  break;
-                }
-              }
-            }
-          }
-        }
-
-        // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
-        // counterpart. The rewriting (== or match) is based on the following:
-        // - name, ordinal and compareTo are final and implemented only on java.lang.Enum,
-        // - equals, hashCode are final and implemented in java.lang.Enum and java.lang.Object,
-        // - getClass is final and implemented only in java.lang.Object,
-        // - toString is non-final, implemented in java.lang.Object, java.lang.Enum and possibly
-        //   also in the unboxed enum class.
-        if (instruction.isInvokeMethodWithReceiver()) {
-          InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
-          DexType enumType = getEnumClassTypeOrNull(invoke.getReceiver(), convertedEnums);
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (enumType != null) {
-            if (invokedMethod == factory.enumMembers.ordinalMethod
-                || invokedMethod.match(factory.enumMembers.hashCode)) {
-              replaceEnumInvoke(
-                  iterator,
-                  invoke,
-                  getSharedUtilityClass().ensureOrdinalMethod(appView, context, eventConsumer));
-              continue;
-            } else if (invokedMethod.match(factory.enumMembers.equals)) {
-              replaceEnumInvoke(
-                  iterator,
-                  invoke,
-                  getSharedUtilityClass().ensureEqualsMethod(appView, context, eventConsumer));
-              continue;
-            } else if (invokedMethod == factory.enumMembers.compareTo
-                || invokedMethod == factory.enumMembers.compareToWithObject) {
-              replaceEnumInvoke(
-                  iterator,
-                  invoke,
-                  getSharedUtilityClass().ensureCompareToMethod(appView, context, eventConsumer));
-              continue;
-            } else if (invokedMethod == factory.enumMembers.nameMethod) {
-              rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
-              continue;
-            } else if (invokedMethod.match(factory.enumMembers.toString)) {
-              DexMethod reboundMethod =
-                  invokedMethod.withHolder(unboxedEnumsData.representativeType(enumType), factory);
-              DexMethod lookupMethod =
-                  enumUnboxingLens
-                      .lookupMethod(
-                          reboundMethod,
-                          context.getReference(),
-                          invoke.getType(),
-                          enumUnboxingLens.getPrevious())
-                      .getReference();
-              // If the SuperEnum had declared a toString() override, then the unboxer moves it to
-              // the local utility class method corresponding to that override.
-              // If a SubEnum had declared a toString() override, then the unboxer records a
-              // synthetic move from SuperEnum.toString() to the dispatch method on the local
-              // utility class.
-              // When they are the same, then there are no overrides of toString().
-              if (lookupMethod == reboundMethod) {
-                rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
-              } else {
-                DexClassAndMethod dexClassAndMethod = appView.definitionFor(lookupMethod);
-                assert dexClassAndMethod != null;
-                assert dexClassAndMethod.isProgramMethod();
-                replaceEnumInvoke(iterator, invoke, dexClassAndMethod.asProgramMethod());
-              }
-              continue;
-            } else if (invokedMethod == factory.objectMembers.getClass) {
-              rewriteNullCheck(iterator, invoke, context, eventConsumer);
-              continue;
-            } else if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
-              DexMethod refinedDispatchMethodReference =
-                  enumUnboxingLens.lookupRefinedDispatchMethod(
-                      invokedMethod,
-                      context.getReference(),
-                      invoke.getType(),
-                      enumUnboxingLens.getPrevious(),
-                      invoke.getArgument(0).getAbstractValue(appView, context),
-                      enumType);
-              if (refinedDispatchMethodReference != null) {
-                DexClassAndMethod refinedDispatchMethod =
-                    appView.definitionFor(refinedDispatchMethodReference);
-                assert refinedDispatchMethod != null;
-                assert refinedDispatchMethod.isProgramMethod();
-                replaceEnumInvoke(iterator, invoke, refinedDispatchMethod.asProgramMethod());
-              }
-              continue;
-            }
-          } else if (invokedMethod == factory.stringBuilderMethods.appendObject
-              || invokedMethod == factory.stringBufferMethods.appendObject) {
-            // Rewrites stringBuilder.append(enumInstance) as if it was
-            // stringBuilder.append(String.valueOf(unboxedEnumInstance));
-            Value enumArg = invoke.getArgument(1);
-            DexType enumArgType = getEnumClassTypeOrNull(enumArg, convertedEnums);
-            if (enumArgType != null) {
-              ProgramMethod stringValueOfMethod =
-                  getLocalUtilityClass(enumArgType)
-                      .ensureStringValueOfMethod(appView, context, eventConsumer);
-              InvokeStatic toStringInvoke =
-                  InvokeStatic.builder()
-                      .setMethod(stringValueOfMethod)
-                      .setSingleArgument(enumArg)
-                      .setFreshOutValue(appView, code)
-                      .setPosition(invoke)
-                      .build();
-              DexMethod newAppendMethod =
-                  invokedMethod == factory.stringBuilderMethods.appendObject
-                      ? factory.stringBuilderMethods.appendString
-                      : factory.stringBufferMethods.appendString;
-              List<Value> arguments =
-                  ImmutableList.of(invoke.getReceiver(), toStringInvoke.outValue());
-              InvokeVirtual invokeAppendString =
-                  new InvokeVirtual(newAppendMethod, invoke.clearOutValue(), arguments);
-              invokeAppendString.setPosition(invoke.getPosition());
-              iterator.replaceCurrentInstruction(toStringInvoke);
-              if (block.hasCatchHandlers()) {
-                iterator
-                    .splitCopyCatchHandlers(code, blocks, appView.options())
-                    .listIterator(code)
-                    .add(invokeAppendString);
-              } else {
-                iterator.add(invokeAppendString);
-              }
-              continue;
-            }
-          }
+        } else if (instruction.isIf()) {
+          rewriteIf(code, convertedEnums, iterator, instruction.asIf());
+        } else if (instruction.isInvokeMethodWithReceiver()) {
+          rewriteInvokeMethodWithReceiver(
+              code,
+              eventConsumer,
+              convertedEnums,
+              blocks,
+              block,
+              iterator,
+              instruction.asInvokeMethodWithReceiver());
         } else if (instruction.isInvokeStatic()) {
           rewriteInvokeStatic(
               instruction.asInvokeStatic(),
               code,
-              context,
               convertedEnums,
               iterator,
               affectedPhis,
               eventConsumer);
-        }
-        if (instruction.isStaticGet()) {
-          StaticGet staticGet = instruction.asStaticGet();
-          DexField field = staticGet.getField();
-          DexType holder = field.holder;
-          if (!unboxedEnumsData.isUnboxedEnum(holder)) {
-            continue;
-          }
-          if (staticGet.hasUnusedOutValue()) {
-            iterator.removeOrReplaceByDebugLocalRead();
-            continue;
-          }
-          affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
-          if (unboxedEnumsData.matchesValuesField(field)) {
-            // Load the size of this enum's $VALUES array before the current instruction.
-            iterator.previous();
-            Value sizeValue =
-                iterator.insertConstIntInstruction(
-                    code, options, unboxedEnumsData.getValuesSize(holder));
-            iterator.next();
-
-            // Replace Enum.$VALUES by a call to: int[] SharedUtilityClass.values(int size).
-            InvokeStatic invoke =
-                InvokeStatic.builder()
-                    .setMethod(getSharedUtilityClass().getValuesMethod(context, eventConsumer))
-                    .setFreshOutValue(appView, code)
-                    .setSingleArgument(sizeValue)
-                    .build();
-            iterator.replaceCurrentInstruction(invoke);
-
-            convertedEnums.put(invoke, holder);
-
-            // Check if the call to SharedUtilityClass.values(size) is followed by a call to
-            // clone(). If so, remove it, since SharedUtilityClass.values(size) returns a fresh
-            // array. This is needed because the javac generated implementation of MyEnum.values()
-            // is implemented as `return $VALUES.clone()`.
-            removeRedundantValuesArrayCloning(invoke, instructionsToRemove, seenBlocks);
-          } else if (unboxedEnumsData.hasUnboxedValueFor(field)) {
-            // Replace by ordinal + 1 for null check (null is 0).
-            ConstNumber intConstant =
-                code.createIntConstant(unboxedEnumsData.getUnboxedValue(field));
-            iterator.replaceCurrentInstruction(intConstant);
-            convertedEnums.put(intConstant, holder);
-          } else {
-            // Nothing to do, handled by lens code rewriting.
-          }
-        }
-
-        if (instruction.isInstanceGet()) {
-          InstanceGet instanceGet = instruction.asInstanceGet();
-          DexType holder = instanceGet.getField().holder;
-          if (unboxedEnumsData.isUnboxedEnum(holder)) {
-            ProgramMethod fieldMethod =
-                ensureInstanceFieldMethod(instanceGet.getField(), context, eventConsumer);
-            Value rewrittenOutValue =
-                code.createValue(
-                    TypeElement.fromDexType(fieldMethod.getReturnType(), maybeNull(), appView));
-            Value in = instanceGet.object();
-            if (in.getType().isNullType()) {
-              iterator.previous();
-              in = iterator.insertConstIntInstruction(code, options, 0);
-              iterator.next();
-            }
-            InvokeStatic invoke =
-                new InvokeStatic(
-                    fieldMethod.getReference(), rewrittenOutValue, ImmutableList.of(in));
-            iterator.replaceCurrentInstruction(invoke);
-            if (unboxedEnumsData.isUnboxedEnum(instanceGet.getField().type)) {
-              convertedEnums.put(invoke, instanceGet.getField().type);
-            }
-          }
-        }
-
-        // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
-        if (instruction.isArrayAccess()) {
-          ArrayAccess arrayAccess = instruction.asArrayAccess();
-          DexType enumType = getEnumArrayTypeOrNull(arrayAccess, convertedEnums);
-          if (enumType != null) {
-            if (arrayAccess.hasOutValue()) {
-              affectedPhis.addAll(arrayAccess.outValue().uniquePhiUsers());
-            }
-            arrayAccess = arrayAccess.withMemberType(MemberType.INT);
-            iterator.replaceCurrentInstruction(arrayAccess);
-            convertedEnums.put(arrayAccess, enumType);
-            if (arrayAccess.isArrayPut()) {
-              ArrayPut arrayPut = arrayAccess.asArrayPut();
-              if (arrayPut.value().getType().isNullType()) {
-                iterator.previous();
-                arrayPut.replacePutValue(iterator.insertConstIntInstruction(code, options, 0));
-                iterator.next();
-              }
-            }
-          }
-          assert validateArrayAccess(arrayAccess);
-        }
-
-        if (instruction.isNewUnboxedEnumInstance()) {
+        } else if (instruction.isStaticGet()) {
+          rewriteStaticGet(
+              code,
+              eventConsumer,
+              affectedPhis,
+              convertedEnums,
+              seenBlocks,
+              instructionsToRemove,
+              iterator,
+              instruction.asStaticGet());
+        } else if (instruction.isInstanceGet()) {
+          rewriteInstanceGet(
+              code, eventConsumer, convertedEnums, iterator, instruction.asInstanceGet());
+        } else if (instruction.isArrayAccess()) {
+          rewriteArrayAccess(
+              code, affectedPhis, convertedEnums, iterator, instruction.asArrayAccess());
+        } else if (instruction.isNewUnboxedEnumInstance()) {
           NewUnboxedEnumInstance newUnboxedEnumInstance = instruction.asNewUnboxedEnumInstance();
           assert unboxedEnumsData.isUnboxedEnum(newUnboxedEnumInstance.getType());
           iterator.replaceCurrentInstruction(
@@ -440,14 +226,267 @@
     return affectedPhis;
   }
 
+  private void rewriteArrayAccess(
+      IRCode code,
+      Set<Phi> affectedPhis,
+      Map<Instruction, DexType> convertedEnums,
+      InstructionListIterator iterator,
+      ArrayAccess arrayAccess) {
+    // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
+    DexType enumType = getEnumArrayTypeOrNull(arrayAccess, convertedEnums);
+    if (enumType != null) {
+      if (arrayAccess.hasOutValue()) {
+        affectedPhis.addAll(arrayAccess.outValue().uniquePhiUsers());
+      }
+      arrayAccess = arrayAccess.withMemberType(MemberType.INT);
+      iterator.replaceCurrentInstruction(arrayAccess);
+      convertedEnums.put(arrayAccess, enumType);
+      if (arrayAccess.isArrayPut()) {
+        ArrayPut arrayPut = arrayAccess.asArrayPut();
+        if (arrayPut.value().getType().isNullType()) {
+          iterator.previous();
+          arrayPut.replacePutValue(iterator.insertConstIntInstruction(code, options, 0));
+          iterator.next();
+        }
+      }
+    }
+    assert validateArrayAccess(arrayAccess);
+  }
+
+  private void rewriteIf(
+      IRCode code,
+      Map<Instruction, DexType> convertedEnums,
+      InstructionListIterator iterator,
+      If ifInstruction) {
+    if (!ifInstruction.isZeroTest()) {
+      for (int operandIndex = 0; operandIndex < 2; operandIndex++) {
+        Value operand = ifInstruction.getOperand(operandIndex);
+        DexType enumType = getEnumClassTypeOrNull(operand, convertedEnums);
+        if (enumType != null) {
+          int otherOperandIndex = 1 - operandIndex;
+          Value otherOperand = ifInstruction.getOperand(otherOperandIndex);
+          if (otherOperand.getType().isNullType()) {
+            iterator.previous();
+            ifInstruction.replaceValue(
+                otherOperandIndex, iterator.insertConstIntInstruction(code, options, 0));
+            iterator.next();
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  private void rewriteInstanceGet(
+      IRCode code,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer,
+      Map<Instruction, DexType> convertedEnums,
+      InstructionListIterator iterator,
+      InstanceGet instanceGet) {
+    DexType holder = instanceGet.getField().holder;
+    if (unboxedEnumsData.isUnboxedEnum(holder)) {
+      ProgramMethod fieldMethod =
+          ensureInstanceFieldMethod(instanceGet.getField(), code.context(), eventConsumer);
+      Value rewrittenOutValue =
+          code.createValue(
+              TypeElement.fromDexType(fieldMethod.getReturnType(), maybeNull(), appView));
+      Value in = instanceGet.object();
+      if (in.getType().isNullType()) {
+        iterator.previous();
+        in = iterator.insertConstIntInstruction(code, options, 0);
+        iterator.next();
+      }
+      InvokeStatic invoke =
+          new InvokeStatic(fieldMethod.getReference(), rewrittenOutValue, ImmutableList.of(in));
+      iterator.replaceCurrentInstruction(invoke);
+      if (unboxedEnumsData.isUnboxedEnum(instanceGet.getField().type)) {
+        convertedEnums.put(invoke, instanceGet.getField().type);
+      }
+    }
+  }
+
+  private void rewriteStaticGet(
+      IRCode code,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer,
+      Set<Phi> affectedPhis,
+      Map<Instruction, DexType> convertedEnums,
+      Set<BasicBlock> seenBlocks,
+      Set<Instruction> instructionsToRemove,
+      InstructionListIterator iterator,
+      StaticGet staticGet) {
+    DexField field = staticGet.getField();
+    DexType holder = field.holder;
+    if (!unboxedEnumsData.isUnboxedEnum(holder)) {
+      return;
+    }
+    if (staticGet.hasUnusedOutValue()) {
+      iterator.removeOrReplaceByDebugLocalRead();
+      return;
+    }
+    affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
+    if (unboxedEnumsData.matchesValuesField(field)) {
+      // Load the size of this enum's $VALUES array before the current instruction.
+      iterator.previous();
+      Value sizeValue =
+          iterator.insertConstIntInstruction(code, options, unboxedEnumsData.getValuesSize(holder));
+      iterator.next();
+
+      // Replace Enum.$VALUES by a call to: int[] SharedUtilityClass.values(int size).
+      InvokeStatic invoke =
+          InvokeStatic.builder()
+              .setMethod(getSharedUtilityClass().getValuesMethod(code.context(), eventConsumer))
+              .setFreshOutValue(appView, code)
+              .setSingleArgument(sizeValue)
+              .build();
+      iterator.replaceCurrentInstruction(invoke);
+
+      convertedEnums.put(invoke, holder);
+
+      // Check if the call to SharedUtilityClass.values(size) is followed by a call to
+      // clone(). If so, remove it, since SharedUtilityClass.values(size) returns a fresh
+      // array. This is needed because the javac generated implementation of MyEnum.values()
+      // is implemented as `return $VALUES.clone()`.
+      removeRedundantValuesArrayCloning(invoke, instructionsToRemove, seenBlocks);
+    } else if (unboxedEnumsData.hasUnboxedValueFor(field)) {
+      // Replace by ordinal + 1 for null check (null is 0).
+      ConstNumber intConstant = code.createIntConstant(unboxedEnumsData.getUnboxedValue(field));
+      iterator.replaceCurrentInstruction(intConstant);
+      convertedEnums.put(intConstant, holder);
+    } else {
+      // Nothing to do, handled by lens code rewriting.
+    }
+  }
+
+  // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
+  // counterpart. The rewriting (== or match) is based on the following:
+  // - name, ordinal and compareTo are final and implemented only on java.lang.Enum,
+  // - equals, hashCode are final and implemented in java.lang.Enum and java.lang.Object,
+  // - getClass is final and implemented only in java.lang.Object,
+  // - toString is non-final, implemented in java.lang.Object, java.lang.Enum and possibly
+  //   also in the unboxed enum class.
+  private void rewriteInvokeMethodWithReceiver(
+      IRCode code,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer,
+      Map<Instruction, DexType> convertedEnums,
+      BasicBlockIterator blocks,
+      BasicBlock block,
+      InstructionListIterator iterator,
+      InvokeMethodWithReceiver invoke) {
+    ProgramMethod context = code.context();
+    // If the receiver is null, then the invoke is not rewritten even if the receiver is an
+    // unboxed enum, but we end up with null.ordinal() or similar which has the correct behavior.
+    DexType enumType = getEnumClassTypeOrNull(invoke.getReceiver(), convertedEnums);
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (enumType != null) {
+      if (invokedMethod == factory.enumMembers.ordinalMethod
+          || invokedMethod.match(factory.enumMembers.hashCode)) {
+        replaceEnumInvoke(
+            iterator,
+            invoke,
+            getSharedUtilityClass().ensureOrdinalMethod(appView, context, eventConsumer));
+      } else if (invokedMethod.match(factory.enumMembers.equals)) {
+        replaceEnumInvoke(
+            iterator,
+            invoke,
+            getSharedUtilityClass().ensureEqualsMethod(appView, context, eventConsumer));
+      } else if (invokedMethod == factory.enumMembers.compareTo
+          || invokedMethod == factory.enumMembers.compareToWithObject) {
+        replaceEnumInvoke(
+            iterator,
+            invoke,
+            getSharedUtilityClass().ensureCompareToMethod(appView, context, eventConsumer));
+      } else if (invokedMethod == factory.enumMembers.nameMethod) {
+        rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
+      } else if (invokedMethod.match(factory.enumMembers.toString)) {
+        DexMethod reboundMethod =
+            invokedMethod.withHolder(unboxedEnumsData.representativeType(enumType), factory);
+        DexMethod lookupMethod =
+            enumUnboxingLens
+                .lookupMethod(
+                    reboundMethod,
+                    context.getReference(),
+                    invoke.getType(),
+                    enumUnboxingLens.getPrevious())
+                .getReference();
+        // If the SuperEnum had declared a toString() override, then the unboxer moves it to
+        // the local utility class method corresponding to that override.
+        // If a SubEnum had declared a toString() override, then the unboxer records a
+        // synthetic move from SuperEnum.toString() to the dispatch method on the local
+        // utility class.
+        // When they are the same, then there are no overrides of toString().
+        if (lookupMethod == reboundMethod) {
+          rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
+        } else {
+          DexClassAndMethod dexClassAndMethod = appView.definitionFor(lookupMethod);
+          assert dexClassAndMethod != null;
+          assert dexClassAndMethod.isProgramMethod();
+          replaceEnumInvoke(iterator, invoke, dexClassAndMethod.asProgramMethod());
+        }
+      } else if (invokedMethod == factory.objectMembers.getClass) {
+        rewriteNullCheck(iterator, invoke, context, eventConsumer);
+      } else if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
+        DexMethod refinedDispatchMethodReference =
+            enumUnboxingLens.lookupRefinedDispatchMethod(
+                invokedMethod,
+                context.getReference(),
+                invoke.getType(),
+                enumUnboxingLens.getPrevious(),
+                invoke.getArgument(0).getAbstractValue(appView, context),
+                enumType);
+        if (refinedDispatchMethodReference != null) {
+          DexClassAndMethod refinedDispatchMethod =
+              appView.definitionFor(refinedDispatchMethodReference);
+          assert refinedDispatchMethod != null;
+          assert refinedDispatchMethod.isProgramMethod();
+          replaceEnumInvoke(iterator, invoke, refinedDispatchMethod.asProgramMethod());
+        }
+      }
+    } else if (invokedMethod == factory.stringBuilderMethods.appendObject
+        || invokedMethod == factory.stringBufferMethods.appendObject) {
+      // Rewrites stringBuilder.append(enumInstance) as if it was
+      // stringBuilder.append(String.valueOf(unboxedEnumInstance));
+      Value enumArg = invoke.getArgument(1);
+      DexType enumArgType = getEnumClassTypeOrNull(enumArg, convertedEnums);
+      if (enumArgType != null) {
+        ProgramMethod stringValueOfMethod =
+            getLocalUtilityClass(enumArgType)
+                .ensureStringValueOfMethod(appView, context, eventConsumer);
+        InvokeStatic toStringInvoke =
+            InvokeStatic.builder()
+                .setMethod(stringValueOfMethod)
+                .setSingleArgument(enumArg)
+                .setFreshOutValue(appView, code)
+                .setPosition(invoke)
+                .build();
+        DexMethod newAppendMethod =
+            invokedMethod == factory.stringBuilderMethods.appendObject
+                ? factory.stringBuilderMethods.appendString
+                : factory.stringBufferMethods.appendString;
+        List<Value> arguments = ImmutableList.of(invoke.getReceiver(), toStringInvoke.outValue());
+        InvokeVirtual invokeAppendString =
+            new InvokeVirtual(newAppendMethod, invoke.clearOutValue(), arguments);
+        invokeAppendString.setPosition(invoke.getPosition());
+        iterator.replaceCurrentInstruction(toStringInvoke);
+        if (block.hasCatchHandlers()) {
+          iterator
+              .splitCopyCatchHandlers(code, blocks, appView.options())
+              .listIterator(code)
+              .add(invokeAppendString);
+        } else {
+          iterator.add(invokeAppendString);
+        }
+      }
+    }
+  }
+
   private void rewriteInvokeStatic(
       InvokeStatic invoke,
       IRCode code,
-      ProgramMethod context,
       Map<Instruction, DexType> convertedEnums,
       InstructionListIterator instructionIterator,
       Set<Phi> affectedPhis,
       EnumUnboxerMethodProcessorEventConsumer eventConsumer) {
+    ProgramMethod context = code.context();
     DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
     if (singleTarget == null) {
       return;
@@ -508,13 +547,28 @@
         rewriteStringValueOf(invoke, context, convertedEnums, instructionIterator, eventConsumer);
       } else if (invokedMethod == factory.objectsMethods.equals) {
         assert invoke.arguments().size() == 2;
-        Value argument = invoke.getFirstArgument();
-        DexType enumType = getEnumClassTypeOrNull(argument, convertedEnums);
-        if (enumType != null) {
+        if (Iterables.any(
+            invoke.arguments(), arg -> getEnumClassTypeOrNull(arg, convertedEnums) != null)) {
+          // If any of the input is null, replace it by const 0.
+          // If both inputs are null, no rewriting happen here.
+          List<Value> newArguments = new ArrayList<>(invoke.arguments().size());
+          for (Value arg : invoke.arguments()) {
+            if (arg.getType().isNullType()) {
+              Value constZero = insertConstZero(code);
+              newArguments.add(constZero);
+            } else {
+              assert getEnumClassTypeOrNull(arg, convertedEnums) != null;
+              newArguments.add(arg);
+            }
+          }
           replaceEnumInvoke(
               instructionIterator,
               invoke,
-              getSharedUtilityClass().ensureObjectsEqualsMethod(appView, context, eventConsumer));
+              getSharedUtilityClass().ensureObjectsEqualsMethod(appView, context, eventConsumer),
+              newArguments);
+        } else {
+          assert invoke.getArgument(0).getType().isReferenceType();
+          assert invoke.getArgument(1).getType().isReferenceType();
         }
       }
       return;
@@ -687,11 +741,19 @@
 
   private void replaceEnumInvoke(
       InstructionListIterator iterator, InvokeMethod invoke, ProgramMethod method) {
+    replaceEnumInvoke(iterator, invoke, method, invoke.arguments());
+  }
+
+  private void replaceEnumInvoke(
+      InstructionListIterator iterator,
+      InvokeMethod invoke,
+      ProgramMethod method,
+      List<Value> arguments) {
     InvokeStatic replacement =
         new InvokeStatic(
             method.getReference(),
             invoke.hasUnusedOutValue() ? null : invoke.outValue(),
-            invoke.arguments());
+            arguments);
     assert !replacement.hasOutValue()
         || !replacement.getInvokedMethod().getReturnType().isVoidType();
     iterator.replaceCurrentInstruction(replacement);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index 5ed16f7..2bc3346 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -117,6 +117,8 @@
       }
     }
     code.removeAllDeadAndTrivialPhis();
+    code.removeRedundantBlocks();
+    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.HAS_CHANGED;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java
index 6602826..5abcb38 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.code.CfCheckCast;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfIf;
 import com.android.tools.r8.cf.code.CfIfCmp;
 import com.android.tools.r8.cf.code.CfInstanceFieldRead;
 import com.android.tools.r8.cf.code.CfInstruction;
@@ -153,6 +154,9 @@
     @Override
     public CfCode generateCfCode() {
       // This generates something along the lines of:
+      // if (other == null) {
+      //   return false;
+      // }
       // if (this.getClass() != other.getClass()) {
       //     return false;
       // }
@@ -160,10 +164,22 @@
       //     recordInstance.getFieldsAsObjects(),
       //     ((RecordClass) other).getFieldsAsObjects());
       DexItemFactory factory = appView.dexItemFactory();
-      List<CfInstruction> instructions = new ArrayList<>();
+      int numberOfInstructions = 22;
+      List<CfInstruction> instructions = new ArrayList<>(numberOfInstructions);
+      CfLabel notNullLabel = new CfLabel();
       CfLabel fieldCmp = new CfLabel();
       ValueType recordType = ValueType.fromDexType(getHolder());
       ValueType objectType = ValueType.fromDexType(factory.objectType);
+      instructions.add(new CfLoad(objectType, 1));
+      instructions.add(new CfIf(IfType.NE, ValueType.OBJECT, notNullLabel));
+      instructions.add(new CfConstNumber(0, ValueType.INT));
+      instructions.add(new CfReturn(ValueType.INT));
+      instructions.add(notNullLabel);
+      instructions.add(
+          CfFrame.builder()
+              .appendLocal(FrameType.initialized(getHolder()))
+              .appendLocal(FrameType.initialized(appView.dexItemFactory().objectType))
+              .build());
       instructions.add(new CfLoad(recordType, 0));
       instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
       instructions.add(new CfLoad(objectType, 1));
@@ -186,6 +202,7 @@
           new CfInvoke(
               Opcodes.INVOKESTATIC, factory.javaUtilArraysMethods.equalsObjectArray, false));
       instructions.add(new CfReturn(ValueType.INT));
+      assert instructions.size() == numberOfInstructions;
       return standardCfCodeFromInstructions(instructions);
     }
   }
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 fab9f6f..43da39b 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
@@ -105,15 +105,11 @@
       DexType parameterType,
       Action onChangedAction) {
     assert parameterType.isClassType();
-    boolean allowNullOrAbstractValue = true;
-    boolean allowNonConstantNumbers = false;
     AbstractValue oldAbstractValue = abstractValue;
     abstractValue =
-        abstractValue.join(
-            parameterState.getAbstractValue(appView),
-            appView.abstractValueFactory(),
-            allowNullOrAbstractValue,
-            allowNonConstantNumbers);
+        appView
+            .getAbstractValueParameterJoiner()
+            .join(abstractValue, parameterState.getAbstractValue(appView), parameterType);
     DynamicType oldDynamicType = dynamicType;
     DynamicType joinedDynamicType = dynamicType.join(appView, parameterState.getDynamicType());
     DynamicType widenedDynamicType =
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 429e514..3b3f3cb 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
@@ -56,15 +56,11 @@
       DexType parameterType,
       Action onChangedAction) {
     assert parameterType.isPrimitiveType();
-    boolean allowNullOrAbstractValue = false;
-    boolean allowNonConstantNumbers = false;
     AbstractValue oldAbstractValue = abstractValue;
     abstractValue =
-        abstractValue.join(
-            parameterState.abstractValue,
-            appView.abstractValueFactory(),
-            allowNullOrAbstractValue,
-            allowNonConstantNumbers);
+        appView
+            .getAbstractValueParameterJoiner()
+            .join(abstractValue, parameterState.abstractValue, parameterType);
     if (abstractValue.isUnknown()) {
       return unknown();
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemover.java
index ad329a1..85e7fdd 100644
--- a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemover.java
@@ -135,31 +135,27 @@
 
     // Collect all redundant bridges to remove.
     ProgramMethodSet bridgesToRemove = removeRedundantBridgesConcurrently(executorService);
-    if (bridgesToRemove.isEmpty()) {
-      timing.end();
-      return;
-    }
+    if (!bridgesToRemove.isEmpty()) {
+      pruneApp(bridgesToRemove, executorService, timing);
 
-    pruneApp(bridgesToRemove, executorService, timing);
+      if (!lensBuilder.isEmpty()) {
+        appView.setGraphLens(lensBuilder.build(appView));
+      }
 
-    if (!lensBuilder.isEmpty()) {
-      appView.setGraphLens(lensBuilder.build(appView));
-    }
-
-    if (memberRebindingIdentityLens != null) {
-      for (ProgramMethod bridgeToRemove : bridgesToRemove) {
-        DexClassAndMethod resolvedMethod =
-            appView
-                .appInfo()
-                .resolveMethodOn(bridgeToRemove.getHolder(), bridgeToRemove.getReference())
-                .getResolutionPair();
-        memberRebindingIdentityLens.addNonReboundMethodReference(
-            bridgeToRemove.getReference(), resolvedMethod.getReference());
+      if (memberRebindingIdentityLens != null) {
+        for (ProgramMethod bridgeToRemove : bridgesToRemove) {
+          DexClassAndMethod resolvedMethod =
+              appView
+                  .appInfo()
+                  .resolveMethodOn(bridgeToRemove.getHolder(), bridgeToRemove.getReference())
+                  .getResolutionPair();
+          memberRebindingIdentityLens.addNonReboundMethodReference(
+              bridgeToRemove.getReference(), resolvedMethod.getReference());
+        }
       }
     }
-
     appView.notifyOptimizationFinishedForTesting();
-    appView.appInfo().notifyRedundantBridgeRemoverFinished(true);
+    appView.appInfo().notifyRedundantBridgeRemoverFinished(memberRebindingIdentityLens == null);
     timing.end();
   }
 
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 1ea86b5..4bbc625 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -80,6 +80,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
+import com.android.tools.r8.graph.PermittedSubclassAttribute;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramDerivedContext;
 import com.android.tools.r8.graph.ProgramField;
@@ -2098,6 +2099,23 @@
       }
     }
 
+    // Mark types in permitted-subclasses attributes referenced.
+    List<PermittedSubclassAttribute> permittedSubclassAttributes =
+        clazz.getPermittedSubclassAttributes();
+    if (!permittedSubclassAttributes.isEmpty()) {
+      BiConsumer<DexType, ProgramDerivedContext> missingClassConsumer =
+          options.reportMissingClassesInPermittedSubclassesAttributes
+              ? this::reportMissingClass
+              : this::ignoreMissingClass;
+      for (PermittedSubclassAttribute permittedSubclassAttribute : permittedSubclassAttributes) {
+        recordTypeReference(
+            permittedSubclassAttribute.getPermittedSubclass(),
+            clazz,
+            this::recordNonProgramClass,
+            missingClassConsumer);
+      }
+    }
+
     KeepReason reason = KeepReason.reachableFromLiveType(clazz.type);
 
     for (DexType iface : clazz.getInterfaces()) {
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
index 48312bd..7d25797 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
@@ -69,6 +69,7 @@
     // Rewrite field instructions that reference a pruned field.
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     BasicBlockIterator blockIterator = code.listIterator();
+    boolean hasChanged = false;
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
       InstructionListIterator instructionIterator = block.listIterator(code);
@@ -76,34 +77,38 @@
         Instruction instruction = instructionIterator.next();
         switch (instruction.opcode()) {
           case INSTANCE_GET:
-            rewriteInstanceGet(
-                code,
-                instructionIterator,
-                instruction.asInstanceGet(),
-                affectedValues,
-                prunedFields);
+            hasChanged |=
+                rewriteInstanceGet(
+                    code,
+                    instructionIterator,
+                    instruction.asInstanceGet(),
+                    affectedValues,
+                    prunedFields);
             break;
           case INSTANCE_PUT:
-            rewriteInstancePut(instructionIterator, instruction.asInstancePut(), prunedFields);
+            hasChanged |=
+                rewriteInstancePut(instructionIterator, instruction.asInstancePut(), prunedFields);
             break;
           case STATIC_GET:
-            rewriteStaticGet(
-                code,
-                instructionIterator,
-                instruction.asStaticGet(),
-                affectedValues,
-                context,
-                initializedClassesWithContexts,
-                prunedFields);
+            hasChanged |=
+                rewriteStaticGet(
+                    code,
+                    instructionIterator,
+                    instruction.asStaticGet(),
+                    affectedValues,
+                    context,
+                    initializedClassesWithContexts,
+                    prunedFields);
             break;
           case STATIC_PUT:
-            rewriteStaticPut(
-                code,
-                instructionIterator,
-                instruction.asStaticPut(),
-                context,
-                initializedClassesWithContexts,
-                prunedFields);
+            hasChanged |=
+                rewriteStaticPut(
+                    code,
+                    instructionIterator,
+                    instruction.asStaticPut(),
+                    context,
+                    initializedClassesWithContexts,
+                    prunedFields);
             break;
           default:
             break;
@@ -113,9 +118,12 @@
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
+    if (hasChanged) {
+      code.removeRedundantBlocks();
+    }
   }
 
-  private void rewriteInstanceGet(
+  private boolean rewriteInstanceGet(
       IRCode code,
       InstructionListIterator instructionIterator,
       InstanceGet instanceGet,
@@ -123,27 +131,29 @@
       Map<DexField, ProgramField> prunedFields) {
     ProgramField prunedField = prunedFields.get(instanceGet.getField());
     if (prunedField == null) {
-      return;
+      return false;
     }
 
     insertDefaultValueForFieldGet(
         code, instructionIterator, instanceGet, affectedValues, prunedField);
     removeOrReplaceInstanceFieldInstructionWithNullCheck(instructionIterator, instanceGet);
+    return true;
   }
 
-  private void rewriteInstancePut(
+  private boolean rewriteInstancePut(
       InstructionListIterator instructionIterator,
       InstancePut instancePut,
       Map<DexField, ProgramField> prunedFields) {
     ProgramField prunedField = prunedFields.get(instancePut.getField());
     if (prunedField == null) {
-      return;
+      return false;
     }
 
     removeOrReplaceInstanceFieldInstructionWithNullCheck(instructionIterator, instancePut);
+    return true;
   }
 
-  private void rewriteStaticGet(
+  private boolean rewriteStaticGet(
       IRCode code,
       InstructionListIterator instructionIterator,
       StaticGet staticGet,
@@ -153,16 +163,17 @@
       Map<DexField, ProgramField> prunedFields) {
     ProgramField prunedField = prunedFields.get(staticGet.getField());
     if (prunedField == null) {
-      return;
+      return false;
     }
 
     insertDefaultValueForFieldGet(
         code, instructionIterator, staticGet, affectedValues, prunedField);
     removeOrReplaceStaticFieldInstructionByInitClass(
         code, instructionIterator, context, initializedClassesWithContexts, prunedField);
+    return true;
   }
 
-  private void rewriteStaticPut(
+  private boolean rewriteStaticPut(
       IRCode code,
       InstructionListIterator instructionIterator,
       StaticPut staticPut,
@@ -171,11 +182,12 @@
       Map<DexField, ProgramField> prunedFields) {
     ProgramField prunedField = prunedFields.get(staticPut.getField());
     if (prunedField == null) {
-      return;
+      return false;
     }
 
     removeOrReplaceStaticFieldInstructionByInitClass(
         code, instructionIterator, context, initializedClassesWithContexts, prunedField);
+    return true;
   }
 
   private void insertDefaultValueForFieldGet(
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
index 9898d34..c199247 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
@@ -257,6 +257,11 @@
       return self();
     }
 
+    public Joiner allowPermittedSubclassesRemoval() {
+      builder.allowPermittedSubclassesRemoval();
+      return self();
+    }
+
     @Override
     public Joiner asClassJoiner() {
       return this;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index bec7ae6..9d98730 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1050,6 +1050,8 @@
             builder.getModifiersBuilder().setAllowsAccessModification(true);
           } else if (acceptString("repackage")) {
             builder.getModifiersBuilder().setAllowsRepackaging(true);
+          } else if (acceptString("permittedsubclassesremoval")) {
+            builder.getModifiersBuilder().setAllowsPermittedSubclassesRemoval(true);
           } else if (options.isTestingOptionsEnabled()) {
             if (acceptString("annotationremoval")) {
               builder.getModifiersBuilder().setAllowsAnnotationRemoval(true);
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
index ec825c3..6073953 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
@@ -15,6 +15,7 @@
     private boolean allowsOptimization = false;
     private boolean allowsObfuscation = false;
     private boolean includeDescriptorClasses = false;
+    private boolean allowsPermittedSubclassesRemoval = false;
 
     private Builder() {}
 
@@ -61,6 +62,11 @@
       return this;
     }
 
+    public Builder setAllowsPermittedSubclassesRemoval(boolean allowsPermittedSubclassesRemoval) {
+      this.allowsPermittedSubclassesRemoval = allowsPermittedSubclassesRemoval;
+      return this;
+    }
+
     public void setIncludeDescriptorClasses(boolean includeDescriptorClasses) {
       this.includeDescriptorClasses = includeDescriptorClasses;
     }
@@ -73,7 +79,8 @@
           allowsShrinking,
           allowsOptimization,
           allowsObfuscation,
-          includeDescriptorClasses);
+          includeDescriptorClasses,
+          allowsPermittedSubclassesRemoval);
     }
   }
 
@@ -84,6 +91,7 @@
   public final boolean allowsOptimization;
   public final boolean allowsObfuscation;
   public final boolean includeDescriptorClasses;
+  public final boolean allowsPermittedSubclassesRemoval;
 
   private ProguardKeepRuleModifiers(
       boolean allowsAccessModification,
@@ -92,7 +100,8 @@
       boolean allowsShrinking,
       boolean allowsOptimization,
       boolean allowsObfuscation,
-      boolean includeDescriptorClasses) {
+      boolean includeDescriptorClasses,
+      boolean allowsPermittedSubclassesRemoval) {
     this.allowsAccessModification = allowsAccessModification;
     this.allowsAnnotationRemoval = allowsAnnotationRemoval;
     this.allowsRepackaging = allowsRepackaging;
@@ -100,6 +109,7 @@
     this.allowsOptimization = allowsOptimization;
     this.allowsObfuscation = allowsObfuscation;
     this.includeDescriptorClasses = includeDescriptorClasses;
+    this.allowsPermittedSubclassesRemoval = allowsPermittedSubclassesRemoval;
   }
 
   /**
@@ -116,7 +126,8 @@
         && allowsObfuscation
         && allowsOptimization
         && allowsShrinking
-        && !includeDescriptorClasses;
+        && !includeDescriptorClasses
+        && allowsPermittedSubclassesRemoval;
   }
 
   @Override
@@ -131,7 +142,8 @@
         && allowsShrinking == that.allowsShrinking
         && allowsOptimization == that.allowsOptimization
         && allowsObfuscation == that.allowsObfuscation
-        && includeDescriptorClasses == that.includeDescriptorClasses;
+        && includeDescriptorClasses == that.includeDescriptorClasses
+        && allowsPermittedSubclassesRemoval == that.allowsPermittedSubclassesRemoval;
   }
 
   @Override
@@ -143,7 +155,8 @@
         allowsShrinking,
         allowsOptimization,
         allowsObfuscation,
-        includeDescriptorClasses);
+        includeDescriptorClasses,
+        allowsPermittedSubclassesRemoval);
   }
 
   @Override
@@ -156,6 +169,7 @@
     appendWithComma(builder, allowsShrinking, "allowshrinking");
     appendWithComma(builder, allowsOptimization, "allowoptimization");
     appendWithComma(builder, includeDescriptorClasses, "includedescriptorclasses");
+    appendWithComma(builder, allowsPermittedSubclassesRemoval, "allowpermittedsubclassesremoval");
     return builder.toString();
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 8678717..afe4006 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -1665,6 +1665,16 @@
         includeDescriptorClasses(item, keepRule, preconditionEvent);
         context.markAsUsed();
       }
+
+      if (item.isProgramClass()
+          && appView.options().isKeepPermittedSubclassesEnabled()
+          && !modifiers.allowsPermittedSubclassesRemoval) {
+        dependentMinimumKeepInfo
+            .getOrCreateMinimumKeepInfoFor(preconditionEvent, item.getReference())
+            .asClassJoiner()
+            .disallowPermittedSubclassesRemoval();
+        context.markAsUsed();
+      }
     }
 
     private boolean isRepackagingDisallowed(
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 3688415..ec15263 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
+import com.android.tools.r8.graph.PermittedSubclassAttribute;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.RecordComponentInfo;
@@ -226,6 +227,7 @@
             PredicateUtils.not(isReachableInstanceField(reachableInstanceFields)));
       }
     }
+    clazz.removePermittedSubclassAttribute(this::isAttributeReferencingPrunedType);
     unusedItemsPrinter.visited();
     assert verifyNoDeadFields(clazz);
   }
@@ -319,6 +321,10 @@
     return context == null || isTypeMissing(context) || !isTypeLive(context);
   }
 
+  private boolean isAttributeReferencingPrunedType(PermittedSubclassAttribute attr) {
+    return !isTypeLive(attr.getPermittedSubclass());
+  }
+
   private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> int firstUnreachableIndex(
       List<D> items, Predicate<D> live) {
     for (int i = 0; i < items.size(); i++) {
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 c284670..d49b405 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -141,7 +141,15 @@
 
   public enum LineNumberOptimization {
     OFF,
-    ON
+    ON;
+
+    public boolean isOff() {
+      return this == OFF;
+    }
+
+    public boolean isOn() {
+      return this == ON;
+    }
   }
 
   public enum DesugarState {
@@ -876,6 +884,7 @@
   public boolean ignoreMissingClasses = false;
   public boolean reportMissingClassesInEnclosingMethodAttribute = false;
   public boolean reportMissingClassesInInnerClassAttributes = false;
+  public boolean reportMissingClassesInPermittedSubclassesAttributes = false;
   public boolean disableGenericSignatureValidation = false;
   public boolean disableInnerClassSeparatorValidationWhenRepackaging = false;
 
@@ -2585,7 +2594,7 @@
   }
 
   public boolean canUseDexPc2PcAsDebugInformation() {
-    return isGeneratingDex() && lineNumberOptimization == LineNumberOptimization.ON;
+    return isGeneratingDex() && lineNumberOptimization.isOn();
   }
 
   // Debug entries may be dropped only if the source file content allows being omitted from
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
index 2a044e5..bbc27a2 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMemberMap.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.utils.collections;
 
 import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.utils.TriPredicate;
 import com.google.common.base.Equivalence.Wrapper;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
@@ -77,6 +79,12 @@
         .removeIf(entry -> predicate.test(entry.getKey().get(), entry.getValue()));
   }
 
+  public boolean removeIf(TriPredicate<K, V, Entry<?, V>> predicate) {
+    return backing
+        .entrySet()
+        .removeIf(entry -> predicate.test(entry.getKey().get(), entry.getValue(), entry));
+  }
+
   public int size() {
     return backing.size();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
index a6a2a06..e80a0a7 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
@@ -26,7 +26,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SourcePosition;
-import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -176,7 +175,7 @@
 
   public DexPositionToNoPcMappedRangeMapper(AppView<?> appView) {
     this.appView = appView;
-    isIdentityMapping = appView.options().lineNumberOptimization == LineNumberOptimization.OFF;
+    isIdentityMapping = appView.options().lineNumberOptimization.isOff();
   }
 
   public List<MappedPosition> optimizeDexCodePositions(
diff --git a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
index 7fe3d32..487ac5f 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
@@ -1,4 +1,3 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
@@ -45,7 +44,6 @@
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.IntBox;
-import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.OneShotCollectionConsumer;
 import com.android.tools.r8.utils.OriginalSourceFiles;
@@ -56,6 +54,7 @@
 import it.unimi.dsi.fastutil.ints.Int2IntSortedMap;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -79,7 +78,7 @@
       PositionRangeAllocator.createCardinalPositionRangeAllocator();
   private final NonCardinalPositionRangeAllocator nonCardinalRangeCache =
       PositionRangeAllocator.createNonCardinalPositionRangeAllocator();
-  private final int maxGap = 1000;
+  private final int maxGap;
 
   private MappedPositionToClassNameMapperBuilder(
       AppView<?> appView, OriginalSourceFiles originalSourceFiles) {
@@ -88,6 +87,7 @@
     classNameMapperBuilder = ClassNameMapper.builder();
     classNameMapperBuilder.setCurrentMapVersion(
         appView.options().getMapFileVersion().toMapVersionMappingInformation());
+    maxGap = appView.options().lineNumberOptimization.isOn() ? 1000 : 0;
   }
 
   public static String getPrunedInlinedClassObfuscatedPrefix() {
@@ -246,7 +246,7 @@
           residualMethod,
           originalMethod,
           originalType)) {
-        assert appView.options().lineNumberOptimization == LineNumberOptimization.OFF
+        assert appView.options().lineNumberOptimization.isOff()
             || hasAtMostOnePosition(appView, definition)
             || appView.isCfByteCodePassThrough(definition);
         return this;
@@ -290,6 +290,8 @@
         methodSpecificMappingInformation.add(OutlineMappingInformation.builder().build());
       }
 
+      mappedPositions.sort(Comparator.comparing(MappedPosition::getObfuscatedLine));
+
       // Update memberNaming with the collected positions, merging multiple positions into a
       // single region whenever possible.
       for (int i = 0; i < mappedPositions.size(); /* updated in body */ ) {
@@ -317,14 +319,7 @@
               || firstMappedPosition.getPosition().getOutlineCallee() != null) {
             break;
           }
-          // The mapped positions are not guaranteed to be in order, so maintain first and last
-          // position.
-          if (firstMappedPosition.getObfuscatedLine() > currentMappedPosition.getObfuscatedLine()) {
-            firstMappedPosition = currentMappedPosition;
-          }
-          if (lastMappedPosition.getObfuscatedLine() < currentMappedPosition.getObfuscatedLine()) {
-            lastMappedPosition = currentMappedPosition;
-          }
+          lastMappedPosition = currentMappedPosition;
         }
         Range obfuscatedRange =
             nonCardinalRangeCache.get(
@@ -385,9 +380,7 @@
         }
         i = j;
       }
-      // TODO(b/287210793): Enable assertion again.
-      assert true
-          || mappedPositions.size() <= 1
+      assert mappedPositions.size() <= 1
           || getBuilder().hasNoOverlappingRangesForSignature(residualSignature);
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java b/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java
index 3f0288f..861c9dc 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionRemapper.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.utils.CfLineToMethodMapper;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.Pair;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -34,8 +33,7 @@
 
   static PositionRemapper getPositionRemapper(
       AppView<?> appView, CfLineToMethodMapper cfLineToMethodMapper) {
-    boolean identityMapping =
-        appView.options().lineNumberOptimization == LineNumberOptimization.OFF;
+    boolean identityMapping = appView.options().lineNumberOptimization.isOff();
     PositionRemapper positionRemapper =
         identityMapping
             ? new IdentityPositionRemapper()
diff --git a/src/test/examplesJava17/records/SimpleRecord.java b/src/test/examplesJava17/records/SimpleRecord.java
index 1f8fca4..3f908dc 100644
--- a/src/test/examplesJava17/records/SimpleRecord.java
+++ b/src/test/examplesJava17/records/SimpleRecord.java
@@ -14,5 +14,22 @@
     System.out.println(janeDoe.age);
     System.out.println(janeDoe.name());
     System.out.println(janeDoe.age());
+
+    // Test equals with self.
+    System.out.println(janeDoe.equals(janeDoe));
+
+    // Test equals with structurally equals Person.
+    Person otherJaneDoe = new Person("Jane Doe", 42);
+    System.out.println(janeDoe.equals(otherJaneDoe));
+    System.out.println(otherJaneDoe.equals(janeDoe));
+
+    // Test equals with not-structually equals Person.
+    Person johnDoe = new Person("John Doe", 42);
+    System.out.println(janeDoe.equals(johnDoe));
+    System.out.println(johnDoe.equals(janeDoe));
+
+    // Test equals with Object and null.
+    System.out.println(janeDoe.equals(new Object()));
+    System.out.println(janeDoe.equals(null));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/KotlinTestBase.java b/src/test/java/com/android/tools/r8/KotlinTestBase.java
index 35e20e9..f314547 100644
--- a/src/test/java/com/android/tools/r8/KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/KotlinTestBase.java
@@ -91,8 +91,7 @@
   }
 
   protected Path getJavaJarFile(String folder) {
-    return Paths.get(ToolHelper.TESTS_BUILD_DIR, RSRC,
-        targetVersion.getFolderName(), folder + ".java" + FileUtils.JAR_EXTENSION);
+    return Paths.get(ToolHelper.TESTS_BUILD_DIR, RSRC, folder + FileUtils.JAR_EXTENSION);
   }
 
   protected Path getMappingfile(String folder, String mappingFileName) {
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index ec3a345..3bad712 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -80,8 +80,6 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.PreloadedClassFileProvider;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesClass;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.TestDescriptionWatcher;
@@ -1688,18 +1686,6 @@
     return clazz.getTypeName();
   }
 
-  public static ClassReference examplesClassReference(Class<? extends ExamplesClass> clazz) {
-    return Reference.classFromTypeName(examplesTypeName(clazz));
-  }
-
-  public static String examplesTypeName(Class<? extends ExamplesClass> clazz) {
-    try {
-      return ReflectiveBuildPathUtils.resolveClassName(clazz);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   public static AndroidApiLevel apiLevelWithDefaultInterfaceMethodsSupport() {
     return AndroidApiLevel.N;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
index 63b1982..c9740c3 100644
--- a/src/test/java/com/android/tools/r8/TestBaseBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
@@ -10,8 +10,6 @@
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesRootPackage;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
 import java.util.Arrays;
@@ -56,12 +54,6 @@
     return self();
   }
 
-  public T addExamplesProgramFiles(Class<? extends ExamplesRootPackage> rootPackage)
-      throws Exception {
-    Collection<Path> files = ReflectiveBuildPathUtils.allClassFiles(rootPackage);
-    return addProgramFiles(files);
-  }
-
   public T addLibraryProvider(ClassFileResourceProvider provider) {
     builder.addLibraryResourceProvider(provider);
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index f27b3b8..9b4e4fd 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
+import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
@@ -314,6 +315,10 @@
     return addKeepMainRule(mainClass.getTypeName());
   }
 
+  public T addKeepMainRule(ClassReference mainClass) {
+    return addKeepMainRule(mainClass.getTypeName());
+  }
+
   public T addKeepMainRules(Class<?>... mainClasses) {
     for (Class<?> mainClass : mainClasses) {
       this.addKeepMainRule(mainClass);
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 42b728e..447399f 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -119,8 +119,8 @@
   public static final String LIBS_DIR = BUILD_DIR + "libs/";
   public static final String THIRD_PARTY_DIR = getProjectRoot() + "third_party/";
   public static final String TOOLS_DIR = getProjectRoot() + "tools/";
-  public static final String TESTS_DIR = "src/test/";
-  public static final String TESTS_SOURCE_DIR = "src/test/java";
+  public static final String TESTS_DIR = getProjectRoot() + "src/test/";
+  public static final String TESTS_SOURCE_DIR = TESTS_DIR + "java/";
   public static final String EXAMPLES_DIR = TESTS_DIR + "examples/";
   public static final String EXAMPLES_ANDROID_N_DIR = TESTS_DIR + "examplesAndroidN/";
   public static final String EXAMPLES_ANDROID_O_DIR = TESTS_DIR + "examplesAndroidO/";
@@ -140,7 +140,6 @@
   public static final String GENERATED_PROTO_BUILD_DIR = GENERATED_TEST_BUILD_DIR + "proto/";
   public static final String SMALI_BUILD_DIR = THIRD_PARTY_DIR + "smali/";
   public static final String JAVA_CLASSES_DIR = BUILD_DIR + "classes/java/";
-  public static final String JDK_11_TESTS_CLASSES_DIR = JAVA_CLASSES_DIR + "jdk11Tests/";
 
   public static final String R8_TEST_BUCKET = "r8-test-results";
 
@@ -178,7 +177,7 @@
   private static final String PROGUARD6_0_1 =
       THIRD_PARTY_DIR + "proguard/proguard6.0.1/bin/proguard";
   private static final String PROGUARD = PROGUARD5_2_1;
-  public static final Path JACOCO_ROOT = Paths.get("third_party", "jacoco", "0.8.6");
+  public static final Path JACOCO_ROOT = Paths.get(THIRD_PARTY_DIR, "jacoco", "0.8.6");
   public static final Path JACOCO_AGENT = JACOCO_ROOT.resolve(Paths.get("lib", "jacocoagent.jar"));
   public static final Path JACOCO_CLI = JACOCO_ROOT.resolve(Paths.get("lib", "jacococli.jar"));
   public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS =
@@ -1322,11 +1321,6 @@
     return getClassPathForTests().resolve(Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY)));
   }
 
-  public static String getJarEntryForTestPackage(Package pkg) {
-    List<String> parts = getNamePartsForTestPackage(pkg);
-    return String.join("/", parts);
-  }
-
   private static List<String> getNamePartsForTestClass(Class<?> clazz) {
     List<String> parts = Lists.newArrayList(clazz.getTypeName().split("\\."));
     parts.set(parts.size() - 1, parts.get(parts.size() - 1) + ".class");
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
index 57d23d8..f69b30f 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.benchmarks.appdumps;
 
+import com.android.tools.r8.LibraryDesugaringTestConfiguration;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.benchmarks.BenchmarkBase;
 import com.android.tools.r8.benchmarks.BenchmarkConfig;
 import com.android.tools.r8.benchmarks.BenchmarkConfigError;
@@ -17,6 +19,7 @@
 import com.android.tools.r8.dump.CompilerDump;
 import com.android.tools.r8.dump.DumpOptions;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -146,6 +149,15 @@
     return CompilerDump.fromArchive(dump, environment.getTemp().newFolder().toPath());
   }
 
+  private static void addDesugaredLibrary(
+      TestCompilerBuilder<?, ?, ?, ?, ?> builder, CompilerDump dump) {
+    Path config = dump.getDesugaredLibraryFile();
+    if (Files.exists(config)) {
+      builder.enableCoreLibraryDesugaring(
+          LibraryDesugaringTestConfiguration.forSpecification(config));
+    }
+  }
+
   private static BenchmarkMethod runR8(AppDumpBenchmarkBuilder builder) {
     return environment ->
         BenchmarkBase.runner(environment.getConfig())
@@ -159,6 +171,7 @@
                       .addLibraryFiles(dump.getLibraryArchive())
                       .addKeepRuleFiles(dump.getProguardConfigFile())
                       .setMinApi(dumpProperties.getMinApi())
+                      .apply(b -> addDesugaredLibrary(b, dump))
                       .allowUnnecessaryDontWarnWildcards()
                       .allowUnusedDontWarnPatterns()
                       .allowUnusedProguardConfigurationRules()
@@ -184,6 +197,7 @@
                       .addProgramFiles(dump.getProgramArchive())
                       .addLibraryFiles(dump.getLibraryArchive())
                       .setMinApi(dumpProperties.getMinApi())
+                      .apply(b -> addDesugaredLibrary(b, dump))
                       .benchmarkCompile(results)
                       .benchmarkCodeSize(results);
                 });
@@ -219,6 +233,7 @@
                             .addClasspathFiles(dump.getProgramArchive())
                             .addLibraryFiles(dump.getLibraryArchive())
                             .setMinApi(dumpProperties.getMinApi())
+                            .apply(b -> addDesugaredLibrary(b, dump))
                             .setIntermediate(true)
                             .benchmarkCompile(results.getSubResults(builder.nameForProgramPart()))
                             .writeToZip());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
index 30b4939..f3ab716 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
@@ -4,6 +4,14 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.jar;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestClassMergingTest;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostA;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostA$NestMemberA;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostA$NestMemberB;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostB;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostB$NestMemberA;
+import static com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.HorizontalClassMergingTestSources.nestHostB$NestMemberB;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
@@ -12,17 +20,13 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ThrowableConsumer;
-import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestClassMergingTest;
-import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestHostA;
-import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestHostB;
+import com.android.tools.r8.examples.JavaExampleClassProxy;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesClass;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesJava11RootPackage;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesPackage;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
+import java.nio.file.Path;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.junit.Test;
@@ -30,21 +34,27 @@
 
 public class NestClassMergingTestRunner extends HorizontalClassMergingTestBase {
 
-  public static class R extends ExamplesJava11RootPackage {
-    public static class horizontalclassmerging extends ExamplesPackage {
-      public static class NestClassMergingTest extends ExamplesClass {}
+  public static class HorizontalClassMergingTestSources {
 
-      public static class NestHostA extends ExamplesClass {
-        public static class NestMemberA extends ExamplesClass {}
+    private static final String EXAMPLE_FILE = "examplesJava11";
 
-        public static class NestMemberB extends ExamplesClass {}
-      }
+    public static final JavaExampleClassProxy nestClassMergingTest =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestClassMergingTest");
+    public static final JavaExampleClassProxy nestHostA =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostA");
+    public static final JavaExampleClassProxy nestHostA$NestMemberA =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostA$NestMemberA");
+    public static final JavaExampleClassProxy nestHostA$NestMemberB =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostA$NestMemberB");
+    public static final JavaExampleClassProxy nestHostB =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostB");
+    public static final JavaExampleClassProxy nestHostB$NestMemberA =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostB$NestMemberA");
+    public static final JavaExampleClassProxy nestHostB$NestMemberB =
+        new JavaExampleClassProxy(EXAMPLE_FILE, "horizontalclassmerging/NestHostB$NestMemberB");
 
-      public static class NestHostB extends ExamplesClass {
-        public static class NestMemberA extends ExamplesClass {}
-
-        public static class NestMemberB extends ExamplesClass {}
-      }
+    public static Path jar() {
+      return JavaExampleClassProxy.examplesJar(EXAMPLE_FILE + "/horizontalclassmerging");
     }
   }
 
@@ -70,21 +80,21 @@
                   if (parameters.canUseNestBasedAccesses()) {
                     inspector
                         .assertIsCompleteMergeGroup(
-                            classRef(NestHostA.class),
-                            classRef(NestHostA.NestMemberA.class),
-                            classRef(NestHostA.NestMemberB.class))
+                            nestHostA.getClassReference(),
+                            nestHostA$NestMemberA.getClassReference(),
+                            nestHostA$NestMemberB.getClassReference())
                         .assertIsCompleteMergeGroup(
-                            classRef(NestHostB.class),
-                            classRef(NestHostB.NestMemberA.class),
-                            classRef(NestHostB.NestMemberB.class));
+                            nestHostB.getClassReference(),
+                            nestHostB$NestMemberA.getClassReference(),
+                            nestHostB$NestMemberB.getClassReference());
                   } else {
                     inspector.assertIsCompleteMergeGroup(
-                        classRef(NestHostA.class),
-                        classRef(NestHostA.NestMemberA.class),
-                        classRef(NestHostA.NestMemberB.class),
-                        classRef(NestHostB.class),
-                        classRef(NestHostB.NestMemberA.class),
-                        classRef(NestHostB.NestMemberB.class));
+                        nestHostA.getClassReference(),
+                        nestHostA$NestMemberA.getClassReference(),
+                        nestHostA$NestMemberB.getClassReference(),
+                        nestHostB.getClassReference(),
+                        nestHostB$NestMemberA.getClassReference(),
+                        nestHostB$NestMemberB.getClassReference());
                   }
                 }));
   }
@@ -99,15 +109,17 @@
                     inspector ->
                         inspector
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostA.class), classRef(NestHostA.NestMemberA.class))
+                                nestHostA.getClassReference(),
+                                nestHostA$NestMemberA.getClassReference())
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostB.class), classRef(NestHostB.NestMemberA.class))
+                                nestHostB.getClassReference(),
+                                nestHostB$NestMemberA.getClassReference())
                             .assertClassReferencesNotMerged(
-                                classRef(NestHostA.NestMemberB.class),
-                                classRef(NestHostB.NestMemberB.class)))
+                                nestHostA$NestMemberB.getClassReference(),
+                                nestHostB$NestMemberB.getClassReference()))
                 .addNoHorizontalClassMergingRule(
-                    examplesTypeName(NestHostA.NestMemberB.class),
-                    examplesTypeName(NestHostB.NestMemberB.class))
+                    nestHostA$NestMemberB.getClassReference().getTypeName(),
+                    nestHostB$NestMemberB.getClassReference().getTypeName())
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
@@ -116,17 +128,17 @@
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
                                     .collect(Collectors.toSet());
-                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                            if (candidateClassReferences.contains(nestHostA.getClassReference())) {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostA.class),
-                                      classRef(NestHostA.NestMemberA.class)),
+                                      nestHostA.getClassReference(),
+                                      nestHostA$NestMemberA.getClassReference()),
                                   candidateClassReferences);
                             } else {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostB.class),
-                                      classRef(NestHostB.NestMemberA.class)),
+                                      nestHostB.getClassReference(),
+                                      nestHostB$NestMemberA.getClassReference()),
                                   candidateClassReferences);
                             }
                             return Iterables.find(
@@ -134,9 +146,9 @@
                                 candidate -> {
                                   ClassReference classReference = candidate.getClassReference();
                                   return classReference.equals(
-                                          classRef(NestHostA.NestMemberA.class))
+                                          nestHostA$NestMemberA.getClassReference())
                                       || classReference.equals(
-                                          classRef(NestHostB.NestMemberA.class));
+                                          nestHostB$NestMemberA.getClassReference());
                                 });
                           };
                     }));
@@ -152,15 +164,17 @@
                     inspector ->
                         inspector
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostA.class), classRef(NestHostA.NestMemberB.class))
+                                nestHostA.getClassReference(),
+                                nestHostA$NestMemberB.getClassReference())
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostB.class), classRef(NestHostB.NestMemberB.class))
+                                nestHostB.getClassReference(),
+                                nestHostB$NestMemberB.getClassReference())
                             .assertClassReferencesNotMerged(
-                                classRef(NestHostA.NestMemberA.class),
-                                classRef(NestHostB.NestMemberA.class)))
+                                nestHostA$NestMemberA.getClassReference(),
+                                nestHostB$NestMemberA.getClassReference()))
                 .addNoHorizontalClassMergingRule(
-                    examplesTypeName(NestHostA.NestMemberA.class),
-                    examplesTypeName(NestHostB.NestMemberA.class))
+                    nestHostA$NestMemberA.getClassReference().getTypeName(),
+                    nestHostB$NestMemberA.getClassReference().getTypeName())
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
@@ -169,17 +183,17 @@
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
                                     .collect(Collectors.toSet());
-                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                            if (candidateClassReferences.contains(nestHostA.getClassReference())) {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostA.class),
-                                      classRef(NestHostA.NestMemberB.class)),
+                                      nestHostA.getClassReference(),
+                                      nestHostA$NestMemberB.getClassReference()),
                                   candidateClassReferences);
                             } else {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostB.class),
-                                      classRef(NestHostB.NestMemberB.class)),
+                                      nestHostB.getClassReference(),
+                                      nestHostB$NestMemberB.getClassReference()),
                                   candidateClassReferences);
                             }
                             return Iterables.find(
@@ -187,9 +201,9 @@
                                 candidate -> {
                                   ClassReference classReference = candidate.getClassReference();
                                   return classReference.equals(
-                                          classRef(NestHostA.NestMemberB.class))
+                                          nestHostA$NestMemberB.getClassReference())
                                       || classReference.equals(
-                                          classRef(NestHostB.NestMemberB.class));
+                                          nestHostB$NestMemberB.getClassReference());
                                 });
                           };
                     }));
@@ -205,15 +219,17 @@
                     inspector ->
                         inspector
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostA.class), classRef(NestHostA.NestMemberA.class))
+                                nestHostA.getClassReference(),
+                                nestHostA$NestMemberA.getClassReference())
                             .assertIsCompleteMergeGroup(
-                                classRef(NestHostB.class), classRef(NestHostB.NestMemberA.class))
+                                nestHostB.getClassReference(),
+                                nestHostB$NestMemberA.getClassReference())
                             .assertClassReferencesNotMerged(
-                                classRef(NestHostA.NestMemberB.class),
-                                classRef(NestHostB.NestMemberB.class)))
+                                nestHostA$NestMemberB.getClassReference(),
+                                nestHostB$NestMemberB.getClassReference()))
                 .addNoHorizontalClassMergingRule(
-                    examplesTypeName(NestHostA.NestMemberB.class),
-                    examplesTypeName(NestHostB.NestMemberB.class))
+                    nestHostA$NestMemberB.getClassReference().getTypeName(),
+                    nestHostB$NestMemberB.getClassReference().getTypeName())
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
@@ -222,25 +238,25 @@
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
                                     .collect(Collectors.toSet());
-                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                            if (candidateClassReferences.contains(nestHostA.getClassReference())) {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostA.class),
-                                      classRef(NestHostA.NestMemberA.class)),
+                                      nestHostA.getClassReference(),
+                                      nestHostA$NestMemberA.getClassReference()),
                                   candidateClassReferences);
                             } else {
                               assertEquals(
                                   ImmutableSet.of(
-                                      classRef(NestHostB.class),
-                                      classRef(NestHostB.NestMemberA.class)),
+                                      nestHostB.getClassReference(),
+                                      nestHostB$NestMemberA.getClassReference()),
                                   candidateClassReferences);
                             }
                             return Iterables.find(
                                 canditates,
                                 candidate -> {
                                   ClassReference classReference = candidate.getClassReference();
-                                  return classReference.equals(classRef(NestHostA.class))
-                                      || classReference.equals(classRef(NestHostB.class));
+                                  return classReference.equals(nestHostA.getClassReference())
+                                      || classReference.equals(nestHostB.getClassReference());
                                 });
                           };
                     }));
@@ -248,22 +264,18 @@
 
   private void runTest(ThrowableConsumer<R8FullTestBuilder> configuration) throws Exception {
     testForR8(parameters.getBackend())
-        .addKeepMainRule(examplesTypeName(NestClassMergingTest.class))
-        .addExamplesProgramFiles(R.class)
+        .addKeepMainRule(nestClassMergingTest.getClassReference())
+        .addProgramFiles(jar())
         .apply(configuration)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), examplesTypeName(NestClassMergingTest.class))
+        .run(parameters.getRuntime(), nestClassMergingTest.getClassReference().getTypeName())
         .assertSuccessWithOutputLines(
             "NestHostA$NestMemberA",
             "NestHostA$NestMemberB",
             "NestHostB$NestMemberA",
             "NestHostB$NestMemberB");
   }
-
-  private static ClassReference classRef(Class<? extends ExamplesClass> clazz) {
-    return examplesClassReference(clazz);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
index 60ca0c6..a6dac7d 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
@@ -93,6 +93,11 @@
         .resolve(Paths.get("third_party", "openjdk", "openjdk-rt-1.8", "rt.jar"));
   }
 
+  public Path getAndroidJar() {
+    return getProjectRoot()
+        .resolve(Paths.get("third_party", "android_jar", "lib-v33", "android.jar"));
+  }
+
   public List<String> getKeepMainRules(Class<?> clazz) {
     return Collections.singletonList(
         "-keep class " + clazz.getName() + " { public static void main(java.lang.String[]); }");
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index 9535f30..d2096f6 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.compilerapi.diagnostics.UnsupportedFeaturesDiagnosticApiTest;
 import com.android.tools.r8.compilerapi.extractmarker.ExtractMarkerApiTest;
 import com.android.tools.r8.compilerapi.globalsynthetics.GlobalSyntheticsTest;
+import com.android.tools.r8.compilerapi.globalsyntheticsgenerator.GlobalSyntheticsGeneratorTest;
 import com.android.tools.r8.compilerapi.inputdependencies.InputDependenciesTest;
 import com.android.tools.r8.compilerapi.mapid.CustomMapIdTest;
 import com.android.tools.r8.compilerapi.mockdata.MockClass;
@@ -63,7 +64,8 @@
           SyntheticContextsConsumerTest.ApiTest.class,
           ExtractMarkerApiTest.ApiTest.class,
           PartitionMapCommandTest.ApiTest.class,
-          CancelCompilationCheckerTest.ApiTest.class);
+          CancelCompilationCheckerTest.ApiTest.class,
+          GlobalSyntheticsGeneratorTest.ApiTest.class);
 
   private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
       ImmutableList.of();
diff --git a/src/test/java/com/android/tools/r8/compilerapi/globalsyntheticsgenerator/GlobalSyntheticsGeneratorTest.java b/src/test/java/com/android/tools/r8/compilerapi/globalsyntheticsgenerator/GlobalSyntheticsGeneratorTest.java
new file mode 100644
index 0000000..6cab4d7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/globalsyntheticsgenerator/GlobalSyntheticsGeneratorTest.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.compilerapi.globalsyntheticsgenerator;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.GlobalSyntheticsGenerator;
+import com.android.tools.r8.GlobalSyntheticsGeneratorCommand;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import org.junit.Test;
+
+public class GlobalSyntheticsGeneratorTest extends CompilerApiTestRunner {
+
+  public GlobalSyntheticsGeneratorTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<? extends CompilerApiTest> binaryTestClass() {
+    return ApiTest.class;
+  }
+
+  @Test
+  public void testGlobalSynthetics() throws Exception {
+    new ApiTest(ApiTest.PARAMETERS)
+        .run(
+            new DexIndexedConsumer.ArchiveConsumer(
+                temp.newFolder().toPath().resolve("output.zip")));
+  }
+
+  public static class ApiTest extends CompilerApiTest {
+
+    public ApiTest(Object parameters) {
+      super(parameters);
+    }
+
+    public void run(ProgramConsumer programConsumer) throws Exception {
+      GlobalSyntheticsGenerator.run(
+          GlobalSyntheticsGeneratorCommand.builder()
+              .addLibraryFiles(getAndroidJar())
+              .setMinApiLevel(33)
+              .setProgramConsumer(programConsumer)
+              .build());
+    }
+
+    @Test
+    public void testGlobalSynthetics() throws Exception {
+      run(DexIndexedConsumer.emptyConsumer());
+    }
+  }
+}
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 fb80241..dfa8cd5 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
@@ -42,7 +42,18 @@
   private static final byte[][] PROGRAM_DATA_2 = RecordTestUtils.getProgramData(RECORD_NAME_2);
   private static final String MAIN_TYPE_2 = RecordTestUtils.getMainType(RECORD_NAME_2);
   private static final String EXPECTED_RESULT_2 =
-      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "true",
+          "true",
+          "false",
+          "false",
+          "false",
+          "false");
 
   private final TestParameters parameters;
 
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 7dbeae0..e208949 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
@@ -29,7 +29,18 @@
   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("Jane Doe", "42", "Jane Doe", "42");
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "true",
+          "true",
+          "false",
+          "false",
+          "false",
+          "false");
 
   @Parameter(0)
   public TestParameters parameters;
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumJdk17CompiledTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumJdk17CompiledTest.java
index 07d4ef7..e00e194 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumJdk17CompiledTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumJdk17CompiledTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.desugar.sealed;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
@@ -18,7 +19,7 @@
 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.Matchers;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,14 +71,15 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz("enum_sealed.Enum");
-    assertThat(clazz, Matchers.isPresentAndRenamed());
-    if (!parameters.isCfRuntime()) {
-      return;
-    }
+    ClassSubject clazz = inspector.clazz(EnumSealed.Enum.typeName());
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(EnumSealed.EnumB.typeName());
+    assertThat(sub1, isPresentAndRenamed());
     assertEquals(
-        keepPermittedSubclassesAttribute ? 1 : 0,
-        clazz.getFinalPermittedSubclassAttributes().size());
+        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
+            ? ImmutableList.of(sub1.asTypeSubject())
+            : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
   }
 
   @Test
@@ -90,6 +92,7 @@
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
         .addKeepMainRule(EnumSealed.Main.typeName())
+        .addKeepClassRulesWithAllowObfuscation(EnumSealed.Enum.typeName())
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), EnumSealed.Main.typeName())
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
new file mode 100644
index 0000000..fdd28aa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+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.HorizontallyMergedClassesInspector;
+import com.google.common.collect.ImmutableList;
+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 SealedClassesExtendsVerticalMergeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class, SubSub.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub2 = inspector.clazz(Sub2.class);
+    assertThat(sub2, isPresentAndRenamed());
+    ClassSubject subSub = inspector.clazz(SubSub.class);
+    assertThat(subSub, isPresentAndRenamed());
+    // TODO(b/227160052): Should be both subSub.asTypeSubject() and sub2.asTypeSubject().
+    assertEquals(
+        parameters.isCfRuntime() ? ImmutableList.of(sub2.asTypeSubject()) : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepPermittedSubclasses(Super.class)
+        .addKeepPermittedSubclasses(Sub2.class)
+        .addKeepMainRule(TestClass.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> {
+              inspector.assertMergedIntoSubtype(Sub1.class);
+            })
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isDexRuntime(),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r ->
+                r.assertFailureWithErrorThatMatches(
+                    containsString("cannot inherit from sealed class")),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new SubSub();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub1, Sub2 */ {}
+
+  static class Sub1 extends Super {}
+
+  static class Sub2 extends Super {}
+
+  static class SubSub extends Sub1 {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesHorizontalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesHorizontalMergeTest.java
new file mode 100644
index 0000000..270637d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesHorizontalMergeTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import 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 SealedClassesHorizontalMergeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertEquals(
+        parameters.isCfRuntime() ? ImmutableList.of(sub1.asTypeSubject()) : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepClassRulesWithAllowObfuscation(Super.class)
+        .addKeepMainRule(TestClass.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(Sub2.class, Sub1.class)
+                  .assertNoOtherClassesMerged();
+            })
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new Sub1();
+      new Sub2();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub1, Sub2 */ {}
+
+  static class Sub1 extends Super {}
+
+  static class Sub2 extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassMergedTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassMergedTest.java
new file mode 100644
index 0000000..b420268
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassMergedTest.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import org.hamcrest.Matcher;
+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 SealedClassesIllegalSubclassMergedTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final Matcher<String> EXPECTED = containsString("cannot inherit from sealed class");
+  static final String EXPECTED_WITHOUT_PERMITTED_SUBCLASSES_ATTRIBUTE_OR_FIXED_ATTRIBUTE =
+      StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}, keepPermittedSubclasses = {1}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17));
+    testForJvm(parameters)
+        .apply(this::addTestClasses)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatMatches(EXPECTED);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertEquals(
+        parameters.isCfRuntime() ? ImmutableList.of(sub1.asTypeSubject()) : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepClassRulesWithAllowObfuscation(Super.class)
+        .addKeepMainRule(TestClass.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(Sub2.class, Sub1.class)
+                  .assertNoOtherClassesMerged();
+            })
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            // On JDK 17 the class merging also prevents "cannot inherit from sealed class".
+            r ->
+                r.assertSuccessWithOutput(
+                    EXPECTED_WITHOUT_PERMITTED_SUBCLASSES_ATTRIBUTE_OR_FIXED_ATTRIBUTE),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class).setPermittedSubclasses(Super.class, Sub1.class).transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new Sub1();
+      new Sub2();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub1 */ {}
+
+  static class Sub1 extends Super {}
+
+  static class Sub2 extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
index 03d9160..240b7a9 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesIllegalSubclassTest.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.desugar.sealed;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -20,7 +22,7 @@
 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.Matchers;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.hamcrest.Matcher;
 import org.junit.Test;
@@ -81,14 +83,19 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(C.class);
-    assertThat(clazz, Matchers.isPresentAndRenamed());
-    if (!parameters.isCfRuntime()) {
-      return;
-    }
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    ClassSubject sub2 = inspector.clazz(Sub2.class);
+    ClassSubject sub3 = inspector.clazz(Sub3.class);
+    assertThat(sub1, isPresentAndNotRenamed());
+    assertThat(sub2, isPresentAndNotRenamed());
+    assertThat(sub3, isPresentAndNotRenamed());
     assertEquals(
-        keepPermittedSubclassesAttribute ? 2 : 0,
-        clazz.getFinalPermittedSubclassAttributes().size());
+        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
+            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
+            : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
   }
 
   @Test
@@ -101,10 +108,8 @@
         .applyIf(
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
-        // Keep the sealed class to ensure the PermittedSubclasses attribute stays live.
-        .addKeepPermittedSubclasses(C.class)
-        // Keep subclasses as the PermittedSubclasses attribute is not rewritten.
-        .addKeepRules("-keep class * extends " + C.class.getTypeName())
+        .addKeepPermittedSubclasses(Super.class)
+        .addKeepRules("-keep class * extends " + Super.class.getTypeName())
         .addKeepMainRule(TestClass.class)
         .compile()
         .inspect(this::inspect)
@@ -122,7 +127,9 @@
   }
 
   public byte[] getTransformedClasses() throws Exception {
-    return transformer(C.class).setPermittedSubclasses(C.class, Sub1.class, Sub2.class).transform();
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
   }
 
   static class TestClass {
@@ -135,11 +142,11 @@
     }
   }
 
-  abstract static class C /* permits Sub1, Sub2 */ {}
+  abstract static class Super /* permits Sub1, Sub2 */ {}
 
-  static class Sub1 extends C {}
+  static class Sub1 extends Super {}
 
-  static class Sub2 extends C {}
+  static class Sub2 extends Super {}
 
-  static class Sub3 extends C {}
+  static class Sub3 extends Super {}
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
new file mode 100644
index 0000000..43f4a41
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+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.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SealedClassesImplementsVerticalMergeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Super.class, Sub1.class, Sub2.class, SubSub.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject iface1 = inspector.clazz(Iface1.class);
+    assertThat(iface1, isPresentAndRenamed());
+    ClassSubject iface2 = inspector.clazz(Iface2.class);
+    assertThat(iface2, isPresentAndRenamed());
+    ClassSubject sub2 = inspector.clazz(Sub2.class);
+    assertThat(sub2, isPresentAndRenamed());
+    ClassSubject subSub = inspector.clazz(SubSub.class);
+    assertThat(subSub, Matchers.isPresentAndRenamed());
+    for (ClassSubject clazz : ImmutableList.of(iface1, iface2)) {
+      assertEquals(
+          // TODO(b/227160052): Should be both subSub.asTypeSubject() and sub2.asTypeSubject().
+          parameters.isCfRuntime() ? ImmutableList.of(sub2.asTypeSubject()) : ImmutableList.of(),
+          clazz.getFinalPermittedSubclassAttributes());
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepPermittedSubclasses(Super.class, Iface1.class, Iface2.class, Sub2.class)
+        .addKeepMainRule(TestClass.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> {
+              inspector.assertMergedIntoSubtype(Sub1.class);
+            })
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isDexRuntime(),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r ->
+                r.assertFailureWithErrorThatMatches(
+                    containsString("cannot implement sealed interface")),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(Iface1.class)
+            .setPermittedSubclasses(Iface1.class, Sub1.class, Sub2.class)
+            .transform(),
+        transformer(Iface2.class)
+            .setPermittedSubclasses(Iface2.class, Sub1.class, Sub2.class)
+            .transform());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new SubSub();
+      System.out.println("Success!");
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  interface Iface1 /* permits Sub1, Sub2 */ {}
+
+  @NoUnusedInterfaceRemoval
+  interface Iface2 /* permits Sub1, Sub2 */ {}
+
+  abstract static class Super {}
+
+  static class Sub1 extends Super implements Iface1, Iface2 {}
+
+  static class Sub2 extends Super implements Iface1 {}
+
+  static class SubSub extends Sub1 {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesJdk17CompiledTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesJdk17CompiledTest.java
index 2b7d981..c51bb03 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesJdk17CompiledTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesJdk17CompiledTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.desugar.sealed;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
@@ -18,7 +19,7 @@
 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.Matchers;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,14 +71,17 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz("sealed.Compiler");
-    assertThat(clazz, Matchers.isPresentAndRenamed());
-    if (!parameters.isCfRuntime()) {
-      return;
-    }
+    ClassSubject clazz = inspector.clazz(Sealed.Compiler.typeName());
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sealed.R8Compiler.typeName());
+    ClassSubject sub2 = inspector.clazz(Sealed.D8Compiler.typeName());
+    assertThat(sub1, isPresentAndRenamed());
+    assertThat(sub2, isPresentAndRenamed());
     assertEquals(
-        keepPermittedSubclassesAttribute ? 2 : 0,
-        clazz.getFinalPermittedSubclassAttributes().size());
+        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
+            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
+            : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
   }
 
   @Test
@@ -89,8 +93,8 @@
         .applyIf(
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
-        // Keep the sealed class to ensure the PermittedSubclasses attribute stays live.
         .addKeepPermittedSubclasses(Sealed.Compiler.typeName())
+        .addKeepRules("-keep,allowobfuscation class * extends " + Sealed.Compiler.typeName())
         .addKeepMainRule(Sealed.Main.typeName())
         .compile()
         .inspect(this::inspect)
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesMergeTest.java
new file mode 100644
index 0000000..bcd774a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesMergeTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import 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 SealedClassesMergeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertEquals(
+        parameters.isCfRuntime() ? ImmutableList.of(sub1.asTypeSubject()) : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepClassRulesWithAllowObfuscation(Super.class)
+        .addKeepMainRule(TestClass.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(Sub2.class, Sub1.class)
+                  .assertNoOtherClassesMerged();
+            })
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new Sub1();
+      new Sub2();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub1, Sub2 */ {}
+
+  static class Sub1 extends Super {}
+
+  static class Sub2 extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesShrinkingTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesShrinkingTest.java
new file mode 100644
index 0000000..1893e3b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesShrinkingTest.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import 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 SealedClassesShrinkingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, UsedSub.class, UnusedSub.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(UsedSub.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertEquals(
+        parameters.isCfRuntime() ? ImmutableList.of(sub1.asTypeSubject()) : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepClassRulesWithAllowObfuscation(Super.class, UsedSub.class)
+        .addKeepMainRule(TestClass.class)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, UsedSub.class, UnusedSub.class)
+        .transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new UsedSub();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits UsedSub, UnusedSub */ {}
+
+  static class UsedSub extends Super {}
+
+  static class UnusedSub extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
index 3aa1a38..997e4fb 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.desugar.sealed;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -18,7 +20,7 @@
 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.Matchers;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,12 +37,16 @@
   @Parameter(1)
   public boolean keepPermittedSubclassesAttribute;
 
+  @Parameter(2)
+  public boolean repackage;
+
   static final String EXPECTED = StringUtils.lines("Success!");
 
-  @Parameters(name = "{0}, keepPermittedSubclasses = {1}")
+  @Parameters(name = "{0}, keepPermittedSubclasses = {1}, repackage = {2}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values(),
         BooleanUtils.values());
   }
 
@@ -76,14 +82,22 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(C.class);
-    assertThat(clazz, Matchers.isPresentAndRenamed());
-    if (!parameters.isCfRuntime()) {
-      return;
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    ClassSubject sub2 = inspector.clazz(Sub2.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertThat(sub2, isPresentAndRenamed());
+    if (repackage) {
+      assertEquals(-1, sub1.getFinalName().indexOf('.'));
+    } else {
+      assertTrue(sub1.getFinalName().startsWith(getClass().getPackage().getName()));
     }
     assertEquals(
-        keepPermittedSubclassesAttribute ? 2 : 0,
-        clazz.getFinalPermittedSubclassAttributes().size());
+        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
+            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
+            : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
   }
 
   @Test
@@ -96,8 +110,9 @@
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
         // Keep the sealed class to ensure the PermittedSubclasses attribute stays live.
-        .addKeepPermittedSubclasses(C.class)
+        .addKeepPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
         .addKeepMainRule(TestClass.class)
+        .applyIf(repackage, b -> b.addKeepRules("-repackageclasses"))
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
@@ -108,7 +123,9 @@
   }
 
   public byte[] getTransformedClasses() throws Exception {
-    return transformer(C.class).setPermittedSubclasses(C.class, Sub1.class, Sub2.class).transform();
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
   }
 
   static class TestClass {
@@ -120,9 +137,9 @@
     }
   }
 
-  abstract static class C /* permits Sub1, Sub2 */ {}
+  public abstract static class Super /* permits Sub1, Sub2 */ {}
 
-  static class Sub1 extends C {}
+  public static class Sub1 extends Super {}
 
-  static class Sub2 extends C {}
+  public static class Sub2 extends Super {}
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTestAllowPermittedSubclassesRemovalTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTestAllowPermittedSubclassesRemovalTest.java
new file mode 100644
index 0000000..c220e9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTestAllowPermittedSubclassesRemovalTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.sealed;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+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.Matchers;
+import com.google.common.collect.ImmutableList;
+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 SealedClassesTestAllowPermittedSubclassesRemovalTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  static final String EXPECTED = StringUtils.lines("Success!");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class)
+        .addProgramClassFileData(getTransformedClasses());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Super.class);
+    assertThat(clazz, Matchers.isPresentAndNotRenamed());
+    ClassSubject sub1 = inspector.clazz(Sub1.class);
+    ClassSubject sub2 = inspector.clazz(Sub2.class);
+    assertThat(sub1, isPresentAndRenamed());
+    assertThat(sub2, isPresentAndRenamed());
+    assertEquals(
+        parameters.isCfRuntime()
+            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
+            : ImmutableList.of(),
+        clazz.getFinalPermittedSubclassAttributes());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .apply(this::addTestClasses)
+        .setMinApi(parameters)
+        .addKeepAttributePermittedSubclasses()
+        .addKeepRules("-keep,allowpermittedsubclassesremoval class " + Super.class.getTypeName())
+        .addKeepClassRulesWithAllowObfuscation(Sub1.class, Sub2.class)
+        .addKeepMainRule(TestClass.class)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        .transform();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new Sub1();
+      new Sub2();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub1, Sub2 */ {}
+
+  static class Sub1 extends Super {}
+
+  static class Sub2 extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxNull2ArgumentTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxNull2ArgumentTest.java
new file mode 100644
index 0000000..1130986
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxNull2ArgumentTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.enumunboxing;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.Objects;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** This is a regression test for b/287193321. */
+@RunWith(Parameterized.class)
+public class EnumUnboxNull2ArgumentTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(options -> options.testing.disableLir())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("true", "null");
+  }
+
+  public enum MyEnum {
+    FOO("1"),
+    BAR("2");
+
+    final String value;
+
+    MyEnum(String value) {
+      this.value = value;
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      // Delay observing that arguments to bar is null until we've inlined foo() and getEnum().
+      String foo = foo();
+      String[] bar = bar(getEnum(), foo);
+      // To ensure bar(MyEnum,String) is not inlined in the first round we add a few additional
+      // calls that will be stripped during IR-processing of main.
+      if (foo != null) {
+        bar(MyEnum.FOO, foo);
+        bar(MyEnum.BAR, foo);
+      }
+      for (String b : bar) {
+        System.out.println(b);
+      }
+    }
+
+    public static String[] bar(MyEnum myEnum, String foo) {
+      if (System.currentTimeMillis() > 1) {
+        MyEnum e = System.currentTimeMillis() > 1 ? null : MyEnum.FOO;
+        // Ensure that the construction is in a separate block than entry() to have constant
+        // canonicalization align the two null values into one.
+        return new String[] {Objects.toString(Objects.equals(myEnum, e)), foo};
+      }
+      return new String[] {};
+    }
+
+    public static MyEnum getEnum() {
+      return null;
+    }
+
+    public static String foo() {
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/CrossEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/CrossEnumMergingTest.java
new file mode 100644
index 0000000..5c6518b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/CrossEnumMergingTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.enumunboxing.enummerging;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import java.util.List;
+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 CrossEnumMergingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public CrossEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(CrossEnumMergingTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addEnumUnboxingInspector(
+            inspector -> inspector.assertUnboxed(TestEnumReturn.class, TestEnumArg.class))
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("TEST2", "TEST1", "1", "2", "2", "3");
+  }
+
+  public enum TestEnumReturn {
+    TEST1 {
+      @Override
+      public TestEnumReturn other() {
+        return TEST2;
+      }
+    },
+    TEST2 {
+      @Override
+      public TestEnumReturn other() {
+        return TEST1;
+      }
+    };
+
+    public abstract TestEnumReturn other();
+  }
+
+  public enum TestEnumArg {
+    TEST1 {
+      @Override
+      public void print(TestEnumArg arg) {
+        System.out.println(arg.ordinal() + 1);
+      }
+    },
+    TEST2 {
+      @Override
+      public void print(TestEnumArg arg) {
+        System.out.println(arg.ordinal() + 2);
+      }
+    };
+
+    public abstract void print(TestEnumArg arg);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(TestEnumReturn.TEST1.other().toString());
+      System.out.println(TestEnumReturn.TEST2.other().toString());
+
+      TestEnumArg.TEST1.print(TestEnumArg.TEST1);
+      TestEnumArg.TEST1.print(TestEnumArg.TEST2);
+      TestEnumArg.TEST2.print(TestEnumArg.TEST1);
+      TestEnumArg.TEST2.print(TestEnumArg.TEST2);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index 0ff1942..e74dd87 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.kotlin;
 
-
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
@@ -131,6 +131,16 @@
                     .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(o -> o.testing.enableLir())
                     .addOptionsModification(disableClassInliner))
-        .inspect(inspector -> checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName()));
+        .inspect(
+            inspector -> {
+              // This changes depending on when we dead-code eliminate.
+              if (kotlinParameters.is(KotlinCompilerVersion.KOTLINC_1_5_0)
+                  || kotlinParameters.is(KotlinCompilerVersion.KOTLINC_1_6_0)
+                  || testParameters.isDexRuntime()) {
+                checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName());
+              } else {
+                checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
+              }
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java
new file mode 100644
index 0000000..af6954b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromPermittedSubclassesAttributeTest.java
@@ -0,0 +1,152 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.missingclasses;
+
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilationIf;
+import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.diagnostic.DefinitionContext;
+import com.android.tools.r8.diagnostic.internal.DefinitionClassContextImpl;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+public class MissingClassReferencedFromPermittedSubclassesAttributeTest
+    extends MissingClassesTestBase {
+
+  private static final DefinitionContext referencedFrom =
+      DefinitionClassContextImpl.builder()
+          .setClassContext(Reference.classFromClass(Super.class))
+          .setOrigin(getOrigin(Super.class))
+          .build();
+
+  @Parameters(name = "{1}, report: {0}")
+  public static List<Object[]> refinedData() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  private final boolean reportMissingClassesInPermittedSubclassesAttributes;
+
+  public MissingClassReferencedFromPermittedSubclassesAttributeTest(
+      boolean reportMissingClassesInPermittedSubclassesAttributes, TestParameters parameters) {
+    super(parameters);
+    this.reportMissingClassesInPermittedSubclassesAttributes =
+        reportMissingClassesInPermittedSubclassesAttributes;
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // Missing classes stays in the PermittedSubclasses attribute.
+    assertEquals(
+        parameters.isCfRuntime()
+            ? ImmutableList.of(
+                inspector.clazz(Sub.class).asTypeSubject(),
+                inspector.getTypeSubject(MissingSub.class.getTypeName()))
+            : ImmutableList.of(),
+        inspector.clazz(Super.class).getFinalPermittedSubclassAttributes());
+  }
+
+  @Test()
+  public void testNoRules() throws Exception {
+    assertFailsCompilationIf(
+        reportMissingClassesInPermittedSubclassesAttributes,
+        () ->
+            compileWithExpectedDiagnostics(
+                Main.class,
+                reportMissingClassesInPermittedSubclassesAttributes
+                    ? diagnostics -> inspectDiagnosticsWithNoRules(diagnostics, referencedFrom)
+                    : TestDiagnosticMessages::assertNoMessages,
+                this::configure));
+  }
+
+  @Test
+  public void testDontWarnSuperClass() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            TestDiagnosticMessages::assertNoMessages,
+            addDontWarn(Super.class).andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  @Test
+  public void testDontWarnMissingClass() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            TestDiagnosticMessages::assertNoMessages,
+            addDontWarn(MissingSub.class).andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  @Test
+  public void testIgnoreWarnings() throws Exception {
+    compileWithExpectedDiagnostics(
+            Main.class,
+            reportMissingClassesInPermittedSubclassesAttributes
+                ? diagnostics -> inspectDiagnosticsWithIgnoreWarnings(diagnostics, referencedFrom)
+                : TestDiagnosticMessages::assertNoMessages,
+            addIgnoreWarnings(reportMissingClassesInPermittedSubclassesAttributes)
+                .andThen(this::configure))
+        .inspect(this::inspect);
+  }
+
+  void configure(R8FullTestBuilder builder) {
+    try {
+      builder
+          .addKeepAttributePermittedSubclasses()
+          .addProgramClasses(Sub.class)
+          .addProgramClassFileData(getTransformedClasses())
+          .addKeepClassRulesWithAllowObfuscation(Super.class, Sub.class)
+          .addOptionsModification(
+              options -> {
+                // We do not report missing classes from permitted subclasses attributes by default.
+                assertFalse(options.reportMissingClassesInPermittedSubclassesAttributes);
+                options.reportMissingClassesInPermittedSubclassesAttributes =
+                    reportMissingClassesInPermittedSubclassesAttributes;
+              })
+          .applyIf(
+              !reportMissingClassesInPermittedSubclassesAttributes,
+              // The -dontwarn Main and -dontwarn MissingClass tests will have unused -dontwarn
+              // rules.
+              R8TestBuilder::allowUnusedDontWarnPatterns);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  ClassReference getMissingClassReference() {
+    return Reference.classFromClass(MissingSub.class);
+  }
+
+  public byte[] getTransformedClasses() throws Exception {
+    return transformer(Super.class)
+        .setPermittedSubclasses(Super.class, Sub.class, MissingSub.class)
+        .transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new Sub();
+      System.out.println("Success!");
+    }
+  }
+
+  abstract static class Super /* permits Sub, MissingSub */ {}
+
+  static class Sub extends Super {}
+
+  static class MissingSub extends Super {}
+}
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
index 5cc5fe1..91c8722 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassesTestBase.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder.DiagnosticsConsumer;
 import com.android.tools.r8.TestDiagnosticMessages;
@@ -75,27 +76,27 @@
     compileWithExpectedDiagnostics(mainClass, diagnosticsConsumer, null);
   }
 
-  public void compileWithExpectedDiagnostics(
+  public R8TestCompileResult compileWithExpectedDiagnostics(
       Class<?> mainClass,
       DiagnosticsConsumer diagnosticsConsumer,
       ThrowableConsumer<R8FullTestBuilder> configuration)
       throws CompilationFailedException {
-    internalCompileWithExpectedDiagnostics(
+    return internalCompileWithExpectedDiagnostics(
         diagnosticsConsumer,
         builder ->
             builder.addProgramClasses(mainClass).addKeepMainRule(mainClass).apply(configuration));
   }
 
-  public void compileWithExpectedDiagnostics(
+  public R8TestCompileResult compileWithExpectedDiagnostics(
       ThrowableConsumer<R8FullTestBuilder> configuration, DiagnosticsConsumer diagnosticsConsumer)
       throws CompilationFailedException {
-    internalCompileWithExpectedDiagnostics(diagnosticsConsumer, configuration);
+    return internalCompileWithExpectedDiagnostics(diagnosticsConsumer, configuration);
   }
 
-  private void internalCompileWithExpectedDiagnostics(
+  private R8TestCompileResult internalCompileWithExpectedDiagnostics(
       DiagnosticsConsumer diagnosticsConsumer, ThrowableConsumer<R8FullTestBuilder> configuration)
       throws CompilationFailedException {
-    testForR8(parameters.getBackend())
+    return testForR8(parameters.getBackend())
         .apply(configuration)
         .setMinApi(parameters)
         .compileWithExpectedDiagnostics(diagnosticsConsumer);
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
index 7f536a5..016d4ac 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
@@ -56,7 +56,6 @@
 
   @Test
   public void testSourceFileAndLineNumberTable() throws Exception {
-    assumeTrue("b/288405478", mode.isRelease());
     runTest(
         ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -68,7 +67,6 @@
 
   @Test
   public void testLineNumberTableOnly() throws Exception {
-    assumeTrue("b/288405478", mode.isRelease());
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
     runTest(
@@ -81,7 +79,6 @@
 
   @Test
   public void testNoLineNumberTable() throws Exception {
-    assumeTrue("b/288405478", mode.isRelease());
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
     runTest(
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
index 6d03704..2577312 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -45,7 +45,18 @@
   private static final String RECORD_NAME = "SimpleRecord";
   private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
   private static final String EXPECTED_RESULT =
-      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "true",
+          "true",
+          "false",
+          "false",
+          "false",
+          "false");
 
   private static final ClassReference MAIN_REFERENCE =
       Reference.classFromTypeName(RecordTestUtils.getMainType(RECORD_NAME));
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index b390f85..fb55a41 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -168,7 +168,10 @@
             .enableProguardTestOptions()
             .minification(minify.isMinify())
             .setMinApi(parameters)
-            .addKeepRuleFiles(ListUtils.map(keepRulesFiles, Paths::get))
+            .addKeepRuleFiles(
+                ListUtils.map(
+                    keepRulesFiles,
+                    keepRulesFile -> Paths.get(ToolHelper.getProjectRoot(), keepRulesFile)))
             .addLibraryFiles(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "shakinglib.jar"))
             .addDefaultRuntimeLibrary(parameters)
             .addOptionsModification(
diff --git a/src/test/java/com/android/tools/r8/utils/ReflectiveBuildPathUtils.java b/src/test/java/com/android/tools/r8/utils/ReflectiveBuildPathUtils.java
deleted file mode 100644
index ed991f2..0000000
--- a/src/test/java/com/android/tools/r8/utils/ReflectiveBuildPathUtils.java
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright (c) 2020, 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 com.android.tools.r8.ToolHelper;
-import com.google.common.collect.Iterables;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-public class ReflectiveBuildPathUtils {
-
-  public interface PackageUtils {
-    String getPackageName() throws Exception;
-
-    Path getPackagePath() throws Exception;
-
-    PackageUtils getParentPackageUtils() throws Exception;
-
-    Iterable<PackageUtils> getAllPackageUtils();
-  }
-
-  public interface ClassUtils {
-    String getClassName() throws Exception;
-
-    String getSimpleClassName() throws Exception;
-
-    Path getClassPath() throws Exception;
-  }
-
-  public abstract static class ExamplesRootPackage implements PackageUtils {
-    public abstract Path getPackagePath();
-
-    @Override
-    public String getPackageName() {
-      return "";
-    }
-
-    @Override
-    public PackageUtils getParentPackageUtils() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterable<PackageUtils> getAllPackageUtils() {
-      return instantiateAllPackageUtils(getClass());
-    }
-  }
-
-  public abstract static class ExamplesJava11RootPackage extends ExamplesRootPackage {
-    @Override
-    public Path getPackagePath() {
-      return Paths.get(ToolHelper.EXAMPLES_JAVA11_BUILD_DIR);
-    }
-  }
-
-  public abstract static class ExamplesPackage implements PackageUtils {
-    public List<String> getName() {
-      return Collections.singletonList(getClass().getSimpleName());
-    }
-
-    @Override
-    public PackageUtils getParentPackageUtils() throws Exception {
-      return (PackageUtils) getClass().getDeclaringClass().getConstructor().newInstance();
-    }
-
-    @Override
-    public Path getPackagePath() throws Exception {
-      Path path = getParentPackageUtils().getPackagePath();
-      for (String folder : getName()) {
-        path = path.resolve(folder);
-      }
-      return path;
-    }
-
-    @Override
-    public String getPackageName() throws Exception {
-      return getParentPackageUtils().getPackageName() + String.join(".", getName()) + ".";
-    }
-
-    @Override
-    public Iterable<PackageUtils> getAllPackageUtils() {
-      return instantiateAllPackageUtils(getClass());
-    }
-  }
-
-  public static class ExamplesClass implements PackageUtils, ClassUtils {
-    @Override
-    public PackageUtils getParentPackageUtils() throws Exception {
-      return (PackageUtils) getClass().getDeclaringClass().getConstructor().newInstance();
-    }
-
-    public String getSimpleClassName() throws Exception {
-      Object parent = getClass().getDeclaringClass().getConstructor().newInstance();
-      if (parent instanceof ClassUtils) {
-        return ((ClassUtils) parent).getSimpleClassName() + "$" + getClass().getSimpleName();
-      } else {
-        return getClass().getSimpleName();
-      }
-    }
-
-    @Override
-    public String getClassName() throws Exception {
-      return getParentPackageUtils().getPackageName() + getSimpleClassName();
-    }
-
-    @Override
-    public Path getClassPath() throws Exception {
-      return getParentPackageUtils().getPackagePath().resolve(getSimpleClassName() + ".class");
-    }
-
-    @Override
-    public String getPackageName() throws Exception {
-      return getParentPackageUtils().getPackageName();
-    }
-
-    @Override
-    public Path getPackagePath() throws Exception {
-      return getParentPackageUtils().getPackagePath();
-    }
-
-    @Override
-    public Iterable<PackageUtils> getAllPackageUtils() {
-      return instantiateAllPackageUtils(getClass());
-    }
-  }
-
-  public static Iterable<PackageUtils> instantiateAllPackageUtils(Class<?> parentClazz) {
-    Collection<PackageUtils> children = instantiatePackageUtils(parentClazz);
-    return Iterables.concat(
-        children,
-        Iterables.concat(Iterables.transform(children, PackageUtils::getAllPackageUtils)));
-  }
-
-  public static Collection<PackageUtils> instantiatePackageUtils(Class<?> parentClazz) {
-    Collection<PackageUtils> packageUtils = new ArrayList<>();
-    for (Class<?> clazz : parentClazz.getDeclaredClasses()) {
-      try {
-        Object obj = clazz.getConstructor().newInstance();
-        if (obj instanceof PackageUtils) {
-          packageUtils.add((PackageUtils) obj);
-        }
-      } catch (Exception ex) {
-      }
-    }
-    return packageUtils;
-  }
-
-  public static String resolveClassName(Class<? extends ExamplesClass> clazz) throws Exception {
-    return clazz.getConstructor().newInstance().getClassName();
-  }
-
-  public static Collection<Path> allClassFiles(Class<? extends ExamplesRootPackage> clazz)
-      throws Exception {
-    Collection<Path> classFiles = new ArrayList<>();
-    for (PackageUtils util : clazz.getConstructor().newInstance().getAllPackageUtils()) {
-      if (util instanceof ClassUtils) {
-        classFiles.add(((ClassUtils) util).getClassPath());
-      }
-    }
-    return classFiles;
-  }
-}
diff --git a/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1 b/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
index 2133707..94616ba 100644
--- a/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
+++ b/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
@@ -1 +1 @@
-9d322829ca871ff117d24fb8d7cb30001552a3ed
\ No newline at end of file
+13ccbabcc23194ef50c8c3331bf4ef7cee364b75
\ No newline at end of file
diff --git a/third_party/kotlin/kotlin-compiler-1.3.11.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-1.3.11.tar.gz.sha1
deleted file mode 100644
index 05e8466..0000000
--- a/third_party/kotlin/kotlin-compiler-1.3.11.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-b3ce5c6ba25ebbbbf62a49be56aad845eabae860
\ No newline at end of file
diff --git a/third_party/kotlin/kotlin-compiler-1.3.41.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-1.3.41.tar.gz.sha1
deleted file mode 100644
index e9de3b3..0000000
--- a/third_party/kotlin/kotlin-compiler-1.3.41.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-8d2ddeeaaf4366a419627a31ef3276a6f96afe40
\ No newline at end of file
diff --git a/third_party/kotlin/kotlin-compiler-1.5.0-M2.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-1.5.0-M2.tar.gz.sha1
deleted file mode 100644
index e0b251e..0000000
--- a/third_party/kotlin/kotlin-compiler-1.5.0-M2.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-7368d9ee73d01fc609d7ded1bf0506f72aa3ac66
\ No newline at end of file
diff --git a/tools/__init__.py b/tools/__init__.py
new file mode 100644
index 0000000..bb02b91
--- /dev/null
+++ b/tools/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
diff --git a/tools/check-cherry-picks.py b/tools/check-cherry-picks.py
index adf763c..3d66d3c 100755
--- a/tools/check-cherry-picks.py
+++ b/tools/check-cherry-picks.py
@@ -139,7 +139,7 @@
           is_cherry_pick = True
           # If the change is in the release mappings check for holes.
           if missing_from:
-            found_errors = change_error(
+            found_errors |= change_error(
                 change,
                 'Error: missing Change-Id %s on branch %s. '
                 'Is present on %s and again on %s.' % (
@@ -150,7 +150,7 @@
           # The change is not in the non-dev part of the branch, so we need to
           # check that the fork from main included the change.
           if not is_commit_in(commit_on_main, newer_branch):
-            found_errors = change_error(
+            found_errors |= change_error(
                 change,
                 'Error: missing Change-Id %s on branch %s. '
                 'Is present on %s and on main as commit %s.' % (