Merge commit 'd94b8b458cf8f712c9f4598dfd6e174847fb4c91' into dev-release
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 661516d..5c3f1b9 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -77,6 +77,42 @@
       }
     }
     builders {
+      name: "cached"
+      swarming_host: "chrome-swarming.appspot.com"
+      swarming_tags: "vpython:native-python-wrapper"
+      dimensions: "cores:8"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Ubuntu-20.04"
+      dimensions: "pool:luci.r8.ci"
+      exe {
+        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "builder_group": "internal.client.r8",'
+        '  "recipe": "rex",'
+        '  "test_options": ['
+        '    "--runtimes=dex-default",'
+        '    "--command_cache_dir=/tmp/ccache",'
+        '    "--tool=r8",'
+        '    "--no_internal",'
+        '    "--one_line_per_test",'
+        '    "--archive_failures"'
+        '  ]'
+        '}'
+      priority: 25
+      execution_timeout_secs: 21600
+      expiration_secs: 126000
+      build_numbers: YES
+      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+    }
+    builders {
       name: "check"
       swarming_host: "chrome-swarming.appspot.com"
       swarming_tags: "vpython:native-python-wrapper"
diff --git a/infra/config/global/generated/luci-milo.cfg b/infra/config/global/generated/luci-milo.cfg
index 8914541..b0f2546 100644
--- a/infra/config/global/generated/luci-milo.cfg
+++ b/infra/config/global/generated/luci-milo.cfg
@@ -21,6 +21,11 @@
     short_name: "check"
   }
   builders {
+    name: "buildbucket/luci.r8.ci/cached"
+    category: "R8"
+    short_name: "cached"
+  }
+  builders {
     name: "buildbucket/luci.r8.ci/linux-dex_default"
     category: "R8"
     short_name: "dex_default"
diff --git a/infra/config/global/generated/luci-notify.cfg b/infra/config/global/generated/luci-notify.cfg
index 521dee4..048dc7a 100644
--- a/infra/config/global/generated/luci-notify.cfg
+++ b/infra/config/global/generated/luci-notify.cfg
@@ -36,6 +36,18 @@
   }
   builders {
     bucket: "ci"
+    name: "cached"
+    repository: "https://r8.googlesource.com/r8"
+  }
+}
+notifiers {
+  notifications {
+    on_failure: true
+    on_new_failure: true
+    notify_blamelist {}
+  }
+  builders {
+    bucket: "ci"
     name: "check"
     repository: "https://r8.googlesource.com/r8"
   }
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index f990fdb..03691dd3 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -35,6 +35,21 @@
   }
 }
 job {
+  id: "cached"
+  realm: "ci"
+  acl_sets: "ci"
+  triggering_policy {
+    kind: GREEDY_BATCHING
+    max_concurrent_invocations: 1
+    max_batch_size: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "cached"
+  }
+}
+job {
   id: "check"
   realm: "ci"
   acl_sets: "ci"
@@ -812,6 +827,7 @@
   realm: "ci"
   acl_sets: "ci"
   triggers: "archive"
+  triggers: "cached"
   triggers: "check"
   triggers: "desugared_library-head"
   triggers: "desugared_library-jdk11_head"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index c2ce866..358def3 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -301,6 +301,27 @@
   expiration_timeout = time.hour * 35,
 )
 
+r8_builder(
+  "cached",
+  category = "R8",
+  dimensions = get_dimensions(),
+  triggering_policy = scheduler.policy(
+      kind = scheduler.GREEDY_BATCHING_KIND,
+      max_batch_size = 1,
+      max_concurrent_invocations = 1
+  ),
+  priority = 25,
+  properties = {
+      "test_options" : ["--runtimes=dex-default",
+      "--command_cache_dir=/tmp/ccache"] + common_test_options,
+      "builder_group" : "internal.client.r8"
+  },
+  execution_timeout = time.hour * 6,
+  expiration_timeout = time.hour * 35,
+)
+
+
+
 r8_tester_with_default("linux-dex_default", ["--runtimes=dex-default"])
 r8_tester_with_default("linux-none", ["--runtimes=none"])
 r8_tester_with_default("linux-jdk8", ["--runtimes=jdk8"])
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java
new file mode 100644
index 0000000..634611f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java
@@ -0,0 +1,12 @@
+// 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;
+
+@Keep
+public interface AndroidResourceConsumer {
+
+  void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler);
+
+  default void finished(DiagnosticsHandler handler) {}
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceInput.java b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
new file mode 100644
index 0000000..87ed346
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
@@ -0,0 +1,36 @@
+// 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;
+
+import java.io.InputStream;
+
+/**
+ * Base interface for android resources (resource table, manifest, res folder files)
+ *
+ * <p>Android resources are provided to R8 to allow for combined code and resource shrinking. This
+ * allows us to reason about resources, and transitively other resources and code throughout the
+ * entire compilation pipeline.
+ */
+@Keep
+public interface AndroidResourceInput extends Resource {
+  enum Kind {
+    // The AndroidManifest.xml file in proto format.
+    MANIFEST,
+    // The resource table, in proto format.
+    RESOURCE_TABLE,
+    // An xml file within the res folder, in proto format if not inside res/raw, otherwise
+    // in UTF-8 format.
+    XML_FILE,
+    // Any other binary file withing the res folder.
+    RES_FOLDER_FILE
+  }
+
+  // The path, within the app, of the resource.
+  ResourcePath getPath();
+
+  Kind getKind();
+
+  InputStream getByteStream() throws ResourceException;
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceOutput.java b/src/main/java/com/android/tools/r8/AndroidResourceOutput.java
new file mode 100644
index 0000000..6b204b5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceOutput.java
@@ -0,0 +1,20 @@
+// 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;
+
+/**
+ * Base interface for android resources (resource table, manifest, res folder files)
+ *
+ * <p>Android resources are provided to R8 to allow for combined code and resource shrinking. This
+ * allows us to reason about resources, and transitively other resources and code throughout the
+ * entire compilation pipeline.
+ */
+@Keep
+public interface AndroidResourceOutput extends Resource {
+  // The path, within the app, of the resource.
+  ResourcePath getPath();
+
+  ByteDataView getByteDataView();
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceProvider.java b/src/main/java/com/android/tools/r8/AndroidResourceProvider.java
new file mode 100644
index 0000000..cb44ff0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceProvider.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import java.util.Collection;
+
+/** Android resource provider. */
+@Keep
+public interface AndroidResourceProvider {
+
+  // Provide all android resources
+  Collection<AndroidResourceInput> getAndroidResources() throws ResourceException;
+
+  default void finished(DiagnosticsHandler handler) {}
+}
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
new file mode 100644
index 0000000..c601e80
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
@@ -0,0 +1,30 @@
+// 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;
+
+import com.android.tools.r8.utils.ArchiveBuilder;
+import com.android.tools.r8.utils.OutputBuilder;
+import java.nio.file.Path;
+
+public class ArchiveProtoAndroidResourceConsumer implements AndroidResourceConsumer {
+  private final OutputBuilder outputBuilder;
+
+  public ArchiveProtoAndroidResourceConsumer(Path outputPath) {
+    this.outputBuilder = new ArchiveBuilder(outputPath);
+    this.outputBuilder.open();
+  }
+
+  @Override
+  public void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler) {
+    outputBuilder.addFile(
+        androidResource.getPath().location(),
+        androidResource.getByteDataView(),
+        diagnosticsHandler);
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    outputBuilder.close(handler);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
new file mode 100644
index 0000000..ecf9970
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
@@ -0,0 +1,123 @@
+// 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;
+
+import com.android.tools.r8.AndroidResourceInput.Kind;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Provides android resources from an archive.
+ *
+ * <p>The descriptor index is built eagerly upon creating the provider and subsequent requests for
+ * resources in the descriptor set will then force the read of zip entry contents.
+ */
+@Keep
+public class ArchiveProtoAndroidResourceProvider implements AndroidResourceProvider {
+  private final Path archive;
+  private final Origin origin;
+
+  private static final String MANIFEST_NAME = "AndroidManifest.xml";
+  private static final String RESOURCE_TABLE = "resources.pb";
+  private static final String RES_FOLDER = "res/";
+  private static final String XML_SUFFIX = ".xml";
+
+  /**
+   * Creates an android resource provider from an archive.
+   *
+   * @param archive Zip archive to provide resources from.
+   * @param origin Origin of the archive.
+   */
+  public ArchiveProtoAndroidResourceProvider(Path archive, Origin origin) {
+    this.archive = archive;
+    this.origin = origin;
+  }
+
+  @Override
+  public Collection<AndroidResourceInput> getAndroidResources() throws ResourceException {
+    try (ZipFile zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
+      List<AndroidResourceInput> resources = new ArrayList<>();
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        String name = entry.getName();
+        ByteAndroidResourceInput resource =
+            new ByteAndroidResourceInput(
+                name,
+                getKindFromName(name),
+                ByteStreams.toByteArray(zipFile.getInputStream(entry)),
+                new ArchiveEntryOrigin(name, origin));
+        resources.add(resource);
+      }
+      return resources;
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
+    }
+  }
+
+  private Kind getKindFromName(String name) {
+    if (name.equals(MANIFEST_NAME)) {
+      return Kind.MANIFEST;
+    }
+    if (name.equals(RESOURCE_TABLE)) {
+      return Kind.RESOURCE_TABLE;
+    }
+    if (!name.startsWith(RES_FOLDER)) {
+      throw new CompilationError("Unexpected non resource entry " + name, origin);
+    }
+    if (name.endsWith(XML_SUFFIX)) {
+      return Kind.XML_FILE;
+    }
+    return Kind.RES_FOLDER_FILE;
+  }
+
+  private static class ByteAndroidResourceInput implements AndroidResourceInput {
+
+    private final String name;
+    private final Kind kind;
+    private final byte[] bytes;
+    private final Origin origin;
+
+    public ByteAndroidResourceInput(String name, Kind kind, byte[] bytes, Origin origin) {
+      this.name = name;
+      this.kind = kind;
+      this.bytes = bytes;
+      this.origin = origin;
+    }
+
+    @Override
+    public ResourcePath getPath() {
+      return () -> name;
+    }
+
+    @Override
+    public Kind getKind() {
+      return kind;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      return new ByteArrayInputStream(bytes);
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 7bd8767..0ce7a4e 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -62,16 +62,17 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
-import com.android.tools.r8.optimize.AccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
+import com.android.tools.r8.optimize.accessmodification.AccessModifier;
 import com.android.tools.r8.optimize.bridgehoisting.BridgeHoisting;
 import com.android.tools.r8.optimize.fields.FieldFinalizer;
 import com.android.tools.r8.optimize.interfaces.analysis.CfOpenClosedInterfacesAnalysis;
 import com.android.tools.r8.optimize.proto.ProtoNormalizer;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
 import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.repackaging.Repackaging;
@@ -102,6 +103,7 @@
 import com.android.tools.r8.synthesis.SyntheticFinalization;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.SelfRetraceTest;
@@ -448,14 +450,6 @@
       // to clear the cache, so that we will recompute the type lattice elements.
       appView.dexItemFactory().clearTypeElementsCache();
 
-      AccessModifier.run(appViewWithLiveness, executorService, timing);
-      if (appView.graphLens().isPublicizerLens()) {
-        // We can now remove redundant bridges. Note that we do not need to update the
-        // invoke-targets here, as the existing invokes will simply dispatch to the now
-        // visible super-method. MemberRebinding, if run, will then dispatch it correctly.
-        new RedundantBridgeRemover(appView.withLiveness()).run(null, executorService, timing);
-      }
-
       // This pass attempts to reduce the number of nests and nest size to allow further passes, and
       // should therefore be run after the publicizer.
       new NestReducer(appViewWithLiveness).run(executorService, timing);
@@ -463,6 +457,10 @@
       new MemberRebindingAnalysis(appViewWithLiveness).run(executorService);
       appViewWithLiveness.appInfo().notifyMemberRebindingFinished(appViewWithLiveness);
 
+      assert ArtProfileCompletenessChecker.verify(appView);
+
+      AccessModifier.run(appViewWithLiveness, executorService, timing);
+
       boolean isKotlinLibraryCompilationWithInlinePassThrough =
           options.enableCfByteCodePassThrough && appView.hasCfByteCodePassThroughMethods();
 
@@ -808,6 +806,12 @@
 
       new DesugaredLibraryKeepRuleGenerator(appView).runIfNecessary(timing);
 
+      if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
+        // Currently this is simply a pass through of all resources.
+        writeAndroidResources(
+            options.androidResourceProvider, options.androidResourceConsumer, appView.reporter());
+      }
+
       // Generate the resulting application resources.
       writeApplication(appView, inputApp, executorService);
 
@@ -826,6 +830,44 @@
     }
   }
 
+  private void writeAndroidResources(
+      AndroidResourceProvider androidResourceProvider,
+      AndroidResourceConsumer androidResourceConsumer,
+      DiagnosticsHandler diagnosticsHandler) {
+    try {
+      for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+        androidResourceConsumer.accept(
+            new AndroidResourceOutput() {
+              @Override
+              public ResourcePath getPath() {
+                return androidResource.getPath();
+              }
+
+              @Override
+              public ByteDataView getByteDataView() {
+                try {
+                  return ByteDataView.of(ByteStreams.toByteArray(androidResource.getByteStream()));
+                } catch (IOException | ResourceException e) {
+                  diagnosticsHandler.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+                }
+                return null;
+              }
+
+              @Override
+              public Origin getOrigin() {
+                return androidResource.getOrigin();
+              }
+            },
+            diagnosticsHandler);
+      }
+    } catch (ResourceException e) {
+      throw new RuntimeException("Cannot write android resources", e);
+    } finally {
+      androidResourceConsumer.finished(diagnosticsHandler);
+      androidResourceProvider.finished(diagnosticsHandler);
+    }
+  }
+
   private static boolean allReferencesAssignedApiLevel(
       AppView<? extends AppInfoWithClassHierarchy> appView) {
     if (!appView.options().apiModelingOptions().isCheckAllApiReferencesAreSet()
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7ec72c1..4e89913 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -135,6 +135,8 @@
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations = false;
     private SemanticVersion fakeCompilerVersion = null;
+    private AndroidResourceProvider androidResourceProvider = null;
+    private AndroidResourceConsumer androidResourceConsumer = null;
 
     private final ProguardConfigurationParserOptions.Builder parserOptionsBuilder =
         ProguardConfigurationParserOptions.builder().readEnvironment();
@@ -506,6 +508,28 @@
       return super.addStartupProfileProviders(startupProfileProviders);
     }
 
+    /**
+     * Exprimental API for supporting android resource shrinking.
+     *
+     * <p>Add an android resource provider, providing the resource table, manifest and res table
+     * entries.
+     */
+    public Builder setAndroidResourceProvider(AndroidResourceProvider provider) {
+      this.androidResourceProvider = provider;
+      return this;
+    }
+
+    /**
+     * Exprimental API for supporting android resource shrinking.
+     *
+     * <p>Add an android resource consumer, consuming the resource table, manifest and res table
+     * entries.
+     */
+    public Builder setAndroidResourceConsumer(AndroidResourceConsumer consumer) {
+      this.androidResourceConsumer = consumer;
+      return this;
+    }
+
     @Override
     void validate() {
       if (isPrintHelp()) {
@@ -659,7 +683,9 @@
               getArtProfilesForRewriting(),
               getStartupProfileProviders(),
               getClassConflictResolver(),
-              getCancelCompilationChecker());
+              getCancelCompilationChecker(),
+              androidResourceProvider,
+              androidResourceConsumer);
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -847,6 +873,8 @@
   private final FeatureSplitConfiguration featureSplitConfiguration;
   private final String synthesizedClassPrefix;
   private final boolean enableMissingLibraryApiModeling;
+  private final AndroidResourceProvider androidResourceProvider;
+  private final AndroidResourceConsumer androidResourceConsumer;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -941,7 +969,9 @@
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
       ClassConflictResolver classConflictResolver,
-      CancelCompilationChecker cancelCompilationChecker) {
+      CancelCompilationChecker cancelCompilationChecker,
+      AndroidResourceProvider androidResourceProvider,
+      AndroidResourceConsumer androidResourceConsumer) {
     super(
         inputApp,
         mode,
@@ -986,6 +1016,8 @@
     this.featureSplitConfiguration = featureSplitConfiguration;
     this.synthesizedClassPrefix = synthesizedClassPrefix;
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
+    this.androidResourceProvider = androidResourceProvider;
+    this.androidResourceConsumer = androidResourceConsumer;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -1010,6 +1042,8 @@
     featureSplitConfiguration = null;
     synthesizedClassPrefix = null;
     enableMissingLibraryApiModeling = false;
+    androidResourceProvider = null;
+    androidResourceConsumer = null;
   }
 
   public DexItemFactory getDexItemFactory() {
@@ -1182,6 +1216,9 @@
 
     internal.cancelCompilationChecker = getCancelCompilationChecker();
 
+    internal.androidResourceProvider = androidResourceProvider;
+    internal.androidResourceConsumer = androidResourceConsumer;
+
     if (!DETERMINISTIC_DEBUGGING) {
       assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
       internal.threadCount = getThreadCount();
diff --git a/src/main/java/com/android/tools/r8/ResourcePath.java b/src/main/java/com/android/tools/r8/ResourcePath.java
new file mode 100644
index 0000000..eab4d7e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ResourcePath.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+@Keep
+public interface ResourcePath {
+
+  // The location within the apk, bundle or resource directory, e.g., res/xml/foo.xml
+  String location();
+}
diff --git a/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java b/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
index dee0f55..d9f6def 100644
--- a/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
+++ b/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexWritableCode;
+import com.android.tools.r8.graph.DexWritableCode.DexWritableCacheKey;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.naming.ClassNameMapper;
@@ -24,7 +25,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class DefaultMixedSectionLayoutStrategy extends MixedSectionLayoutStrategy {
 
@@ -67,9 +70,46 @@
     return getCodeLayoutForClasses(mixedSectionOffsets.getClassesWithData());
   }
 
+  private static class DeduplicatedCodeCounts {
+    private Map<DexWritableCacheKey, Integer> counts;
+    private final AppView<?> appView;
+
+    private DeduplicatedCodeCounts(AppView<?> appView) {
+      this.appView = appView;
+    }
+
+    public void addCode(DexWritableCode dexWritableCode, ProgramMethod method) {
+      assert appView.options().canUseCanonicalizedCodeObjects();
+      if (counts == null) {
+        counts = new HashMap<>();
+      }
+      DexWritableCacheKey cacheKey =
+          dexWritableCode.getCacheLookupKey(method, appView.dexItemFactory());
+      if (!counts.containsKey(cacheKey)) {
+        counts.put(cacheKey, 1);
+      } else {
+        counts.put(cacheKey, counts.get(cacheKey) + 1);
+      }
+    }
+
+    public int getCount(ProgramMethod method) {
+      if (counts == null) {
+        assert !appView.options().canUseCanonicalizedCodeObjects()
+            || method.getDefinition().getDexWritableCodeOrNull() == null;
+        return 1;
+      }
+      DexWritableCode dexWritableCodeOrNull = method.getDefinition().getDexWritableCodeOrNull();
+      DexWritableCacheKey cacheLookupKey =
+          dexWritableCodeOrNull.getCacheLookupKey(method, appView.dexItemFactory());
+      assert counts.containsKey(cacheLookupKey);
+      return counts.get(cacheLookupKey);
+    }
+  }
+
   final Collection<ProgramMethod> getCodeLayoutForClasses(Collection<DexProgramClass> classes) {
-    ProgramMethodMap<String> codeToSignatureMap = ProgramMethodMap.create();
+    ProgramMethodMap<String> codeToDexSortingKeyMap = ProgramMethodMap.create();
     List<ProgramMethod> codesSorted = new ArrayList<>();
+    DeduplicatedCodeCounts codeCounts = new DeduplicatedCodeCounts(appView);
     for (DexProgramClass clazz : classes) {
       clazz.forEachProgramMethodMatching(
           DexEncodedMethod::hasCode,
@@ -77,13 +117,23 @@
             DexWritableCode code = method.getDefinition().getDexWritableCodeOrNull();
             assert code != null || method.getDefinition().shouldNotHaveCode();
             if (code != null) {
+              if (appView.options().canUseCanonicalizedCodeObjects()) {
+                codeCounts.addCode(code, method);
+              }
               codesSorted.add(method);
-              codeToSignatureMap.put(
+              codeToDexSortingKeyMap.put(
                   method, getKeyForDexCodeSorting(method, appView.app().getProguardMap()));
             }
           });
     }
-    codesSorted.sort(Comparator.comparing(codeToSignatureMap::get));
+    Comparator<ProgramMethod> defaultCodeSorting =
+        Comparator.comparing(codeToDexSortingKeyMap::get);
+    if (appView.options().canUseCanonicalizedCodeObjects()) {
+      codesSorted.sort(
+          Comparator.comparingInt(codeCounts::getCount).thenComparing(defaultCodeSorting));
+    } else {
+      codesSorted.sort(defaultCodeSorting);
+    }
     return codesSorted;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index a76bdf5..3332852 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.PositionBuilder;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -148,6 +149,14 @@
 
   public abstract int estimatedDexCodeSizeUpperBoundInBytes();
 
+  public final boolean isLirCode() {
+    return asLirCode() != null;
+  }
+
+  public LirCode<Integer> asLirCode() {
+    return null;
+  }
+
   public CfCode asCfCode() {
     throw new Unreachable(getClass().getCanonicalName() + ".asCfCode()");
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index a45d542..67082ae 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -43,6 +43,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation.AnnotatedKind;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.ValueType;
@@ -1353,6 +1354,17 @@
     return code == null ? null : code.asDexWritableCode();
   }
 
+  public DexEncodedMethod rewrittenWithLens(
+      GraphLens lens, GraphLens appliedLens, DexDefinitionSupplier definitions) {
+    assert this != SENTINEL;
+    DexMethod newMethodReference = lens.getRenamedMethodSignature(getReference(), appliedLens);
+    DexClass newHolder = definitions.definitionFor(newMethodReference.getHolderType());
+    assert newHolder != null;
+    DexEncodedMethod newMethod = newHolder.lookupMethod(newMethodReference);
+    assert newMethod != null;
+    return newMethod;
+  }
+
   public static Builder syntheticBuilder() {
     return new Builder(true);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 41ca6f5..854ba5b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2979,28 +2979,14 @@
     return createProto(returnType, parameters.toArray(DexType.EMPTY_ARRAY));
   }
 
-  public DexProto protoWithDifferentFirstParameter(DexProto proto, DexType firstParameter) {
-    DexType[] parameterTypes = proto.parameters.values.clone();
-    parameterTypes[0] = firstParameter;
-    return createProto(proto.returnType, parameterTypes);
-  }
-
   public DexProto prependHolderToProto(DexMethod method) {
-    return prependTypeToProto(method.holder, method.proto);
+    return method.getProto().prependParameter(method.getHolderType(), this);
   }
 
   public DexProto prependHolderToProtoIf(DexMethod method, boolean condition) {
     return condition ? prependHolderToProto(method) : method.getProto();
   }
 
-  public DexProto prependTypeToProto(DexType extraFirstType, DexProto initialProto) {
-    DexType[] parameterTypes = new DexType[initialProto.parameters.size() + 1];
-    parameterTypes[0] = extraFirstType;
-    System.arraycopy(
-        initialProto.parameters.values, 0, parameterTypes, 1, initialProto.parameters.size());
-    return createProto(initialProto.returnType, parameterTypes);
-  }
-
   public DexProto appendTypeToProto(DexProto initialProto, DexType extraLastType) {
     DexType[] parameterTypes = new DexType[initialProto.parameters.size() + 1];
     System.arraycopy(
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index fe2f81d..1ad1e1b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -325,7 +325,7 @@
 
   public DexMethod withExtraArgumentPrepended(DexType type, DexItemFactory dexItemFactory) {
     return dexItemFactory.createMethod(
-        holder, dexItemFactory.prependTypeToProto(type, proto), name);
+        holder, getProto().prependParameter(type, dexItemFactory), name);
   }
 
   public DexMethod withHolder(DexDefinition definition, DexItemFactory dexItemFactory) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProto.java b/src/main/java/com/android/tools/r8/graph/DexProto.java
index 80ff41d..b85f0da 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProto.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProto.java
@@ -91,6 +91,13 @@
     return parameters.size();
   }
 
+  public DexProto prependParameter(DexType parameter, DexItemFactory dexItemFactory) {
+    DexType[] parameterTypes = new DexType[getParameters().size() + 1];
+    parameterTypes[0] = parameter;
+    System.arraycopy(getParameters().getBacking(), 0, parameterTypes, 1, getParameters().size());
+    return dexItemFactory.createProto(getReturnType(), parameterTypes);
+  }
+
   @Override
   public String toString() {
     return "Proto " + shorty + " " + returnType + " " + parameters;
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
index c1f869c..9987912 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
@@ -201,4 +201,20 @@
     }
     return appView.options().debug || getOrComputeReachabilitySensitive(appView);
   }
+
+  public ProgramMethod rewrittenWithLens(
+      GraphLens lens, GraphLens appliedLens, DexDefinitionSupplier definitions) {
+    DexMethod newMethod = lens.getRenamedMethodSignature(getReference(), appliedLens);
+    if (newMethod == getReference() && !getDefinition().isObsolete()) {
+      assert verifyIsConsistentWithLookup(definitions);
+      return this;
+    }
+    return asProgramMethodOrNull(definitions.definitionFor(newMethod));
+  }
+
+  private boolean verifyIsConsistentWithLookup(DexDefinitionSupplier definitions) {
+    DexClassAndMethod lookupMethod = definitions.definitionFor(getReference());
+    assert getDefinition() == lookupMethod.getDefinition();
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
index 6a72244..a897bd0 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescriptionMethodOptimizationInfoFixer.java
@@ -149,12 +149,12 @@
   }
 
   /**
-   * Function for rewriting the unused arguments on a piece of method optimization info after
-   * prototype changes were made.
+   * Function for rewriting a BitSet that stores a bit per argument on a piece of method
+   * optimization info after prototype changes were made.
    */
   @Override
-  public BitSet fixupUnusedArguments(BitSet unusedArguments) {
-    return fixupArgumentInfo(unusedArguments);
+  public BitSet fixupArguments(BitSet arguments) {
+    return fixupArgumentInfo(arguments);
   }
 
   private BitSet fixupArgumentInfo(BitSet bitSet) {
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
index ca19375..78c6259 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
 import java.util.Collection;
 import java.util.Map;
@@ -16,11 +17,11 @@
 
 public class VerticallyMergedClasses implements MergedClasses {
 
-  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
+  private final BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
   private final BidirectionalManyToOneMap<DexType, DexType> mergedInterfaces;
 
   public VerticallyMergedClasses(
-      BidirectionalManyToOneMap<DexType, DexType> mergedClasses,
+      BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
       BidirectionalManyToOneMap<DexType, DexType> mergedInterfaces) {
     this.mergedClasses = mergedClasses;
     this.mergedInterfaces = mergedInterfaces;
@@ -37,6 +38,10 @@
     mergedClasses.forEachManyToOneMapping(consumer);
   }
 
+  public BidirectionalManyToOneRepresentativeMap<DexType, DexType> getBidirectionalMap() {
+    return mergedClasses;
+  }
+
   public Map<DexType, DexType> getForwardMap() {
     return mergedClasses.getForwardMap();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
index a736fe9..8154252 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
@@ -112,48 +112,38 @@
   }
 
   @Override
-  public DexType getOriginalType(DexType type) {
-    return renamedTypeNames.getRepresentativeKeyOrDefault(type, type);
-  }
-
-  @Override
   public Iterable<DexType> getOriginalTypes(DexType type) {
     Set<DexType> originalTypes = renamedTypeNames.getKeys(type);
     return originalTypes.isEmpty() ? ImmutableList.of(type) : originalTypes;
   }
 
   @Override
-  public DexField getOriginalFieldSignature(DexField field) {
-    return originalFieldSignatures.getOrDefault(field, field);
-  }
-
-  @Override
-  public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
-    if (this == codeLens) {
-      return originalField;
-    }
-    return originalFieldSignatures.inverse().getOrDefault(originalField, originalField);
-  }
-
-  @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    return this != applied
-        ? originalMethodSignatures.inverse().getOrDefault(originalMethod, originalMethod)
-        : originalMethod;
-  }
-
-  @Override
   public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
       DexMethod method, GraphLens codeLens) {
     return GraphLens.getIdentityLens().lookupPrototypeChangesForMethodDefinition(method, codeLens);
   }
 
   @Override
-  public DexType internalDescribeLookupClassType(DexType previous) {
+  public DexType getPreviousClassType(DexType type) {
+    return renamedTypeNames.getRepresentativeKeyOrDefault(type, type);
+  }
+
+  @Override
+  public DexType getNextClassType(DexType previous) {
     return renamedTypeNames.getOrDefault(previous, previous);
   }
 
   @Override
+  public DexField getPreviousFieldSignature(DexField field) {
+    return originalFieldSignatures.getOrDefault(field, field);
+  }
+
+  @Override
+  public DexField getNextFieldSignature(DexField field) {
+    return originalFieldSignatures.inverse().getOrDefault(field, field);
+  }
+
+  @Override
   public DexMethod getPreviousMethodSignature(DexMethod method) {
     if (extraOriginalMethodSignatures.containsKey(method)) {
       return extraOriginalMethodSignatures.get(method);
diff --git a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
index 1635380..0b5348c 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
@@ -20,18 +20,6 @@
   }
 
   @Override
-  public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
-    return this != codeLens ? getPrevious().getRenamedFieldSignature(originalField) : originalField;
-  }
-
-  @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    return this != applied
-        ? getPrevious().getRenamedMethodSignature(originalMethod, applied)
-        : originalMethod;
-  }
-
-  @Override
   public boolean isClearCodeRewritingLens() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
index bf57958..ffab2e0 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
@@ -32,13 +32,8 @@
   // Class lookup APIs.
 
   @Override
-  protected DexType internalDescribeLookupClassType(DexType previous) {
-    return previous;
-  }
-
-  @Override
-  public DexType getOriginalType(DexType type) {
-    return getPrevious().getOriginalType(type);
+  protected DexType getNextClassType(DexType type) {
+    return type;
   }
 
   @Override
@@ -46,6 +41,11 @@
     return getPrevious().getOriginalTypes(type);
   }
 
+  @Override
+  public DexType getPreviousClassType(DexType type) {
+    return type;
+  }
+
   // Field lookup APIs.
 
   @Override
@@ -54,16 +54,13 @@
   }
 
   @Override
-  public DexField getOriginalFieldSignature(DexField field) {
-    return getPrevious().getOriginalFieldSignature(field);
+  public DexField getPreviousFieldSignature(DexField field) {
+    return field;
   }
 
   @Override
-  public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
-    if (this == codeLens) {
-      return originalField;
-    }
-    return getPrevious().getRenamedFieldSignature(originalField);
+  public DexField getNextFieldSignature(DexField field) {
+    return field;
   }
 
   // Method lookup APIs.
@@ -84,14 +81,6 @@
     return method;
   }
 
-  @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens codeLens) {
-    if (this == codeLens) {
-      return originalMethod;
-    }
-    return getNextMethodSignature(getPrevious().getRenamedMethodSignature(originalMethod));
-  }
-
   // Prototype lookup APIs.
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index 5c884ce..7aff661 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -3,14 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph.lens;
 
-import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
 import static com.android.tools.r8.utils.collections.ThrowingSet.isThrowingSet;
+import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -42,13 +40,16 @@
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiFunction;
+import java.util.function.Predicate;
 
 /**
  * A GraphLens implements a virtual view on top of the graph, used to delay global rewrites until
@@ -70,21 +71,11 @@
 
   public abstract static class Builder {
 
-    protected final MutableBidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap =
-        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
     protected final MutableBidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap =
         BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
-    protected final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
 
     protected Builder() {}
 
-    public void map(DexType from, DexType to) {
-      if (from == to) {
-        return;
-      }
-      typeMap.put(from, to);
-    }
-
     public void move(DexMethod from, DexMethod to) {
       if (from == to) {
         return;
@@ -92,13 +83,6 @@
       methodMap.put(from, to);
     }
 
-    public void move(DexField from, DexField to) {
-      if (from == to) {
-        return;
-      }
-      fieldMap.put(from, to);
-    }
-
     public abstract GraphLens build(AppView<?> appView);
   }
 
@@ -112,41 +96,55 @@
     return false;
   }
 
-  public abstract DexType getOriginalType(DexType type);
+  @Deprecated
+  public final DexType getOriginalType(DexType type) {
+    GraphLens appliedLens = getIdentityLens();
+    return getOriginalType(type, appliedLens);
+  }
+
+  public final DexType getOriginalType(DexType type, GraphLens appliedLens) {
+    return getOriginalReference(type, appliedLens, NonIdentityGraphLens::getPreviousClassType);
+  }
 
   public abstract Iterable<DexType> getOriginalTypes(DexType type);
 
-  public abstract DexField getOriginalFieldSignature(DexField field);
-
-  public final DexMember<?, ?> getOriginalMemberSignature(DexMember<?, ?> member) {
-    return member.apply(this::getOriginalFieldSignature, this::getOriginalMethodSignature);
+  @Deprecated
+  public final DexField getOriginalFieldSignature(DexField field) {
+    GraphLens appliedLens = getIdentityLens();
+    return getOriginalFieldSignature(field, appliedLens);
   }
 
+  public final DexField getOriginalFieldSignature(DexField field, GraphLens appliedLens) {
+    return getOriginalReference(
+        field, appliedLens, NonIdentityGraphLens::getPreviousFieldSignature);
+  }
+
+  @Deprecated
   public final DexMethod getOriginalMethodSignature(DexMethod method) {
-    return getOriginalMethodSignature(method, null);
+    GraphLens appliedLens = getIdentityLens();
+    return getOriginalMethodSignature(method, appliedLens);
   }
 
-  public final DexMethod getOriginalMethodSignature(DexMethod method, GraphLens atGraphLens) {
-    GraphLens current = this;
-    DexMethod original = method;
-    while (current.isNonIdentityLens() && current != atGraphLens) {
-      NonIdentityGraphLens nonIdentityLens = current.asNonIdentityLens();
-      original = nonIdentityLens.getPreviousMethodSignature(original);
-      current = nonIdentityLens.getPrevious();
-    }
-    assert atGraphLens == null ? current.isIdentityLens() : (current == atGraphLens);
-    return original;
+  public final DexMethod getOriginalMethodSignature(DexMethod method, GraphLens appliedLens) {
+    return getOriginalReference(
+        method, appliedLens, NonIdentityGraphLens::getPreviousMethodSignature);
   }
 
   public final DexMethod getOriginalMethodSignatureForMapping(DexMethod method) {
+    GraphLens appliedLens = getIdentityLens();
+    return getOriginalReference(
+        method, appliedLens, NonIdentityGraphLens::getPreviousMethodSignatureForMapping);
+  }
+
+  private <T extends DexReference> T getOriginalReference(
+      T reference, GraphLens appliedLens, BiFunction<NonIdentityGraphLens, T, T> previousFn) {
     GraphLens current = this;
-    DexMethod original = method;
-    while (current.isNonIdentityLens()) {
+    T original = reference;
+    while (current.isNonIdentityLens() && current != appliedLens) {
       NonIdentityGraphLens nonIdentityLens = current.asNonIdentityLens();
-      original = nonIdentityLens.getPreviousMethodSignatureForMapping(original);
+      original = previousFn.apply(nonIdentityLens, original);
       current = nonIdentityLens.getPrevious();
     }
-    assert current.isIdentityLens();
     return original;
   }
 
@@ -158,11 +156,16 @@
         method -> getRenamedMethodSignature(method, codeLens));
   }
 
+  @Deprecated
   public final DexField getRenamedFieldSignature(DexField originalField) {
-    return getRenamedFieldSignature(originalField, null);
+    GraphLens appliedLens = getIdentityLens();
+    return getRenamedFieldSignature(originalField, appliedLens);
   }
 
-  public abstract DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens);
+  public final DexField getRenamedFieldSignature(DexField originalField, GraphLens appliedLens) {
+    return getRenamedReference(
+        originalField, appliedLens, NonIdentityGraphLens::getNextFieldSignature);
+  }
 
   public final DexMember<?, ?> getRenamedMemberSignature(
       DexMember<?, ?> originalMember, GraphLens codeLens) {
@@ -171,50 +174,41 @@
         : getRenamedMethodSignature(originalMember.asDexMethod(), codeLens);
   }
 
+  @Deprecated
   public final DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
-    return getRenamedMethodSignature(originalMethod, null);
+    GraphLens appliedLens = getIdentityLens();
+    return getRenamedMethodSignature(originalMethod, appliedLens);
   }
 
-  public abstract DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied);
-
-  public DexEncodedMethod mapDexEncodedMethod(
-      DexEncodedMethod originalEncodedMethod, DexDefinitionSupplier definitions) {
-    return mapDexEncodedMethod(originalEncodedMethod, definitions, null);
+  public final DexMethod getRenamedMethodSignature(DexMethod method, GraphLens appliedLens) {
+    return getRenamedReference(method, appliedLens, NonIdentityGraphLens::getNextMethodSignature);
   }
 
-  public DexEncodedMethod mapDexEncodedMethod(
-      DexEncodedMethod originalEncodedMethod,
-      DexDefinitionSupplier definitions,
-      GraphLens applied) {
-    assert originalEncodedMethod != DexEncodedMethod.SENTINEL;
-    DexMethod newMethod = getRenamedMethodSignature(originalEncodedMethod.getReference(), applied);
-    // Note that:
-    // * Even if `newMethod` is the same as `originalEncodedMethod.method`, we still need to look it
-    //   up, since `originalEncodedMethod` may be obsolete.
-    // * We can't directly use AppInfo#definitionFor(DexMethod) since definitions may not be
-    //   updated either yet.
-    DexClass newHolder = definitions.definitionFor(newMethod.holder);
-    assert newHolder != null;
-    DexEncodedMethod newEncodedMethod = newHolder.lookupMethod(newMethod);
-    assert newEncodedMethod != null;
-    return newEncodedMethod;
+  private <T extends DexReference> T getRenamedReference(
+      T reference, GraphLens appliedLens, BiFunction<NonIdentityGraphLens, T, T> nextFn) {
+    return getRenamedReference(reference, appliedLens, nextFn, alwaysFalse());
   }
 
-  public ProgramMethod mapProgramMethod(
-      ProgramMethod oldMethod, DexDefinitionSupplier definitions) {
-    DexMethod newMethod = getRenamedMethodSignature(oldMethod.getReference());
-    if (newMethod == oldMethod.getReference() && !oldMethod.getDefinition().isObsolete()) {
-      assert verifyIsConsistentWithLookup(oldMethod, definitions);
-      return oldMethod;
+  private <T extends DexReference> T getRenamedReference(
+      T reference,
+      GraphLens appliedLens,
+      BiFunction<NonIdentityGraphLens, T, T> nextFn,
+      Predicate<T> stoppingCriterion) {
+    GraphLens current = this;
+    Deque<NonIdentityGraphLens> lenses = new ArrayDeque<>();
+    while (current.isNonIdentityLens() && current != appliedLens) {
+      NonIdentityGraphLens nonIdentityLens = current.asNonIdentityLens();
+      lenses.addLast(nonIdentityLens);
+      current = nonIdentityLens.getPrevious();
     }
-    return asProgramMethodOrNull(definitions.definitionFor(newMethod));
-  }
-
-  private static boolean verifyIsConsistentWithLookup(
-      ProgramMethod method, DexDefinitionSupplier definitions) {
-    DexClassAndMethod lookupMethod = definitions.definitionFor(method.getReference());
-    assert method.getDefinition() == lookupMethod.getDefinition();
-    return true;
+    while (!lenses.isEmpty()) {
+      NonIdentityGraphLens lens = lenses.removeLast();
+      reference = nextFn.apply(lens, reference);
+      if (stoppingCriterion.test(reference)) {
+        break;
+      }
+    }
+    return reference;
   }
 
   // Predicate indicating if a rewritten reference is a simple renaming, meaning the move from one
@@ -248,17 +242,24 @@
 
   public abstract String lookupPackageName(String pkg);
 
-  public DexType lookupClassType(DexType type) {
-    return lookupClassType(type, getIdentityLens());
+  @Deprecated
+  public final DexType lookupClassType(DexType type) {
+    GraphLens appliedLens = getIdentityLens();
+    return lookupClassType(type, appliedLens);
   }
 
-  public abstract DexType lookupClassType(DexType type, GraphLens applied);
+  public final DexType lookupClassType(DexType type, GraphLens appliedLens) {
+    return getRenamedReference(
+        type, appliedLens, NonIdentityGraphLens::getNextClassType, DexType::isPrimitiveType);
+  }
 
+  @Deprecated
   public DexType lookupType(DexType type) {
-    return lookupType(type, getIdentityLens());
+    GraphLens appliedLens = getIdentityLens();
+    return lookupType(type, appliedLens);
   }
 
-  public abstract DexType lookupType(DexType type, GraphLens applied);
+  public abstract DexType lookupType(DexType type, GraphLens appliedLens);
 
   public final MethodLookupResult lookupInvokeDirect(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.DIRECT);
diff --git a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
index 02c6a27..e72f128 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
@@ -37,31 +37,11 @@
   }
 
   @Override
-  public DexType getOriginalType(DexType type) {
-    return type;
-  }
-
-  @Override
   public Iterable<DexType> getOriginalTypes(DexType type) {
     return IterableUtils.singleton(type);
   }
 
   @Override
-  public DexField getOriginalFieldSignature(DexField field) {
-    return field;
-  }
-
-  @Override
-  public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
-    return originalField;
-  }
-
-  @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    return originalMethod;
-  }
-
-  @Override
   public String lookupPackageName(String pkg) {
     return pkg;
   }
@@ -72,12 +52,6 @@
   }
 
   @Override
-  public DexType lookupClassType(DexType type, GraphLens applied) {
-    assert type.isClassType();
-    return type;
-  }
-
-  @Override
   public MethodLookupResult lookupMethod(
       DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
     assert codeLens == null || codeLens.isIdentityLens();
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
index 694425b..93c76a0 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
@@ -16,14 +16,13 @@
 import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
-import java.util.Collections;
 import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
- * GraphLens implementation with a parent lens using a simple mapping for type, method and field
- * mapping.
+ * GraphLens implementation with a parent lens where the mapping of types, methods and fields can be
+ * one-to-one, one-to-many, or many-to-one.
  *
  * <p>Subclasses can override the lookup methods.
  *
@@ -37,11 +36,12 @@
       new EmptyBidirectionalOneToOneMap<>();
   protected static final EmptyBidirectionalOneToOneMap<DexMethod, DexMethod> EMPTY_METHOD_MAP =
       new EmptyBidirectionalOneToOneMap<>();
-  protected static final Map<DexType, DexType> EMPTY_TYPE_MAP = Collections.emptyMap();
+  protected static final EmptyBidirectionalOneToOneMap<DexType, DexType> EMPTY_TYPE_MAP =
+      new EmptyBidirectionalOneToOneMap<>();
 
   protected final BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap;
   protected final Function<DexMethod, DexMethod> methodMap;
-  protected final Map<DexType, DexType> typeMap;
+  protected final BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap;
 
   // Map that stores the new signature of methods that have been affected by class merging, unused
   // argument removal, repackaging, synthetic finalization, etc. This is needed to generate a
@@ -81,7 +81,7 @@
       AppView<?> appView,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap,
-      Map<DexType, DexType> typeMap) {
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap) {
     this(appView, fieldMap, methodMap.getForwardMap(), typeMap, methodMap);
   }
 
@@ -89,7 +89,7 @@
       AppView<?> appView,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
-      Map<DexType, DexType> typeMap,
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap,
       BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
     this(appView, fieldMap, methodMap::get, typeMap, newMethodSignatures);
     assert !typeMap.isEmpty()
@@ -102,7 +102,7 @@
       AppView<?> appView,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       Function<DexMethod, DexMethod> methodMap,
-      Map<DexType, DexType> typeMap,
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap,
       BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
     super(appView);
     this.fieldMap = fieldMap;
@@ -111,17 +111,18 @@
     this.newMethodSignatures = newMethodSignatures;
   }
 
-  protected DexType internalGetOriginalType(DexType previous) {
-    return previous;
-  }
-
-  protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
-    return IterableUtils.singleton(internalGetOriginalType(previous));
+  @Override
+  public DexType getPreviousClassType(DexType type) {
+    return typeMap.getRepresentativeKeyOrDefault(type, type);
   }
 
   @Override
-  public DexType getOriginalType(DexType type) {
-    return getPrevious().getOriginalType(internalGetOriginalType(type));
+  protected DexType getNextClassType(DexType type) {
+    return typeMap.getRepresentativeValueOrDefault(type, type);
+  }
+
+  protected Iterable<DexType> internalGetOriginalTypes(DexType type) {
+    return IterableUtils.singleton(getPreviousClassType(type));
   }
 
   @Override
@@ -130,35 +131,6 @@
   }
 
   @Override
-  public DexField getOriginalFieldSignature(DexField field) {
-    DexField originalField = fieldMap.getRepresentativeKeyOrDefault(field, field);
-    return getPrevious().getOriginalFieldSignature(originalField);
-  }
-
-  @Override
-  public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
-    if (this == codeLens) {
-      return originalField;
-    }
-    DexField renamedField = getPrevious().getRenamedFieldSignature(originalField, codeLens);
-    return internalGetNextFieldSignature(renamedField);
-  }
-
-  @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    if (this == applied) {
-      return originalMethod;
-    }
-    DexMethod renamedMethod = getPrevious().getRenamedMethodSignature(originalMethod, applied);
-    return getNextMethodSignature(renamedMethod);
-  }
-
-  @Override
-  protected DexType internalDescribeLookupClassType(DexType previous) {
-    return typeMap.getOrDefault(previous, previous);
-  }
-
-  @Override
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
     if (previous.hasReboundReference()) {
       // Rewrite the rebound reference and then "fixup" the non-rebound reference.
@@ -167,14 +139,12 @@
           previous.getReference() == previous.getReboundReference()
               ? rewrittenReboundReference
               : rewrittenReboundReference.withHolder(
-                  internalDescribeLookupClassType(previous.getReference().getHolderType()),
-                  dexItemFactory());
+                  getNextClassType(previous.getReference().getHolderType()), dexItemFactory());
       return FieldLookupResult.builder(this)
           .setReboundReference(rewrittenReboundReference)
           .setReference(rewrittenNonReboundReference)
-          .setReadCastType(previous.getRewrittenReadCastType(this::internalDescribeLookupClassType))
-          .setWriteCastType(
-              previous.getRewrittenWriteCastType(this::internalDescribeLookupClassType))
+          .setReadCastType(previous.getRewrittenReadCastType(this::getNextClassType))
+          .setWriteCastType(previous.getRewrittenWriteCastType(this::getNextClassType))
           .build();
     } else {
       // TODO(b/168282032): We should always have the rebound reference, so this should become
@@ -182,9 +152,8 @@
       DexField rewrittenReference = previous.getRewrittenReference(fieldMap);
       return FieldLookupResult.builder(this)
           .setReference(rewrittenReference)
-          .setReadCastType(previous.getRewrittenReadCastType(this::internalDescribeLookupClassType))
-          .setWriteCastType(
-              previous.getRewrittenWriteCastType(this::internalDescribeLookupClassType))
+          .setReadCastType(previous.getRewrittenReadCastType(this::getNextClassType))
+          .setWriteCastType(previous.getRewrittenWriteCastType(this::getNextClassType))
           .build();
     }
   }
@@ -201,8 +170,7 @@
               ? rewrittenReboundReference
               : // This assumes that the holder will always be moved in lock-step with the method!
               rewrittenReboundReference.withHolder(
-                  internalDescribeLookupClassType(previous.getReference().getHolderType()),
-                  dexItemFactory());
+                  getNextClassType(previous.getReference().getHolderType()), dexItemFactory());
       return MethodLookupResult.builder(this)
           .setReference(rewrittenReference)
           .setReboundReference(rewrittenReboundReference)
@@ -253,7 +221,13 @@
     return prototypeChanges;
   }
 
-  protected DexField internalGetNextFieldSignature(DexField field) {
+  @Override
+  public DexField getPreviousFieldSignature(DexField field) {
+    return fieldMap.getRepresentativeKeyOrDefault(field, field);
+  }
+
+  @Override
+  public DexField getNextFieldSignature(DexField field) {
     return fieldMap.getOrDefault(field, field);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
index ccbacc3..7680134 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
@@ -98,33 +98,22 @@
   }
 
   @Override
-  public final DexType lookupType(DexType type, GraphLens applied) {
-    if (this == applied) {
-      return type;
-    }
-    if (type.isPrimitiveType() || type.isVoidType() || type.isNullValueType()) {
-      return type;
+  public final DexType lookupType(DexType type, GraphLens appliedLens) {
+    if (type.isClassType()) {
+      return lookupClassType(type, appliedLens);
     }
     if (type.isArrayType()) {
       DexType result = arrayTypeCache.get(type);
       if (result == null) {
         DexType baseType = type.toBaseType(dexItemFactory);
-        DexType newType = lookupType(baseType);
+        DexType newType = lookupType(baseType, appliedLens);
         result = baseType == newType ? type : type.replaceBaseType(newType, dexItemFactory);
         arrayTypeCache.put(type, result);
       }
       return result;
     }
-    return lookupClassType(type);
-  }
-
-  @Override
-  public final DexType lookupClassType(DexType type, GraphLens applied) {
-    assert type.isClassType() : "Expected class type, but was `" + type.toSourceString() + "`";
-    if (this == applied) {
-      return type;
-    }
-    return internalDescribeLookupClassType(getPrevious().lookupClassType(type));
+    assert type.isNullValueType() || type.isPrimitiveType() || type.isVoidType();
+    return type;
   }
 
   @Override
@@ -169,10 +158,14 @@
   protected abstract MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context, GraphLens codeLens);
 
-  protected abstract DexType internalDescribeLookupClassType(DexType previous);
+  protected abstract DexType getNextClassType(DexType type);
+
+  public abstract DexField getPreviousFieldSignature(DexField field);
 
   public abstract DexMethod getPreviousMethodSignature(DexMethod method);
 
+  public abstract DexType getPreviousClassType(DexType type);
+
   /***
    * The previous mapping for a method often coincides with the previous method signature, but it
    * may not, for example for bridges inserted in vertically merged classes where the original
@@ -182,6 +175,8 @@
     return getPreviousMethodSignature(method);
   }
 
+  public abstract DexField getNextFieldSignature(DexField field);
+
   public abstract DexMethod getNextMethodSignature(DexMethod method);
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index cd2586e..b8f049d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -42,7 +42,7 @@
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
-    super(appView, fieldMap, methodMap, mergedClasses.getForwardMap(), newMethodSignatures);
+    super(appView, fieldMap, methodMap, mergedClasses.getBidirectionalMap(), newMethodSignatures);
     this.methodExtraParameters = methodExtraParameters;
     this.mergedClasses = mergedClasses;
   }
@@ -53,6 +53,11 @@
   }
 
   @Override
+  public DexType getPreviousClassType(DexType type) {
+    return type;
+  }
+
+  @Override
   public boolean isHorizontalClassMergerGraphLens() {
     return true;
   }
@@ -94,7 +99,7 @@
             lookup.getReference().getType() != previous.getReference().getType()
                 ? lookupType(previous.getReference().getType())
                 : null)
-        .setWriteCastType(previous.getRewrittenWriteCastType(this::internalDescribeLookupClassType))
+        .setWriteCastType(previous.getRewrittenWriteCastType(this::getNextClassType))
         .build();
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index 9caf1a9..11313e4 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -8,19 +8,20 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
 public class HorizontallyMergedClasses implements MergedClasses {
 
-  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
+  private final BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
 
-  public HorizontallyMergedClasses(BidirectionalManyToOneMap<DexType, DexType> mergedClasses) {
+  public HorizontallyMergedClasses(
+      BidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses) {
     this.mergedClasses = mergedClasses;
   }
 
@@ -89,6 +90,10 @@
     return this.hasBeenMergedIntoDifferentType(type) || isMergeTarget(type);
   }
 
+  BidirectionalManyToOneRepresentativeMap<DexType, DexType> getBidirectionalMap() {
+    return mergedClasses;
+  }
+
   Map<DexType, DexType> getForwardMap() {
     return mergedClasses.getForwardMap();
   }
@@ -106,8 +111,8 @@
 
   public static class Builder {
 
-    private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
-        BidirectionalManyToOneHashMap.newIdentityHashMap();
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
+        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
 
     void add(DexType source, DexType target) {
       assert !mergedClasses.containsKey(source);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index 09e9911..b4d26cd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -280,6 +280,9 @@
       // Cleanup.
       code.removeBlocks(blocksToRemove);
       code.removeAllDeadAndTrivialPhis();
+      code.removeRedundantBlocks();
+
+      assert code.isConsistentSSA(appView);
 
       return code;
     }
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 e6a6d31..ec226d9 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
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.constant;
 
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -16,6 +17,8 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.StringSwitch;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.BitSet;
@@ -31,9 +34,8 @@
  * "Constant Propagation with Conditional Branches".
  * https://www.cs.utexas.edu/users/lin/cs380c/wegman.pdf
  */
-public class SparseConditionalConstantPropagation {
+public class SparseConditionalConstantPropagation extends CodeRewriterPass<AppInfo> {
 
-  private final AppView<?> appView;
   private final IRCode code;
   private final Map<Value, LatticeElement> mapping = new HashMap<>();
   // TODO(b/270398965): Replace LinkedList.
@@ -43,19 +45,29 @@
   @SuppressWarnings("JdkObsolete")
   private final Deque<BasicBlock> flowEdges = new LinkedList<>();
 
-  private final int maxBlockNumber;
   private final BitSet[] executableFlowEdges;
   private final BitSet visitedBlocks;
 
   public SparseConditionalConstantPropagation(AppView<?> appView, IRCode code) {
-    this.appView = appView;
+    super(appView);
     this.code = code;
-    maxBlockNumber = code.getCurrentBlockNumber() + 1;
+    int maxBlockNumber = code.getCurrentBlockNumber() + 1;
     executableFlowEdges = new BitSet[maxBlockNumber];
     visitedBlocks = new BitSet(maxBlockNumber);
   }
 
-  public void run() {
+  @Override
+  protected String getTimingId() {
+    return "SparseConditionalConstantPropagation";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code) {
+    return true;
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     BasicBlock firstBlock = code.entryBlock();
     visitInstructions(firstBlock);
 
@@ -82,11 +94,12 @@
         }
       }
     }
-    rewriteCode();
+    rewriteConstants();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 
-  private void rewriteCode() {
+  private void rewriteConstants() {
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     List<BasicBlock> blockToAnalyze = new ArrayList<>();
     mapping.entrySet().stream()
@@ -258,7 +271,7 @@
   private void setExecutableEdge(int from, int to) {
     BitSet previousExecutable = executableFlowEdges[to];
     if (previousExecutable == null) {
-      previousExecutable = new BitSet(maxBlockNumber);
+      previousExecutable = new BitSet(executableFlowEdges.length);
       executableFlowEdges[to] = previousExecutable;
     }
     previousExecutable.set(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..ca741c9 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
@@ -36,8 +36,14 @@
     this.fieldBitAccessAnalysis =
         options.enableFieldBitAccessAnalysis ? new FieldBitAccessAnalysis() : null;
     this.fieldAssignmentTracker = new FieldAssignmentTracker(appView);
-    this.fieldReadForInvokeReceiverAnalysis = new FieldReadForInvokeReceiverAnalysis(appView);
-    this.fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
+    if (options.testing.canUseLir(appView)) {
+      // When using LIR the bytecode metadata is computed later during finalization via IR.
+      this.fieldReadForInvokeReceiverAnalysis = null;
+      this.fieldReadForWriteAnalysis = null;
+    } else {
+      this.fieldReadForInvokeReceiverAnalysis = new FieldReadForInvokeReceiverAnalysis(appView);
+      this.fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
+    }
   }
 
   @VisibleForTesting
@@ -111,4 +117,32 @@
       }
     }
   }
+
+  public static BytecodeMetadataProvider computeBytecodeMetadata(
+      IRCode irCode, AppView<AppInfoWithLiveness> appView) {
+    // This rebuilding of metadata should only be used in the LIR pipeline where the info is
+    // discarded when translating from IR to LIR.
+    assert appView.options().testing.canUseLir(appView);
+    BytecodeMetadataProvider bytecodeMetadataProvider = BytecodeMetadataProvider.empty();
+    if (irCode.metadata().mayHaveFieldInstruction()) {
+      BytecodeMetadataProvider.Builder builder = BytecodeMetadataProvider.builder();
+      FieldReadForInvokeReceiverAnalysis fieldReadForInvokeReceiverAnalysis =
+          new FieldReadForInvokeReceiverAnalysis(appView);
+      FieldReadForWriteAnalysis fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
+      for (Instruction instruction : irCode.instructions()) {
+        if (instruction.isFieldInstruction()) {
+          FieldInstruction fieldInstruction = instruction.asFieldInstruction();
+          ProgramField field =
+              appView.appInfo().resolveField(fieldInstruction.getField()).getProgramField();
+          if (field != null) {
+            fieldReadForInvokeReceiverAnalysis.recordFieldAccess(
+                fieldInstruction, field, builder, irCode.context());
+            fieldReadForWriteAnalysis.recordFieldAccess(fieldInstruction, field, builder);
+          }
+        }
+      }
+      bytecodeMetadataProvider = builder.build();
+    }
+    return bytecodeMetadataProvider;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
index 976020c6..6289b5d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
@@ -23,7 +23,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  FieldReadForInvokeReceiverAnalysis(AppView<AppInfoWithLiveness> appView) {
+  public FieldReadForInvokeReceiverAnalysis(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForWriteAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForWriteAnalysis.java
index 6a5c420..2155676 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForWriteAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForWriteAnalysis.java
@@ -21,7 +21,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  FieldReadForWriteAnalysis(AppView<AppInfoWithLiveness> appView) {
+  public FieldReadForWriteAnalysis(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
   }
 
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 2a2813f..8941176 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
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.conversion.passes.CommonSubexpressionElimination;
 import com.android.tools.r8.ir.conversion.passes.DexConstantOptimizer;
 import com.android.tools.r8.ir.conversion.passes.KnownArrayLengthRewriter;
+import com.android.tools.r8.ir.conversion.passes.MoveResultRewriter;
 import com.android.tools.r8.ir.conversion.passes.NaturalIntLoopRemover;
 import com.android.tools.r8.ir.conversion.passes.ParentConstructorHoistingCodeRewriter;
 import com.android.tools.r8.ir.conversion.passes.SplitBranch;
@@ -182,7 +183,7 @@
     this.classInitializerDefaultsOptimization =
         new ClassInitializerDefaultsOptimization(appView, this);
     this.stringOptimizer = new StringOptimizer(appView);
-    this.deadCodeRemover = new DeadCodeRemover(appView, codeRewriter);
+    this.deadCodeRemover = new DeadCodeRemover(appView);
     this.assertionsRewriter = new AssertionsRewriter(appView);
     this.idempotentFunctionCallCanonicalizer = new IdempotentFunctionCallCanonicalizer(appView);
     this.neverMerge =
@@ -383,7 +384,7 @@
   private void processAndFinalizeSimpleSynthesizedMethod(ProgramMethod method) {
     IRCode code = method.buildIR(appView);
     assert code != null;
-    codeRewriter.rewriteMoveResult(code);
+    new MoveResultRewriter(appView).run(code, Timing.empty());
     removeDeadCodeAndFinalizeIR(code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
   }
 
@@ -615,12 +616,14 @@
 
     assertionsRewriter.run(method, code, deadCodeRemover, timing);
     CheckNotNullConverter.runIfNecessary(appView, code);
+    previous = printMethod(code, "IR after disable assertions (SSA)", previous);
 
     if (serviceLoaderRewriter != null) {
       assert appView.appInfo().hasLiveness();
       timing.begin("Rewrite service loaders");
       serviceLoaderRewriter.rewrite(code, methodProcessor, methodProcessingContext);
       timing.end();
+      previous = printMethod(code, "IR after service rewriting (SSA)", previous);
     }
 
     if (identifierNameStringMarker != null) {
@@ -628,12 +631,14 @@
       identifierNameStringMarker.decoupleIdentifierNameStringsInMethod(code);
       timing.end();
       assert code.isConsistentSSA(appView);
+      previous = printMethod(code, "IR after identifier-name strings (SSA)", previous);
     }
 
     if (memberValuePropagation != null) {
       timing.begin("Propagate member values");
       memberValuePropagation.run(code);
       timing.end();
+      previous = printMethod(code, "IR after member-value propagation (SSA)", previous);
     }
 
     if (enumValueOptimizer != null) {
@@ -641,16 +646,16 @@
       timing.begin("Remove switch maps");
       enumValueOptimizer.removeSwitchMaps(code);
       timing.end();
+      previous = printMethod(code, "IR after enum-value optimization (SSA)", previous);
     }
 
     if (instanceInitializerOutliner != null) {
       instanceInitializerOutliner.rewriteInstanceInitializers(
           code, context, methodProcessor, methodProcessingContext);
       assert code.verifyTypes(appView);
+      previous = printMethod(code, "IR after instance initializer outlining (SSA)", previous);
     }
 
-    previous = printMethod(code, "IR after disable assertions (SSA)", previous);
-
     // Update the IR code if collected call site optimization info has something useful.
     // While aggregation of parameter information at call sites would be more precise than static
     // types, those could be still less precise at one single call site, where specific arguments
@@ -750,17 +755,11 @@
     }
     commonSubexpressionElimination.run(code, timing);
     new ArrayConstructionSimplifier(appView).run(code, timing);
-    timing.begin("Rewrite move result");
-    codeRewriter.rewriteMoveResult(code);
-    timing.end();
+    new MoveResultRewriter(appView).run(code, timing);
     if (options.enableStringConcatenationOptimization && !isDebugMode) {
-      timing.begin("Rewrite string concat");
-      StringBuilderAppendOptimizer.run(appView, code);
-      timing.end();
+      new StringBuilderAppendOptimizer(appView).run(code, timing);
     }
-    timing.begin("Propagate sparse conditionals");
-    new SparseConditionalConstantPropagation(appView, code).run();
-    timing.end();
+    new SparseConditionalConstantPropagation(appView, code).run(code, timing);
     timing.begin("Rewrite always throwing instructions");
     new ThrowCatchOptimizer(appView).optimizeAlwaysThrowingInstructions(code);
     timing.end();
@@ -930,7 +929,7 @@
       assert code.isConsistentSSA(appView);
 
       // TODO(b/214496607): Remove when dynamic types are safe w.r.t. interface assignment rules.
-      codeRewriter.rewriteMoveResult(code);
+      new MoveResultRewriter(appView).run(code, timing);
     }
 
     // Assert that we do not have unremoved non-sense code in the output, e.g., v <- non-null NULL.
@@ -1049,6 +1048,7 @@
     if (stringSwitchRemover != null) {
       stringSwitchRemover.run(code);
     }
+    code.removeRedundantBlocks();
     deadCodeRemover.run(code, timing);
     finalizeIR(
         code,
@@ -1066,7 +1066,11 @@
     if (options.testing.roundtripThroughLir) {
       code = roundtripThroughLir(code, feedback, bytecodeMetadataProvider, timing);
     }
-    if (options.isGeneratingClassFiles()) {
+    if (options.testing.canUseLir(appView)) {
+      timing.begin("IR->LIR");
+      finalizeToLir(code, feedback, bytecodeMetadataProvider, timing);
+      timing.end();
+    } else if (options.isGeneratingClassFiles()) {
       timing.begin("IR->CF");
       finalizeToCf(code, feedback, bytecodeMetadataProvider, timing);
       timing.end();
@@ -1106,7 +1110,7 @@
       IRCode code, S strategy, String name, Timing timing) {
     timing.begin("IR->LIR (" + name + ")");
     LirCode<EV> lirCode =
-        IR2LirConverter.translate(code, strategy.getEncodingStrategy(), appView.dexItemFactory());
+        IR2LirConverter.translate(code, strategy.getEncodingStrategy(), appView.options());
     timing.end();
     // Check that printing does not fail.
     String lirString = lirCode.toString();
@@ -1114,11 +1118,26 @@
     timing.begin("LIR->IR (" + name + ")");
     IRCode irCode =
         Lir2IRConverter.translate(
-            code.context(), lirCode, strategy.getDecodingStrategy(lirCode), appView);
+            code.context(), lirCode, strategy.getDecodingStrategy(lirCode, null), appView);
     timing.end();
     return irCode;
   }
 
+  private void finalizeToLir(
+      IRCode code,
+      OptimizationFeedback feedback,
+      BytecodeMetadataProvider bytecodeMetadataProvider,
+      Timing timing) {
+    assert deadCodeRemover.verifyNoDeadCode(code);
+    assert BytecodeMetadataProvider.empty() == bytecodeMetadataProvider;
+    LirCode<Integer> lirCode =
+        IR2LirConverter.translate(
+            code, LirStrategy.getDefaultStrategy().getEncodingStrategy(), appView.options());
+    ProgramMethod method = code.context();
+    method.setCode(lirCode, appView);
+    markProcessed(code, feedback);
+  }
+
   private void finalizeToCf(
       IRCode code,
       OptimizationFeedback feedback,
@@ -1256,4 +1275,32 @@
       inliner.onMethodCodePruned(method);
     }
   }
+
+  public void finalizeLirMethodToOutputFormat(ProgramMethod method) {
+    Code code = method.getDefinition().getCode();
+    if (!(code instanceof LirCode)) {
+      return;
+    }
+    Timing onThreadTiming = Timing.empty();
+    IRCode irCode = method.buildIR(appView);
+    BytecodeMetadataProvider bytecodeMetadataProvider =
+        FieldAccessAnalysis.computeBytecodeMetadata(irCode, appView.withLiveness());
+    // During processing optimization info may cause previously live code to become dead.
+    // E.g., we may now have knowledge that an invoke does not have side effects.
+    // Thus, we re-run the dead-code remover now as it is assumed complete by CF/DEX finalization.
+    deadCodeRemover.run(irCode, onThreadTiming);
+    if (options.isGeneratingClassFiles()) {
+      method.setCode(
+          new IRToCfFinalizer(appView, deadCodeRemover)
+              .finalizeCode(irCode, bytecodeMetadataProvider, onThreadTiming),
+          appView);
+    } else {
+      assert options.isGeneratingDex();
+      method.setCode(
+          new IRToDexFinalizer(appView, deadCodeRemover)
+              .finalizeCode(irCode, bytecodeMetadataProvider, onThreadTiming),
+          appView);
+      updateHighestSortingStrings(method.getDefinition());
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
index dbd35f7..8c161b8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
@@ -38,6 +38,13 @@
     }
     DexEncodedMethod method = code.method();
     code.traceBlocks();
+    workaroundBugs(code);
+    // Perform register allocation.
+    RegisterAllocator registerAllocator = performRegisterAllocation(code, method, timing);
+    return new DexBuilder(code, bytecodeMetadataProvider, registerAllocator, options).build();
+  }
+
+  private void workaroundBugs(IRCode code) {
     RuntimeWorkaroundCodeRewriter.workaroundNumberConversionRegisterAllocationBug(code, options);
     // Workaround massive dex2oat memory use for self-recursive methods.
     RuntimeWorkaroundCodeRewriter.workaroundDex2OatInliningIssue(appView, code);
@@ -46,9 +53,7 @@
     RuntimeWorkaroundCodeRewriter.workaroundDex2OatLinkedListBug(code, options);
     RuntimeWorkaroundCodeRewriter.workaroundForwardingInitializerBug(code, options);
     RuntimeWorkaroundCodeRewriter.workaroundExceptionTargetingLoopHeaderBug(code, options);
-    // Perform register allocation.
-    RegisterAllocator registerAllocator = performRegisterAllocation(code, method, timing);
-    return new DexBuilder(code, bytecodeMetadataProvider, registerAllocator, options).build();
+    assert code.isConsistentSSA(appView);
   }
 
   private RegisterAllocator performRegisterAllocation(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index c1a7b55..7327760 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -75,6 +75,9 @@
 
   void classInitializerMayBePostponed(DexEncodedMethod method);
 
+  void setParametersWithBitwiseOperations(
+      ProgramMethod method, BitSet parametersWithBitwiseOperations);
+
   void setUnusedArguments(ProgramMethod method, BitSet unusedArguments);
 
   // Unset methods.
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 f8946d4..1187f29 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.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.io.IOException;
@@ -100,6 +101,7 @@
       lastWaveDone(postMethodProcessorBuilder, executorService);
       eventConsumer.finished(appView);
       assert appView.graphLens() == graphLensForPrimaryOptimizationPass;
+      finalizeLirToOutputFormat(timing, executorService);
       timing.end();
     }
 
@@ -176,6 +178,7 @@
         eventConsumer.finished(appView);
         assert appView.graphLens() == graphLensForSecondaryOptimizationPass;
       }
+      finalizeLirToOutputFormat(timing, executorService);
       timing.end();
     }
 
@@ -207,9 +210,28 @@
 
     // Assure that no more optimization feedback left after post processing.
     assert feedback.noUpdatesLeft();
+    finalizeLirToOutputFormat(timing, executorService);
     return builder.build();
   }
 
+  private void finalizeLirToOutputFormat(Timing timing, ExecutorService executorService)
+      throws ExecutionException {
+    if (!options.testing.canUseLir(appView)) {
+      return;
+    }
+    String output = options.isGeneratingClassFiles() ? "CF" : "DEX";
+    timing.begin("LIR->IR->" + output);
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz -> clazz.forEachProgramMethod(this::finalizeLirMethodToOutputFormat),
+        executorService);
+    appView
+        .getSyntheticItems()
+        .getPendingSyntheticClasses()
+        .forEach(clazz -> clazz.forEachProgramMethod(this::finalizeLirMethodToOutputFormat));
+    timing.end();
+  }
+
   private void clearDexMethodCompilationState() {
     appView.appInfo().classes().forEach(this::clearDexMethodCompilationState);
   }
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 63e6b36..d6c709b 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
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewArrayFilledData;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions;
 import com.android.tools.r8.utils.SetUtils;
@@ -92,12 +93,15 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks);
     while (worklist.hasNext()) {
       BasicBlock block = worklist.next();
       simplifyArrayConstructionBlock(block, worklist, code, appView.options());
     }
+    // Do only when the rewriter pass has changed something.
+    code.removeRedundantBlocks();
+    return CodeRewriterResult.NONE;
   }
 
   @Override
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 24fad2e..f684ff5 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
@@ -27,6 +27,7 @@
 import com.android.tools.r8.ir.code.Ushr;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.Xor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
@@ -248,7 +249,7 @@
   }
 
   @Override
-  public void rewriteCode(IRCode code) {
+  public CodeRewriterResult rewriteCode(IRCode code) {
     InstructionListIterator iterator = code.instructionListIterator();
     while (iterator.hasNext()) {
       Instruction next = iterator.next();
@@ -268,6 +269,7 @@
     code.removeAllDeadAndTrivialPhis();
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 
   private void successiveSimplification(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
index 94e30d3..aba955c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
 
@@ -31,38 +32,43 @@
     return (AppView<? extends T>) appView;
   }
 
-  public final void run(
+  public final CodeRewriterResult run(
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext,
       Timing timing) {
-    timing.time(getTimingId(), () -> run(code, methodProcessor, methodProcessingContext));
+    return timing.time(getTimingId(), () -> run(code, methodProcessor, methodProcessingContext));
   }
 
-  public final void run(IRCode code, Timing timing) {
-    timing.time(getTimingId(), () -> run(code, null, null));
+  public final CodeRewriterResult run(IRCode code, Timing timing) {
+    return timing.time(getTimingId(), () -> run(code, null, null));
   }
 
-  private void run(
+  private CodeRewriterResult run(
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     if (shouldRewriteCode(code)) {
-      rewriteCode(code, methodProcessor, methodProcessingContext);
+      return rewriteCode(code, methodProcessor, methodProcessingContext);
     }
+    return noChange();
+  }
+
+  protected CodeRewriterResult noChange() {
+    return CodeRewriterResult.NO_CHANGE;
   }
 
   protected abstract String getTimingId();
 
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     throw new Unreachable("Should Override or use overload");
   }
 
-  protected void rewriteCode(
+  protected CodeRewriterResult rewriteCode(
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    rewriteCode(code);
+    return rewriteCode(code);
   }
 
   protected abstract boolean shouldRewriteCode(IRCode code);
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 5d88ded..7057abc 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
@@ -39,7 +40,7 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     int noCandidate = code.reserveMarkingColor();
     if (hasCSECandidate(code, noCandidate)) {
       final ListMultimap<Wrapper<Instruction>, Value> instructionToValue =
@@ -79,6 +80,7 @@
     code.returnMarkingColor(noCandidate);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 
   private static class CSEExpressionEquivalence extends Equivalence<Instruction> {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index c6e07ed..bcfd88c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
 import com.android.tools.r8.utils.LazyBox;
 import com.google.common.collect.Iterables;
@@ -63,9 +64,10 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     useDedicatedConstantForLitInstruction(code);
     shortenLiveRanges(code, constantCanonicalizer);
+    return CodeRewriterResult.NONE;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
index 9ae7aab..291f8b9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import java.util.Set;
 
 public class KnownArrayLengthRewriter extends CodeRewriterPass<AppInfo> {
@@ -32,7 +33,7 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     InstructionListIterator iterator = code.instructionListIterator();
     while (iterator.hasNext()) {
       Instruction current = iterator.next();
@@ -78,5 +79,6 @@
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
new file mode 100644
index 0000000..18c3757
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
@@ -0,0 +1,148 @@
+// 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.conversion.passes;
+
+import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AssumeRemover;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.ListIterator;
+import java.util.Set;
+
+public class MoveResultRewriter extends CodeRewriterPass<AppInfo> {
+
+  public MoveResultRewriter(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getTimingId() {
+    return "MoveResultRewriter";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code) {
+    return options.isGeneratingDex() && code.metadata().mayHaveInvokeMethod();
+  }
+
+  // Replace result uses for methods where something is known about what is returned.
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    AssumeRemover assumeRemover = new AssumeRemover(appView, code);
+    boolean changed = false;
+    boolean mayHaveRemovedTrivialPhi = false;
+    Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
+    ListIterator<BasicBlock> blockIterator = code.listIterator();
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      if (blocksToBeRemoved.contains(block)) {
+        continue;
+      }
+
+      InstructionListIterator iterator = block.listIterator(code);
+      while (iterator.hasNext()) {
+        InvokeMethod invoke = iterator.next().asInvokeMethod();
+        if (invoke == null || !invoke.hasOutValue() || invoke.outValue().hasLocalInfo()) {
+          continue;
+        }
+
+        // Check if the invoked method is known to return one of its arguments.
+        DexClassAndMethod target = invoke.lookupSingleTarget(appView, code.context());
+        if (target == null) {
+          continue;
+        }
+
+        MethodOptimizationInfo optimizationInfo = target.getDefinition().getOptimizationInfo();
+        if (!optimizationInfo.returnsArgument()) {
+          continue;
+        }
+
+        int argumentIndex = optimizationInfo.getReturnedArgument();
+        // Replace the out value of the invoke with the argument and ignore the out value.
+        if (argumentIndex < 0 || !checkArgumentType(invoke, argumentIndex)) {
+          continue;
+        }
+
+        Value argument = invoke.arguments().get(argumentIndex);
+        Value outValue = invoke.outValue();
+        assert outValue.verifyCompatible(argument.outType());
+
+        // Make sure that we are only narrowing information here. Note, in cases where we cannot
+        // find the definition of types, computing lessThanOrEqual will return false unless it is
+        // object.
+        if (!argument.getType().lessThanOrEqual(outValue.getType(), appView)) {
+          continue;
+        }
+
+        Set<Value> affectedValues =
+            argument.getType().equals(outValue.getType())
+                ? Collections.emptySet()
+                : outValue.affectedValues();
+
+        assumeRemover.markAssumeDynamicTypeUsersForRemoval(outValue);
+        mayHaveRemovedTrivialPhi |= outValue.numberOfPhiUsers() > 0;
+        outValue.replaceUsers(argument);
+        invoke.setOutValue(null);
+        changed = true;
+
+        if (!affectedValues.isEmpty()) {
+          new TypeAnalysis(appView).narrowing(affectedValues);
+        }
+      }
+    }
+    assumeRemover.removeMarkedInstructions(blocksToBeRemoved).finish();
+    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    if (!blocksToBeRemoved.isEmpty()) {
+      code.removeBlocks(blocksToBeRemoved);
+      code.removeAllDeadAndTrivialPhis(affectedValues);
+      assert code.getUnreachableBlocks().isEmpty();
+    } else if (mayHaveRemovedTrivialPhi || assumeRemover.mayHaveIntroducedTrivialPhi()) {
+      code.removeAllDeadAndTrivialPhis(affectedValues);
+    }
+    if (!affectedValues.isEmpty()) {
+      new TypeAnalysis(appView).narrowing(affectedValues);
+    }
+    if (changed) {
+      code.removeRedundantBlocks();
+    }
+    assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.hasChanged(changed);
+  }
+
+  private boolean checkArgumentType(InvokeMethod invoke, int argumentIndex) {
+    // TODO(sgjesse): Insert cast if required.
+    TypeElement returnType =
+        TypeElement.fromDexType(invoke.getInvokedMethod().proto.returnType, maybeNull(), appView);
+    TypeElement argumentType =
+        TypeElement.fromDexType(getArgumentType(invoke, argumentIndex), maybeNull(), appView);
+    return appView.enableWholeProgramOptimizations()
+        ? argumentType.lessThanOrEqual(returnType, appView)
+        : argumentType.equals(returnType);
+  }
+
+  private DexType getArgumentType(InvokeMethod invoke, int argumentIndex) {
+    if (invoke.isInvokeStatic()) {
+      return invoke.getInvokedMethod().proto.parameters.values[argumentIndex];
+    }
+    if (argumentIndex == 0) {
+      return invoke.getInvokedMethod().holder;
+    }
+    return invoke.getInvokedMethod().proto.parameters.values[argumentIndex - 1];
+  }
+}
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 be7840f..2238b82 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Sub;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.Set;
@@ -40,7 +41,7 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     boolean loopRemoved = false;
     for (BasicBlock comparisonBlockCandidate : code.blocks) {
       if (isComparisonBlock(comparisonBlockCandidate)) {
@@ -52,6 +53,7 @@
       code.removeRedundantBlocks();
       assert code.isConsistentSSA(appView);
     }
+    return CodeRewriterResult.NONE;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
index 4f96ce7..6a7c040 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.IterableUtils;
@@ -48,10 +49,11 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     for (InvokeDirect invoke : getOrComputeSideEffectFreeConstructorCalls(code)) {
       hoistSideEffectFreeConstructorCall(code, invoke);
     }
+    return CodeRewriterResult.NONE;
   }
 
   private void hoistSideEffectFreeConstructorCall(IRCode code, InvokeDirect invoke) {
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 fbb3afd..e4bac3e 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
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
@@ -43,24 +44,22 @@
   }
 
   /**
-   * Simplify Boolean branches for example: <code>
-   * boolean b = i == j; if (b) { ... } else { ... }
+   * Simplify Boolean branches for example: <code> boolean b = i == j; if (b) { ... } else { ... }
    * </code> ends up first creating a branch for the boolean b, then a second branch on b. D8/R8
-   * rewrites to: <code>
-   * if (i == j) { ... } else { ... }
+   * rewrites to: <code> if (i == j) { ... } else { ... }
    * </code> More complex control flow are also supported to some extent, including cases where the
    * input of the second branch comes from a set of dependent phis, and a subset of the inputs are
    * known boolean values.
    */
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     List<BasicBlock> candidates = computeCandidates(code);
     if (candidates.isEmpty()) {
-      return;
+      return CodeRewriterResult.NONE;
     }
     Map<Goto, BasicBlock> newTargets = findGotosToRetarget(candidates);
     if (newTargets.isEmpty()) {
-      return;
+      return CodeRewriterResult.NONE;
     }
     retargetGotos(newTargets);
     Set<Value> affectedValues = Sets.newIdentityHashSet();
@@ -74,6 +73,7 @@
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 
   private void retargetGotos(Map<Goto, BasicBlock> newTargets) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
index cbce46f..77104d5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
@@ -54,7 +55,7 @@
   }
 
   @Override
-  protected void rewriteCode(
+  protected CodeRewriterResult rewriteCode(
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
@@ -124,6 +125,7 @@
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
+    return CodeRewriterResult.NONE;
   }
 
   enum RemoveCheckCastInstructionIfTrivialResult {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
index 4f8ee2e..a08d82a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -33,7 +34,7 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     assert code.isConsistentGraph(appView);
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     // Rewrite all non-fallthrough targets to the end of trivial goto chains and remove
@@ -73,6 +74,7 @@
     }
     assert removedTrivialGotos(code);
     assert code.isConsistentGraph(appView);
+    return CodeRewriterResult.NONE;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/result/CodeRewriterResult.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/result/CodeRewriterResult.java
new file mode 100644
index 0000000..68d5c0d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/result/CodeRewriterResult.java
@@ -0,0 +1,40 @@
+// 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.conversion.passes.result;
+
+import com.android.tools.r8.errors.Unreachable;
+
+public interface CodeRewriterResult {
+
+  CodeRewriterResult NO_CHANGE = new DefaultCodeRewriterResult(false);
+  CodeRewriterResult HAS_CHANGED = new DefaultCodeRewriterResult(true);
+  CodeRewriterResult NONE =
+      new CodeRewriterResult() {
+        @Override
+        public boolean hasChanged() {
+          throw new Unreachable();
+        }
+      };
+
+  static CodeRewriterResult hasChanged(boolean hasChanged) {
+    return hasChanged ? HAS_CHANGED : NO_CHANGE;
+  }
+
+  class DefaultCodeRewriterResult implements CodeRewriterResult {
+
+    private final boolean hasChanged;
+
+    public DefaultCodeRewriterResult(boolean hasChanged) {
+      this.hasChanged = hasChanged;
+    }
+
+    @Override
+    public boolean hasChanged() {
+      return hasChanged;
+    }
+  }
+
+  boolean hasChanged();
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index f715e77..bfcb585 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -1910,8 +1910,8 @@
     }
 
     @Override
-    public DexProto getProto(DexItemFactory itemFactory) {
-      return itemFactory.prependTypeToProto(receiverType, super.getProto(itemFactory));
+    public DexProto getProto(DexItemFactory factory) {
+      return method.getProto().prependParameter(receiverType, factory);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
index 9a75734..5460d4f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
@@ -225,10 +225,7 @@
       ProgramMethod context,
       MethodProcessingContext methodProcessingContext) {
     DexMethod method = invoke.getMethod();
-    DexProto newProto =
-        invoke.isInvokeStatic()
-            ? method.proto
-            : factory.prependTypeToProto(method.getHolderType(), method.getProto());
+    DexProto newProto = factory.prependHolderToProtoIf(method, !invoke.isInvokeStatic());
     DexMethod returnConversion =
         computeReturnConversion(
             method, false, eventConsumer, context, methodProcessingContext::createUniqueContext);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
index 422dc72..7ac6713 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
@@ -429,11 +428,9 @@
 
     public InterfaceProcessorNestedGraphLens(
         AppView<?> appView,
-        BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
         BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap,
-        Map<DexType, DexType> typeMap,
         BidirectionalOneToOneMap<DexMethod, DexMethod> extraNewMethodSignatures) {
-      super(appView, fieldMap, methodMap, typeMap);
+      super(appView, NestedGraphLens.EMPTY_FIELD_MAP, methodMap, NestedGraphLens.EMPTY_TYPE_MAP);
       this.extraNewMethodSignatures = extraNewMethodSignatures;
     }
 
@@ -474,11 +471,10 @@
 
       @Override
       public InterfaceProcessorNestedGraphLens build(AppView<?> appView) {
-        if (fieldMap.isEmpty() && methodMap.isEmpty() && extraNewMethodSignatures.isEmpty()) {
+        if (methodMap.isEmpty() && extraNewMethodSignatures.isEmpty()) {
           return null;
         }
-        return new InterfaceProcessorNestedGraphLens(
-            appView, fieldMap, methodMap, typeMap, extraNewMethodSignatures);
+        return new InterfaceProcessorNestedGraphLens(appView, methodMap, extraNewMethodSignatures);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
index 1653ae2..6646ce0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
@@ -85,6 +85,7 @@
     assert done;
     irConverter.removeDeadCodeAndFinalizeIR(
         irCode, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
+    irConverter.finalizeLirMethodToOutputFormat(programMethod);
   }
 
   public void rewriteRecordFieldArray(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
index e1f8d9b..e32a6f5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
@@ -344,10 +344,11 @@
       DexEncodedMethod method, IRCode code, DeadCodeRemover deadCodeRemover, Timing timing) {
     if (enabled) {
       timing.begin("Rewrite assertions");
-      if (runInternal(method, code)) {
+      boolean needsDeadCodeRemoval = runInternal(method, code);
+      code.removeRedundantBlocks();
+      if (needsDeadCodeRemoval) {
         deadCodeRemover.run(code, timing);
       }
-      code.removeRedundantBlocks();
       assert code.isConsistentSSA(appView);
       timing.end();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index 8d2b4bb..9c94862 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -301,6 +301,9 @@
       fieldsWithStaticValues.forEach(DexEncodedField::setStaticValue);
     }
 
+    if (!fieldsWithStaticValues.isEmpty()) {
+      code.removeRedundantBlocks();
+    }
     return new ClassInitializerDefaultsResult(fieldsWithStaticValues);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 4984711..e696879 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -5,13 +5,11 @@
 package com.android.tools.r8.ir.optimize;
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -35,16 +33,13 @@
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeInterface;
-import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Move;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LazyBox;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -59,7 +54,6 @@
 import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
 import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -68,11 +62,9 @@
 
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
-  private final InternalOptions options;
 
   public CodeRewriter(AppView<?> appView) {
     this.appView = appView;
-    this.options = appView.options();
     this.dexItemFactory = appView.dexItemFactory();
   }
 
@@ -134,112 +126,6 @@
     assert Streams.stream(code.instructions()).noneMatch(Instruction::isAssume);
   }
 
-  private boolean checkArgumentType(InvokeMethod invoke, int argumentIndex) {
-    // TODO(sgjesse): Insert cast if required.
-    TypeElement returnType =
-        TypeElement.fromDexType(invoke.getInvokedMethod().proto.returnType, maybeNull(), appView);
-    TypeElement argumentType =
-        TypeElement.fromDexType(getArgumentType(invoke, argumentIndex), maybeNull(), appView);
-    return appView.enableWholeProgramOptimizations()
-        ? argumentType.lessThanOrEqual(returnType, appView)
-        : argumentType.equals(returnType);
-  }
-
-  private DexType getArgumentType(InvokeMethod invoke, int argumentIndex) {
-    if (invoke.isInvokeStatic()) {
-      return invoke.getInvokedMethod().proto.parameters.values[argumentIndex];
-    }
-    if (argumentIndex == 0) {
-      return invoke.getInvokedMethod().holder;
-    }
-    return invoke.getInvokedMethod().proto.parameters.values[argumentIndex - 1];
-  }
-
-  // Replace result uses for methods where something is known about what is returned.
-  public boolean rewriteMoveResult(IRCode code) {
-    if (options.isGeneratingClassFiles() || !code.metadata().mayHaveInvokeMethod()) {
-      return false;
-    }
-
-    AssumeRemover assumeRemover = new AssumeRemover(appView, code);
-    boolean changed = false;
-    boolean mayHaveRemovedTrivialPhi = false;
-    Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
-    ListIterator<BasicBlock> blockIterator = code.listIterator();
-    while (blockIterator.hasNext()) {
-      BasicBlock block = blockIterator.next();
-      if (blocksToBeRemoved.contains(block)) {
-        continue;
-      }
-
-      InstructionListIterator iterator = block.listIterator(code);
-      while (iterator.hasNext()) {
-        InvokeMethod invoke = iterator.next().asInvokeMethod();
-        if (invoke == null || !invoke.hasOutValue() || invoke.outValue().hasLocalInfo()) {
-          continue;
-        }
-
-        // Check if the invoked method is known to return one of its arguments.
-        DexClassAndMethod target = invoke.lookupSingleTarget(appView, code.context());
-        if (target == null) {
-          continue;
-        }
-
-        MethodOptimizationInfo optimizationInfo = target.getDefinition().getOptimizationInfo();
-        if (!optimizationInfo.returnsArgument()) {
-          continue;
-        }
-
-        int argumentIndex = optimizationInfo.getReturnedArgument();
-        // Replace the out value of the invoke with the argument and ignore the out value.
-        if (argumentIndex < 0 || !checkArgumentType(invoke, argumentIndex)) {
-          continue;
-        }
-
-        Value argument = invoke.arguments().get(argumentIndex);
-        Value outValue = invoke.outValue();
-        assert outValue.verifyCompatible(argument.outType());
-
-        // Make sure that we are only narrowing information here. Note, in cases where we cannot
-        // find the definition of types, computing lessThanOrEqual will return false unless it is
-        // object.
-        if (!argument.getType().lessThanOrEqual(outValue.getType(), appView)) {
-          continue;
-        }
-
-        Set<Value> affectedValues =
-            argument.getType().equals(outValue.getType())
-                ? Collections.emptySet()
-                : outValue.affectedValues();
-
-        assumeRemover.markAssumeDynamicTypeUsersForRemoval(outValue);
-        mayHaveRemovedTrivialPhi |= outValue.numberOfPhiUsers() > 0;
-        outValue.replaceUsers(argument);
-        invoke.setOutValue(null);
-        changed = true;
-
-        if (!affectedValues.isEmpty()) {
-          new TypeAnalysis(appView).narrowing(affectedValues);
-        }
-      }
-    }
-    assumeRemover.removeMarkedInstructions(blocksToBeRemoved).finish();
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    if (!blocksToBeRemoved.isEmpty()) {
-      code.removeBlocks(blocksToBeRemoved);
-      code.removeAllDeadAndTrivialPhis(affectedValues);
-      assert code.getUnreachableBlocks().isEmpty();
-    } else if (mayHaveRemovedTrivialPhi || assumeRemover.mayHaveIntroducedTrivialPhi()) {
-      code.removeAllDeadAndTrivialPhis(affectedValues);
-    }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
-    code.removeRedundantBlocks();
-    assert code.isConsistentSSA(appView);
-    return changed;
-  }
-
   public static void removeOrReplaceByDebugLocalWrite(
       Instruction currentInstruction, InstructionListIterator it, Value inValue, Value outValue) {
     if (outValue.hasLocalInfo() && outValue.getLocalInfo() != inValue.getLocalInfo()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index a207486..e9b180c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueIsDeadAnalysis;
 import com.android.tools.r8.ir.conversion.passes.BranchSimplifier;
+import com.android.tools.r8.ir.conversion.passes.MoveResultRewriter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.IterableUtils;
@@ -35,21 +36,15 @@
 public class DeadCodeRemover {
 
   private final AppView<?> appView;
-  private final CodeRewriter codeRewriter;
 
-  public DeadCodeRemover(AppView<?> appView, CodeRewriter codeRewriter) {
+  public DeadCodeRemover(AppView<?> appView) {
     this.appView = appView;
-    this.codeRewriter = codeRewriter;
-  }
-
-  public CodeRewriter getCodeRewriter() {
-    return codeRewriter;
   }
 
   public void run(IRCode code, Timing timing) {
     timing.begin("Remove dead code");
 
-    codeRewriter.rewriteMoveResult(code);
+    new MoveResultRewriter(appView).run(code, timing);
 
     BranchSimplifier branchSimplifier = new BranchSimplifier(appView);
 
@@ -75,7 +70,7 @@
   }
 
   public boolean verifyNoDeadCode(IRCode code) {
-    assert !codeRewriter.rewriteMoveResult(code);
+    assert !new MoveResultRewriter(appView).run(code, Timing.empty()).hasChanged();
     assert !removeUnneededCatchHandlers(code);
     ValueIsDeadAnalysis valueIsDeadAnalysis = new ValueIsDeadAnalysis(appView, code);
     for (BasicBlock block : code.blocks) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
deleted file mode 100644
index bd0376b..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
+++ /dev/null
@@ -1,276 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.ir.optimize;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexMember;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.SubtypingInfo;
-import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.WorkList;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Deque;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.function.BiFunction;
-import java.util.function.Predicate;
-
-// Per-class collection of member signatures.
-public abstract class MemberPoolCollection<R extends DexMember<?, R>> {
-
-  final Equivalence<R> equivalence;
-  final AppView<AppInfoWithLiveness> appView;
-  final SubtypingInfo subtypingInfo;
-  final Map<DexClass, MemberPool<R>> memberPools = new ConcurrentHashMap<>();
-
-  MemberPoolCollection(
-      AppView<AppInfoWithLiveness> appView,
-      Equivalence<R> equivalence,
-      SubtypingInfo subtypingInfo) {
-    this.appView = appView;
-    this.equivalence = equivalence;
-    this.subtypingInfo = subtypingInfo;
-  }
-
-  public void buildAll(ExecutorService executorService, Timing timing) throws ExecutionException {
-    timing.begin("Building member pool collection");
-    try {
-      List<Future<?>> futures = new ArrayList<>();
-
-      // Generate a future for each class that will build the member pool collection for the
-      // corresponding class. Note that, we visit the classes using a top-down class hierarchy
-      // traversal, since this ensures that we do not visit library classes that are not
-      // reachable from any program class.
-      TopDownClassHierarchyTraversal.forAllClasses(appView)
-          .visit(appView.appInfo().classes(), clazz -> submit(clazz, futures, executorService));
-      ThreadUtils.awaitFutures(futures);
-    } finally {
-      timing.end();
-    }
-  }
-
-  public MemberPool<R> buildForHierarchy(
-      DexClass clazz, ExecutorService executorService, Timing timing) throws ExecutionException {
-    timing.begin("Building member pool collection");
-    try {
-      List<Future<?>> futures = new ArrayList<>();
-      submitAll(
-          getAllSuperTypesInclusive(clazz, memberPools::containsKey), futures, executorService);
-      submitAll(getAllSubTypesExclusive(clazz, memberPools::containsKey), futures, executorService);
-      ThreadUtils.awaitFutures(futures);
-    } finally {
-      timing.end();
-    }
-    return get(clazz);
-  }
-
-  public boolean hasPool(DexClass clazz) {
-    return memberPools.containsKey(clazz);
-  }
-
-  public MemberPool<R> get(DexClass clazz) {
-    assert hasPool(clazz);
-    return memberPools.get(clazz);
-  }
-
-  public boolean markIfNotSeen(DexClass clazz, R reference) {
-    MemberPool<R> memberPool = get(clazz);
-    Wrapper<R> key = equivalence.wrap(reference);
-    if (memberPool.hasSeen(key)) {
-      return true;
-    }
-    memberPool.seen(key);
-    return false;
-  }
-
-  private void submitAll(
-      Iterable<? extends DexClass> classes,
-      List<Future<?>> futures,
-      ExecutorService executorService) {
-    for (DexClass clazz : classes) {
-      submit(clazz, futures, executorService);
-    }
-  }
-
-  private void submit(DexClass clazz, List<Future<?>> futures, ExecutorService executorService) {
-    futures.add(executorService.submit(computeMemberPoolForClass(clazz)));
-  }
-
-  abstract Runnable computeMemberPoolForClass(DexClass clazz);
-
-  private Set<DexClass> getAllSuperTypesInclusive(
-      DexClass subject, Predicate<DexClass> stoppingCriterion) {
-    Set<DexClass> superTypes = new HashSet<>();
-    Deque<DexClass> worklist = new ArrayDeque<>();
-    worklist.add(subject);
-    while (!worklist.isEmpty()) {
-      DexClass clazz = worklist.pop();
-      if (stoppingCriterion.test(clazz)) {
-        continue;
-      }
-      if (superTypes.add(clazz)) {
-        if (clazz.superType != null) {
-          addNonNull(worklist, appView.definitionFor(clazz.superType));
-        }
-        for (DexType interfaceType : clazz.interfaces.values) {
-          addNonNull(worklist, appView.definitionFor(interfaceType));
-        }
-      }
-    }
-    return superTypes;
-  }
-
-  private Set<DexClass> getAllSubTypesExclusive(
-      DexClass subject, Predicate<DexClass> stoppingCriterion) {
-    Set<DexClass> subTypes = new HashSet<>();
-    Deque<DexClass> worklist = new ArrayDeque<>();
-    subtypingInfo.forAllImmediateExtendsSubtypes(
-        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
-    subtypingInfo.forAllImmediateImplementsSubtypes(
-        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
-    while (!worklist.isEmpty()) {
-      DexClass clazz = worklist.pop();
-      if (stoppingCriterion.test(clazz)) {
-        continue;
-      }
-      if (subTypes.add(clazz)) {
-        subtypingInfo.forAllImmediateExtendsSubtypes(
-            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
-        subtypingInfo.forAllImmediateImplementsSubtypes(
-            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
-      }
-    }
-    return subTypes;
-  }
-
-  public static class MemberPool<T> {
-
-    private final DexClass clazz;
-    private final Equivalence<T> equivalence;
-    private MemberPool<T> superType;
-    private final Set<MemberPool<T>> interfaces = new HashSet<>();
-    private final Set<MemberPool<T>> subTypes = new HashSet<>();
-    private final Set<Wrapper<T>> memberPool = new HashSet<>();
-
-    MemberPool(Equivalence<T> equivalence, DexClass clazz) {
-      this.equivalence = equivalence;
-      this.clazz = clazz;
-    }
-
-    synchronized void linkSupertype(MemberPool<T> superType) {
-      assert this.superType == null;
-      this.superType = superType;
-    }
-
-    synchronized void linkSubtype(MemberPool<T> subType) {
-      boolean added = subTypes.add(subType);
-      assert added;
-    }
-
-    synchronized void linkInterface(MemberPool<T> itf) {
-      boolean added = interfaces.add(itf);
-      assert added;
-    }
-
-    public void seen(T member) {
-      seen(equivalence.wrap(member));
-    }
-
-    public synchronized void seen(Wrapper<T> member) {
-      boolean added = memberPool.add(member);
-      assert added;
-    }
-
-    public boolean hasSeen(Wrapper<T> member) {
-      return fold(member, false, true, (t, ignored) -> true);
-    }
-
-    public boolean hasSeenDirectly(Wrapper<T> member) {
-      return here(member, false, (t, ignored) -> true);
-    }
-
-    public boolean hasSeenStrictlyAbove(Wrapper<T> member) {
-      return above(member, false, false, true, (t, ignored) -> true);
-    }
-
-    public boolean hasSeenStrictlyBelow(Wrapper<T> member) {
-      return below(member, false, true, (t, ignored) -> true);
-    }
-
-    private <S> S above(
-        Wrapper<T> member,
-        boolean inclusive,
-        S value,
-        S terminator,
-        BiFunction<DexClass, S, S> accumulator) {
-      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this);
-      while (workList.hasNext()) {
-        MemberPool<T> next = workList.next();
-        if (inclusive) {
-          value = next.here(member, value, accumulator);
-          if (value == terminator) {
-            return value;
-          }
-        }
-        inclusive = true;
-        if (next.superType != null) {
-          workList.addIfNotSeen(next.superType);
-        }
-        workList.addIfNotSeen(next.interfaces);
-      }
-      return value;
-    }
-
-    private <S> S here(Wrapper<T> member, S value, BiFunction<DexClass, S, S> accumulator) {
-      if (memberPool.contains(member)) {
-        return accumulator.apply(clazz, value);
-      }
-      return value;
-    }
-
-    public <S> S below(
-        Wrapper<T> member, S value, S terminator, BiFunction<DexClass, S, S> accumulator) {
-      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this.subTypes);
-      while (workList.hasNext()) {
-        MemberPool<T> next = workList.next();
-        value = next.here(member, value, accumulator);
-        if (value == terminator) {
-          return value;
-        }
-        workList.addIfNotSeen(next.interfaces);
-        workList.addIfNotSeen(next.subTypes);
-      }
-      return value;
-    }
-
-    public <S> S fold(
-        Wrapper<T> member, S initialValue, S terminator, BiFunction<DexClass, S, S> accumulator) {
-      S value = above(member, true, initialValue, terminator, accumulator);
-      if (value == terminator) {
-        return value;
-      }
-      return below(member, initialValue, terminator, accumulator);
-    }
-  }
-
-  private static <T> void addNonNull(Collection<T> collection, T item) {
-    if (item != null) {
-      collection.add(item);
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
deleted file mode 100644
index 78eb343..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.optimize;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.SubtypingInfo;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.google.common.base.Predicates;
-import java.util.function.Predicate;
-
-// Per-class collection of method signatures.
-//
-// Example use cases:
-// *) in publicizer,
-//   to determine if a private method does not collide with methods in that class hierarchy.
-// *) in vertical class merger,
-//   before moving a default interface method to its subtype, check if it does not collide with one
-//   in the given class hierarchy.
-public class MethodPoolCollection extends MemberPoolCollection<DexMethod> {
-
-  private final Predicate<DexEncodedMethod> methodTester;
-
-  public MethodPoolCollection(AppView<AppInfoWithLiveness> appView, SubtypingInfo subtypingInfo) {
-    this(appView, subtypingInfo, Predicates.alwaysTrue());
-  }
-
-  public MethodPoolCollection(
-      AppView<AppInfoWithLiveness> appView,
-      SubtypingInfo subtypingInfo,
-      Predicate<DexEncodedMethod> methodTester) {
-    super(appView, MethodSignatureEquivalence.get(), subtypingInfo);
-    this.methodTester = methodTester;
-  }
-
-  public static boolean excludesPrivateInstanceMethod(DexEncodedMethod method) {
-    return !method.isPrivateMethod() || method.isStatic();
-  }
-
-  @Override
-  Runnable computeMemberPoolForClass(DexClass clazz) {
-    return () -> {
-      MemberPool<DexMethod> methodPool =
-          memberPools.computeIfAbsent(clazz, k -> new MemberPool<>(equivalence, k));
-      clazz.forEachMethod(
-          encodedMethod -> {
-            if (methodTester.test(encodedMethod)) {
-              methodPool.seen(equivalence.wrap(encodedMethod.getReference()));
-            }
-          });
-      if (clazz.superType != null) {
-        DexClass superClazz = appView.definitionFor(clazz.superType);
-        if (superClazz != null) {
-          MemberPool<DexMethod> superPool =
-              memberPools.computeIfAbsent(
-                  superClazz, k -> new MemberPool<>(equivalence, superClazz));
-          superPool.linkSubtype(methodPool);
-          methodPool.linkSupertype(superPool);
-        }
-      }
-      if (clazz.isInterface()) {
-        for (DexType subtype : subtypingInfo.allImmediateSubtypes(clazz.type)) {
-          DexClass subClazz = appView.definitionFor(subtype);
-          if (subClazz != null) {
-            MemberPool<DexMethod> childPool =
-                memberPools.computeIfAbsent(subClazz, k -> new MemberPool<>(equivalence, subClazz));
-            methodPool.linkSubtype(childPool);
-            childPool.linkInterface(methodPool);
-          }
-        }
-      }
-    };
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
index 370a5d7..02e3b25 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
@@ -66,6 +66,7 @@
       code.blocks.add(rethrowBlock);
       // Add catch handler to the block containing the last recursive call.
       newBlock.appendCatchHandler(rethrowBlock, guard);
+      code.removeRedundantBlocks();
     }
   }
 
@@ -77,7 +78,7 @@
   }
 
   private static void rewriteSwitchForMaxIntOnly(IRCode code, AppView<?> appView) {
-    boolean needToSplitCriticalEdges = false;
+    boolean hasChanged = false;
     BranchSimplifier branchSimplifier = new BranchSimplifier(appView);
     ListIterator<BasicBlock> blocksIterator = code.listIterator();
     while (blocksIterator.hasNext()) {
@@ -107,7 +108,7 @@
                   ImmutableList.of(newSwitchSequences),
                   outliers);
             }
-            needToSplitCriticalEdges = true;
+            hasChanged = true;
           }
         }
       }
@@ -116,8 +117,9 @@
     // Rewriting of switches introduces new branching structure. It relies on critical edges
     // being split on the way in but does not maintain this property. We therefore split
     // critical edges at exit.
-    if (needToSplitCriticalEdges) {
+    if (hasChanged) {
       code.splitCriticalEdges();
+      code.removeRedundantBlocks();
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
index 12028fb..cf4a6fe 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
@@ -136,7 +136,7 @@
           break;
 
         case RETURN:
-          // Wil not materialize after class inlining.
+          // Will not materialize after class inlining.
           if (appView.options().isGeneratingClassFiles()) {
             result++;
           } else {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 53f7f63..dd803f9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -27,10 +27,13 @@
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToManyRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToManyRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToManyRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableMap;
@@ -51,7 +54,7 @@
       AppView<?> appView,
       BidirectionalOneToOneMap<DexField, DexField> fieldMap,
       BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> renamedSignatures,
-      Map<DexType, DexType> typeMap,
+      BidirectionalManyToOneRepresentativeMap<DexType, DexType> typeMap,
       Map<DexMethod, DexMethod> methodMap,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod,
       Set<DexMethod> dispatchMethods) {
@@ -232,7 +235,8 @@
 
     private final DexItemFactory dexItemFactory;
     private final AbstractValueFactory abstractValueFactory;
-    private final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> typeMap =
+        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
     private final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
         new BidirectionalOneToOneHashMap<>();
     private final MutableBidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod>
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index c94b03e..46ed560 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 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.info.FieldOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ArrayUtils;
@@ -60,8 +61,9 @@
   }
 
   @Override
-  protected void rewriteCode(IRCode code) {
+  protected CodeRewriterResult rewriteCode(IRCode code) {
     rewriteConstantEnumMethodCalls(code);
+    return CodeRewriterResult.NONE;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index b1e1a69..f596d2a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -148,6 +148,16 @@
   }
 
   @Override
+  public boolean hasParametersWithBitwiseOperations() {
+    return false;
+  }
+
+  @Override
+  public BitSet getParametersWithBitwiseOperations() {
+    return null;
+  }
+
+  @Override
   public BitSet getUnusedArguments() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index 0d42b62..31775ae 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -80,6 +80,10 @@
 
   public abstract SimpleInliningConstraint getSimpleInliningConstraint();
 
+  public abstract boolean hasParametersWithBitwiseOperations();
+
+  public abstract BitSet getParametersWithBitwiseOperations();
+
   public final boolean hasUnusedArguments() {
     assert getUnusedArguments() == null || !getUnusedArguments().isEmpty();
     return getUnusedArguments() != null;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index ba4ba64..892f856 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -101,6 +101,7 @@
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.kotlin.Kotlin.Intrinsics;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
@@ -156,6 +157,7 @@
     computeReturnValueOnlyDependsOnArguments(feedback, definition, code, timing);
     BitSet nonNullParamOrThrow = computeNonNullParamOrThrow(feedback, definition, code, timing);
     computeNonNullParamOnNormalExits(feedback, code, nonNullParamOrThrow, timing);
+    computeParametersWithBitwiseOperations(method, code, feedback, timing);
     computeUnusedArguments(method, code, feedback, timing);
   }
 
@@ -1173,6 +1175,36 @@
     return true;
   }
 
+  private void computeParametersWithBitwiseOperations(
+      ProgramMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
+    timing.begin("Compute parameters with bitwise operations");
+    computeParametersWithBitwiseOperations(method, code, feedback);
+    timing.end();
+  }
+
+  private void computeParametersWithBitwiseOperations(
+      ProgramMethod method, IRCode code, OptimizationFeedback feedback) {
+    BitSet parametersWithBitwiseOperations = new BitSet(method.getParameters().size());
+    InstructionIterator instructionIterator = code.entryBlock().iterator();
+    Argument argument = instructionIterator.next().asArgument();
+    while (argument != null) {
+      if (hasBitwiseOperation(argument)) {
+        int parameterIndex =
+            argument.getIndex() - BooleanUtils.intValue(method.getDefinition().isInstance());
+        parametersWithBitwiseOperations.set(parameterIndex);
+      }
+      argument = instructionIterator.next().asArgument();
+    }
+    if (!parametersWithBitwiseOperations.isEmpty()) {
+      feedback.setParametersWithBitwiseOperations(method, parametersWithBitwiseOperations);
+    }
+  }
+
+  private boolean hasBitwiseOperation(Argument argument) {
+    return argument.getOutType().isInt()
+        && argument.outValue().hasUserThatMatches(Instruction::isAnd);
+  }
+
   private void computeUnusedArguments(
       ProgramMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
     timing.begin("Compute unused arguments");
@@ -1191,6 +1223,8 @@
       }
       argument = instructionIterator.next().asArgument();
     }
-    feedback.setUnusedArguments(method, unusedArguments);
+    if (!unusedArguments.isEmpty()) {
+      feedback.setUnusedArguments(method, unusedArguments);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
index 0044499..4279b3c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoFixer.java
@@ -43,5 +43,5 @@
       SimpleInliningConstraint constraint,
       SimpleInliningConstraintFactory factory);
 
-  public abstract BitSet fixupUnusedArguments(BitSet unusedArguments);
+  public abstract BitSet fixupArguments(BitSet arguments);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 7dcb035..555bd6f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -79,6 +79,7 @@
       NeverSimpleInliningConstraint.getInstance();
 
   private int maxRemovedAndroidLogLevel = MaximumRemovedAndroidLogLevelRule.NOT_SET;
+  private BitSet parametersWithBitwiseOperations = null;
   private BitSet unusedArguments = null;
 
   // To reduce the memory footprint of UpdatableMethodOptimizationInfo, all the boolean fields are
@@ -159,6 +160,7 @@
         .fixupNonNullParamOnNormalExits(fixer)
         .fixupNonNullParamOrThrow(fixer)
         .fixupReturnedArgumentIndex(fixer)
+        .fixupParametersWithBitwiseOperations(fixer)
         .fixupSimpleInliningConstraint(appView, fixer)
         .fixupUnusedArguments(fixer);
   }
@@ -435,13 +437,41 @@
   }
 
   @Override
+  public boolean hasParametersWithBitwiseOperations() {
+    return parametersWithBitwiseOperations != null;
+  }
+
+  @Override
+  public BitSet getParametersWithBitwiseOperations() {
+    return parametersWithBitwiseOperations;
+  }
+
+  public void setParametersWithBitwiseOperations(BitSet parametersWithBitwiseOperations) {
+    if (parametersWithBitwiseOperations != null && !parametersWithBitwiseOperations.isEmpty()) {
+      this.parametersWithBitwiseOperations = parametersWithBitwiseOperations;
+    } else {
+      this.parametersWithBitwiseOperations = null;
+    }
+  }
+
+  public MutableMethodOptimizationInfo fixupParametersWithBitwiseOperations(
+      MethodOptimizationInfoFixer fixer) {
+    return fixupParametersWithBitwiseOperations(fixer.fixupArguments(unusedArguments));
+  }
+
+  public MutableMethodOptimizationInfo fixupParametersWithBitwiseOperations(
+      BitSet parametersWithBitwiseOperations) {
+    setParametersWithBitwiseOperations(parametersWithBitwiseOperations);
+    return this;
+  }
+
+  @Override
   public BitSet getUnusedArguments() {
     return unusedArguments;
   }
 
   public MutableMethodOptimizationInfo fixupUnusedArguments(MethodOptimizationInfoFixer fixer) {
-    fixupUnusedArguments(fixer.fixupUnusedArguments(unusedArguments));
-    return this;
+    return fixupUnusedArguments(fixer.fixupArguments(unusedArguments));
   }
 
   public MutableMethodOptimizationInfo fixupUnusedArguments(BitSet unusedArguments) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index 2148519..bbb1cec 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -277,6 +277,13 @@
   }
 
   @Override
+  public void setParametersWithBitwiseOperations(
+      ProgramMethod method, BitSet parametersWithBitwiseOperations) {
+    getMethodOptimizationInfoForUpdating(method)
+        .setParametersWithBitwiseOperations(parametersWithBitwiseOperations);
+  }
+
+  @Override
   public synchronized void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {
     getMethodOptimizationInfoForUpdating(method).setUnusedArguments(unusedArguments);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index 3f46ce1..c3ff56a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -126,6 +126,10 @@
   public void classInitializerMayBePostponed(DexEncodedMethod method) {}
 
   @Override
+  public void setParametersWithBitwiseOperations(
+      ProgramMethod method, BitSet parametersWithBitwiseOperations) {}
+
+  @Override
   public void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {}
 
   // Unset methods.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index a0c9863..51c5bb6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -213,6 +213,15 @@
   }
 
   @Override
+  public void setParametersWithBitwiseOperations(
+      ProgramMethod method, BitSet parametersWithBitwiseOperations) {
+    method
+        .getDefinition()
+        .getMutableOptimizationInfo()
+        .setParametersWithBitwiseOperations(parametersWithBitwiseOperations);
+  }
+
+  @Override
   public void setUnusedArguments(ProgramMethod method, BitSet unusedArguments) {
     method.getDefinition().getMutableOptimizationInfo().setUnusedArguments(unusedArguments);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index 9cd00c7..1cedcc3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -62,6 +62,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
 import com.android.tools.r8.ir.conversion.SourceCode;
+import com.android.tools.r8.ir.conversion.passes.MoveResultRewriter;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
@@ -1368,7 +1369,7 @@
             applyOutliningCandidate(code);
             converter.printMethod(code, "IR after outlining (SSA)", null);
             converter.memberValuePropagation.run(code);
-            converter.codeRewriter.rewriteMoveResult(code);
+            new MoveResultRewriter(appView).run(code, Timing.empty());
             converter.removeDeadCodeAndFinalizeIR(
                 code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
           },
@@ -1399,7 +1400,7 @@
           // optimizations needed for outlining: rewriteMoveResult() to remove out-values on
           // StringBuilder/StringBuffer method invocations, and removeDeadCode() to remove
           // unused out-values.
-          converter.codeRewriter.rewriteMoveResult(code);
+          new MoveResultRewriter(appView).run(code, Timing.empty());
           converter.deadCodeRemover.run(code, Timing.empty());
           CodeRewriter.removeAssumeInstructions(appView, code);
           consumer.accept(code);
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 2798fb0..2ff8ec9 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
@@ -16,6 +16,7 @@
 import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createToStringNode;
 import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
 
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraProceduralDataflowAnalysisOptions;
@@ -30,6 +31,8 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
@@ -75,29 +78,33 @@
  *
  * <p>Finally, based on all optimizations, the IR is updated to reflect the optimizations.
  */
-public class StringBuilderAppendOptimizer {
+public class StringBuilderAppendOptimizer extends CodeRewriterPass<AppInfo> {
 
-  private final AppView<?> appView;
   private final StringBuilderOracle oracle;
-  private final IRCode code;
-
   private static final int NUMBER_OF_MUNCHING_PASSES = 3;
 
-  private StringBuilderAppendOptimizer(AppView<?> appView, IRCode code) {
-    this.appView = appView;
-    this.code = code;
+  public StringBuilderAppendOptimizer(AppView<?> appView) {
+    super(appView);
     oracle = new DefaultStringBuilderOracle(appView.dexItemFactory());
   }
 
-  public static void run(AppView<?> appView, IRCode code) {
-    new StringBuilderAppendOptimizer(appView, code).run();
+  @Override
+  protected String getTimingId() {
+    return "StringBuilderAppendOptimizer";
   }
 
-  private void run() {
-    Map<Value, StringBuilderNode> stringBuilderGraphs = computeStringBuilderGraphs();
-    Map<Instruction, StringBuilderAction> actions = optimizeOnGraphs(stringBuilderGraphs);
+  @Override
+  protected boolean shouldRewriteCode(IRCode code) {
+    return code.metadata().mayHaveNewInstance()
+        || code.metadata().mayHaveInvokeMethodWithReceiver();
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    Map<Value, StringBuilderNode> stringBuilderGraphs = computeStringBuilderGraphs(code);
+    Map<Instruction, StringBuilderAction> actions = optimizeOnGraphs(code, stringBuilderGraphs);
     if (actions.isEmpty()) {
-      return;
+      return CodeRewriterResult.NO_CHANGE;
     }
     InstructionListIterator it = code.instructionListIterator();
     while (it.hasNext()) {
@@ -108,6 +115,7 @@
       }
     }
     code.removeAllDeadAndTrivialPhis();
+    return CodeRewriterResult.HAS_CHANGED;
   }
 
   private static class StringBuilderGraphState {
@@ -137,7 +145,7 @@
    * answer, for a given instruction, is a string builder value escaping and all escaped values
    * at the instruction.
    */
-  private Map<Value, StringBuilderNode> computeStringBuilderGraphs() {
+  private Map<Value, StringBuilderNode> computeStringBuilderGraphs(IRCode code) {
     StringBuilderEscapeTransferFunction transferFunction =
         new StringBuilderEscapeTransferFunction(oracle);
     IntraproceduralDataflowAnalysis<StringBuilderEscapeState> analysis =
@@ -516,7 +524,7 @@
    * the munching and care about performance.
    */
   private Map<Instruction, StringBuilderAction> optimizeOnGraphs(
-      Map<Value, StringBuilderNode> stringBuilderGraphs) {
+      IRCode code, Map<Value, StringBuilderNode> stringBuilderGraphs) {
     Map<Instruction, StringBuilderAction> actions = new IdentityHashMap<>();
     // Build state to allow munching over the string builder graphs.
     Map<StringBuilderNode, NewInstanceNode> newInstances = new IdentityHashMap<>();
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
index ee53ceb..3bc2fe6 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -12,6 +11,7 @@
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
@@ -24,24 +24,22 @@
 
 public class IR2LirConverter<EV> {
 
-  private final DexItemFactory factory;
   private final IRCode irCode;
   private final LirEncodingStrategy<Value, EV> strategy;
   private final LirBuilder<Value, EV> builder;
 
   private IR2LirConverter(
-      DexItemFactory factory, IRCode irCode, LirEncodingStrategy<Value, EV> strategy) {
-    this.factory = factory;
+      InternalOptions options, IRCode irCode, LirEncodingStrategy<Value, EV> strategy) {
     this.irCode = irCode;
     this.strategy = strategy;
     this.builder =
-        new LirBuilder<>(irCode.context().getReference(), strategy, factory)
+        new LirBuilder<>(irCode.context().getReference(), strategy, options)
             .setMetadata(irCode.metadata());
   }
 
   public static <EV> LirCode<EV> translate(
-      IRCode irCode, LirEncodingStrategy<Value, EV> strategy, DexItemFactory factory) {
-    return new IR2LirConverter<>(factory, irCode, strategy).internalTranslate();
+      IRCode irCode, LirEncodingStrategy<Value, EV> strategy, InternalOptions options) {
+    return new IR2LirConverter<>(options, irCode, strategy).internalTranslate();
   }
 
   private void recordBlock(BasicBlock block, int blockIndex) {
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 8777089..4315a85 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -15,6 +15,11 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.proto.ArgumentInfo;
+import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
+import com.android.tools.r8.graph.proto.RemovedArgumentInfo;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -25,6 +30,7 @@
 import com.android.tools.r8.ir.code.ArrayLength;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.CanonicalPositions;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.Cmp;
@@ -116,7 +122,36 @@
       LirCode<EV> lirCode,
       LirDecodingStrategy<Value, EV> strategy,
       AppView<?> appView) {
-    Parser<EV> parser = new Parser<>(lirCode, method.getReference(), appView, strategy);
+    return translate(
+        method,
+        lirCode,
+        strategy,
+        appView,
+        new NumberGenerator(),
+        null,
+        RewrittenPrototypeDescription.none(),
+        appView.graphLens().getOriginalMethodSignature(method.getReference()));
+  }
+
+  public static <EV> IRCode translate(
+      ProgramMethod method,
+      LirCode<EV> lirCode,
+      LirDecodingStrategy<Value, EV> strategy,
+      AppView<?> appView,
+      NumberGenerator valueNumberGenerator,
+      Position callerPosition,
+      RewrittenPrototypeDescription protoChanges,
+      DexMethod originalMethod) {
+    Parser<EV> parser =
+        new Parser<>(
+            lirCode,
+            originalMethod,
+            method.getDefinition().isD8R8Synthesized(),
+            appView,
+            strategy,
+            valueNumberGenerator,
+            callerPosition,
+            protoChanges);
     parser.parseArguments(method);
     parser.ensureDebugInfo();
     lirCode.forEach(view -> view.accept(parser));
@@ -134,8 +169,9 @@
     private final AppView<?> appView;
     private final LirCode<EV> code;
     private final LirDecodingStrategy<Value, EV> strategy;
-    private final NumberGenerator valueNumberGenerator = new NumberGenerator();
+    private final NumberGenerator valueNumberGenerator;
     private final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
+    private final RewrittenPrototypeDescription protoChanges;
 
     private final Int2ReferenceMap<BasicBlock> blocks = new Int2ReferenceOpenHashMap<>();
 
@@ -145,18 +181,62 @@
     private Position currentPosition;
     private PositionEntry nextPositionEntry = null;
     private int nextIndexInPositionsTable = 0;
+    private final PositionEntry[] positionTable;
+
+    private final boolean buildForInlining;
 
     public Parser(
         LirCode<EV> code,
         DexMethod method,
+        boolean isD8R8Synthesized,
         AppView<?> appView,
-        LirDecodingStrategy<Value, EV> strategy) {
+        LirDecodingStrategy<Value, EV> strategy,
+        NumberGenerator valueNumberGenerator,
+        Position callerPosition,
+        RewrittenPrototypeDescription protoChanges) {
       super(code);
       this.appView = appView;
       this.code = code;
       this.strategy = strategy;
-      // Recreate the preamble position. This is active for arguments and code with no positions.
-      currentPosition = SyntheticPosition.builder().setLine(0).setMethod(method).build();
+      this.valueNumberGenerator = valueNumberGenerator;
+      this.protoChanges = protoChanges;
+      assert protoChanges != null;
+      if (callerPosition == null) {
+        buildForInlining = false;
+        positionTable = code.getPositionTable();
+        // Recreate the preamble position. This is active for arguments and code with no positions.
+        currentPosition = SyntheticPosition.builder().setLine(0).setMethod(method).build();
+      } else {
+        buildForInlining = true;
+        PositionEntry[] inlineePositions = code.getPositionTable();
+        Position inlineePreamble = null;
+        if (inlineePositions.length > 0 && inlineePositions[0].fromInstructionIndex == 0) {
+          inlineePreamble = inlineePositions[0].position;
+        }
+        CanonicalPositions canonicalPositions =
+            new CanonicalPositions(
+                callerPosition,
+                inlineePositions.length,
+                method,
+                isD8R8Synthesized,
+                inlineePreamble);
+        currentPosition = canonicalPositions.getPreamblePosition();
+        positionTable = new PositionEntry[inlineePositions.length];
+        for (int i = 0; i < inlineePositions.length; i++) {
+          PositionEntry inlineeEntry = inlineePositions[i];
+          Position inlineePosition = inlineeEntry.position;
+          positionTable[i] =
+              new PositionEntry(
+                  inlineeEntry.fromInstructionIndex,
+                  canonicalPositions.getCanonical(
+                      inlineePosition
+                          .builderWithCopy()
+                          .setCallerPosition(
+                              canonicalPositions.canonicalizeCallerPosition(
+                                  inlineePosition.getCallerPosition()))
+                          .build()));
+        }
+      }
     }
 
     @Override
@@ -197,23 +277,47 @@
 
     private void advanceNextPositionEntry() {
       nextPositionEntry =
-          nextIndexInPositionsTable < code.getPositionTable().length
-              ? code.getPositionTable()[nextIndexInPositionsTable++]
+          nextIndexInPositionsTable < positionTable.length
+              ? positionTable[nextIndexInPositionsTable++]
               : null;
     }
 
     public void parseArguments(ProgramMethod method) {
+      ArgumentInfoCollection argumentsInfo = protoChanges.getArgumentInfoCollection();
       currentBlock = getBasicBlock(ENTRY_BLOCK_INDEX);
       boolean hasReceiverArgument = !method.getDefinition().isStatic();
-      assert code.getArgumentCount()
-          == method.getParameters().size() + (hasReceiverArgument ? 1 : 0);
+
+      int index = 0;
       if (hasReceiverArgument) {
+        assert argumentsInfo.getNewArgumentIndex(0) == 0;
         addThisArgument(method.getHolderType());
+        index++;
       }
-      int index = hasReceiverArgument ? 1 : 0;
-      for (DexType parameter : method.getParameters()) {
-        addArgument(parameter, index++);
+
+      int originalNumberOfArguments =
+          method.getParameters().size()
+              + argumentsInfo.numberOfRemovedArguments()
+              + method.getDefinition().getFirstNonReceiverArgumentIndex()
+              - protoChanges.numberOfExtraParameters();
+
+      int numberOfRemovedArguments = 0;
+      while (index < originalNumberOfArguments) {
+        ArgumentInfo argumentInfo = argumentsInfo.getArgumentInfo(index);
+        if (argumentInfo.isRemovedArgumentInfo()) {
+          RemovedArgumentInfo removedArgumentInfo = argumentInfo.asRemovedArgumentInfo();
+          addArgument(removedArgumentInfo.getType(), index++);
+          numberOfRemovedArguments++;
+        } else if (argumentInfo.isRewrittenTypeInfo()) {
+          RewrittenTypeInfo rewrittenTypeInfo = argumentInfo.asRewrittenTypeInfo();
+          int newArgumentIndex = argumentsInfo.getNewArgumentIndex(index, numberOfRemovedArguments);
+          assert method.getArgumentType(newArgumentIndex) == rewrittenTypeInfo.getNewType();
+          addArgument(rewrittenTypeInfo.getOldType(), index++);
+        } else {
+          int newArgumentIndex = argumentsInfo.getNewArgumentIndex(index, numberOfRemovedArguments);
+          addArgument(method.getArgumentType(newArgumentIndex), index++);
+        }
       }
+
       // Set up position state after adding arguments.
       advanceNextPositionEntry();
     }
@@ -252,8 +356,14 @@
           }
         }
       }
-      for (int i = 0; i < peekNextInstructionIndex(); ++i) {
-        valueNumberGenerator.next();
+      if (!buildForInlining) {
+        // The decoding strategy will increment this on demand when built for inlining.
+        // Not incrementing for normal building results in nice order of instruction index and
+        // value number.
+        int lastValueIndex = getCurrentValueIndex();
+        for (int i = 0; i < lastValueIndex; ++i) {
+          valueNumberGenerator.next();
+        }
       }
       return new IRCode(
           appView.options(),
@@ -262,7 +372,7 @@
           blockList,
           valueNumberGenerator,
           basicBlockNumberGenerator,
-          code.getMetadata(),
+          code.getMetadataForIR(),
           method.getOrigin(),
           new MutableMethodConversionOptions(appView.options()));
     }
@@ -365,7 +475,7 @@
               index, typeElement, code::getDebugLocalInfo);
       Argument argument = new Argument(dest, index, type.isBooleanType());
       assert currentBlock != null;
-      assert currentPosition.isSyntheticPosition();
+      assert currentPosition.isSyntheticPosition() || buildForInlining;
       argument.setPosition(currentPosition);
       currentBlock.getInstructions().add(argument);
       argument.setBlock(currentBlock);
@@ -512,7 +622,7 @@
     public void onConstClass(DexType type, boolean ignoreCompatRules) {
       Value dest =
           getOutValueForNextInstruction(
-              type.toTypeElement(appView, Nullability.definitelyNotNull()));
+              TypeElement.classClassType(appView, Nullability.definitelyNotNull()));
       addInstruction(new ConstClass(dest, type, ignoreCompatRules));
     }
 
@@ -727,8 +837,12 @@
 
     @Override
     public void onReturn(EV value) {
-      addInstruction(new Return(getValue(value)));
-      closeCurrentBlock();
+      if (protoChanges.hasBeenChangedToReturnVoid()) {
+        onReturnVoid();
+      } else {
+        addInstruction(new Return(getValue(value)));
+        closeCurrentBlock();
+      }
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index f6ca030..8c8e4c5 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.lightir.LirCode.PositionEntry;
 import com.android.tools.r8.lightir.LirCode.TryCatchTable;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -60,6 +61,7 @@
   private static final long DOUBLE_0 = Double.doubleToRawLongBits(0);
   private static final long DOUBLE_1 = Double.doubleToRawLongBits(1);
 
+  private final boolean useDexEstimationStrategy;
   private final DexItemFactory factory;
   private final ByteArrayWriter byteWriter = new ByteArrayWriter();
   private final LirWriter writer = new LirWriter(byteWriter);
@@ -140,8 +142,10 @@
     }
   }
 
-  public LirBuilder(DexMethod method, LirEncodingStrategy<V, EV> strategy, DexItemFactory factory) {
-    this.factory = factory;
+  public LirBuilder(
+      DexMethod method, LirEncodingStrategy<V, EV> strategy, InternalOptions options) {
+    useDexEstimationStrategy = options.isGeneratingDex();
+    factory = options.dexItemFactory();
     constants = new Reference2IntOpenHashMap<>();
     positionTable = new ArrayList<>();
     this.strategy = strategy;
@@ -174,7 +178,9 @@
 
   public LirBuilder<V, EV> setCurrentPosition(Position position) {
     assert position != null;
-    currentPosition = position;
+    if (!position.isNone()) {
+      currentPosition = position;
+    }
     return this;
   }
 
@@ -721,7 +727,8 @@
         instructionCount,
         tryCatchTable,
         debugTable,
-        strategy.getStrategyInfo());
+        strategy.getStrategyInfo(),
+        useDexEstimationStrategy);
   }
 
   private int getCmpOpcode(NumericType type, Cmp.Bias bias) {
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index f1c04ec..75a1aa3 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -3,20 +3,43 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.dex.code.CfOrDexInstruction;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ArgumentUse;
+import com.android.tools.r8.graph.ClasspathMethod;
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeInstructionMetadata;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadata;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.google.common.collect.ImmutableMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.Map;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
-public class LirCode<EV> implements Iterable<LirInstructionView> {
+public class LirCode<EV> extends Code implements Iterable<LirInstructionView> {
 
   public static class PositionEntry {
     final int fromInstructionIndex;
@@ -72,7 +95,9 @@
 
   private final LirStrategyInfo<EV> strategyInfo;
 
-  private final IRMetadata metadata;
+  private final boolean useDexEstimationStrategy;
+
+  private final IRMetadata irMetadata;
 
   /** Constant pool of items. */
   private final DexItem[] constants;
@@ -95,13 +120,13 @@
   private final DebugLocalInfoTable<EV> debugLocalInfoTable;
 
   public static <V, EV> LirBuilder<V, EV> builder(
-      DexMethod method, LirEncodingStrategy<V, EV> strategy, DexItemFactory factory) {
-    return new LirBuilder<>(method, strategy, factory);
+      DexMethod method, LirEncodingStrategy<V, EV> strategy, InternalOptions options) {
+    return new LirBuilder<>(method, strategy, options);
   }
 
   /** Should be constructed using {@link LirBuilder}. */
   LirCode(
-      IRMetadata metadata,
+      IRMetadata irMetadata,
       DexItem[] constants,
       PositionEntry[] positions,
       int argumentCount,
@@ -109,8 +134,9 @@
       int instructionCount,
       TryCatchTable tryCatchTable,
       DebugLocalInfoTable<EV> debugLocalInfoTable,
-      LirStrategyInfo<EV> strategyInfo) {
-    this.metadata = metadata;
+      LirStrategyInfo<EV> strategyInfo,
+      boolean useDexEstimationStrategy) {
+    this.irMetadata = irMetadata;
     this.constants = constants;
     this.positionTable = positions;
     this.argumentCount = argumentCount;
@@ -119,6 +145,24 @@
     this.tryCatchTable = tryCatchTable;
     this.debugLocalInfoTable = debugLocalInfoTable;
     this.strategyInfo = strategyInfo;
+    this.useDexEstimationStrategy = useDexEstimationStrategy;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public LirCode<Integer> asLirCode() {
+    // TODO(b/225838009): Unchecked cast will be removed once the encoding strategy is definitive.
+    return (LirCode<Integer>) this;
+  }
+
+  @Override
+  protected int computeHashCode() {
+    throw new Unreachable("LIR code should not be subject to hashing.");
+  }
+
+  @Override
+  protected boolean computeEquals(Object other) {
+    throw new Unreachable("LIR code should not be subject to equality checks.");
   }
 
   public EV decodeValueIndex(int encodedValueIndex, int currentValueIndex) {
@@ -143,8 +187,8 @@
     return instructionCount;
   }
 
-  public IRMetadata getMetadata() {
-    return metadata;
+  public IRMetadata getMetadataForIR() {
+    return irMetadata;
   }
 
   public DexItem getConstantItem(int index) {
@@ -172,12 +216,184 @@
   }
 
   @Override
+  public BytecodeMetadata<? extends CfOrDexInstruction> getMetadata() {
+    // Bytecode metadata is recomputed when finalizing via IR.
+    throw new Unreachable();
+  }
+
+  @Override
+  public BytecodeInstructionMetadata getMetadata(CfOrDexInstruction instruction) {
+    // Bytecode metadata is recomputed when finalizing via IR.
+    throw new Unreachable();
+  }
+
+  @Override
   public LirIterator iterator() {
     return new LirIterator(new ByteArrayIterator(instructions));
   }
 
   @Override
+  public IRCode buildIR(
+      ProgramMethod method,
+      AppView<?> appView,
+      Origin origin,
+      MutableMethodConversionOptions conversionOptions) {
+    LirCode<Integer> typedLir = asLirCode();
+    return Lir2IRConverter.translate(
+        method,
+        typedLir,
+        LirStrategy.getDefaultStrategy().getDecodingStrategy(typedLir, null),
+        appView);
+  }
+
+  @Override
+  public IRCode buildInliningIR(
+      ProgramMethod context,
+      ProgramMethod method,
+      AppView<?> appView,
+      GraphLens codeLens,
+      NumberGenerator valueNumberGenerator,
+      Position callerPosition,
+      Origin origin,
+      RewrittenPrototypeDescription protoChanges) {
+    assert valueNumberGenerator != null;
+    assert callerPosition != null;
+    assert protoChanges != null;
+    LirCode<Integer> typedLir = asLirCode();
+    IRCode irCode =
+        Lir2IRConverter.translate(
+            method,
+            typedLir,
+            LirStrategy.getDefaultStrategy().getDecodingStrategy(typedLir, valueNumberGenerator),
+            appView,
+            valueNumberGenerator,
+            callerPosition,
+            protoChanges,
+            appView.graphLens().getOriginalMethodSignature(method.getReference()));
+    // TODO(b/225838009): Should we keep track of which code objects need to be narrowed?
+    //   In particular, the encoding of phis does not maintain interfaces.
+    new TypeAnalysis(appView).narrowing(irCode);
+    return irCode;
+  }
+
+  @Override
+  public void registerCodeReferences(ProgramMethod method, UseRegistry registry) {
+    assert registry.getTraversalContinuation().shouldContinue();
+    LirUseRegistryCallback<EV> registryCallbacks = new LirUseRegistryCallback<>(this, registry);
+    for (LirInstructionView view : this) {
+      registryCallbacks.onInstructionView(view);
+      if (registry.getTraversalContinuation().shouldBreak()) {
+        return;
+      }
+    }
+    if (tryCatchTable != null) {
+      for (CatchHandlers<Integer> handler : tryCatchTable.tryCatchHandlers.values()) {
+        for (DexType guard : handler.getGuards()) {
+          registry.registerExceptionGuard(guard);
+          if (registry.getTraversalContinuation().shouldBreak()) {
+            return;
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public void registerCodeReferencesForDesugaring(ClasspathMethod method, UseRegistry registry) {
+    throw new Unimplemented();
+  }
+
+  @Override
+  public Int2ReferenceMap<DebugLocalInfo> collectParameterInfo(
+      DexEncodedMethod encodedMethod, AppView<?> appView) {
+    throw new Unimplemented();
+  }
+
+  @Override
+  public void registerArgumentReferences(DexEncodedMethod method, ArgumentUse registry) {
+    throw new Unimplemented();
+  }
+
+  @Override
   public String toString() {
     return new LirPrinter<>(this).prettyPrint();
   }
+
+  @Override
+  public String toString(DexEncodedMethod method, RetracerForCodePrinting retracer) {
+    // TODO(b/225838009): Add retracing to printer.
+    return toString();
+  }
+
+  @Override
+  public int estimatedDexCodeSizeUpperBoundInBytes() {
+    throw new Unimplemented();
+  }
+
+  @Override
+  public int estimatedSizeForInlining() {
+    if (useDexEstimationStrategy) {
+      LirSizeEstimation<EV> estimation = new LirSizeEstimation<>(this);
+      for (LirInstructionView view : this) {
+        estimation.onInstructionView(view);
+      }
+      return estimation.getSizeEstimate();
+    } else {
+      // TODO(b/225838009): Currently the size estimation for CF has size one for each instruction
+      //  (even switches!) and ignores stack instructions, thus loads to arguments are not included.
+      //  The result is a much smaller estimate than for DEX. Once LIR is in place we should use the
+      //  same estimate for both.
+      return instructionCount;
+    }
+  }
+
+  @Override
+  public boolean estimatedSizeForInliningAtMost(int threshold) {
+    if (useDexEstimationStrategy) {
+      LirSizeEstimation<EV> estimation = new LirSizeEstimation<>(this);
+      for (LirInstructionView view : this) {
+        estimation.onInstructionView(view);
+        if (estimation.getSizeEstimate() > threshold) {
+          return false;
+        }
+      }
+      return true;
+    } else {
+      return estimatedSizeForInlining() <= threshold;
+    }
+  }
+
+  @Override
+  public Code getCodeAsInlining(DexMethod caller, DexEncodedMethod callee, DexItemFactory factory) {
+    throw new Unimplemented();
+  }
+
+  @Override
+  public boolean isEmptyVoidMethod() {
+    for (LirInstructionView view : this) {
+      int opcode = view.getOpcode();
+      if (opcode != LirOpcodes.RETURN && opcode != LirOpcodes.DEBUGPOS) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean hasMonitorInstructions() {
+    for (LirInstructionView view : this) {
+      int opcode = view.getOpcode();
+      if (opcode == LirOpcodes.MONITORENTER || opcode == LirOpcodes.MONITOREXIT) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void forEachPosition(Consumer<Position> positionConsumer) {
+    for (PositionEntry entry : positionTable) {
+      positionConsumer.accept(entry.position);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirDecodingStrategy.java b/src/main/java/com/android/tools/r8/lightir/LirDecodingStrategy.java
index 0b0a51d..f0eaeef 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirDecodingStrategy.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirDecodingStrategy.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Phi;
 import java.util.function.Function;
 import java.util.function.IntFunction;
@@ -13,6 +14,16 @@
 /** Abstraction for how to decode SSA values (and basic blocks) when reading LIR. */
 public abstract class LirDecodingStrategy<V, EV> {
 
+  private final NumberGenerator valueNumberGenerator;
+
+  public LirDecodingStrategy(NumberGenerator valueNumberGenerator) {
+    this.valueNumberGenerator = valueNumberGenerator;
+  }
+
+  public final int getValueNumber(int encodedValueIndex) {
+    return valueNumberGenerator == null ? encodedValueIndex : valueNumberGenerator.next();
+  }
+
   public abstract V getValue(EV encodedValue, LirStrategyInfo<EV> strategyInfo);
 
   public abstract V getValueDefinitionForInstructionIndex(
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 6803d0b..fc1ccc6 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -151,7 +151,7 @@
   // int JSR = 168;
   // int RET = 169;
   int TABLESWITCH = 170;
-  int LOOKUPSWITCH = 171;
+  // int LOOKUPSWITCH = 171;
   // int IRETURN = 172;
   // int LRETURN = 173;
   // int FRETURN = 174;
@@ -447,8 +447,7 @@
         // case RET: return "RET";
       case TABLESWITCH:
         return "TABLESWITCH";
-      case LOOKUPSWITCH:
-        return "LOOKUPSWITCH";
+        // case LOOKUPSWITCH:
       case ARETURN:
         return "ARETURN";
       case RETURN:
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index 444df4e..d979ada 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -448,13 +448,21 @@
     onFieldInstruction(field);
   }
 
-  public abstract void onInstanceGet(DexField field, EV object);
+  public void onInstanceGet(DexField field, EV object) {
+    onFieldInstruction(field);
+  }
 
-  public abstract void onInstancePut(DexField field, EV object, EV value);
+  public void onInstancePut(DexField field, EV object, EV value) {
+    onFieldInstruction(field);
+  }
 
-  public abstract void onNewArrayEmpty(DexType type, EV size);
+  public void onNewArrayEmpty(DexType type, EV size) {
+    onInstruction();
+  }
 
-  public abstract void onThrow(EV exception);
+  public void onThrow(EV exception) {
+    onInstruction();
+  }
 
   public void onReturnVoid() {
     onInstruction();
@@ -493,9 +501,17 @@
     onInstruction();
   }
 
-  public abstract void onMonitorEnter(EV value);
+  public void onMonitorInstruction(EV value) {
+    onInstruction();
+  }
 
-  public abstract void onMonitorExit(EV value);
+  public void onMonitorEnter(EV value) {
+    onMonitorInstruction(value);
+  }
+
+  public void onMonitorExit(EV value) {
+    onMonitorInstruction(value);
+  }
 
   public void onNewUnboxedEnumInstance(DexType type, int ordinal) {
     onInstruction();
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index 96d9cef..cfe1a08 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.NumericType;
@@ -61,7 +62,7 @@
   }
 
   private String fmtValueIndex(EV valueIndex) {
-    return valueIndex.toString();
+    return "v" + valueIndex.toString();
   }
 
   private String fmtInsnIndex(int instructionIndex) {
@@ -86,6 +87,22 @@
       advanceToNextValueIndex();
     }
     code.forEach(this::onInstructionView);
+    if (code.getTryCatchTable() != null) {
+      builder.append("try-catch-handlers:\n");
+      code.getTryCatchTable()
+          .tryCatchHandlers
+          .forEach(
+              (index, handlers) -> {
+                builder.append(index).append(":\n");
+                for (CatchHandler<Integer> handler : handlers) {
+                  builder
+                      .append(handler.getGuard())
+                      .append(" -> ")
+                      .append(handler.getTarget())
+                      .append('\n');
+                }
+              });
+    }
     return builder.toString();
   }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
new file mode 100644
index 0000000..eaa93d0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
@@ -0,0 +1,339 @@
+// 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.lightir;
+
+import com.android.tools.r8.dex.code.DexAget;
+import com.android.tools.r8.dex.code.DexAput;
+import com.android.tools.r8.dex.code.DexArrayLength;
+import com.android.tools.r8.dex.code.DexBase1Format;
+import com.android.tools.r8.dex.code.DexBase2Format;
+import com.android.tools.r8.dex.code.DexBase3Format;
+import com.android.tools.r8.dex.code.DexCheckCast;
+import com.android.tools.r8.dex.code.DexConst16;
+import com.android.tools.r8.dex.code.DexConst4;
+import com.android.tools.r8.dex.code.DexConstClass;
+import com.android.tools.r8.dex.code.DexConstString;
+import com.android.tools.r8.dex.code.DexConstWide16;
+import com.android.tools.r8.dex.code.DexFillArrayData;
+import com.android.tools.r8.dex.code.DexFillArrayDataPayload;
+import com.android.tools.r8.dex.code.DexFilledNewArray;
+import com.android.tools.r8.dex.code.DexGoto;
+import com.android.tools.r8.dex.code.DexInstanceOf;
+import com.android.tools.r8.dex.code.DexInvokeCustom;
+import com.android.tools.r8.dex.code.DexMonitorEnter;
+import com.android.tools.r8.dex.code.DexMonitorExit;
+import com.android.tools.r8.dex.code.DexMove;
+import com.android.tools.r8.dex.code.DexMoveException;
+import com.android.tools.r8.dex.code.DexNewArray;
+import com.android.tools.r8.dex.code.DexNewInstance;
+import com.android.tools.r8.dex.code.DexNotInt;
+import com.android.tools.r8.dex.code.DexNotLong;
+import com.android.tools.r8.dex.code.DexPackedSwitch;
+import com.android.tools.r8.dex.code.DexPackedSwitchPayload;
+import com.android.tools.r8.dex.code.DexSget;
+import com.android.tools.r8.dex.code.DexThrow;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
+
+public class LirSizeEstimation<EV> extends LirParsedInstructionCallback<EV> implements LirOpcodes {
+
+  private int sizeEstimate = 0;
+
+  LirSizeEstimation(LirCode<EV> code) {
+    super(code);
+  }
+
+  @Override
+  public int getCurrentValueIndex() {
+    // We don't use value information.
+    return 0;
+  }
+
+  public int getSizeEstimate() {
+    return sizeEstimate;
+  }
+
+  /**
+   * Most size information can be found just using opcode.
+   *
+   * <p>We overwrite the base view callback and only in the few payload instruction cases do we make
+   * use of the parsed-instruction callbacks.
+   */
+  @Override
+  public void onInstructionView(LirInstructionView view) {
+    sizeEstimate += instructionSize(view.getOpcode(), view);
+  }
+
+  @Override
+  public void onIntSwitch(EV unusedValue, IntSwitchPayload payload) {
+    sizeEstimate +=
+        DexPackedSwitch.SIZE
+            + DexPackedSwitchPayload.SIZE
+            + (2 * payload.keys.length)
+            + (2 * payload.targets.length);
+  }
+
+  @Override
+  public void onNewArrayFilledData(int elementWidth, long size, short[] data, EV unusedSrc) {
+    sizeEstimate += DexFillArrayData.SIZE + DexFillArrayDataPayload.SIZE + 4 + data.length;
+  }
+
+  private int instructionSize(int opcode, LirInstructionView view) {
+    switch (opcode) {
+      case TABLESWITCH:
+      case NEWARRAYFILLEDDATA:
+        // The payload instructions use the "parsed callback" to compute the payloads.
+        super.onInstructionView(view);
+        // The full size is added by the callbacks so return zero here.
+        return 0;
+
+      case ACONST_NULL:
+      case ICONST_M1:
+      case ICONST_0:
+      case ICONST_1:
+      case ICONST_2:
+      case ICONST_3:
+      case ICONST_4:
+      case ICONST_5:
+        return DexConst4.SIZE;
+
+      case LCONST_0:
+      case LCONST_1:
+      case FCONST_0:
+      case FCONST_1:
+      case FCONST_2:
+      case DCONST_0:
+      case DCONST_1:
+        return DexConstWide16.SIZE;
+
+      case LDC:
+        // Most of the const loads are the same size (2).
+        return DexConstString.SIZE;
+
+      case IALOAD:
+      case LALOAD:
+      case FALOAD:
+      case DALOAD:
+      case AALOAD:
+      case BALOAD:
+      case CALOAD:
+      case SALOAD:
+        // The loads are all size 2.
+        return DexAget.SIZE;
+
+      case IASTORE:
+      case LASTORE:
+      case FASTORE:
+      case DASTORE:
+      case AASTORE:
+      case BASTORE:
+      case CASTORE:
+      case SASTORE:
+        // The loads are all size 2.
+        return DexAput.SIZE;
+
+      case IADD:
+      case LADD:
+      case FADD:
+      case DADD:
+      case ISUB:
+      case LSUB:
+      case FSUB:
+      case DSUB:
+      case IMUL:
+      case LMUL:
+      case FMUL:
+      case DMUL:
+      case IDIV:
+      case LDIV:
+      case FDIV:
+      case DDIV:
+      case IREM:
+      case LREM:
+      case FREM:
+      case DREM:
+        // The binary ops are all size 2.
+        return DexBase2Format.SIZE;
+
+      case INEG:
+      case LNEG:
+      case FNEG:
+      case DNEG:
+        // The negs are all size 1.
+        return DexBase1Format.SIZE;
+
+      case ISHL:
+      case LSHL:
+      case ISHR:
+      case LSHR:
+      case IUSHR:
+      case LUSHR:
+      case IAND:
+      case LAND:
+      case IOR:
+      case LOR:
+      case IXOR:
+      case LXOR:
+        // The binary ops are all size 2.
+        return DexBase2Format.SIZE;
+
+      case I2L:
+      case I2F:
+      case I2D:
+      case L2I:
+      case L2F:
+      case L2D:
+      case F2I:
+      case F2L:
+      case F2D:
+      case D2I:
+      case D2L:
+      case D2F:
+      case I2B:
+      case I2C:
+      case I2S:
+        // Number conversions are all size 1.
+        return DexBase1Format.SIZE;
+
+      case LCMP:
+      case FCMPL:
+      case FCMPG:
+      case DCMPL:
+      case DCMPG:
+        return DexBase2Format.SIZE;
+
+      case IFEQ:
+      case IFNE:
+      case IFLT:
+      case IFGE:
+      case IFGT:
+      case IFLE:
+      case IF_ICMPEQ:
+      case IF_ICMPNE:
+      case IF_ICMPLT:
+      case IF_ICMPGE:
+      case IF_ICMPGT:
+      case IF_ICMPLE:
+      case IF_ACMPEQ:
+      case IF_ACMPNE:
+        return DexBase2Format.SIZE;
+
+      case GOTO:
+        return DexGoto.SIZE;
+
+      case ARETURN:
+      case RETURN:
+        return DexBase1Format.SIZE;
+
+      case GETSTATIC:
+      case PUTSTATIC:
+      case GETFIELD:
+      case PUTFIELD:
+        return DexBase2Format.SIZE;
+
+      case INVOKEVIRTUAL:
+      case INVOKESPECIAL:
+      case INVOKESTATIC:
+      case INVOKEINTERFACE:
+        return DexBase3Format.SIZE;
+
+      case INVOKEDYNAMIC:
+        return DexInvokeCustom.SIZE;
+
+      case NEW:
+        return DexNewInstance.SIZE;
+      case NEWARRAY:
+        return DexNewArray.SIZE;
+      case ARRAYLENGTH:
+        return DexArrayLength.SIZE;
+      case ATHROW:
+        return DexThrow.SIZE;
+      case CHECKCAST:
+        return DexCheckCast.SIZE;
+      case INSTANCEOF:
+        return DexInstanceOf.SIZE;
+      case MONITORENTER:
+        return DexMonitorEnter.SIZE;
+      case MONITOREXIT:
+        return DexMonitorExit.SIZE;
+      case MULTIANEWARRAY:
+        return DexFilledNewArray.SIZE;
+
+      case IFNULL:
+      case IFNONNULL:
+        return DexBase1Format.SIZE;
+
+        // Non-CF instructions.
+      case ICONST:
+      case LCONST:
+      case FCONST:
+      case DCONST:
+        return DexConst16.SIZE;
+
+      case INVOKESTATIC_ITF:
+      case INVOKEDIRECT:
+      case INVOKEDIRECT_ITF:
+      case INVOKESUPER:
+      case INVOKESUPER_ITF:
+        return DexBase3Format.SIZE;
+
+      case DEBUGPOS:
+        // Often debug positions will be associated with instructions so assume size 0.
+        return 0;
+
+      case PHI:
+        // Assume a move per phi.
+        return DexMove.SIZE;
+
+      case FALLTHROUGH:
+        // Hopefully fallthrough points will not materialize as instructions.
+        return 0;
+
+      case MOVEEXCEPTION:
+        return DexMoveException.SIZE;
+
+      case DEBUGLOCALWRITE:
+        return DexMove.SIZE;
+
+      case INVOKENEWARRAY:
+        return DexFilledNewArray.SIZE;
+
+      case ITEMBASEDCONSTSTRING:
+        return DexConstString.SIZE;
+
+      case NEWUNBOXEDENUMINSTANCE:
+        return DexConst16.SIZE;
+
+      case INOT:
+        return DexNotInt.SIZE;
+      case LNOT:
+        return DexNotLong.SIZE;
+
+      case DEBUGLOCALREAD:
+        // These reads do not materialize after register allocation.
+        return 0;
+
+      case INITCLASS:
+        return DexSget.SIZE;
+
+      case INVOKEPOLYMORPHIC:
+        return DexBase3Format.SIZE;
+
+      case RECORDFIELDVALUES:
+        // Rewritten to new-array in DEX.
+        return DexNewArray.SIZE;
+
+      case CHECKCAST_SAFE:
+      case CHECKCAST_IGNORE_COMPAT:
+        return DexCheckCast.SIZE;
+
+      case CONSTCLASS_IGNORE_COMPAT:
+        return DexConstClass.SIZE;
+
+      default:
+        throw new Unreachable("Unexpected LIR opcode: " + opcode);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LirStrategy.java b/src/main/java/com/android/tools/r8/lightir/LirStrategy.java
index 108586f..07ac579 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirStrategy.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirStrategy.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.code.Value;
@@ -29,9 +30,15 @@
  *     the possible encoding for phi and non-phi values.
  */
 public abstract class LirStrategy<V, EV> {
+
+  public static LirStrategy<Value, Integer> getDefaultStrategy() {
+    return new PhiInInstructionsStrategy();
+  }
+
   public abstract LirEncodingStrategy<V, EV> getEncodingStrategy();
 
-  public abstract LirDecodingStrategy<V, EV> getDecodingStrategy(LirCode<EV> code);
+  public abstract LirDecodingStrategy<V, EV> getDecodingStrategy(
+      LirCode<EV> code, NumberGenerator valueNumberGenerator);
 
   /**
    * Encoding of a value with a phi-bit.
@@ -139,8 +146,9 @@
     }
 
     @Override
-    public LirDecodingStrategy<Value, PhiOrValue> getDecodingStrategy(LirCode<PhiOrValue> code) {
-      return new DecodingStrategy(code);
+    public LirDecodingStrategy<Value, PhiOrValue> getDecodingStrategy(
+        LirCode<PhiOrValue> code, NumberGenerator valueNumberGenerator) {
+      return new DecodingStrategy(code, valueNumberGenerator);
     }
 
     private static class StrategyInfo extends LirStrategyInfo<PhiOrValue> {
@@ -234,7 +242,8 @@
       private final Value[] values;
       private final int firstPhiValueIndex;
 
-      DecodingStrategy(LirCode<PhiOrValue> code) {
+      DecodingStrategy(LirCode<PhiOrValue> code, NumberGenerator valueNumberGenerator) {
+        super(valueNumberGenerator);
         values = new Value[code.getArgumentCount() + code.getInstructionCount()];
         int phiValueIndex = -1;
         for (LirInstructionView view : code) {
@@ -270,7 +279,7 @@
         int index = decode(encodedValue, strategyInfo);
         Value value = values[index];
         if (value == null) {
-          value = new Value(index, TypeElement.getBottom(), null);
+          value = new Value(getValueNumber(index), TypeElement.getBottom(), null);
           values[index] = value;
         }
         return value;
@@ -284,7 +293,7 @@
         DebugLocalInfo localInfo = getLocalInfo.apply(encodedValue);
         Value value = values[index];
         if (value == null) {
-          value = new Value(index, type, localInfo);
+          value = new Value(getValueNumber(index), type, localInfo);
           values[index] = value;
         } else {
           value.setType(type);
@@ -306,7 +315,8 @@
         PhiOrValue encodedValue = getEncodedPhiForAbsoluteValueIndex(valueIndex, strategyInfo);
         BasicBlock block = getBlock.apply(encodedValue.getBlockIndex());
         DebugLocalInfo localInfo = getLocalInfo.apply(encodedValue);
-        Phi phi = new Phi(valueIndex, block, type, localInfo, RegisterReadType.NORMAL);
+        Phi phi =
+            new Phi(getValueNumber(valueIndex), block, type, localInfo, RegisterReadType.NORMAL);
         Value value = values[valueIndex];
         if (value != null) {
           // A fake ssa value has already been created, replace the users by the actual phi.
diff --git a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
new file mode 100644
index 0000000..67f7a4e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
@@ -0,0 +1,153 @@
+// 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.lightir;
+
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.graph.UseRegistry.MethodHandleUse;
+import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import java.util.List;
+
+public class LirUseRegistryCallback<EV> extends LirParsedInstructionCallback<EV> {
+
+  private final UseRegistry registry;
+
+  public LirUseRegistryCallback(LirCode<EV> code, UseRegistry registry) {
+    super(code);
+    this.registry = registry;
+  }
+
+  @Override
+  public int getCurrentValueIndex() {
+    // The registry of instructions does not require knowledge of value indexes.
+    return 0;
+  }
+
+  @Override
+  public void onCheckCast(DexType type, EV value, boolean ignoreCompatRules) {
+    registry.registerCheckCast(type, ignoreCompatRules);
+  }
+
+  @Override
+  public void onSafeCheckCast(DexType type, EV value) {
+    registry.registerSafeCheckCast(type);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void onConstClass(DexType type, boolean ignoreCompatRules) {
+    registry.registerConstClass(type, null, ignoreCompatRules);
+  }
+
+  @Override
+  public void onConstMethodHandle(DexMethodHandle methodHandle) {
+    registry.registerMethodHandle(methodHandle, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
+  }
+
+  @Override
+  public void onConstMethodType(DexProto methodType) {
+    registry.registerProto(methodType);
+  }
+
+  @Override
+  public void onDexItemBasedConstString(
+      DexReference item, NameComputationInfo<?> nameComputationInfo) {
+    if (nameComputationInfo.needsToRegisterReference()) {
+      assert item.isDexType();
+      registry.registerTypeReference(item.asDexType());
+    }
+  }
+
+  @Override
+  public void onInitClass(DexType clazz) {
+    registry.registerInitClass(clazz);
+  }
+
+  @Override
+  public void onInstanceGet(DexField field, EV object) {
+    registry.registerInstanceFieldRead(field);
+  }
+
+  @Override
+  public void onInstancePut(DexField field, EV object, EV value) {
+    registry.registerInstanceFieldWrite(field);
+  }
+
+  @Override
+  public void onStaticGet(DexField field) {
+    registry.registerStaticFieldRead(field);
+  }
+
+  @Override
+  public void onStaticPut(DexField field, EV value) {
+    registry.registerStaticFieldWrite(field);
+  }
+
+  @Override
+  public void onInstanceOf(DexType type, EV value) {
+    registry.registerInstanceOf(type);
+  }
+
+  @Override
+  public void onInvokeCustom(DexCallSite callSite, List<EV> arguments) {
+    registry.registerCallSite(callSite);
+  }
+
+  @Override
+  public void onInvokeDirect(DexMethod method, List<EV> arguments, boolean isInterface) {
+    registry.registerInvokeDirect(method);
+  }
+
+  @Override
+  public void onInvokeInterface(DexMethod method, List<EV> arguments) {
+    registry.registerInvokeInterface(method);
+  }
+
+  @Override
+  public void onInvokeStatic(DexMethod method, List<EV> arguments, boolean isInterface) {
+    registry.registerInvokeStatic(method);
+  }
+
+  @Override
+  public void onInvokeSuper(DexMethod method, List<EV> arguments, boolean isInterface) {
+    registry.registerInvokeSuper(method);
+  }
+
+  @Override
+  public void onInvokeVirtual(DexMethod method, List<EV> arguments) {
+    registry.registerInvokeVirtual(method);
+  }
+
+  @Override
+  public void onInvokeMultiNewArray(DexType type, List<EV> arguments) {
+    registry.registerTypeReference(type);
+  }
+
+  @Override
+  public void onInvokeNewArray(DexType type, List<EV> arguments) {
+    registry.registerTypeReference(type);
+  }
+
+  @Override
+  public void onNewArrayEmpty(DexType type, EV size) {
+    registry.registerTypeReference(type);
+  }
+
+  @Override
+  public void onNewInstance(DexType clazz) {
+    registry.registerNewInstance(clazz);
+  }
+
+  @Override
+  public void onNewUnboxedEnumInstance(DexType type, int ordinal) {
+    registry.registerNewUnboxedEnumInstance(type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java b/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
index 0428126..6e9366f 100644
--- a/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
+++ b/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.code.Value;
@@ -23,8 +24,9 @@
   }
 
   @Override
-  public LirDecodingStrategy<Value, Integer> getDecodingStrategy(LirCode<Integer> code) {
-    return new DecodingStrategy(code);
+  public LirDecodingStrategy<Value, Integer> getDecodingStrategy(
+      LirCode<Integer> code, NumberGenerator valueNumberGenerator) {
+    return new DecodingStrategy(code, valueNumberGenerator);
   }
 
   private static class EncodingStrategy extends LirEncodingStrategy<Value, Integer> {
@@ -94,7 +96,8 @@
 
     private final Value[] values;
 
-    DecodingStrategy(LirCode<Integer> code) {
+    DecodingStrategy(LirCode<Integer> code, NumberGenerator valueNumberGenerator) {
+      super(valueNumberGenerator);
       values = new Value[code.getArgumentCount() + code.getInstructionCount()];
     }
 
@@ -103,7 +106,7 @@
       int index = encodedValue;
       Value value = values[index];
       if (value == null) {
-        value = new Value(index, TypeElement.getBottom(), null);
+        value = new Value(getValueNumber(index), TypeElement.getBottom(), null);
         values[index] = value;
       }
       return value;
@@ -115,7 +118,7 @@
       DebugLocalInfo localInfo = getLocalInfo.apply(index);
       Value value = values[index];
       if (value == null) {
-        value = new Value(index, type, localInfo);
+        value = new Value(getValueNumber(index), type, localInfo);
         values[index] = value;
       } else {
         value.setType(type);
@@ -138,7 +141,8 @@
         LirStrategyInfo<Integer> strategyInfo) {
       BasicBlock block = getBlock.apply(valueIndex);
       DebugLocalInfo localInfo = getLocalInfo.apply(valueIndex);
-      Phi phi = new Phi(valueIndex, block, type, localInfo, RegisterReadType.NORMAL);
+      Phi phi =
+          new Phi(getValueNumber(valueIndex), block, type, localInfo, RegisterReadType.NORMAL);
       Value value = values[valueIndex];
       if (value != null) {
         // A fake ssa value has already been created, replace the users by the actual phi.
diff --git a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
deleted file mode 100644
index dea6346..0000000
--- a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.optimize;
-
-import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
-import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
-import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.FieldAccessFlags;
-import com.android.tools.r8.graph.InnerClassAttribute;
-import com.android.tools.r8.graph.MethodAccessFlags;
-import com.android.tools.r8.graph.ProgramDefinition;
-import com.android.tools.r8.graph.ProgramField;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.SubtypingInfo;
-import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
-import com.android.tools.r8.ir.optimize.MethodPoolCollection;
-import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.OptionalBool;
-import com.android.tools.r8.utils.Timing;
-import com.google.common.base.Equivalence.Wrapper;
-import java.util.LinkedHashSet;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-public final class AccessModifier {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final SubtypingInfo subtypingInfo;
-  private final MethodPoolCollection methodPoolCollection;
-
-  private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
-
-  private AccessModifier(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
-    this.subtypingInfo = appView.appInfo().computeSubtypingInfo();
-    this.methodPoolCollection =
-        // We will add private instance methods when we promote them.
-        new MethodPoolCollection(
-            appView, subtypingInfo, MethodPoolCollection::excludesPrivateInstanceMethod);
-  }
-
-  /**
-   * Marks all package private and protected methods and fields as public. Makes all private static
-   * methods public. Makes private instance methods public final instance methods, if possible.
-   *
-   * <p>This will destructively update the DexApplication passed in as argument.
-   */
-  public static void run(
-      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
-      throws ExecutionException {
-    if (appView.options().getProguardConfiguration().isAccessModificationAllowed()) {
-      timing.begin("Access modification");
-      new AccessModifier(appView).internalRun(executorService, timing);
-      timing.end();
-    }
-  }
-
-  private void internalRun(ExecutorService executorService, Timing timing)
-      throws ExecutionException {
-    // Phase 1: Collect methods to check if private instance methods don't have conflicts.
-    methodPoolCollection.buildAll(executorService, timing);
-
-    // Phase 2: Visit classes and promote class/member to public if possible.
-    timing.begin("Phase 2: promoteToPublic");
-    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
-    processType(appView.dexItemFactory().objectType);
-    timing.end();
-
-    PublicizerLens publicizerLens = lensBuilder.build(appView);
-    if (publicizerLens != null) {
-      appView.setGraphLens(publicizerLens);
-    }
-
-    appView.notifyOptimizationFinishedForTesting();
-  }
-
-  private void doPublicize(ProgramDefinition definition) {
-    definition.getAccessFlags().promoteToPublic();
-  }
-
-  private void processType(DexType type) {
-    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
-    if (clazz != null) {
-      processClass(clazz);
-    }
-    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
-  }
-
-  private void processClass(DexProgramClass clazz) {
-    if (appView.appInfo().isAccessModificationAllowed(clazz)) {
-      doPublicize(clazz);
-    }
-
-    // Publicize fields.
-    clazz.forEachProgramField(this::processField);
-
-    // Publicize methods.
-    Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
-    clazz.forEachProgramMethod(
-        method -> {
-          if (publicizeMethod(method)) {
-            privateInstanceMethods.add(method.getDefinition());
-          }
-        });
-    if (!privateInstanceMethods.isEmpty()) {
-      clazz.virtualizeMethods(privateInstanceMethods);
-    }
-
-    // Publicize inner class attribute.
-    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
-    if (attr != null) {
-      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
-      clazz.replaceInnerClassAttributeForThisClass(
-          new InnerClassAttribute(
-              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
-    }
-  }
-
-  private void processField(ProgramField field) {
-    if (appView.appInfo().isAccessModificationAllowed(field)) {
-      publicizeField(field);
-    }
-  }
-
-  private void publicizeField(ProgramField field) {
-    FieldAccessFlags flags = field.getAccessFlags();
-    if (!flags.isPublic()) {
-      flags.promoteToPublic();
-    }
-  }
-
-  private boolean publicizeMethod(ProgramMethod method) {
-    MethodAccessFlags accessFlags = method.getAccessFlags();
-    if (accessFlags.isPublic()) {
-      return false;
-    }
-    // If this method is mentioned in keep rules, do not transform (rule applications changed).
-    DexEncodedMethod definition = method.getDefinition();
-    if (!appView.appInfo().isAccessModificationAllowed(method)) {
-      // TODO(b/131130038): Also do not publicize package-private and protected methods that are
-      //  kept.
-      if (definition.isPrivate()) {
-        return false;
-      }
-    }
-
-    if (method.getDefinition().isInstanceInitializer() || accessFlags.isProtected()) {
-      doPublicize(method);
-      return false;
-    }
-
-    if (accessFlags.isPackagePrivate()) {
-      // If we publicize a package private method we have to ensure there is no overrides of it. We
-      // could potentially publicize a method if it only has package-private overrides.
-      // TODO(b/182136236): See if we can break the hierarchy for clusters.
-      MemberPool<DexMethod> memberPool = methodPoolCollection.get(method.getHolder());
-      Wrapper<DexMethod> methodKey = MethodSignatureEquivalence.get().wrap(method.getReference());
-      if (memberPool.below(
-          methodKey,
-          false,
-          true,
-          (clazz, ignored) ->
-              !method.getContextType().getPackageName().equals(clazz.getType().getPackageName()))) {
-        return false;
-      }
-      doPublicize(method);
-      return false;
-    }
-
-    assert accessFlags.isPrivate();
-
-    if (accessFlags.isStatic()) {
-      // For private static methods we can just relax the access to public, since
-      // even though JLS prevents from declaring static method in derived class if
-      // an instance method with same signature exists in superclass, JVM actually
-      // does not take into account access of the static methods.
-      doPublicize(method);
-      return false;
-    }
-
-    // We can't publicize private instance methods in interfaces or methods that are copied from
-    // interfaces to lambda-desugared classes because this will be added as a new default method.
-    // TODO(b/111118390): It might be possible to transform it into static methods, though.
-    if (method.getHolder().isInterface() || accessFlags.isSynthetic()) {
-      return false;
-    }
-
-    boolean wasSeen = methodPoolCollection.markIfNotSeen(method.getHolder(), method.getReference());
-    if (wasSeen) {
-      // We can't do anything further because even renaming is not allowed due to the keep rule.
-      if (!appView.appInfo().isMinificationAllowed(method)) {
-        return false;
-      }
-      // TODO(b/111118390): Renaming will enable more private instance methods to be publicized.
-      return false;
-    }
-    lensBuilder.add(method.getReference());
-    accessFlags.promoteToFinal();
-    doPublicize(method);
-    // The method just became public and is therefore not a library override.
-    definition.setLibraryMethodOverride(OptionalBool.FALSE);
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
new file mode 100644
index 0000000..ea25b09
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
@@ -0,0 +1,314 @@
+// 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.optimize.accessmodification;
+
+import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
+import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
+import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.FieldAccessInfo;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierTraversal.BottomUpTraversalState;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
+import com.android.tools.r8.optimize.utils.ConcurrentNonProgramMethodsCollection;
+import com.android.tools.r8.optimize.utils.NonProgramMethodsCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class AccessModifier {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+  private final AccessModifierLens.Builder lensBuilder = AccessModifierLens.builder();
+  private final NonProgramMethodsCollection nonProgramMethodsCollection;
+  private final InternalOptions options;
+
+  private AccessModifier(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.immediateSubtypingInfo =
+        ImmediateProgramSubtypingInfo.createWithDeterministicOrder(appView);
+    this.nonProgramMethodsCollection =
+        ConcurrentNonProgramMethodsCollection.createVirtualMethodsCollection(appView);
+    this.options = appView.options();
+  }
+
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    timing.begin("Access modification");
+    if (appView.options().getAccessModifierOptions().isAccessModificationEnabled()) {
+      new AccessModifier(appView)
+          .processStronglyConnectedComponents(executorService)
+          .installLens(executorService, timing);
+    }
+    timing.end();
+  }
+
+  private AccessModifier processStronglyConnectedComponents(ExecutorService executorService)
+      throws ExecutionException {
+    // Compute the connected program classes and process the components in parallel.
+    List<Set<DexProgramClass>> stronglyConnectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
+    ThreadUtils.processItems(
+        stronglyConnectedComponents, this::processStronglyConnectedComponent, executorService);
+    return this;
+  }
+
+  private void processStronglyConnectedComponent(Set<DexProgramClass> stronglyConnectedComponent) {
+    // Perform a top-down traversal over the class hierarchy.
+    new AccessModifierTraversal(
+            appView,
+            immediateSubtypingInfo,
+            this,
+            AccessModifierNamingState.createInitialNamingState(
+                appView, stronglyConnectedComponent, nonProgramMethodsCollection))
+        .run(ListUtils.sort(stronglyConnectedComponent, Comparator.comparing(DexClass::getType)));
+  }
+
+  private void installLens(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    if (!lensBuilder.isEmpty()) {
+      appView.rewriteWithLens(lensBuilder.build(appView), executorService, timing);
+    }
+  }
+
+  // Publicizing of classes and members.
+
+  void processClass(
+      DexProgramClass clazz,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    publicizeClass(clazz);
+    publicizeFields(clazz);
+    publicizeMethods(clazz, namingState, traversalState);
+    // TODO(b/278736230): Also finalize classes and methods here.
+    finalizeFields(clazz);
+  }
+
+  private void publicizeClass(DexProgramClass clazz) {
+    if (isAccessModificationAllowed(clazz) && !clazz.getAccessFlags().isPublic()) {
+      clazz.getAccessFlags().promoteToPublic();
+    }
+
+    // Update inner class attribute.
+    // TODO(b/285494837): Carry-over from the legacy access modifier. We should never publicize
+    //  items unconditionally, but account for keep info.
+    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
+    if (attr != null) {
+      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
+      clazz.replaceInnerClassAttributeForThisClass(
+          new InnerClassAttribute(
+              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
+    }
+  }
+
+  private void publicizeFields(DexProgramClass clazz) {
+    clazz.forEachProgramField(this::publicizeField);
+  }
+
+  private void publicizeField(ProgramField field) {
+    if (isAccessModificationAllowed(field) && !field.getAccessFlags().isPublic()) {
+      field.getAccessFlags().promoteToPublic();
+    }
+  }
+
+  private void publicizeMethods(
+      DexProgramClass clazz,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    // Create a local naming state to keep track of the methods present on the current class.
+    // Start by reserving the pinned method signatures on the current class.
+    BiMap<DexMethod, DexMethod> localNamingState = HashBiMap.create();
+    clazz.forEachProgramMethod(
+        method -> {
+          if (!method.getDefinition().isInitializer() && !isRenamingAllowed(method)) {
+            localNamingState.put(method.getReference(), method.getReference());
+          }
+        });
+    clazz
+        .getMethodCollection()
+        .<ProgramMethod>replaceClassAndMethods(
+            method -> publicizeMethod(method, localNamingState, namingState, traversalState));
+  }
+
+  private DexEncodedMethod publicizeMethod(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    if (accessFlags.isPublic() || !isAccessModificationAllowed(method)) {
+      return commitMethod(method, localNamingState, namingState);
+    }
+
+    if (method.getDefinition().isInstanceInitializer()
+        || (accessFlags.isPackagePrivate()
+            && !traversalState.hasIllegalOverrideOfPackagePrivateMethod(method))
+        || accessFlags.isProtected()) {
+      method.getAccessFlags().promoteToPublic();
+      return commitMethod(method, localNamingState, namingState);
+    }
+
+    if (accessFlags.isPrivate()) {
+      if (isRenamingAllowed(method)) {
+        method.getAccessFlags().promoteToPublic();
+        return commitMethod(method, localNamingState, namingState);
+      }
+      assert localNamingState.containsKey(method.getReference());
+      assert localNamingState.get(method.getReference()) == method.getReference();
+      if (namingState.isFree(method.getMethodSignature())) {
+        method.getAccessFlags().promoteToPublic();
+        namingState.addBlockedMethodSignature(method.getMethodSignature());
+      }
+      return commitMethod(method, method.getReference());
+    }
+
+    // TODO(b/279126633): Add support for publicizing package-private methods by renaming.
+    assert accessFlags.isPackagePrivate();
+    assert traversalState.hasIllegalOverrideOfPackagePrivateMethod(method);
+    return commitMethod(method, localNamingState, namingState);
+  }
+
+  private DexMethod getAndReserveNewMethodReference(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState) {
+    if (method.getDefinition().isInitializer()) {
+      return method.getReference();
+    }
+    if (!isRenamingAllowed(method)) {
+      assert localNamingState.containsKey(method.getReference());
+      assert localNamingState.get(method.getReference()) == method.getReference();
+      assert method.getAccessFlags().isPrivate()
+          || method
+              .getMethodSignature()
+              .equals(namingState.getReservedSignature(method.getMethodSignature()));
+      return method.getReference();
+    }
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (method.getAccessFlags().isPrivate()) {
+      // Find a fresh method name and reserve it for the current class.
+      DexMethod newMethodReference =
+          dexItemFactory.createFreshMethodNameWithoutHolder(
+              method.getName().toString(),
+              method.getProto(),
+              method.getHolderType(),
+              candidate ->
+                  !localNamingState.containsValue(candidate)
+                      && namingState.isFree(candidate.getSignature()));
+      localNamingState.put(method.getReference(), newMethodReference);
+      return newMethodReference;
+    }
+    // Check if a mapping already exists for this method signature.
+    if (!method.getAccessFlags().isPromotedFromPrivateToPublic()) {
+      DexMethodSignature reservedSignature =
+          namingState.getReservedSignature(method.getMethodSignature());
+      if (reservedSignature != null) {
+        return reservedSignature.withHolder(method, appView.dexItemFactory());
+      }
+    }
+    // Find a fresh method name and block/reserve it globally.
+    DexMethod newMethodReference =
+        dexItemFactory.createFreshMethodNameWithoutHolder(
+            method.getName().toString(),
+            method.getProto(),
+            method.getHolderType(),
+            candidate ->
+                !localNamingState.containsValue(candidate)
+                    && namingState.isFree(candidate.getSignature()));
+    if (method.getAccessFlags().belongsToVirtualPool()) {
+      if (method.getAccessFlags().isPromotedFromPrivateToPublic()) {
+        namingState.addBlockedMethodSignature(newMethodReference.getSignature());
+      } else {
+        namingState.addRenaming(method.getMethodSignature(), newMethodReference.getSignature());
+      }
+    }
+    return newMethodReference;
+  }
+
+  private boolean isAccessModificationAllowed(ProgramDefinition definition) {
+    // TODO(b/278687711): Also check that the definition does not have any illegal accesses to it.
+    return appView.getKeepInfo(definition).isAccessModificationAllowed(options);
+  }
+
+  private boolean isRenamingAllowed(ProgramMethod method) {
+    KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+    return keepInfo.isOptimizationAllowed(options) && keepInfo.isShrinkingAllowed(options);
+  }
+
+  private DexEncodedMethod commitMethod(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState) {
+    return commitMethod(
+        method, getAndReserveNewMethodReference(method, localNamingState, namingState));
+  }
+
+  private DexEncodedMethod commitMethod(ProgramMethod method, DexMethod newMethodReference) {
+    DexProgramClass holder = method.getHolder();
+    if (newMethodReference != method.getReference()) {
+      lensBuilder.recordMove(method.getReference(), newMethodReference);
+      method =
+          new ProgramMethod(
+              holder, method.getDefinition().toTypeSubstitutedMethod(newMethodReference));
+    }
+    if (method.getAccessFlags().isPromotedFromPrivateToPublic()
+        && method.getAccessFlags().belongsToVirtualPool()) {
+      lensBuilder.addPublicizedPrivateVirtualMethod(method.getHolder(), newMethodReference);
+      method.getDefinition().setLibraryMethodOverride(OptionalBool.FALSE);
+    }
+    return method.getDefinition();
+  }
+
+  // Finalization of classes and members.
+
+  private void finalizeFields(DexProgramClass clazz) {
+    clazz.forEachProgramField(this::finalizeField);
+  }
+
+  private void finalizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    FieldAccessInfo accessInfo =
+        appView.appInfo().getFieldAccessInfoCollection().get(field.getReference());
+    if (!appView.getKeepInfo(field).isPinned(options)
+        && !accessInfo.hasReflectiveWrite()
+        && !accessInfo.isWrittenFromMethodHandle()
+        && accessInfo.isWrittenOnlyInMethodSatisfying(
+            method ->
+                method.getDefinition().isInitializer()
+                    && method.getAccessFlags().isStatic() == flags.isStatic()
+                    && method.getHolder() == field.getHolder())
+        && !flags.isFinal()
+        && !flags.isVolatile()) {
+      flags.promoteToFinal();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
new file mode 100644
index 0000000..76da486
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.accessmodification;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class AccessModifierLens extends DefaultNonIdentityGraphLens {
+
+  private final BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap;
+
+  // Private interface methods that have been publicized. Invokes targeting these methods must be
+  // rewritten from invoke-direct to invoke-interface.
+  private final Set<DexMethod> publicizedPrivateInterfaceMethods;
+
+  // Private class methods that have been publicized. Invokes targeting these methods must be
+  // rewritten from invoke-direct to invoke-virtual.
+  private final Set<DexMethod> publicizedPrivateVirtualMethods;
+
+  AccessModifierLens(
+      AppView<AppInfoWithLiveness> appView,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap,
+      Set<DexMethod> publicizedPrivateInterfaceMethods,
+      Set<DexMethod> publicizedPrivateVirtualMethods) {
+    super(appView);
+    this.methodMap = methodMap;
+    this.publicizedPrivateInterfaceMethods = publicizedPrivateInterfaceMethods;
+    this.publicizedPrivateVirtualMethods = publicizedPrivateVirtualMethods;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public DexMethod getNextMethodSignature(DexMethod method) {
+    return methodMap.getOrDefault(method, method);
+  }
+
+  @Override
+  public DexMethod getPreviousMethodSignature(DexMethod method) {
+    return methodMap.getRepresentativeKeyOrDefault(method, method);
+  }
+
+  @Override
+  public MethodLookupResult internalDescribeLookupMethod(
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    assert !previous.hasReboundReference();
+    DexMethod newMethod = getNextMethodSignature(previous.getReference());
+    InvokeType newInvokeType = previous.getType();
+    if (previous.getType() == InvokeType.DIRECT) {
+      if (publicizedPrivateInterfaceMethods.contains(newMethod)) {
+        newInvokeType = InvokeType.INTERFACE;
+      } else if (publicizedPrivateVirtualMethods.contains(newMethod)) {
+        newInvokeType = InvokeType.VIRTUAL;
+      }
+    }
+    if (newInvokeType != previous.getType() || newMethod != previous.getReference()) {
+      return MethodLookupResult.builder(this)
+          .setReference(newMethod)
+          .setPrototypeChanges(previous.getPrototypeChanges())
+          .setType(newInvokeType)
+          .build();
+    }
+    return previous;
+  }
+
+  public static class Builder {
+
+    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMap =
+        new BidirectionalOneToOneHashMap<>();
+    private final Set<DexMethod> publicizedPrivateInterfaceMethods = Sets.newConcurrentHashSet();
+    private final Set<DexMethod> publicizedPrivateVirtualMethods = Sets.newConcurrentHashSet();
+
+    public Builder addPublicizedPrivateVirtualMethod(DexProgramClass holder, DexMethod method) {
+      if (holder.isInterface()) {
+        publicizedPrivateInterfaceMethods.add(method);
+      } else {
+        publicizedPrivateVirtualMethods.add(method);
+      }
+      return this;
+    }
+
+    public Builder recordMove(DexMethod from, DexMethod to) {
+      assert from != to;
+      synchronized (methodMap) {
+        methodMap.put(from, to);
+      }
+      return this;
+    }
+
+    public boolean isEmpty() {
+      return methodMap.isEmpty()
+          && publicizedPrivateInterfaceMethods.isEmpty()
+          && publicizedPrivateVirtualMethods.isEmpty();
+    }
+
+    public AccessModifierLens build(AppView<AppInfoWithLiveness> appView) {
+      assert !isEmpty();
+      return new AccessModifierLens(
+          appView, methodMap, publicizedPrivateInterfaceMethods, publicizedPrivateVirtualMethods);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java
new file mode 100644
index 0000000..cae79be
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java
@@ -0,0 +1,86 @@
+// 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.optimize.accessmodification;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ClasspathOrLibraryClass;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.optimize.utils.NonProgramMethodsCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.collections.DexMethodSignatureBiMap;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class AccessModifierNamingState {
+
+  // The set of private method signatures that have been publicized. These method signatures are
+  // "blocked" to ensure that virtual methods with the same method signature are given a different
+  // name.
+  private final DexMethodSignatureSet blockedMethodSignatures = DexMethodSignatureSet.create();
+
+  // Records which method signatures in the component have been mapped to. This uses a bidirectional
+  // map to allow efficiently finding a fresh method signature in the component.
+  private final DexMethodSignatureBiMap<DexMethodSignature> reservedMethodSignatures;
+
+  private AccessModifierNamingState(
+      DexMethodSignatureBiMap<DexMethodSignature> reservedMethodSignatures) {
+    this.reservedMethodSignatures = reservedMethodSignatures;
+  }
+
+  static AccessModifierNamingState createInitialNamingState(
+      AppView<AppInfoWithLiveness> appView,
+      Set<DexProgramClass> stronglyConnectedComponent,
+      NonProgramMethodsCollection nonProgramMethodsCollection) {
+    DexMethodSignatureBiMap<DexMethodSignature> reservedSignatures =
+        new DexMethodSignatureBiMap<>();
+    Set<ClasspathOrLibraryClass> seenNonProgramClasses = Sets.newIdentityHashSet();
+    for (DexProgramClass clazz : stronglyConnectedComponent) {
+      // Reserve the signatures that are pinned in this class.
+      clazz.forEachProgramMethodMatching(
+          method -> !method.isInstanceInitializer() && !method.getAccessFlags().isPrivate(),
+          method -> {
+            KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+            InternalOptions options = appView.options();
+            if (!keepInfo.isOptimizationAllowed(options) || !keepInfo.isShrinkingAllowed(options)) {
+              DexMethodSignature methodSignature = method.getMethodSignature();
+              reservedSignatures.put(methodSignature, methodSignature);
+            }
+          });
+      // Reserve the signatures in the library.
+      clazz.forEachImmediateSuperClassMatching(
+          appView,
+          (supertype, superclass) ->
+              superclass != null
+                  && !superclass.isProgramClass()
+                  && seenNonProgramClasses.add(superclass.asClasspathOrLibraryClass()),
+          (supertype, superclass) ->
+              reservedSignatures.putAllToIdentity(
+                  nonProgramMethodsCollection.getOrComputeNonProgramMethods(
+                      superclass.asClasspathOrLibraryClass())));
+    }
+    return new AccessModifierNamingState(reservedSignatures);
+  }
+
+  void addBlockedMethodSignature(DexMethodSignature signature) {
+    blockedMethodSignatures.add(signature);
+  }
+
+  void addRenaming(DexMethodSignature signature, DexMethodSignature newSignature) {
+    reservedMethodSignatures.put(signature, newSignature);
+  }
+
+  DexMethodSignature getReservedSignature(DexMethodSignature signature) {
+    return reservedMethodSignatures.get(signature);
+  }
+
+  boolean isFree(DexMethodSignature signature) {
+    return !blockedMethodSignatures.contains(signature)
+        && !reservedMethodSignatures.containsValue(signature);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
new file mode 100644
index 0000000..a9d69d6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
@@ -0,0 +1,21 @@
+// 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.optimize.accessmodification;
+
+import com.android.tools.r8.utils.InternalOptions;
+
+public class AccessModifierOptions {
+
+  private InternalOptions options;
+
+  public AccessModifierOptions(InternalOptions options) {
+    this.options = options;
+  }
+
+  public boolean isAccessModificationEnabled() {
+    return options.hasProguardConfiguration()
+        && options.getProguardConfiguration().isAccessModificationAllowed();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
new file mode 100644
index 0000000..1fef67f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
@@ -0,0 +1,177 @@
+// 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.optimize.accessmodification;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+class AccessModifierTraversal extends DepthFirstTopDownClassHierarchyTraversal {
+
+  private final AccessModifier accessModifier;
+  private final AccessModifierNamingState namingState;
+
+  private final Map<DexType, TraversalState> states = new IdentityHashMap<>();
+
+  AccessModifierTraversal(
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      AccessModifier accessModifier,
+      AccessModifierNamingState namingState) {
+    super(appView, immediateSubtypingInfo);
+    this.accessModifier = accessModifier;
+    this.namingState = namingState;
+  }
+
+  /** Predicate that specifies which program classes the depth-first traversal should start from. */
+  @Override
+  public boolean isRoot(DexProgramClass clazz) {
+    return Iterables.all(
+        clazz.allImmediateSupertypes(),
+        supertype -> asProgramClassOrNull(appView.definitionFor(supertype)) == null);
+  }
+
+  /** Called when {@param clazz} is visited for the first time during the downwards traversal. */
+  @Override
+  public void visit(DexProgramClass clazz) {
+    // TODO(b/279126633): Store a top down traversal state for the current class, which contains the
+    //  protected and public method signatures when traversing downwards to enable publicizing of
+    //  package private methods with illegal overrides.
+    states.put(clazz.getType(), TopDownTraversalState.empty());
+  }
+
+  /** Called during backtracking when all subclasses of {@param clazz} have been processed. */
+  @Override
+  public void prune(DexProgramClass clazz) {
+    // Remove the traversal state since all subclasses have now been processed.
+    states.remove(clazz.getType());
+
+    // Remove and join the bottom up traversal states of the subclasses.
+    BottomUpTraversalState state = new BottomUpTraversalState();
+    forEachSubClass(
+        clazz,
+        subclass -> {
+          BottomUpTraversalState subState =
+              MapUtils.removeOrDefault(states, subclass.getType(), BottomUpTraversalState.empty())
+                  .asBottomUpTraversalState();
+          state.add(subState);
+        });
+
+    // Apply access modification to the class and its members.
+    accessModifier.processClass(clazz, namingState, state);
+
+    // Add the methods of the current class.
+    clazz.forEachProgramVirtualMethod(state::addMethod);
+
+    // Store the bottom up traversal state for the current class.
+    if (state.isEmpty()) {
+      states.remove(clazz.getType());
+    } else {
+      states.put(clazz.getType(), state);
+    }
+  }
+
+  abstract static class TraversalState {
+
+    BottomUpTraversalState asBottomUpTraversalState() {
+      return null;
+    }
+
+    TopDownTraversalState asTopDownTraversalState() {
+      return null;
+    }
+  }
+
+  // TODO(b/279126633): Collect the protected and public method signatures when traversing downwards
+  //  to enable publicizing of package private methods with illegal overrides.
+  static class TopDownTraversalState extends TraversalState {
+
+    private static final TopDownTraversalState EMPTY = new TopDownTraversalState();
+
+    static TopDownTraversalState empty() {
+      return EMPTY;
+    }
+
+    @Override
+    TopDownTraversalState asTopDownTraversalState() {
+      return this;
+    }
+
+    boolean isEmpty() {
+      return true;
+    }
+  }
+
+  static class BottomUpTraversalState extends TraversalState {
+
+    private static final BottomUpTraversalState EMPTY =
+        new BottomUpTraversalState(DexMethodSignatureMap.empty());
+
+    // The set of non-private virtual methods below the current class.
+    DexMethodSignatureMap<Set<String>> nonPrivateVirtualMethods;
+
+    BottomUpTraversalState() {
+      this(DexMethodSignatureMap.create());
+    }
+
+    BottomUpTraversalState(DexMethodSignatureMap<Set<String>> packagePrivateMethods) {
+      this.nonPrivateVirtualMethods = packagePrivateMethods;
+    }
+
+    static BottomUpTraversalState empty() {
+      return EMPTY;
+    }
+
+    @Override
+    BottomUpTraversalState asBottomUpTraversalState() {
+      return this;
+    }
+
+    void add(BottomUpTraversalState backtrackingState) {
+      backtrackingState.nonPrivateVirtualMethods.forEach(
+          (methodSignature, packageDescriptors) ->
+              this.nonPrivateVirtualMethods
+                  .computeIfAbsent(methodSignature, ignoreKey(HashSet::new))
+                  .addAll(packageDescriptors));
+    }
+
+    void addMethod(ProgramMethod method) {
+      assert method.getDefinition().belongsToVirtualPool();
+      nonPrivateVirtualMethods
+          .computeIfAbsent(method.getMethodSignature(), ignoreKey(Sets::newIdentityHashSet))
+          .add(method.getHolderType().getPackageDescriptor());
+    }
+
+    boolean hasIllegalOverrideOfPackagePrivateMethod(ProgramMethod method) {
+      assert method.getAccessFlags().isPackagePrivate();
+      String methodPackageDescriptor = method.getHolderType().getPackageDescriptor();
+      return Iterables.any(
+          nonPrivateVirtualMethods.getOrDefault(
+              method.getMethodSignature(), Collections.emptySet()),
+          methodOverridePackageDescriptor ->
+              !methodOverridePackageDescriptor.equals(methodPackageDescriptor));
+    }
+
+    boolean isEmpty() {
+      return nonPrivateVirtualMethods.isEmpty();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
index 4441690..553962d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
@@ -160,7 +160,7 @@
 
   @Override
   public DexField fixupFieldReference(DexField field) {
-    return graphLens.internalGetNextFieldSignature(field);
+    return graphLens.getNextFieldSignature(field);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
index 42213aa..af130e8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
@@ -86,21 +86,6 @@
   }
 
   @Override
-  public DexMethod getPreviousMethodSignature(DexMethod method) {
-    return super.getPreviousMethodSignature(method);
-  }
-
-  @Override
-  public DexField internalGetNextFieldSignature(DexField field) {
-    return super.internalGetNextFieldSignature(field);
-  }
-
-  @Override
-  public DexMethod getNextMethodSignature(DexMethod method) {
-    return super.getNextMethodSignature(method);
-  }
-
-  @Override
   protected InvokeType mapInvocationType(
       DexMethod newMethod, DexMethod originalMethod, InvokeType type) {
     return hasPrototypeChanges(newMethod)
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
index acdb13b..aa0bde9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
@@ -230,7 +230,7 @@
 
       ProgramField resolvedField = resolutionResult.getSingleProgramField();
       DexField rewrittenFieldReference =
-          graphLens.internalGetNextFieldSignature(resolvedField.getReference());
+          graphLens.getNextFieldSignature(resolvedField.getReference());
       if (rewrittenFieldReference != resolvedField.getReference()) {
         markAffected();
       }
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
index 69f9248..694096a 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
-import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import java.util.Set;
 
@@ -24,12 +23,6 @@
   }
 
   @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    DexMethod renamedMethod = getPrevious().getRenamedMethodSignature(originalMethod, applied);
-    return getNextMethodSignature(renamedMethod);
-  }
-
-  @Override
   public DexMethod getPreviousMethodSignature(DexMethod method) {
     Set<DexMethod> bridges = bridgeToHoistedBridgeMap.getKeys(method);
     return bridges.isEmpty() ? method : bridges.iterator().next();
diff --git a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
index 3307010..9de23a8 100644
--- a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
@@ -33,13 +33,8 @@
   // Methods.
 
   @Override
-  public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
-    if (this == applied) {
-      return originalMethod;
-    }
-    DexMethod previousMethodSignature =
-        getPrevious().getRenamedMethodSignature(originalMethod, applied);
-    return methodMap.getOrDefault(previousMethodSignature, previousMethodSignature);
+  public DexMethod getNextMethodSignature(DexMethod method) {
+    return methodMap.getOrDefault(method, method);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
index 7a462ed..7abe044 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
@@ -16,20 +16,18 @@
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import java.util.Map;
 
 public class RepackagingLens extends NestedGraphLens {
 
-  private final BiMap<DexType, DexType> newTypes;
+  private final BidirectionalOneToOneMap<DexType, DexType> newTypes;
   private final Map<String, String> packageRenamings;
 
   private RepackagingLens(
       AppView<AppInfoWithLiveness> appView,
       BidirectionalOneToOneMap<DexField, DexField> newFieldSignatures,
       BidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures,
-      BiMap<DexType, DexType> newTypes,
+      BidirectionalOneToOneMap<DexType, DexType> newTypes,
       Map<String, String> packageRenamings) {
     super(appView, newFieldSignatures, newMethodSignatures, newTypes);
     this.newTypes = newTypes;
@@ -42,12 +40,6 @@
   }
 
   @Override
-  public DexType getOriginalType(DexType type) {
-    DexType previous = newTypes.inverse().getOrDefault(type, type);
-    return getPrevious().getOriginalType(previous);
-  }
-
-  @Override
   public <T extends DexReference> boolean isSimpleRenaming(T from, T to) {
     if (from == to) {
       assert false : "The from and to references should not be equal";
@@ -87,7 +79,8 @@
 
   public static class Builder {
 
-    protected final BiMap<DexType, DexType> newTypes = HashBiMap.create();
+    protected final MutableBidirectionalOneToOneMap<DexType, DexType> newTypes =
+        new BidirectionalOneToOneHashMap<>();
     protected final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
         new BidirectionalOneToOneHashMap<>();
     protected final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures =
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 79165d1..48312bd 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
@@ -47,7 +47,7 @@
   EnqueuerDeferredTracingRewriter(AppView<? extends AppInfoWithClassHierarchy> appView) {
     this.appView = appView;
     this.codeRewriter = new CodeRewriter(appView);
-    this.deadCodeRemover = new DeadCodeRemover(appView, codeRewriter);
+    this.deadCodeRemover = new DeadCodeRemover(appView);
   }
 
   public CodeRewriter getCodeRewriter() {
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 4c152b6..a7106fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -68,8 +69,6 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
-import com.android.tools.r8.ir.optimize.MethodPoolCollection;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.synthetic.AbstractSynthesizedCode;
@@ -84,7 +83,9 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
@@ -157,7 +158,6 @@
   private final InternalOptions options;
   private final SubtypingInfo subtypingInfo;
   private final ExecutorService executorService;
-  private final MethodPoolCollection methodPoolCollection;
   private final Timing timing;
   private Collection<DexMethod> invokes;
   private final AndroidApiLevelCompute apiLevelCompute;
@@ -168,8 +168,8 @@
   private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
 
   // Map from source class to target class.
-  private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
-      BidirectionalManyToOneHashMap.newIdentityHashMap();
+  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
+      BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
 
   private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedInterfaces =
       BidirectionalManyToOneHashMap.newIdentityHashMap();
@@ -197,7 +197,6 @@
     this.mainDexInfo = appInfo.getMainDexInfo();
     this.subtypingInfo = appInfo.computeSubtypingInfo();
     this.executorService = executorService;
-    this.methodPoolCollection = new MethodPoolCollection(appView, subtypingInfo);
     this.lensBuilder = new VerticalClassMergerGraphLens.Builder(appView.dexItemFactory());
     this.apiLevelCompute = appView.apiLevelCompute();
     this.timing = timing;
@@ -998,21 +997,17 @@
           // due to the way invoke-super works on default interface methods. In order to be able
           // to hit this method directly after the merge, we need to make it public, and find a
           // method name that does not collide with one in the hierarchy of this class.
-          MemberPool<DexMethod> methodPoolForTarget =
-              methodPoolCollection.buildForHierarchy(target, executorService, timing);
-          resultingMethod =
-              renameMethod(
-                  virtualMethod,
-                  method ->
-                      availableMethodSignatures.test(method)
-                          && !methodPoolForTarget.hasSeen(
-                              MethodSignatureEquivalence.get().wrap(method)),
-                  Rename.ALWAYS,
-                  appView.dexItemFactory().prependHolderToProto(virtualMethod.getReference()));
+          DexItemFactory dexItemFactory = appView.dexItemFactory();
+          String resultingMethodBaseName =
+              virtualMethod.getName().toString() + '$' + source.getTypeName().replace('.', '$');
+          DexMethod resultingMethodReference =
+              dexItemFactory.createMethod(
+                  target.getType(),
+                  virtualMethod.getProto().prependParameter(source.getType(), dexItemFactory),
+                  dexItemFactory.createGloballyFreshMemberString(resultingMethodBaseName));
+          assert availableMethodSignatures.test(resultingMethodReference);
+          resultingMethod = virtualMethod.toTypeSubstitutedMethod(resultingMethodReference);
           makeStatic(resultingMethod);
-
-          // Update method pool collection now that we are adding a new public method.
-          methodPoolForTarget.seen(resultingMethod.getReference());
         } else {
           // This virtual method could be called directly from a sub class via an invoke-super in-
           // struction. Therefore, we translate this virtual method into an instance method with a
@@ -1891,36 +1886,31 @@
     }
 
     @Override
-    public DexType getOriginalType(DexType type) {
-      throw new Unreachable();
-    }
-
-    @Override
     public Iterable<DexType> getOriginalTypes(DexType type) {
       throw new Unreachable();
     }
 
     @Override
-    public DexField getOriginalFieldSignature(DexField field) {
+    public DexType getPreviousClassType(DexType type) {
       throw new Unreachable();
     }
 
     @Override
-    public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) {
+    public final DexType getNextClassType(DexType type) {
+      return type == source ? target.type : mergedClasses.getOrDefault(type, type);
+    }
+
+    @Override
+    public DexField getPreviousFieldSignature(DexField field) {
       throw new Unreachable();
     }
 
     @Override
-    public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
+    public DexField getNextFieldSignature(DexField field) {
       throw new Unreachable();
     }
 
     @Override
-    public final DexType internalDescribeLookupClassType(DexType previous) {
-      return previous == source ? target.type : mergedClasses.getOrDefault(previous, previous);
-    }
-
-    @Override
     public DexMethod getPreviousMethodSignature(DexMethod method) {
       throw new Unreachable();
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
index 3aeaad0..0f89af1 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -83,7 +83,7 @@
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> newMethodSignatures,
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges) {
-    super(appView, fieldMap, methodMap, mergedClasses.getForwardMap(), newMethodSignatures);
+    super(appView, fieldMap, methodMap, mergedClasses.getBidirectionalMap(), newMethodSignatures);
     this.appView = appView;
     this.mergedClasses = mergedClasses;
     this.contextualVirtualToDirectMethodMaps = contextualVirtualToDirectMethodMaps;
@@ -98,6 +98,11 @@
   }
 
   @Override
+  public DexType getPreviousClassType(DexType type) {
+    return type;
+  }
+
+  @Override
   protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
     Collection<DexType> originalTypes = mergedClasses.getSourcesFor(previous);
     Iterable<DexType> currentType = IterableUtils.singleton(previous);
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 9ed366e..462895f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -90,11 +90,16 @@
         AppView<?> appView,
         BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
         BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap,
-        Map<DexType, DexType> typeMap) {
+        BidirectionalManyToOneRepresentativeMap<DexType, DexType> typeMap) {
       super(appView, fieldMap, methodMap, typeMap);
     }
 
     @Override
+    public DexType getPreviousClassType(DexType type) {
+      return type;
+    }
+
+    @Override
     public boolean isSyntheticFinalizationGraphLens() {
       return true;
     }
@@ -111,7 +116,8 @@
         BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
     private final MutableBidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap =
         BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
-    private final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> typeMap =
+        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
 
     boolean isEmpty() {
       if (typeMap.isEmpty()) {
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 538b9ee..09f5a8c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -6,6 +6,8 @@
 import static com.android.tools.r8.utils.AndroidApiLevel.B;
 import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault;
 
+import com.android.tools.r8.AndroidResourceConsumer;
+import com.android.tools.r8.AndroidResourceProvider;
 import com.android.tools.r8.CancelCompilationChecker;
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationMode;
@@ -74,6 +76,7 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemovalOptions;
 import com.android.tools.r8.origin.Origin;
@@ -172,6 +175,8 @@
   // The state can only ever transition from false to true.
   private final AtomicBoolean cancelled = new AtomicBoolean(false);
   public CancelCompilationChecker cancelCompilationChecker = null;
+  public AndroidResourceProvider androidResourceProvider = null;
+  public AndroidResourceConsumer androidResourceConsumer = null;
 
   public boolean checkIfCancelled() {
     if (cancelCompilationChecker == null) {
@@ -843,8 +848,7 @@
 
   @Override
   public boolean isAccessModificationEnabled() {
-    return getProguardConfiguration() != null
-        && getProguardConfiguration().isAccessModificationAllowed();
+    return accessModifierOptions.isAccessModificationEnabled();
   }
 
   @Override
@@ -879,6 +883,7 @@
 
   public boolean debug = false;
 
+  private final AccessModifierOptions accessModifierOptions = new AccessModifierOptions(this);
   private final RewriteArrayOptions rewriteArrayOptions = new RewriteArrayOptions();
   private final CallSiteOptimizationOptions callSiteOptimizationOptions =
       new CallSiteOptimizationOptions();
@@ -956,6 +961,10 @@
     return desugarSpecificOptions;
   }
 
+  public AccessModifierOptions getAccessModifierOptions() {
+    return accessModifierOptions;
+  }
+
   public CfCodeAnalysisOptions getCfCodeAnalysisOptions() {
     return cfCodeAnalysisOptions;
   }
@@ -2072,10 +2081,24 @@
 
   public static class TestingOptions {
 
+    public boolean roundtripThroughLir = false;
+    private boolean useLir = false;
+
+    public void enableLir() {
+      useLir = true;
+    }
+
+    public void disableLir() {
+      useLir = false;
+    }
+
+    public boolean canUseLir(AppView<?> appView) {
+      return useLir && appView.enableWholeProgramOptimizations();
+    }
+
     // If false, use the desugared library implementation when desugared library is enabled.
     public boolean alwaysBackportListSetMapMethods = true;
     public boolean neverReuseCfLocalRegisters = false;
-    public boolean roundtripThroughLir = false;
     public boolean checkReceiverAlwaysNullInCallSiteOptimization = true;
     public boolean forceInlineAPIConversions = false;
     public boolean ignoreValueNumbering = false;
diff --git a/src/main/java/com/android/tools/r8/utils/LensUtils.java b/src/main/java/com/android/tools/r8/utils/LensUtils.java
index 4097730..b0eae54 100644
--- a/src/main/java/com/android/tools/r8/utils/LensUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/LensUtils.java
@@ -99,9 +99,10 @@
 
   public static Set<DexEncodedMethod> rewrittenWithRenamedSignature(
       Set<DexEncodedMethod> methods, DexDefinitionSupplier definitions, GraphLens lens) {
+    GraphLens appliedLens = GraphLens.getIdentityLens();
     Set<DexEncodedMethod> result = Sets.newIdentityHashSet();
     for (DexEncodedMethod method : methods) {
-      result.add(lens.mapDexEncodedMethod(method, definitions));
+      result.add(method.rewrittenWithLens(lens, appliedLens, definitions));
     }
     return result;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
index 2961111..aca734e 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
@@ -26,6 +26,10 @@
     this.backing = backing;
   }
 
+  public BidirectionalOneToOneHashMap(Map<K, V> backing) {
+    this(HashBiMap.create(backing));
+  }
+
   @Override
   public void clear() {
     backing.clear();
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
index 0ac1e7c..a53de2f 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
@@ -87,10 +87,11 @@
   }
 
   public ProgramMethodSet rewrittenWithLens(DexDefinitionSupplier definitions, GraphLens lens) {
+    GraphLens appliedLens = GraphLens.getIdentityLens();
     List<ProgramMethod> elementsToRemove = null;
     ProgramMethodSet rewrittenMethods = null;
     for (ProgramMethod method : this) {
-      ProgramMethod rewrittenMethod = lens.mapProgramMethod(method, definitions);
+      ProgramMethod rewrittenMethod = method.rewrittenWithLens(lens, appliedLens, definitions);
       if (rewrittenMethod == null) {
         assert lens.isEnumUnboxerLens();
         // If everything has been unchanged up until now, then record that we should remove this
diff --git a/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java b/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
index aa24dde..79f582b 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
@@ -58,8 +58,12 @@
   @Override
   public SortedProgramMethodSet rewrittenWithLens(
       DexDefinitionSupplier definitions, GraphLens lens) {
+    GraphLens appliedLens = GraphLens.getIdentityLens();
     return create(
-        consumer -> forEach(method -> consumer.accept(lens.mapProgramMethod(method, definitions))));
+        consumer ->
+            forEach(
+                method ->
+                    consumer.accept(method.rewrittenWithLens(lens, appliedLens, definitions))));
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 22f879d..fa1d579 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -132,10 +132,11 @@
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
+        .withOptionConsumer(o -> o.testing.enableLir())
         .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 1, "lambdadesugaring"))
         .run();
   }
 
@@ -172,9 +173,10 @@
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
         .withMinApiLevel(AndroidApiLevel.N)
+        .withOptionConsumer(opts -> opts.testing.enableLir())
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 1, "lambdadesugaring"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 2d41bdb..069acbf 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -593,7 +593,9 @@
     }
 
     public void cacheResult(ProcessResult result) {
-      if (useCache()) {
+      // Only cache succeding runs, otherwise a flaky or killed art run can
+      // put invalid entries into the cache.
+      if (useCache() && result.exitCode == 0) {
         assert artResultCacheLookupKey != null;
         CommandResultCache.getInstance().putResult(result, artResultCacheLookupKey);
       }
@@ -827,19 +829,19 @@
           .put(DexVm.ART_6_0_1_HOST, "bin/art")
           .build();
 
-  private static final List<String> DALVIK_BOOT_LIBS =
-      ImmutableList.of(
-          "core-libart-hostdex.jar",
-          "core-hostdex.jar",
-          "apache-xml-hostdex.jar");
+  private static final List<String> DALVIK_4_0_BOOT_LIBS =
+      ImmutableList.of("core-hostdex.jar", "apache-xml-hostdex.jar");
 
-  private static final List<String> ART_BOOT_LIBS =
-      ImmutableList.of(
-          "core-libart-hostdex.jar",
-          "core-oj-hostdex.jar",
-          "apache-xml-hostdex.jar");
+  private static final List<String> DALVIK_4_4_BOOT_LIBS =
+      ImmutableList.of("core-libart-hostdex.jar", "core-hostdex.jar", "apache-xml-hostdex.jar");
 
-  private static final List<String> NEWER_ART_BOOT_LIBS =
+  private static final List<String> ART_5_TO_6_BOOT_LIBS =
+      ImmutableList.of("core-libart-hostdex.jar");
+
+  private static final List<String> ART_7_TO_10_BOOT_LIBS =
+      ImmutableList.of("core-libart-hostdex.jar", "core-oj-hostdex.jar", "apache-xml-hostdex.jar");
+
+  private static final List<String> ART_12_PLUS_BOOT_LIBS =
       ImmutableList.of(
           "core-libart-hostdex.jar",
           "core-oj-hostdex.jar",
@@ -851,18 +853,18 @@
   static {
     ImmutableMap.Builder<DexVm, List<String>> builder = ImmutableMap.builder();
     builder
-        .put(DexVm.ART_DEFAULT, ART_BOOT_LIBS)
-        .put(DexVm.ART_14_0_0_HOST, NEWER_ART_BOOT_LIBS)
-        .put(DexVm.ART_13_0_0_HOST, NEWER_ART_BOOT_LIBS)
-        .put(DexVm.ART_12_0_0_HOST, NEWER_ART_BOOT_LIBS)
-        .put(DexVm.ART_10_0_0_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_9_0_0_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_8_1_0_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_7_0_0_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_6_0_1_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_5_1_1_HOST, ART_BOOT_LIBS)
-        .put(DexVm.ART_4_4_4_HOST, DALVIK_BOOT_LIBS)
-        .put(DexVm.ART_4_0_4_HOST, DALVIK_BOOT_LIBS);
+        .put(DexVm.ART_DEFAULT, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_14_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
+        .put(DexVm.ART_13_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
+        .put(DexVm.ART_12_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
+        .put(DexVm.ART_10_0_0_HOST, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_9_0_0_HOST, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_8_1_0_HOST, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_7_0_0_HOST, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_6_0_1_HOST, ART_5_TO_6_BOOT_LIBS)
+        .put(DexVm.ART_5_1_1_HOST, ART_5_TO_6_BOOT_LIBS)
+        .put(DexVm.ART_4_4_4_HOST, DALVIK_4_4_BOOT_LIBS)
+        .put(DexVm.ART_4_0_4_HOST, DALVIK_4_0_BOOT_LIBS);
     BOOT_LIBS = builder.build();
   }
 
@@ -886,7 +888,6 @@
     PRODUCT = builder.build();
   }
 
-
   private static Path getDexVmPath(DexVm vm) {
     DexVm.Version version = vm.getVersion();
     Path base = Paths.get(TOOLS_DIR, "linux");
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
index e08fd3e..d491189 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
@@ -52,12 +52,7 @@
               assertThat(mainClassSubject, isPresent());
               assertThat(
                   mainClassSubject.uniqueFieldWithOriginalName("instanceField"),
-                  allOf(
-                      isPresent(),
-                      onlyIf(
-                          allowAccessModification
-                              && !parameters.canInitNewInstanceUsingSuperclassConstructor(),
-                          isFinal())));
+                  allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
               assertThat(
                   mainClassSubject.uniqueFieldWithOriginalName("staticField"),
                   allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/InnerClassAttributePublicizerTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/InnerClassAttributePublicizerTest.java
index 41c73cf..8c3c7c3 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/InnerClassAttributePublicizerTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/InnerClassAttributePublicizerTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.accessrelaxation;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPublic;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -52,6 +53,7 @@
   private void inspect(CodeInspector inspector) {
     ClassSubject classSubject = inspector.clazz(Outer.Inner.class);
     assertThat(classSubject, isPresent());
+    assertThat(classSubject, isPublic());
 
     InnerClassAttribute innerClassAttribute =
         classSubject.getDexProgramClass().getInnerClassAttributeForThisClass();
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/InterfaceMethodAndSiblingConsistentRenamingPublicizerTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/InterfaceMethodAndSiblingConsistentRenamingPublicizerTest.java
new file mode 100644
index 0000000..0cf8434
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/InterfaceMethodAndSiblingConsistentRenamingPublicizerTest.java
@@ -0,0 +1,129 @@
+// 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InterfaceMethodAndSiblingConsistentRenamingPublicizerTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              // Check that B.foo(), C.foo() and I.foo() are all present in the output.
+              ClassSubject bClassSubject = inspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+
+              MethodSubject bMethodSubject = bClassSubject.uniqueMethodWithOriginalName("foo");
+              assertThat(bMethodSubject, isPresent());
+
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
+
+              MethodSubject cMethodSubject = cClassSubject.uniqueMethodWithOriginalName("foo");
+              assertThat(cMethodSubject, isPresent());
+
+              ClassSubject iClassSubject = inspector.clazz(I.class);
+              assertThat(iClassSubject, isPresent());
+
+              MethodSubject iMethodSubject = iClassSubject.uniqueMethodWithOriginalName("foo");
+              assertThat(iMethodSubject, isPresent());
+
+              // Check that B.foo() is renamed to one name, and C.foo() and I.foo() is renamed to
+              // another name. The reason for B.foo() being given another name is that we use a
+              // single naming state for the classes, which means that we must give B.foo() another
+              // name to ensure we do not introduce new method overriding relationships from
+              // publicizing.
+              assertNotEquals(bMethodSubject.getFinalName(), cMethodSubject.getFinalName());
+              assertEquals(cMethodSubject.getFinalName(), iMethodSubject.getFinalName());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("B", "C", "C");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new B().foo();
+      new C().foo();
+      I i = System.currentTimeMillis() > 0 ? new D() : new E();
+      i.foo();
+    }
+  }
+
+  interface I {
+
+    void foo();
+  }
+
+  static class A {}
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class B extends A {
+
+    @NeverInline
+    private void foo() {
+      System.out.println("B");
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C extends A {
+
+    // Implements I.foo().
+    @NeverInline
+    public void foo() {
+      System.out.println("C");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class D extends C implements I {}
+
+  @NoHorizontalClassMerging
+  static class E extends C implements I {
+
+    @Override
+    public void foo() {
+      System.out.println("E");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NoRelaxationForSerializableTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NoRelaxationForSerializableTest.java
index 5c85d69..87a0a49 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NoRelaxationForSerializableTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NoRelaxationForSerializableTest.java
@@ -10,12 +10,12 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 import com.android.tools.r8.NoVerticalClassMerging;
-import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
@@ -25,12 +25,12 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.nio.file.Path;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 @NoVerticalClassMerging
 class MySerializable implements Serializable {
@@ -99,9 +99,8 @@
   );
 
   private final boolean accessModification;
-  private Path configuration;
 
-  @Parameterized.Parameters(name = "{0}, access-modification: {1}")
+  @Parameters(name = "{0}, access-modification: {1}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters()
@@ -116,25 +115,17 @@
     this.accessModification = accessModification;
   }
 
-  @Before
-  public void setUpConfiguration() throws Exception {
-    configuration = temp.newFile("pg.conf").toPath().toAbsolutePath();
-    FileUtils.writeTextFile(configuration, StringUtils.lines(
-        keepMainProguardConfiguration(MAIN),
-        accessModification ? "-allowaccessmodification" : ""
-    ));
-  }
-
   @Test
   public void testProguard_withKeepRules() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(CLASSES)
-        .addKeepRuleFiles(configuration)
+        .addKeepMainRule(MAIN)
         .addKeepRules(KEEPMEMBER_RULES)
         .addInliningAnnotations()
         .addMemberValuePropagationAnnotations()
         .addNoVerticalClassMergingAnnotations()
+        .allowAccessModification(accessModification)
         .compile()
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
@@ -143,34 +134,42 @@
 
   @Test
   public void testR8_withKeepRules() throws Exception {
-    R8TestCompileResult result =
-        testForR8(parameters.getBackend())
-            .addProgramClasses(CLASSES)
-            .addMemberValuePropagationAnnotations()
-            .addNoVerticalClassMergingAnnotations()
-            .enableInliningAnnotations()
-            .addKeepRuleFiles(configuration)
-            .addKeepRules(KEEPMEMBER_RULES)
-            .setMinApi(parameters)
-            .compile()
-            .inspect(this::inspect);
-    // TODO(b/117302947): Need to update ART binary.
-    if (parameters.isCfRuntime()) {
-      result
-          .run(parameters.getRuntime(), MAIN)
-          .assertSuccessWithOutput(EXPECTED_OUTPUT);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .addMemberValuePropagationAnnotations()
+        .addNoVerticalClassMergingAnnotations()
+        .enableInliningAnnotations()
+        .addKeepMainRule(MAIN)
+        .addKeepRules(KEEPMEMBER_RULES)
+        .allowAccessModification(accessModification)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), MAIN)
+        .applyIf(
+            parameters.isCfRuntime()
+                || parameters.getDexRuntimeVersion().isEqualToOneOf(Version.V5_1_1, Version.V8_1_0)
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V10_0_0),
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            parameters.isDexRuntimeVersion(Version.DEFAULT)
+                || parameters.isDexRuntimeVersion(Version.V6_0_1)
+                || parameters.isDexRuntimeVersion(Version.V7_0_0)
+                || parameters.isDexRuntimeVersion(Version.V9_0_0),
+            runResult -> runResult.assertFailureWithErrorThatThrows(UnsatisfiedLinkError.class),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(NoSuchAlgorithmException.class));
   }
 
   @Test
   public void testProguard_withoutKeepRules() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(CLASSES)
         .addInliningAnnotations()
         .addMemberValuePropagationAnnotations()
         .addNoVerticalClassMergingAnnotations()
-        .addKeepRuleFiles(configuration)
+        .addKeepMainRule(MAIN)
+        .allowAccessModification(accessModification)
         .compile()
         .run(parameters.getRuntime(), MAIN)
         .assertFailureWithErrorThatMatches(containsString("Could not deserialize"));
@@ -178,21 +177,30 @@
 
   @Test
   public void testR8_withoutKeepRules() throws Exception {
-    R8TestCompileResult result =
-        testForR8(parameters.getBackend())
-            .addProgramClasses(CLASSES)
-            .addNoVerticalClassMergingAnnotations()
-            .enableInliningAnnotations()
-            .enableMemberValuePropagationAnnotations()
-            .addKeepRuleFiles(configuration)
-            .setMinApi(parameters)
-            .compile();
-    // TODO(b/117302947): Need to update ART binary.
-    if (parameters.isCfRuntime()) {
-      result
-          .run(parameters.getRuntime(), MAIN)
-          .assertFailureWithErrorThatMatches(containsString("Could not deserialize"));
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .addNoVerticalClassMergingAnnotations()
+        .enableInliningAnnotations()
+        .enableMemberValuePropagationAnnotations()
+        .addKeepMainRule(MAIN)
+        .allowAccessModification(accessModification)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), MAIN)
+        .applyIf(
+            parameters.isCfRuntime()
+                || parameters.getDexRuntimeVersion().isEqualToOneOf(Version.V5_1_1, Version.V8_1_0)
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V10_0_0),
+            runResult ->
+                runResult.assertFailureWithErrorThatMatches(
+                    containsString("Could not deserialize")),
+            parameters.isDexRuntimeVersion(Version.DEFAULT)
+                || parameters.isDexRuntimeVersion(Version.V6_0_1)
+                || parameters.isDexRuntimeVersion(Version.V7_0_0)
+                || parameters.isDexRuntimeVersion(Version.V9_0_0),
+            runResult -> runResult.assertFailureWithErrorThatThrows(UnsatisfiedLinkError.class),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(NoSuchAlgorithmException.class));
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index 1c6d936..d41508e 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -47,35 +47,17 @@
   }
 
   @Test
-  public void testStaticMethodRelaxation() throws Exception {
-    String expectedOutput =
-        StringUtils.lines(
-            "A::baz()",
-            "A::bar()",
-            "A::bar(int)",
-            "A::blah(int)",
-            "B::blah(int)",
-            "BB::blah(int)",
-            "A::foo()A::baz()A::bar()A::bar(int)",
-            "B::bar() >> java.lang.IllegalAccessError",
-            "java.lang.IllegalAccessError",
-            "A::foo()A::baz()A::bar()A::bar(int)",
-            "B::blah(int)",
-            "A::foo()A::baz()A::bar()A::bar(int)",
-            "B::bar() >> java.lang.IllegalAccessError",
-            "C::bar(int)java.lang.IllegalAccessErrorB::bar() >> "
-                + "java.lang.IllegalAccessErrorB::bar() >> java.lang.IllegalAccessError",
-            "B::foo()A::foo()A::baz()A::bar()A::bar(int)",
-            "C::blah(int)");
-    Class<?> mainClass = C.class;
-    if (parameters.isCfRuntime()) {
-      // Only run JVM reference on CF runtimes.
-      testForJvm(parameters)
-          .addTestClasspath()
-          .run(parameters.getRuntime(), mainClass)
-          .assertSuccessWithOutput(expectedOutput);
-    }
+  public void testStaticMethodRelaxationJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addTestClasspath()
+        .run(parameters.getRuntime(), C.class)
+        .assertSuccessWithOutput(getExpectedOutputForStaticMethodRelaxationTest());
+  }
 
+  @Test
+  public void testStaticMethodRelaxation() throws Exception {
+    Class<?> mainClass = C.class;
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
@@ -105,9 +87,8 @@
                 "}")
             .allowAccessModification()
             .setMinApi(parameters)
-            .run(parameters.getRuntime(), mainClass);
-
-    assertEquals(expectedOutput, result.getStdOut());
+            .run(parameters.getRuntime(), mainClass)
+            .assertSuccessWithOutput(getExpectedOutputForStaticMethodRelaxationTest());
 
     CodeInspector inspector = result.inspector();
 
@@ -121,13 +102,43 @@
 
     MethodSignature blahMethodSignatureAfterArgumentRemoval =
         new MethodSignature(
-            "blah",
+            enableUnusedArgumentRemoval ? "blah$1" : "blah",
             STRING,
             enableUnusedArgumentRemoval ? ImmutableList.of() : ImmutableList.of("int"));
     assertPublic(inspector, A.class, blahMethodSignatureAfterArgumentRemoval);
     assertPublic(inspector, BB.class, blahMethodSignatureAfterArgumentRemoval);
   }
 
+  private static String getExpectedOutputForStaticMethodRelaxationTest() {
+    return StringUtils.lines(
+        "A::baz()",
+        "A::bar()",
+        "A::bar(int)",
+        "A::blah(int)",
+        "B::blah(int)",
+        "BB::blah(int)",
+        "A::foo()A::baz()A::bar()A::bar(int)",
+        "B::bar() >> java.lang.IllegalAccessError",
+        "java.lang.IllegalAccessError",
+        "A::foo()A::baz()A::bar()A::bar(int)",
+        "B::blah(int)",
+        "A::foo()A::baz()A::bar()A::bar(int)",
+        "B::bar() >> java.lang.IllegalAccessError",
+        "C::bar(int)java.lang.IllegalAccessErrorB::bar() >> "
+            + "java.lang.IllegalAccessErrorB::bar() >> java.lang.IllegalAccessError",
+        "B::foo()A::foo()A::baz()A::bar()A::bar(int)",
+        "C::blah(int)");
+  }
+
+  @Test
+  public void testInstanceMethodRelaxationJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addTestClasspath()
+        .run(parameters.getRuntime(), TestMain.class)
+        .assertSuccessWithOutput(getExpectedOutputForInstanceMethodRelaxationTest());
+  }
+
   @Test
   public void testInstanceMethodRelaxationWithVerticalClassMerging() throws Exception {
     testInstanceMethodRelaxation(true);
@@ -139,29 +150,7 @@
   }
 
   private void testInstanceMethodRelaxation(boolean enableVerticalClassMerging) throws Exception {
-    String expectedOutput =
-        StringUtils.lines(
-            "Base::foo()",
-            "Base::foo1()",
-            "Base::foo2()",
-            "Base::foo3()",
-            "Sub1::foo1()",
-            "Itf1::foo1(0) >> Sub1::foo1()",
-            "Sub1::bar1(0)",
-            "Sub1::foo3()",
-            "Sub2::foo2()",
-            "Itf2::foo2(0) >> Sub2::foo2()",
-            "Sub2::bar2(0)",
-            "Sub2::foo3()");
     Class<?> mainClass = TestMain.class;
-    if (parameters.isCfRuntime()) {
-      // Only run JVM reference on CF runtimes.
-      testForJvm(parameters)
-          .addTestClasspath()
-          .run(parameters.getRuntime(), mainClass)
-          .assertSuccessWithOutput(expectedOutput);
-    }
-
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
@@ -188,7 +177,7 @@
             .run(parameters.getRuntime(), mainClass);
 
     assertEquals(
-        expectedOutput,
+        getExpectedOutputForInstanceMethodRelaxationTest(),
         result
             .getStdOut()
             .replace("java.lang.IncompatibleClassChangeError", "java.lang.IllegalAccessError"));
@@ -199,10 +188,10 @@
     CodeInspector codeInspector = result.inspector();
     assertPublic(codeInspector, Base.class, new MethodSignature("foo", STRING, ImmutableList.of()));
 
-    // Base#foo?() can't be publicized due to Itf<1>#foo<1>().
-    assertNotPublic(
+    // Base#foo?() is publicized by renaming due to Itf<1>#foo<1>().
+    assertPublic(
         codeInspector, Base.class, new MethodSignature("foo1", STRING, ImmutableList.of()));
-    assertNotPublic(
+    assertPublic(
         codeInspector, Base.class, new MethodSignature("foo2", STRING, ImmutableList.of()));
 
     if (!enableVerticalClassMerging) {
@@ -217,4 +206,20 @@
           codeInspector, Sub2.class, new MethodSignature("bar2", STRING, ImmutableList.of("int")));
     }
   }
+
+  private static String getExpectedOutputForInstanceMethodRelaxationTest() {
+    return StringUtils.lines(
+        "Base::foo()",
+        "Base::foo1()",
+        "Base::foo2()",
+        "Base::foo3()",
+        "Sub1::foo1()",
+        "Itf1::foo1(0) >> Sub1::foo1()",
+        "Sub1::bar1(0)",
+        "Sub1::foo3()",
+        "Sub2::foo2()",
+        "Itf2::foo2(0) >> Sub2::foo2()",
+        "Sub2::bar2(0)",
+        "Sub2::foo3()");
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/PrivateOverrideOfPublicMethodTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/PrivateOverrideOfPublicMethodTest.java
new file mode 100644
index 0000000..82ceae1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/PrivateOverrideOfPublicMethodTest.java
@@ -0,0 +1,125 @@
+// 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.accessrelaxation;
+
+import static com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate.onName;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import org.junit.BeforeClass;
+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 PrivateOverrideOfPublicMethodTest extends TestBase {
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("A", "B", "IAE", "A", "B", "A");
+  private static final String EXPECTED_OUTPUT_5_TO_6 =
+      StringUtils.lines("A", "A", "A", "A", "A", "A");
+  private static final String EXPECTED_OUTPUT_7 = StringUtils.lines("A", "B", "A", "A", "B", "A");
+  private static final String EXPECTED_OUTPUT_R8 = StringUtils.lines("A", "B", "B", "A", "B", "A");
+
+  private static byte[] programClassFileData;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @BeforeClass
+  public static void setup() throws IOException {
+    programClassFileData =
+        transformer(B.class)
+            .renameMethod(onName("bar"), "foo")
+            .transformMethodInsnInMethod(
+                "<init>",
+                (opcode, owner, name, descriptor, isInterface, continuation) ->
+                    continuation.visitMethodInsn(
+                        opcode, owner, name.equals("bar") ? "foo" : name, descriptor, isInterface))
+            .transform();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, A.class)
+        .addProgramClassFileData(programClassFileData)
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V5_1_1)
+                && parameters.isDexRuntimeVersionOlderThanOrEqual(Version.V6_0_1),
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT_5_TO_6),
+            parameters.isDexRuntimeVersion(Version.V7_0_0),
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT_7),
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class)
+        .addProgramClassFileData(programClassFileData)
+        .addKeepMainRule(Main.class)
+        .allowAccessModification()
+        .enableInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/278687711): Access modifier should preserve IllegalAccessErrors.
+        .assertSuccessWithOutput(EXPECTED_OUTPUT_R8);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().foo();
+      try {
+        new B().foo();
+      } catch (IllegalAccessError e) {
+        System.out.println("IAE");
+      }
+      A a = System.currentTimeMillis() > 0 ? new A() : new B();
+      a.foo();
+      A b = System.currentTimeMillis() > 0 ? new B() : new A();
+      b.foo();
+    }
+  }
+
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("A");
+    }
+  }
+
+  static class B extends A {
+
+    B() {
+      if (System.currentTimeMillis() > 0) {
+        bar();
+      }
+    }
+
+    // Renamed to foo.
+    @NeverInline
+    private void bar() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/PrivateShadowOfPrivateMethodTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/PrivateShadowOfPrivateMethodTest.java
new file mode 100644
index 0000000..ce6a9d0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/PrivateShadowOfPrivateMethodTest.java
@@ -0,0 +1,103 @@
+// 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPublic;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PrivateShadowOfPrivateMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowAccessModification()
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              // Check that A.foo() is publicized.
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              MethodSubject aMethodSubject = aClassSubject.uniqueMethodWithOriginalName("foo");
+              assertThat(aMethodSubject, isPresent());
+              assertThat(aMethodSubject, isPublic());
+
+              // Check that B.foo is publicized.
+              ClassSubject bClassSubject = inspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+
+              MethodSubject bMethodSubject = bClassSubject.uniqueMethodWithOriginalName("foo");
+              assertThat(bMethodSubject, isPresent());
+              assertThat(bMethodSubject, isPublic());
+
+              // Verify that the two methods are given different names.
+              assertNotEquals(aMethodSubject.getFinalName(), bMethodSubject.getFinalName());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B", "A", "A");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().foo();
+      new B().foo();
+      A a = System.currentTimeMillis() > 0 ? new A() : new B();
+      a.foo();
+      A b = System.currentTimeMillis() > 0 ? new B() : new A();
+      b.foo();
+    }
+  }
+
+  @NeverClassInline
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    private void foo() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B extends A {
+
+    @NeverInline
+    private void foo() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/PublicOverrideOfPrivateMethodTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/PublicOverrideOfPrivateMethodTest.java
new file mode 100644
index 0000000..09d3456
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/PublicOverrideOfPrivateMethodTest.java
@@ -0,0 +1,91 @@
+// 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.accessrelaxation;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PublicOverrideOfPrivateMethodTest extends TestBase {
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("A", "B", "B", "A", "B", "A");
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowAccessModification()
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().foo();
+      new B().foo();
+      A a = System.currentTimeMillis() > 0 ? new A() : new B();
+      a.foo();
+      A b = System.currentTimeMillis() > 0 ? new B() : new A();
+      b.foo();
+    }
+  }
+
+  @NeverClassInline
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    private void foo() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B extends A {
+
+    B() {
+      if (System.currentTimeMillis() > 0) {
+        foo();
+      }
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Base.java b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Base.java
index 0c25396..dcc51cd 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Base.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Base.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.accessrelaxation.privateinstance;
 
+import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 
+@NeverClassInline
 public class Base {
 
   @NeverPropagateValue
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
new file mode 100644
index 0000000..b58587b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -0,0 +1,70 @@
+// 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.androidresources;
+
+import java.nio.charset.StandardCharsets;
+
+public class AndroidResourceTestingUtils {
+
+  // Taken from default empty android studio activity template
+  public static String TEST_MANIFEST =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+          + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+          + "    xmlns:tools=\"http://schemas.android.com/tools\">\n"
+          + "\n"
+          + "    <application\n"
+          + "        android:allowBackup=\"true\"\n"
+          + "        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n"
+          + "        android:fullBackupContent=\"@xml/backup_rules\"\n"
+          + "        android:icon=\"@mipmap/ic_launcher\"\n"
+          + "        android:label=\"@string/app_name\"\n"
+          + "        android:roundIcon=\"@mipmap/ic_launcher_round\"\n"
+          + "        android:supportsRtl=\"true\"\n"
+          + "        android:theme=\"@style/Theme.MyApplication\"\n"
+          + "        tools:targetApi=\"31\">\n"
+          + "        <activity\n"
+          + "            android:name=\".MainActivity\"\n"
+          + "            android:exported=\"true\"\n"
+          + "            android:label=\"@string/app_name\"\n"
+          + "            android:theme=\"@style/Theme.MyApplication.NoActionBar\">\n"
+          + "            <intent-filter>\n"
+          + "                <action android:name=\"android.intent.action.MAIN\" />\n"
+          + "\n"
+          + "                <category android:name=\"android.intent.category.LAUNCHER\" />\n"
+          + "            </intent-filter>\n"
+          + "\n"
+          + "            <meta-data\n"
+          + "                android:name=\"android.app.lib_name\"\n"
+          + "                android:value=\"\" />\n"
+          + "        </activity>\n"
+          + "    </application>\n"
+          + "\n"
+          + "</manifest>";
+
+  // TODO(287399385): Add testing utils for generating/consuming resource tables.
+  public static byte[] TEST_RESOURCE_TABLE = "RESOURCE_TABLE".getBytes(StandardCharsets.UTF_8);
+
+  // The below byte arrays are lifted from the resource shrinkers DummyContent
+
+  // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
+  public static final byte[] TINY_PNG =
+      new byte[] {
+        (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+        (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+        (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 1,
+        (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 58,
+        (byte) 126, (byte) -101, (byte) 85, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 10, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+        (byte) -38, (byte) 99, (byte) 96, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 2, (byte) 0, (byte) 1, (byte) -27, (byte) 39, (byte) -34,
+        (byte) -4, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 73,
+        (byte) 69, (byte) 78, (byte) 68, (byte) -82, (byte) 66, (byte) 96,
+        (byte) -126
+      };
+
+  // The XML document <x/> as a proto packed with AAPT2
+  public static final byte[] TINY_PROTO_XML =
+      new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
new file mode 100644
index 0000000..f0bb82a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
@@ -0,0 +1,84 @@
+// 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.androidresources;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
+import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Arrays;
+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 AndroidResourcesPassthroughTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    String manifestPath = "AndroidManifest.xml";
+    String resourcePath = "resources.pb";
+    String pngPath = "res/drawable/foo.png";
+    String xmlPath = "res/xml/bar.xml";
+    Path resources =
+        ZipBuilder.builder(temp.newFile("resources.zip").toPath())
+            .addText(manifestPath, AndroidResourceTestingUtils.TEST_MANIFEST)
+            .addBytes(resourcePath, AndroidResourceTestingUtils.TEST_RESOURCE_TABLE)
+            .addBytes(pngPath, AndroidResourceTestingUtils.TINY_PNG)
+            .addBytes(xmlPath, AndroidResourceTestingUtils.TINY_PROTO_XML)
+            .build();
+    Path output = temp.newFile("resources_out.zip").toPath();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .addOptionsModification(
+            o -> {
+              o.androidResourceProvider =
+                  new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources));
+              o.androidResourceConsumer = new ArchiveProtoAndroidResourceConsumer(output);
+            })
+        .addKeepMainRule(FooBar.class)
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccessWithOutputLines("Hello World");
+    assertTrue(
+        Arrays.equals(
+            ZipUtils.readSingleEntry(output, manifestPath),
+            AndroidResourceTestingUtils.TEST_MANIFEST.getBytes(StandardCharsets.UTF_8)));
+    assertTrue(
+        Arrays.equals(
+            ZipUtils.readSingleEntry(output, resourcePath),
+            AndroidResourceTestingUtils.TEST_RESOURCE_TABLE));
+    assertTrue(
+        Arrays.equals(
+            ZipUtils.readSingleEntry(output, pngPath), AndroidResourceTestingUtils.TINY_PNG));
+    assertTrue(
+        Arrays.equals(
+            ZipUtils.readSingleEntry(output, xmlPath), AndroidResourceTestingUtils.TINY_PROTO_XML));
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      System.out.println("Hello World");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java
index f0bc1e5..173efa1 100644
--- a/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java
@@ -68,7 +68,7 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    assertEquals(parameters.isCfRuntime() ? 1 : 2, inspector.allClasses().size());
+    assertEquals(1, inspector.allClasses().size());
   }
 
   @Test
@@ -77,6 +77,7 @@
         .addProgramFiles(getProgramFiles(target))
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_CLASS)
+        .addOptionsModification(o -> o.testing.enableLir())
         .run(parameters.getRuntime(), MAIN_CLASS)
         .inspect(this::inspect)
         .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
diff --git a/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java b/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
index 4b0b57a..d5b8f32 100644
--- a/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
+++ b/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.dex;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestCompileResult;
@@ -15,10 +16,14 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.startup.utils.MixedSectionLayoutInspector;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
@@ -31,6 +36,12 @@
   private final TestParameters parameters;
   private static final List<String> EXPECTED = ImmutableList.of("foo", "bar", "foo", "bar");
 
+  private static final int ONE_CLASS_COUNT = 4;
+  private static final int ONE_CLASS_DEDUPLICATED_COUNT = 3;
+
+  private static final int TWO_CLASS_COUNT = 6;
+  private static final int TWO_CLASS_DEDUPLICATED_COUNT = 3;
+
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withDexRuntimes().withAllApiLevels().build();
@@ -97,6 +108,14 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(Foo.class, Bar.class)
             .setMinApi(parameters)
+            .addOptionsModification(
+                options ->
+                    options
+                        .getTestingOptions()
+                        .setMixedSectionLayoutStrategyInspector(
+                            codeLayoutInspector(
+                                parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.S),
+                                true)))
             .addKeepAllClassesRule()
             .compile();
     compile.run(parameters.getRuntime(), Foo.class).assertSuccessWithOutputLines(EXPECTED);
@@ -110,6 +129,14 @@
             .addProgramClasses(Foo.class, Bar.class)
             .addKeepAttributeLineNumberTable()
             .setMinApi(parameters)
+            .addOptionsModification(
+                options ->
+                    options
+                        .getTestingOptions()
+                        .setMixedSectionLayoutStrategyInspector(
+                            codeLayoutInspector(
+                                parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.S),
+                                true)))
             .addKeepAllClassesRule()
             .compile();
     compile.run(parameters.getRuntime(), Foo.class).assertSuccessWithOutputLines(EXPECTED);
@@ -122,6 +149,14 @@
         testForD8(parameters.getBackend())
             .addProgramClasses(Foo.class, Bar.class)
             .setMinApi(parameters)
+            .addOptionsModification(
+                options ->
+                    options
+                        .getTestingOptions()
+                        .setMixedSectionLayoutStrategyInspector(
+                            codeLayoutInspector(
+                                parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.S),
+                                true)))
             .release()
             .internalEnableMappingOutput()
             .compile();
@@ -135,6 +170,11 @@
         testForD8(parameters.getBackend())
             .addProgramClasses(Foo.class, Bar.class)
             .setMinApi(parameters)
+            .addOptionsModification(
+                options ->
+                    options
+                        .getTestingOptions()
+                        .setMixedSectionLayoutStrategyInspector(codeLayoutInspector(false, true)))
             .release()
             .compile();
     compile.run(parameters.getRuntime(), Foo.class).assertSuccessWithOutputLines(EXPECTED);
@@ -142,12 +182,46 @@
     assertSizes(compile.writeToZip(), 6, 6);
   }
 
+  private MixedSectionLayoutInspector codeLayoutInspector(
+      boolean assumeDeduplicated, boolean twoClasses) {
+    return new MixedSectionLayoutInspector() {
+      @Override
+      public void inspectCodeLayout(int virtualFile, Collection<ProgramMethod> layout) {
+        // We have as many methods, the ones sharing code are just last.
+        assertTrue(layout.size() == (twoClasses ? TWO_CLASS_COUNT : ONE_CLASS_COUNT));
+
+        ArrayList<ProgramMethod> programMethods = new ArrayList<>(layout);
+        if (twoClasses) {
+          if (assumeDeduplicated) {
+            // The two init methods are shared.
+            // The two foo methods AND the bar method are shared, so they should be last.
+            // We sort by count, then fully qualified name.
+            assertTrue(programMethods.get(0).toString().contains("Foo.main"));
+            assertTrue(programMethods.get(1).toString().contains("Bar.<init>"));
+            assertTrue(programMethods.get(2).toString().contains("Foo.<init>"));
+            assertTrue(programMethods.get(3).toString().contains("Bar.foo"));
+            assertTrue(programMethods.get(4).toString().contains("Foo.bar"));
+            assertTrue(programMethods.get(5).toString().contains("Foo.foo"));
+          } else {
+            // We sort by name.
+            assertTrue(programMethods.get(0).toString().contains("Bar.<init>"));
+            assertTrue(programMethods.get(1).toString().contains("Bar.foo"));
+            assertTrue(programMethods.get(2).toString().contains("Foo.<init>"));
+            assertTrue(programMethods.get(3).toString().contains("Foo.bar"));
+            assertTrue(programMethods.get(4).toString().contains("Foo.foo"));
+            assertTrue(programMethods.get(5).toString().contains("Foo.main"));
+          }
+        }
+      }
+    };
+  }
+
   private void assertFooSizes(Path output) throws Exception {
-    assertSizes(output, 3, 4);
+    assertSizes(output, ONE_CLASS_DEDUPLICATED_COUNT, ONE_CLASS_COUNT);
   }
 
   private void assertFooAndBarSizes(Path output) throws Exception {
-    assertSizes(output, 3, 6);
+    assertSizes(output, TWO_CLASS_DEDUPLICATED_COUNT, TWO_CLASS_COUNT);
   }
 
   private void assertSizes(Path output, int deduppedSize, int originalSize)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
index 84f5003..ec1f304 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
@@ -172,14 +172,14 @@
       assertNotNull(argument);
       Value argumentValue = argument.outValue();
 
-      BasicBlock block1 = irCode.blocks.get(1);
+      BasicBlock block1 = irCode.blocks.stream().filter(b -> b.getNumber() == 1).findFirst().get();
       assertTrue(block1.exit().isIf());
 
-      BasicBlock block3 = irCode.blocks.get(3);
+      BasicBlock block3 = irCode.blocks.stream().filter(b -> b.getNumber() == 3).findFirst().get();
       assertTrue(block1.getSuccessors().contains(block3));
       assertTrue(block3.exit().isGoto());
 
-      BasicBlock block4 = irCode.blocks.get(4);
+      BasicBlock block4 = irCode.blocks.stream().filter(b -> b.getNumber() == 4).findFirst().get();
       assertSame(block3.getUniqueNormalSuccessor(), block4);
 
       Phi firstPhi =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 5ece012..483ed33 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -12,7 +12,6 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
@@ -42,7 +41,6 @@
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.google.common.collect.Sets;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.junit.Assume;
@@ -312,6 +310,8 @@
             .allowAccessModification()
             .enableInliningAnnotations()
             .addDontObfuscate()
+            // Using LIR changes the inlining heuristics so enable it consistently.
+            .addOptionsModification(o -> o.testing.enableLir())
             .setMinApi(parameters)
             .run(parameters.getRuntime(), main)
             .assertSuccessWithOutput(javaOutput);
@@ -324,16 +324,14 @@
             .filter(FoundClassSubject::isSynthesizedJavaLambdaClass)
             .map(FoundClassSubject::getFinalName)
             .collect(Collectors.toList());
+    assertEquals(Collections.emptyList(), synthesizedJavaLambdaClasses);
 
-    // TODO(b/120814598): Should only be "java.lang.StringBuilder".
     assertEquals(
-        new HashSet<>(synthesizedJavaLambdaClasses),
+        Collections.singleton("java.lang.StringBuilder"),
         collectTypes(clazz.uniqueMethodWithOriginalName("testStatelessLambda")));
-    assertTrue(
-        inspector.allClasses().stream().anyMatch(ClassSubject::isSynthesizedJavaLambdaClass));
 
     assertEquals(
-        Sets.newHashSet("java.lang.StringBuilder"),
+        Collections.singleton("java.lang.StringBuilder"),
         collectTypes(clazz.uniqueMethodWithOriginalName("testStatefulLambda")));
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/constantpropagation/KotlinDefaultArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/constantpropagation/KotlinDefaultArgumentsTest.java
new file mode 100644
index 0000000..9c951c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/constantpropagation/KotlinDefaultArgumentsTest.java
@@ -0,0 +1,177 @@
+// 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.optimize.constantpropagation;
+
+import static com.android.tools.r8.ir.optimize.constantpropagation.KotlinDefaultArgumentsTest.Greeter.getHello;
+import static com.android.tools.r8.ir.optimize.constantpropagation.KotlinDefaultArgumentsTest.Greeter.getWorld;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import 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 KotlinDefaultArgumentsTest extends TestBase {
+
+  enum Effect {
+    MATERIALIZED,
+    NONE,
+    REMOVED
+  }
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testFirstBranchRemoved() throws Exception {
+    test(MainRemovedNone.class, Effect.REMOVED, Effect.NONE);
+  }
+
+  @Test
+  public void testBothBranchesRemoved() throws Exception {
+    test(MainRemovedRemoved.class, Effect.REMOVED, Effect.REMOVED);
+  }
+
+  @Test
+  public void testFirstBranchRemovedAndSecondBranchMaterialized() throws Exception {
+    test(MainRemovedMaterialized.class, Effect.REMOVED, Effect.MATERIALIZED);
+  }
+
+  @Test
+  public void testFirstBranchMaterialized() throws Exception {
+    test(MainMaterializedNone.class, Effect.MATERIALIZED, Effect.NONE);
+  }
+
+  @Test
+  public void testFirstBranchMaterializedAndSecondBranchRemoved() throws Exception {
+    test(MainMaterializedRemoved.class, Effect.MATERIALIZED, Effect.REMOVED);
+  }
+
+  @Test
+  public void testBothBranchesMaterialized() throws Exception {
+    test(MainMaterializedMaterialized.class, Effect.MATERIALIZED, Effect.MATERIALIZED);
+  }
+
+  private void test(Class<?> mainClass, Effect firstBranchEffect, Effect secondBranchEffect)
+      throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(mainClass, Greeter.class)
+        .addKeepMainRule(mainClass)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              // TODO(b/196017578): If the branch effect is NONE, then check that the IF instruction
+              //  AND its body are retained.
+              // TODO(b/196017578): If the branch effect is REMOVED, then check that the IF
+              //  instruction AND its body is removed.
+              // TODO(b/196017578): If the branch effect is MATERIALIZED, then check that the IF
+              //  instruction is removed but NOT its body.
+              // TODO(b/196017578): If the branch effect is MATERIALIZED, then check that the unused
+              //  parameter is removed.
+            })
+        .run(parameters.getRuntime(), mainClass)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  // Test where first parameter X is always given at the call site.
+  // The first branch in greet() should be removed.
+  static class MainRemovedNone {
+
+    public static void main(String[] args) {
+      Greeter.greet(getHello(), null, 2);
+      if (System.currentTimeMillis() < 0) {
+        Greeter.greet(getHello(), getWorld(), 0);
+      }
+    }
+  }
+
+  // Test where both parameters X and Y are always given at the call site.
+  // Both branches in greet() should be removed.
+  static class MainRemovedRemoved {
+
+    public static void main(String[] args) {
+      Greeter.greet(getHello(), getWorld(), 0);
+    }
+  }
+
+  // Test where the first parameter X is always given and the second parameter Y is never given at
+  // the call site.
+  // The first branch in greet() should be removed and the second branch in greet() should be
+  // "materialized".
+  static class MainRemovedMaterialized {
+
+    public static void main(String[] args) {
+      Greeter.greet(getHello(), null, 2);
+    }
+  }
+
+  // Test where the first parameter X is never given at the call site.
+  // The first branch in greet() should be materialized.
+  static class MainMaterializedNone {
+
+    public static void main(String[] args) {
+      Greeter.greet(null, null, 3);
+      if (System.currentTimeMillis() < 0) {
+        Greeter.greet(null, getWorld(), 1);
+      }
+    }
+  }
+
+  // Test where the first parameter X is never given and the second parameter Y is always given at
+  // the call site.
+  // The first branch in greet() should be materialized and the second branch should be removed.
+  static class MainMaterializedRemoved {
+
+    public static void main(String[] args) {
+      Greeter.greet(null, getWorld(), 1);
+    }
+  }
+
+  // Test where none of the parameters X and Y are given at the call site.
+  // Both branches in greet() should be materialized.
+  static class MainMaterializedMaterialized {
+
+    public static void main(String[] args) {
+      Greeter.greet(null, null, 3);
+    }
+  }
+
+  static class Greeter {
+
+    @NeverInline
+    static void greet(String x, String y, int defaults) {
+      if ((defaults & 1) != 0) {
+        x = "Hello";
+      }
+      if ((defaults & 2) != 0) {
+        y = ", world!";
+      }
+      System.out.print(x);
+      System.out.println(y);
+    }
+
+    @NeverInline
+    static String getHello() {
+      return System.currentTimeMillis() > 0 ? "Hello" : "";
+    }
+
+    @NeverInline
+    static String getWorld() {
+      return System.currentTimeMillis() > 0 ? ", world!" : "";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
index 8f8541c..37e6c57 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
@@ -39,7 +39,7 @@
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters)
-        .addOptionsModification(o -> o.testing.roundtripThroughLir = true)
+        .addOptionsModification(o -> o.testing.enableLir())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("42")
         .inspect(inspector -> assertThat(inspector.clazz(anim.class), isAbsent()));
diff --git a/src/test/java/com/android/tools/r8/lightir/LirBasicCallbackTest.java b/src/test/java/com/android/tools/r8/lightir/LirBasicCallbackTest.java
index abd980d..909c083 100644
--- a/src/test/java/com/android/tools/r8/lightir/LirBasicCallbackTest.java
+++ b/src/test/java/com/android/tools/r8/lightir/LirBasicCallbackTest.java
@@ -12,12 +12,12 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.InternalOptions;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -74,10 +74,13 @@
 
   @Test
   public void test() {
-    DexItemFactory factory = new DexItemFactory();
-    DexMethod method = factory.createMethod(Reference.methodFromDescriptor("LFoo;", "bar", "()V"));
+    InternalOptions options = new InternalOptions();
+    DexMethod method =
+        options
+            .dexItemFactory()
+            .createMethod(Reference.methodFromDescriptor("LFoo;", "bar", "()V"));
     LirCode<?> code =
-        LirCode.builder(method, new ThrowingStrategy(), factory)
+        LirCode.builder(method, new ThrowingStrategy(), options)
             .setMetadata(IRMetadata.unknown())
             .addConstNull()
             .addConstInt(42)
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
index ffdda98..ab72433 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
@@ -4,11 +4,11 @@
 
 package com.android.tools.r8.naming.b72391662;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPackagePrivate;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverPropagateValue;
 import com.android.tools.r8.ToolHelper;
@@ -144,20 +144,16 @@
     // Test the totally unused method.
     MethodSubject staticMethod = testClass.uniqueMethodWithOriginalName("unused");
     assertThat(staticMethod, isPresent());
+    assertThat(staticMethod, isPackagePrivate());
     assertEquals(minify, staticMethod.isRenamed());
-    if (shrinker.isR8()) {
-      assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
-    } else {
-      assertFalse(staticMethod.getMethod().accessFlags.isPublic());
-    }
 
     // Test an indirectly referred method.
     staticMethod = testClass.uniqueMethodWithOriginalName("staticMethod");
     assertThat(staticMethod, isPresent());
     assertEquals(minify, staticMethod.isRenamed());
-    boolean publicizeCondition = shrinker.isR8() ? allowAccessModification
-        : minify && repackagePrefix != null && allowAccessModification;
-    assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+    boolean publicizeCondition =
+        shrinker.isProguard() && minify && repackagePrefix != null && allowAccessModification;
+    assertEquals(publicizeCondition, staticMethod.getMethod().isPublic());
   }
 
   @Test
@@ -195,20 +191,16 @@
     // Test the totally unused method.
     MethodSubject staticMethod = testClass.uniqueMethodWithOriginalName("unused");
     assertThat(staticMethod, isPresent());
+    assertThat(staticMethod, isPackagePrivate());
     assertEquals(minify, staticMethod.isRenamed());
-    if (shrinker.isR8()) {
-      assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
-    } else {
-      assertFalse(staticMethod.getMethod().accessFlags.isPublic());
-    }
 
     // Test an indirectly referred method.
     staticMethod = testClass.uniqueMethodWithOriginalName("staticMethod");
     assertThat(staticMethod, isPresent());
     assertEquals(minify, staticMethod.isRenamed());
-    boolean publicizeCondition = shrinker.isR8() ? allowAccessModification
-        : minify && repackagePrefix != null && allowAccessModification;
-    assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+    boolean publicizeCondition =
+        shrinker.isProguard() && minify && repackagePrefix != null && allowAccessModification;
+    assertEquals(publicizeCondition, staticMethod.getMethod().isPublic());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/regress/Regress160394262Test.java b/src/test/java/com/android/tools/r8/regress/Regress160394262Test.java
index c4fa84b..a39c770 100644
--- a/src/test/java/com/android/tools/r8/regress/Regress160394262Test.java
+++ b/src/test/java/com/android/tools/r8/regress/Regress160394262Test.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.regress;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.TestBase;
@@ -51,6 +50,7 @@
         .addKeepMainRule(TestClass.class)
         .addInnerClasses(Regress160394262Test.class)
         .setMinApi(parameters)
+        .addOptionsModification(o -> o.testing.enableLir())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkJoinerIsClassInlined);
@@ -58,14 +58,7 @@
 
   private void checkJoinerIsClassInlined(CodeInspector inspector) {
     assertThat(inspector.clazz(Joiner.class.getTypeName() + "$1"), isAbsent());
-    // TODO(b/160640028): Joiner should be class inlined.
-    //   When line info tables are kept we appear to successfully inline Joiner. Reason unknown.
-    if (parameters.isCfRuntime()
-        || parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport())) {
-      assertThat(inspector.clazz(Joiner.class), isPresent());
-    } else {
-      assertThat(inspector.clazz(Joiner.class), isAbsent());
-    }
+    assertThat(inspector.clazz(Joiner.class), isAbsent());
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
index 9aec797..3f426a5 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
@@ -3,10 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking.ifrule;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPackagePrivate;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
@@ -176,7 +176,7 @@
               assertThat(methodSubject, not(isPresent()));
               methodSubject = classSubject.uniqueMethodWithOriginalName("nonPublicMethod");
               assertThat(methodSubject, isPresent());
-              assertEquals(shrinker.isR8(), methodSubject.getMethod().accessFlags.isPublic());
+              assertThat(methodSubject, isPackagePrivate());
             });
   }
 
@@ -255,7 +255,7 @@
               assertThat(methodSubject, not(isPresent()));
               methodSubject = classSubject.uniqueMethodWithOriginalName("nonPublicMethod");
               assertThat(methodSubject, isPresent());
-              assertEquals(shrinker.isR8(), methodSubject.getMethod().accessFlags.isPublic());
+              assertThat(methodSubject, isPackagePrivate());
             });
   }
 }