Rewrite LIR in class merger when possible

Fixes: b/320432664
Change-Id: Iab102fca300cc674c8ccf6819db3604312faf0ab
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 24f0c28..ed2b2cd 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -728,39 +728,44 @@
       // already have been leveraged.
       OptimizationInfoRemover.run(appView, executorService);
 
-      // Perform repackaging.
-      if (appView.hasLiveness()) {
-        if (options.isRepackagingEnabled()) {
-          new Repackaging(appView.withLiveness()).run(executorService, timing);
+      GenericSignatureContextBuilder genericContextBuilderBeforeFinalMerging = null;
+      if (appView.hasCfByteCodePassThroughMethods()) {
+        LirConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+      } else {
+        // Perform repackaging.
+        if (appView.hasLiveness()) {
+          if (options.isRepackagingEnabled()) {
+            new Repackaging(appView.withLiveness()).run(executorService, timing);
+          }
+          assert Repackaging.verifyIdentityRepackaging(appView.withLiveness(), executorService);
         }
-        assert Repackaging.verifyIdentityRepackaging(appView.withLiveness(), executorService);
+
+        // Rewrite LIR with lens to allow building IR from LIR in class mergers.
+        LirConverter.rewriteLirWithLens(appView, timing, executorService);
+        appView.clearCodeRewritings(executorService, timing);
+
+        if (appView.hasLiveness()) {
+          VerticalClassMerger.createForFinalClassMerging(appView.withLiveness())
+              .runIfNecessary(executorService, timing);
+        }
+
+        // TODO(b/225838009): Move further down.
+        LirConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+        assert appView.dexItemFactory().verifyNoCachedTypeElements();
+
+        genericContextBuilderBeforeFinalMerging = GenericSignatureContextBuilder.create(appView);
+
+        // Run horizontal class merging. This runs even if shrinking is disabled to ensure
+        // synthetics are always merged.
+        HorizontalClassMerger.createForFinalClassMerging(appView)
+            .runIfNecessary(
+                executorService,
+                timing,
+                finalRuntimeTypeCheckInfoBuilder != null
+                    ? finalRuntimeTypeCheckInfoBuilder.build(appView.graphLens())
+                    : null);
       }
 
-      // Rewrite LIR with lens to allow building IR from LIR in class mergers.
-      LirConverter.rewriteLirWithLens(appView, timing, executorService);
-
-      if (appView.hasLiveness()) {
-        VerticalClassMerger.createForFinalClassMerging(appView.withLiveness())
-            .runIfNecessary(executorService, timing);
-      }
-
-      // TODO(b/225838009): Move further down.
-      LirConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-      assert appView.dexItemFactory().verifyNoCachedTypeElements();
-
-      GenericSignatureContextBuilder genericContextBuilderBeforeFinalMerging =
-          GenericSignatureContextBuilder.create(appView);
-
-      // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
-      // are always merged.
-      HorizontalClassMerger.createForFinalClassMerging(appView)
-          .runIfNecessary(
-              executorService,
-              timing,
-              finalRuntimeTypeCheckInfoBuilder != null
-                  ? finalRuntimeTypeCheckInfoBuilder.build(appView.graphLens())
-                  : null);
-
       // Perform minification.
       if (options.getProguardConfiguration().hasApplyMappingFile()) {
         timing.begin("apply-mapping");
@@ -823,8 +828,10 @@
       new KotlinMetadataRewriter(appView).runForR8(executorService);
       timing.end();
 
-      new GenericSignatureRewriter(appView, genericContextBuilderBeforeFinalMerging)
-          .run(appView.appInfo().classes(), executorService);
+      if (genericContextBuilderBeforeFinalMerging != null) {
+        new GenericSignatureRewriter(appView, genericContextBuilderBeforeFinalMerging)
+            .run(appView.appInfo().classes(), executorService);
+      }
 
       assert appView.checkForTesting(
               () ->
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index c09b0fd..84d8da6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -438,12 +438,15 @@
     allCodeProcessed = true;
   }
 
-  public void clearCodeRewritings(ExecutorService executorService) throws ExecutionException {
+  public void clearCodeRewritings(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    timing.begin("Clear code rewritings");
     setGraphLens(new ClearCodeRewritingGraphLens(withClassHierarchy()));
 
     MemberRebindingIdentityLens memberRebindingIdentityLens =
         MemberRebindingIdentityLensFactory.rebuild(withClassHierarchy(), executorService);
     setGraphLens(memberRebindingIdentityLens);
+    timing.end();
   }
 
   public void flattenGraphLenses() {
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 96948b9..aad4cc2 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
@@ -447,10 +447,6 @@
     return null;
   }
 
-  public boolean isPublicizerLens() {
-    return false;
-  }
-
   public boolean isVerticalClassMergerLens() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
index 7da900e..357828b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
@@ -10,6 +10,9 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -113,9 +116,29 @@
   }
 
   public CatchHandlers<T> rewriteWithLens(GraphLens graphLens, GraphLens codeLens) {
+    IntList targetIndicesToRemove = new IntArrayList();
+    Set<DexType> seenGuards = Sets.newIdentityHashSet();
     List<DexType> newGuards =
-        ListUtils.mapOrElse(guards, guard -> graphLens.lookupType(guard, codeLens), null);
-    return newGuards != null ? new CatchHandlers<>(newGuards, targets) : this;
+        ListUtils.mapOrElse(
+            guards,
+            (index, guard) -> {
+              DexType newGuard = graphLens.lookupType(guard, codeLens);
+              if (seenGuards.add(newGuard)) {
+                return newGuard;
+              }
+              targetIndicesToRemove.add(index);
+              return null;
+            },
+            null);
+    if (newGuards == null) {
+      assert targetIndicesToRemove.isEmpty();
+      return this;
+    }
+    List<T> newTargets =
+        targetIndicesToRemove.isEmpty()
+            ? targets
+            : ListUtils.newArrayListWithoutIndices(targets, targetIndicesToRemove);
+    return new CatchHandlers<>(newGuards, newTargets);
   }
 
   public void forEach(BiConsumer<DexType, T> consumer) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index ae74a82..453d999 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 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.DexEncodedMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -622,6 +623,21 @@
     return true;
   }
 
+  public boolean verifyInvokeInterface(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    if (appView.testing().allowInvokeErrors) {
+      return true;
+    }
+    for (InvokeMethod invoke : this.<InvokeMethod>instructions(Instruction::isInvokeMethod)) {
+      DexType holderType = invoke.getInvokedMethod().getHolderType();
+      if (holderType.isArrayType()) {
+        continue;
+      }
+      DexClass holder = appView.definitionFor(holderType, context());
+      assert holder == null || invoke.getInterfaceBit() == holder.isInterface();
+    }
+    return true;
+  }
+
   public boolean hasNoMergedClasses(AppView<? extends AppInfoWithClassHierarchy> appView) {
     MergedClassesCollection mergedClasses = appView.allMergedClasses();
     if (mergedClasses == null) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IROpcodeUtils.java b/src/main/java/com/android/tools/r8/ir/code/IROpcodeUtils.java
new file mode 100644
index 0000000..b798181
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/IROpcodeUtils.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.code;
+
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEDIRECT;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEDIRECT_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEINTERFACE;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESTATIC;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESTATIC_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESUPER;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESUPER_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEVIRTUAL;
+
+import com.android.tools.r8.errors.Unreachable;
+
+public class IROpcodeUtils {
+
+  public static int fromLirInvokeOpcode(int opcode) {
+    switch (opcode) {
+      case INVOKEDIRECT:
+      case INVOKEDIRECT_ITF:
+        return Opcodes.INVOKE_DIRECT;
+      case INVOKEINTERFACE:
+        return Opcodes.INVOKE_INTERFACE;
+      case INVOKESTATIC:
+      case INVOKESTATIC_ITF:
+        return Opcodes.INVOKE_STATIC;
+      case INVOKESUPER:
+      case INVOKESUPER_ITF:
+        return Opcodes.INVOKE_SUPER;
+      case INVOKEVIRTUAL:
+        return Opcodes.INVOKE_VIRTUAL;
+      default:
+        throw new Unreachable();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeType.java b/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
index 8bedc64..28e3846 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeType.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.lightir.LirOpcodes;
 import org.objectweb.asm.Opcodes;
 
 public enum InvokeType {
@@ -186,6 +187,23 @@
     return dexOpcodeRange;
   }
 
+  public int getLirOpcode(boolean isInterface) {
+    switch (this) {
+      case DIRECT:
+        return isInterface ? LirOpcodes.INVOKEDIRECT_ITF : LirOpcodes.INVOKEDIRECT;
+      case INTERFACE:
+        return LirOpcodes.INVOKEINTERFACE;
+      case STATIC:
+        return isInterface ? LirOpcodes.INVOKESTATIC_ITF : LirOpcodes.INVOKESTATIC;
+      case SUPER:
+        return isInterface ? LirOpcodes.INVOKESUPER_ITF : LirOpcodes.INVOKESUPER;
+      case VIRTUAL:
+        return LirOpcodes.INVOKEVIRTUAL;
+      default:
+        throw new Unreachable();
+    }
+  }
+
   public boolean isDirect() {
     return this == DIRECT;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 676fcc5..72e5dac 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -161,7 +161,7 @@
   private final DexItemFactory factory;
   private final InternalOptions options;
 
-  LensCodeRewriter(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public LensCodeRewriter(AppView<? extends AppInfoWithClassHierarchy> appView) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
     this.options = appView.options();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
index 19bb6dc..f1c8a4336 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.passes.FilledNewArrayRewriter;
 import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
@@ -23,6 +22,7 @@
 import com.android.tools.r8.utils.ObjectUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.verticalclassmerging.IncompleteVerticalClassMergerBridgeCode;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -90,7 +90,7 @@
 
     GraphLens graphLens = appView.graphLens();
     assert graphLens.isNonIdentityLens();
-    assert appView.codeLens().isAppliedLens();
+    assert appView.codeLens().isAppliedLens() || appView.codeLens().isClearCodeRewritingLens();
 
     MemberRebindingIdentityLens memberRebindingIdentityLens =
         graphLens.asNonIdentityLens().find(GraphLens::isMemberRebindingIdentityLens);
@@ -104,9 +104,6 @@
     timing.begin("LIR->LIR@" + graphLens.getClass().getTypeName());
     rewriteLirWithUnappliedLens(appView, executorService);
     timing.end();
-
-    // At this point all code has been mapped according to the graph lens.
-    updateCodeLens(appView);
   }
 
   private static void rewriteLirWithUnappliedLens(
@@ -117,10 +114,7 @@
         appView.appInfo().classes(),
         clazz ->
             clazz.forEachProgramMethodMatching(
-                m ->
-                    m.hasCode()
-                        && !m.getCode().isSharedCodeObject()
-                        && !appView.isCfByteCodePassThrough(m),
+                m -> m.hasCode() && m.getCode().isLirCode(),
                 m -> rewriteLirMethodWithLens(m, appView, rewriterUtils)),
         appView.options().getThreadingModule(),
         executorService);
@@ -133,14 +127,8 @@
       ProgramMethod method,
       AppView<? extends AppInfoWithClassHierarchy> appView,
       LensCodeRewriterUtils rewriterUtils) {
-    Code code = method.getDefinition().getCode();
-    if (!code.isLirCode()) {
-      assert false;
-      return;
-    }
-    LirCode<Integer> lirCode = code.asLirCode();
-    LirCode<Integer> rewrittenLirCode =
-        lirCode.rewriteWithSimpleLens(method, appView, rewriterUtils);
+    LirCode<Integer> lirCode = method.getDefinition().getCode().asLirCode();
+    LirCode<Integer> rewrittenLirCode = lirCode.rewriteWithLens(method, appView, rewriterUtils);
     if (ObjectUtils.notIdentical(lirCode, rewrittenLirCode)) {
       method.setCode(rewrittenLirCode, appView);
     }
@@ -171,67 +159,7 @@
     // Clear the reference type cache after conversion to reduce memory pressure.
     appView.dexItemFactory().clearTypeElementsCache();
     // At this point all code has been mapped according to the graph lens.
-    updateCodeLens(appView);
-  }
-
-  private static void updateCodeLens(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    final NonIdentityGraphLens lens = appView.graphLens().asNonIdentityLens();
-    if (lens == null) {
-      assert false;
-      return;
-    }
-
-    // If the current graph lens is the member rebinding identity lens then code lens is simply
-    // the previous lens. This is the same structure as the more complicated case below but where
-    // there is no need to rewrite any previous pointers.
-    if (lens.isMemberRebindingIdentityLens()) {
-      appView.setCodeLens(lens.getPrevious());
-      return;
-    }
-
-    // Otherwise search out where the lens pointing to the member rebinding identity lens.
-    NonIdentityGraphLens lensAfterMemberRebindingIdentityLens =
-        lens.find(p -> p.getPrevious().isMemberRebindingIdentityLens());
-    if (lensAfterMemberRebindingIdentityLens == null) {
-      // With the current compiler structure we expect to always find the lens.
-      assert false;
-      appView.setCodeLens(lens);
-      return;
-    }
-
-    GraphLens codeLens = appView.codeLens();
-    MemberRebindingIdentityLens memberRebindingIdentityLens =
-        lensAfterMemberRebindingIdentityLens.getPrevious().asMemberRebindingIdentityLens();
-
-    // We are assuming that the member rebinding identity lens is always installed after the current
-    // applied lens/code lens and also that there should not be a rebinding lens from the compilers
-    // first phase (this subroutine is only used after IR conversion for now).
-    assert memberRebindingIdentityLens
-        == lens.findPrevious(
-            p -> p == memberRebindingIdentityLens || p == codeLens || p.isMemberRebindingLens());
-
-    // Rewrite the graph lens effects from 'lens' and up to the member rebinding identity lens.
-    MemberRebindingIdentityLens rewrittenMemberRebindingLens =
-        memberRebindingIdentityLens.toRewrittenMemberRebindingIdentityLens(
-            appView, lens, memberRebindingIdentityLens, lens);
-
-    // The current previous pointers for the graph lenses are:
-    //   lens -> ... -> lensAfterMemberRebindingIdentityLens -> memberRebindingIdentityLens -> g
-    // we rewrite them now to:
-    //   rewrittenMemberRebindingLens -> lens -> ... -> lensAfterMemberRebindingIdentityLens -> g
-
-    // The above will construct the new member rebinding lens such that it points to the new
-    // code-lens point already.
-    assert rewrittenMemberRebindingLens.getPrevious() == lens;
-
-    // Update the previous pointer on the new code lens to jump over the old member rebinding
-    // identity lens.
-    lensAfterMemberRebindingIdentityLens.setPrevious(memberRebindingIdentityLens.getPrevious());
-
-    // The applied lens can now be updated and the rewritten member rebinding lens installed as
-    // the current "unapplied lens".
-    appView.setCodeLens(lens);
-    appView.setGraphLens(rewrittenMemberRebindingLens);
+    appView.clearCodeRewritings(executorService, timing);
   }
 
   private static void finalizeLirMethodToOutputFormat(
@@ -245,12 +173,17 @@
     }
     Timing onThreadTiming = Timing.empty();
     LirCode<Integer> lirCode = code.asLirCode();
-    LirCode<Integer> rewrittenLirCode =
-        lirCode.rewriteWithSimpleLens(method, appView, rewriterUtils);
+    LirCode<Integer> rewrittenLirCode = lirCode.rewriteWithLens(method, appView, rewriterUtils);
     if (ObjectUtils.notIdentical(lirCode, rewrittenLirCode)) {
       method.setCode(rewrittenLirCode, appView);
     }
     IRCode irCode = method.buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
+    assert irCode.verifyInvokeInterface(appView);
+    if (lirCode.hasTryCatchTable()) {
+      // Vertical class merging may lead to dead catch handlers.
+      // TODO(b/322762660): Ensure IR is valid immediately after IR building.
+      irCode.removeUnreachableBlocks();
+    }
     FilledNewArrayRewriter filledNewArrayRewriter = new FilledNewArrayRewriter(appView);
     boolean changed = filledNewArrayRewriter.run(irCode, onThreadTiming).hasChanged().toBoolean();
     if (appView.options().isGeneratingDex() && changed) {
@@ -275,6 +208,7 @@
       for (DexEncodedMethod method : clazz.methods(DexEncodedMethod::hasCode)) {
         assert method.getCode().isLirCode()
             || method.getCode().isSharedCodeObject()
+            || method.getCode() instanceof IncompleteVerticalClassMergerBridgeCode
             || appView.isCfByteCodePassThrough(method)
             || appView.options().skipIR;
       }
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 f782127..ae766bd 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
@@ -118,7 +118,7 @@
     // All the code has been processed so the rewriting required by the lenses is done everywhere,
     // we clear lens code rewriting so that the lens rewriter can be re-executed in phase 2 if new
     // lenses with code rewriting are added.
-    appView.clearCodeRewritings(executorService);
+    appView.clearCodeRewritings(executorService, Timing.empty());
 
     // Commit synthetics from the primary optimization pass.
     commitPendingSyntheticItems(appView);
@@ -203,7 +203,7 @@
     // All the code that should be impacted by the lenses inserted between phase 1 and phase 2
     // have now been processed and rewritten, we clear code lens rewriting so that the class
     // staticizer and phase 3 does not perform again the rewriting.
-    appView.clearCodeRewritings(executorService);
+    appView.clearCodeRewritings(executorService, Timing.empty());
 
     // Commit synthetics before creating a builder (otherwise the builder will not include the
     // synthetics.)
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 29c7ca6..74c6cfa 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -6,6 +6,7 @@
 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.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ArgumentUse;
 import com.android.tools.r8.graph.ClasspathMethod;
@@ -490,6 +491,10 @@
     return positionTable;
   }
 
+  public boolean hasTryCatchTable() {
+    return tryCatchTable != null;
+  }
+
   public TryCatchTable getTryCatchTable() {
     return tryCatchTable;
   }
@@ -802,17 +807,18 @@
         metadataMap);
   }
 
-  public LirCode<EV> rewriteWithSimpleLens(
-      ProgramMethod context, AppView<?> appView, LensCodeRewriterUtils rewriterUtils) {
+  public LirCode<EV> rewriteWithLens(
+      ProgramMethod context,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      LensCodeRewriterUtils rewriterUtils) {
     GraphLens graphLens = appView.graphLens();
     assert graphLens.isNonIdentityLens();
     if (graphLens.isMemberRebindingIdentityLens()) {
       return this;
     }
 
-    GraphLens codeLens = context.getDefinition().getCode().getCodeLens(appView);
-    SimpleLensLirRewriter<EV> rewriter =
-        new SimpleLensLirRewriter<>(this, context, graphLens, codeLens, rewriterUtils);
+    LirLensCodeRewriter<EV> rewriter =
+        new LirLensCodeRewriter<>(appView, this, context, rewriterUtils);
     return rewriter.rewrite();
   }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java b/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java
new file mode 100644
index 0000000..12bb785
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LirLensCodeRewriter.java
@@ -0,0 +1,438 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.lightir;
+
+import static com.android.tools.r8.graph.UseRegistry.MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.graph.lens.FieldLookupResult;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.IROpcodeUtils;
+import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.ir.conversion.IRToLirFinalizer;
+import com.android.tools.r8.ir.conversion.LensCodeRewriter;
+import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.lightir.LirBuilder.RecordFieldValuesPayload;
+import com.android.tools.r8.lightir.LirCode.TryCatchTable;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.verticalclassmerging.VerticalClassMergerGraphLens;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LirLensCodeRewriter<EV> extends LirParsedInstructionCallback<EV> {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final ProgramMethod context;
+  private final DexMethod contextReference;
+  private final GraphLens graphLens;
+  private final GraphLens codeLens;
+  private final LensCodeRewriterUtils helper;
+
+  private int numberOfInvokeOpcodeChanges = 0;
+  private Map<LirConstant, LirConstant> constantPoolMapping = null;
+
+  private boolean hasNonTrivialRewritings = false;
+
+  public LirLensCodeRewriter(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      LirCode<EV> code,
+      ProgramMethod context,
+      LensCodeRewriterUtils helper) {
+    super(code);
+    this.appView = appView;
+    this.context = context;
+    this.contextReference = context.getReference();
+    this.graphLens = appView.graphLens();
+    this.codeLens = context.getDefinition().getCode().getCodeLens(appView);
+    this.helper = helper;
+  }
+
+  @Override
+  public int getCurrentValueIndex() {
+    // We do not need to interpret values.
+    return -1;
+  }
+
+  public void onTypeReference(DexType type) {
+    addRewrittenMapping(type, graphLens.lookupType(type, codeLens));
+  }
+
+  public void onFieldReference(DexField field) {
+    FieldLookupResult result = graphLens.lookupFieldResult(field, codeLens);
+    assert !result.hasReadCastType();
+    assert !result.hasWriteCastType();
+    addRewrittenMapping(field, result.getReference());
+  }
+
+  public void onCallSiteReference(DexCallSite callSite) {
+    addRewrittenMapping(callSite, helper.rewriteCallSite(callSite, context));
+  }
+
+  public void onMethodHandleReference(DexMethodHandle methodHandle) {
+    addRewrittenMapping(
+        methodHandle,
+        helper.rewriteDexMethodHandle(methodHandle, NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context));
+  }
+
+  public void onProtoReference(DexProto proto) {
+    addRewrittenMapping(proto, helper.rewriteProto(proto));
+  }
+
+  private void onInvoke(DexMethod method, InvokeType type, boolean isInterface) {
+    MethodLookupResult result = graphLens.lookupMethod(method, contextReference, type, codeLens);
+    if (hasPotentialNonTrivialInvokeRewriting(method, type, result)) {
+      hasNonTrivialRewritings = true;
+      return;
+    }
+    int opcode = type.getLirOpcode(isInterface);
+    DexMethod newMethod = result.getReference();
+    InvokeType newType = result.getType();
+    boolean newIsInterface = lookupIsInterface(method, opcode, result);
+    int newOpcode = newType.getLirOpcode(newIsInterface);
+    assert newMethod.getArity() == method.getArity();
+    if (newOpcode != opcode) {
+      assert type == newType
+              || (type.isVirtual() && newType.isInterface())
+              || (type.isInterface() && newType.isVirtual())
+              || (type.isSuper() && newType.isVirtual())
+          : type + " -> " + newType;
+      numberOfInvokeOpcodeChanges++;
+    } else {
+      // All non-type dependent mappings are just rewritten in the content pool.
+      addRewrittenMapping(method, newMethod);
+    }
+  }
+
+  private boolean hasPotentialNonTrivialInvokeRewriting(
+      DexMethod method, InvokeType type, MethodLookupResult result) {
+    VerticalClassMergerGraphLens verticalClassMergerLens = graphLens.asVerticalClassMergerLens();
+    if (verticalClassMergerLens != null) {
+      if (!result.getPrototypeChanges().isEmpty()) {
+        return true;
+      }
+      for (int argumentIndex = 0;
+          argumentIndex < method.getNumberOfArguments(type.isStatic());
+          argumentIndex++) {
+        DexType argumentType = method.getArgumentType(argumentIndex, type.isStatic());
+        if (verticalClassMergerLens.hasInterfaceBeenMergedIntoClass(argumentType)) {
+          return true;
+        }
+      }
+    }
+    assert result.getPrototypeChanges().isEmpty();
+    return false;
+  }
+
+  private void addRewrittenMapping(LirConstant item, LirConstant rewrittenItem) {
+    if (item == rewrittenItem) {
+      return;
+    }
+    if (constantPoolMapping == null) {
+      constantPoolMapping =
+          new IdentityHashMap<>(
+              // Avoid using initial capacity larger than the number of actual constants.
+              Math.min(getCode().getConstantPool().length, 32));
+    }
+    LirConstant old = constantPoolMapping.put(item, rewrittenItem);
+    if (old != null && old != rewrittenItem) {
+      throw new Unreachable(
+          "Unexpected rewriting of item: "
+              + item
+              + " to two distinct items: "
+              + rewrittenItem
+              + " and "
+              + old);
+    }
+  }
+
+  @Override
+  public void onInstancePut(DexField field, EV object, EV value) {
+    onFieldPut(field);
+  }
+
+  @Override
+  public void onStaticPut(DexField field, EV value) {
+    onFieldPut(field);
+  }
+
+  private void onFieldPut(DexField field) {
+    if (hasPotentialNonTrivialFieldPutRewriting(field)) {
+      hasNonTrivialRewritings = true;
+    }
+  }
+
+  private boolean hasPotentialNonTrivialFieldPutRewriting(DexField field) {
+    VerticalClassMergerGraphLens verticalClassMergerLens = graphLens.asVerticalClassMergerLens();
+    if (verticalClassMergerLens != null
+        && verticalClassMergerLens.hasInterfaceBeenMergedIntoClass(field.getType())) {
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void onInvokeDirect(DexMethod method, List<EV> arguments, boolean isInterface) {
+    onInvoke(method, InvokeType.DIRECT, isInterface);
+  }
+
+  @Override
+  public void onInvokeSuper(DexMethod method, List<EV> arguments, boolean isInterface) {
+    onInvoke(method, InvokeType.SUPER, isInterface);
+  }
+
+  @Override
+  public void onInvokeVirtual(DexMethod method, List<EV> arguments) {
+    onInvoke(method, InvokeType.VIRTUAL, false);
+  }
+
+  @Override
+  public void onInvokeStatic(DexMethod method, List<EV> arguments, boolean isInterface) {
+    onInvoke(method, InvokeType.STATIC, isInterface);
+  }
+
+  @Override
+  public void onInvokeInterface(DexMethod method, List<EV> arguments) {
+    onInvoke(method, InvokeType.INTERFACE, true);
+  }
+
+  private InvokeType getInvokeTypeThatMayChange(int opcode) {
+    if (opcode == LirOpcodes.INVOKEVIRTUAL) {
+      return InvokeType.VIRTUAL;
+    }
+    if (opcode == LirOpcodes.INVOKEINTERFACE) {
+      return InvokeType.INTERFACE;
+    }
+    if (graphLens.isVerticalClassMergerLens()) {
+      if (opcode == LirOpcodes.INVOKESTATIC_ITF) {
+        return InvokeType.STATIC;
+      }
+      if (opcode == LirOpcodes.INVOKESUPER) {
+        return InvokeType.SUPER;
+      }
+    }
+    return null;
+  }
+
+  public LirCode<EV> rewrite() {
+    if (hasNonTrivialMethodChanges()) {
+      return rewriteWithLensCodeRewriter();
+    }
+    assert !hasNonTrivialRewritings;
+    LirCode<EV> rewritten = rewriteConstantPoolAndScanForTypeChanges(getCode());
+    if (hasNonTrivialRewritings) {
+      return rewriteWithLensCodeRewriter();
+    }
+    rewritten = rewriteInstructionsWithInvokeTypeChanges(rewritten);
+    return rewriteTryCatchTable(rewritten);
+  }
+
+  private boolean hasNonTrivialMethodChanges() {
+    VerticalClassMergerGraphLens verticalClassMergerLens = graphLens.asVerticalClassMergerLens();
+    if (verticalClassMergerLens != null) {
+      DexMethod previousReference =
+          verticalClassMergerLens.getPreviousMethodSignature(contextReference);
+      if (verticalClassMergerLens.hasInterfaceBeenMergedIntoClass(
+          previousReference.getReturnType())) {
+        return true;
+      }
+      RewrittenPrototypeDescription prototypeChanges =
+          graphLens.lookupPrototypeChangesForMethodDefinition(context.getReference(), codeLens);
+      if (!prototypeChanges.isEmpty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  private LirCode<EV> rewriteWithLensCodeRewriter() {
+    IRCode code =
+        context.buildIR(
+            appView,
+            MethodConversionOptions.forLirPhase(appView)
+                .disableStringSwitchConversion()
+                .setFinalizeAfterLensCodeRewriter());
+    // MethodProcessor argument is only used by unboxing lenses.
+    MethodProcessor methodProcessor = null;
+    new LensCodeRewriter(appView).rewrite(code, context, methodProcessor);
+    DeadCodeRemover deadCodeRemover = new DeadCodeRemover(appView);
+    deadCodeRemover.run(code, Timing.empty());
+    IRToLirFinalizer finalizer = new IRToLirFinalizer(appView, deadCodeRemover);
+    LirCode<?> rewritten =
+        finalizer.finalizeCode(code, BytecodeMetadataProvider.empty(), Timing.empty());
+    return (LirCode<EV>) rewritten;
+  }
+
+  private LirCode<EV> rewriteConstantPoolAndScanForTypeChanges(LirCode<EV> code) {
+    // The code may need to be rewritten by the lens.
+    // First pass scans just the constant pool to see if any types change or if there are any
+    // fields/methods that need to be examined.
+    boolean hasFieldReference = false;
+    boolean hasPotentialRewrittenMethod = false;
+    for (LirConstant constant : code.getConstantPool()) {
+      // RecordFieldValuesPayload is lowered to NewArrayEmpty before lens code rewriting any LIR.
+      assert !(constant instanceof RecordFieldValuesPayload);
+      if (constant instanceof DexType) {
+        onTypeReference((DexType) constant);
+      } else if (constant instanceof DexField) {
+        onFieldReference((DexField) constant);
+        hasFieldReference = true;
+      } else if (constant instanceof DexCallSite) {
+        onCallSiteReference((DexCallSite) constant);
+      } else if (constant instanceof DexMethodHandle) {
+        onMethodHandleReference((DexMethodHandle) constant);
+      } else if (constant instanceof DexProto) {
+        onProtoReference((DexProto) constant);
+      } else if (!hasPotentialRewrittenMethod && constant instanceof DexMethod) {
+        // We might be able to still fast-case this if we can guarantee the method is never
+        // rewritten. Say it is an java.lang.Object reference or if the lens can fast-check it.
+        hasPotentialRewrittenMethod = true;
+      }
+    }
+
+    // If there are potential method rewritings then we need to iterate the instructions as the
+    // rewriting is instruction-sensitive (i.e., may be dependent on the invoke type).
+    boolean hasPotentialNonTrivialFieldPutRewriting =
+        hasFieldReference && graphLens.isVerticalClassMergerLens();
+    if (hasPotentialNonTrivialFieldPutRewriting || hasPotentialRewrittenMethod) {
+      for (LirInstructionView view : code) {
+        view.accept(this);
+        if (hasNonTrivialRewritings) {
+          return null;
+        }
+      }
+    }
+
+    if (constantPoolMapping == null) {
+      return code;
+    }
+
+    return code.newCodeWithRewrittenConstantPool(
+        item -> constantPoolMapping.getOrDefault(item, item));
+  }
+
+  private LirCode<EV> rewriteInstructionsWithInvokeTypeChanges(LirCode<EV> code) {
+    if (numberOfInvokeOpcodeChanges == 0) {
+      return code;
+    }
+    // Build a small map from method refs to index in case the type-dependent methods are already
+    // in the constant pool.
+    Reference2IntMap<DexMethod> methodIndices = new Reference2IntOpenHashMap<>();
+    LirConstant[] rewrittenConstants = code.getConstantPool();
+    for (int i = 0, length = rewrittenConstants.length; i < length; i++) {
+      LirConstant constant = rewrittenConstants[i];
+      if (constant instanceof DexMethod) {
+        methodIndices.put((DexMethod) constant, i);
+      }
+    }
+
+    IRMetadata irMetadata = code.getMetadataForIR();
+    ByteArrayWriter byteWriter = new ByteArrayWriter();
+    LirWriter lirWriter = new LirWriter(byteWriter);
+    List<LirConstant> methodsToAppend = new ArrayList<>(numberOfInvokeOpcodeChanges);
+    for (LirInstructionView view : code) {
+      int opcode = view.getOpcode();
+      // Instructions that do not have an invoke-type change are just mapped via identity.
+      if (LirOpcodes.isOneByteInstruction(opcode)) {
+        lirWriter.writeOneByteInstruction(opcode);
+        continue;
+      }
+      InvokeType type = getInvokeTypeThatMayChange(opcode);
+      if (type == null) {
+        int size = view.getRemainingOperandSizeInBytes();
+        lirWriter.writeInstruction(opcode, size);
+        while (size-- > 0) {
+          lirWriter.writeOperand(view.getNextU1());
+        }
+        continue;
+      }
+      // This is potentially an invoke with a type change, in such cases the method is mapped with
+      // the instruction updated to the new type. The constant pool is amended with the mapped
+      // method if needed.
+      int constantIndex = view.getNextConstantOperand();
+      DexMethod method = (DexMethod) code.getConstantItem(constantIndex);
+      MethodLookupResult result =
+          graphLens.lookupMethod(method, context.getReference(), type, codeLens);
+      boolean newIsInterface = lookupIsInterface(method, opcode, result);
+      InvokeType newType = result.getType();
+      int newOpcode = newType.getLirOpcode(newIsInterface);
+      if (newOpcode != opcode) {
+        --numberOfInvokeOpcodeChanges;
+        if (newType != type) {
+          irMetadata.record(IROpcodeUtils.fromLirInvokeOpcode(newOpcode));
+        }
+        constantIndex =
+            methodIndices.computeIfAbsent(
+                result.getReference(),
+                ref -> {
+                  methodsToAppend.add(ref);
+                  return rewrittenConstants.length + methodsToAppend.size() - 1;
+                });
+      }
+      int constantIndexSize = ByteUtils.intEncodingSize(constantIndex);
+      int remainingSize = view.getRemainingOperandSizeInBytes();
+      lirWriter.writeInstruction(newOpcode, constantIndexSize + remainingSize);
+      ByteUtils.writeEncodedInt(constantIndex, lirWriter::writeOperand);
+      while (remainingSize-- > 0) {
+        lirWriter.writeOperand(view.getNextU1());
+      }
+    }
+    assert numberOfInvokeOpcodeChanges == 0;
+    // Note that since we assume 'null' in the mapping is identity this may end up with a stale
+    // reference to a no longer used method. That is not an issue as it will be pruned when
+    // building IR again, it is just a small and size overhead.
+    LirCode<EV> newCode =
+        code.copyWithNewConstantsAndInstructions(
+            irMetadata,
+            ArrayUtils.appendElements(code.getConstantPool(), methodsToAppend),
+            byteWriter.toByteArray());
+    return newCode;
+  }
+
+  // TODO(b/157111832): This should be part of the graph lens lookup result.
+  private boolean lookupIsInterface(DexMethod method, int opcode, MethodLookupResult result) {
+    VerticalClassMergerGraphLens verticalClassMergerLens = graphLens.asVerticalClassMergerLens();
+    if (verticalClassMergerLens != null
+        && verticalClassMergerLens.hasInterfaceBeenMergedIntoClass(method.getHolderType())) {
+      DexClass clazz = appView.definitionFor(result.getReference().getHolderType());
+      if (clazz != null) {
+        return clazz.isInterface();
+      }
+    }
+    return LirOpcodeUtils.getInterfaceBitFromInvokeOpcode(opcode);
+  }
+
+  private LirCode<EV> rewriteTryCatchTable(LirCode<EV> code) {
+    TryCatchTable tryCatchTable = code.getTryCatchTable();
+    if (tryCatchTable == null) {
+      return code;
+    }
+    TryCatchTable newTryCatchTable = tryCatchTable.rewriteWithLens(graphLens, codeLens);
+    return code.newCodeWithRewrittenTryCatchTable(newTryCatchTable);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodeUtils.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodeUtils.java
new file mode 100644
index 0000000..944bfce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodeUtils.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.lightir;
+
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEDIRECT;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEDIRECT_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEINTERFACE;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESTATIC;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESTATIC_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESUPER;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKESUPER_ITF;
+import static com.android.tools.r8.lightir.LirOpcodes.INVOKEVIRTUAL;
+
+public class LirOpcodeUtils {
+
+  public static boolean getInterfaceBitFromInvokeOpcode(int opcode) {
+    switch (opcode) {
+      case INVOKEDIRECT_ITF:
+      case INVOKEINTERFACE:
+      case INVOKESTATIC_ITF:
+      case INVOKESUPER_ITF:
+        return true;
+      default:
+        assert opcode == INVOKEDIRECT
+            || opcode == INVOKESTATIC
+            || opcode == INVOKESUPER
+            || opcode == INVOKEVIRTUAL;
+        return false;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java b/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java
deleted file mode 100644
index 1a4d065..0000000
--- a/src/main/java/com/android/tools/r8/lightir/SimpleLensLirRewriter.java
+++ /dev/null
@@ -1,293 +0,0 @@
-// 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 static com.android.tools.r8.graph.UseRegistry.MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY;
-
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.DexCallSite;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexMethodHandle;
-import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.lens.MethodLookupResult;
-import com.android.tools.r8.ir.code.IRMetadata;
-import com.android.tools.r8.ir.code.InvokeType;
-import com.android.tools.r8.ir.code.Opcodes;
-import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
-import com.android.tools.r8.lightir.LirBuilder.RecordFieldValuesPayload;
-import com.android.tools.r8.lightir.LirCode.TryCatchTable;
-import com.android.tools.r8.utils.ArrayUtils;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-public class SimpleLensLirRewriter<EV> extends LirParsedInstructionCallback<EV> {
-
-  private final ProgramMethod context;
-  private final DexMethod contextReference;
-  private final GraphLens graphLens;
-  private final GraphLens codeLens;
-  private final LensCodeRewriterUtils helper;
-
-  private int numberOfInvokeTypeChanges = 0;
-  private Map<LirConstant, LirConstant> constantPoolMapping = null;
-
-  public SimpleLensLirRewriter(
-      LirCode<EV> code,
-      ProgramMethod context,
-      GraphLens graphLens,
-      GraphLens codeLens,
-      LensCodeRewriterUtils helper) {
-    super(code);
-    this.context = context;
-    this.contextReference = context.getReference();
-    this.graphLens = graphLens;
-    this.codeLens = codeLens;
-    this.helper = helper;
-  }
-
-  @Override
-  public int getCurrentValueIndex() {
-    // We do not need to interpret values.
-    return -1;
-  }
-
-  public void onTypeReference(DexType type) {
-    addRewrittenMapping(type, graphLens.lookupType(type, codeLens));
-  }
-
-  public void onFieldReference(DexField field) {
-    addRewrittenMapping(field, graphLens.lookupField(field, codeLens));
-  }
-
-  public void onCallSiteReference(DexCallSite callSite) {
-    addRewrittenMapping(callSite, helper.rewriteCallSite(callSite, context));
-  }
-
-  public void onMethodHandleReference(DexMethodHandle methodHandle) {
-    addRewrittenMapping(
-        methodHandle,
-        helper.rewriteDexMethodHandle(methodHandle, NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context));
-  }
-
-  public void onProtoReference(DexProto proto) {
-    addRewrittenMapping(proto, helper.rewriteProto(proto));
-  }
-
-  private void onInvoke(DexMethod method, InvokeType type) {
-    MethodLookupResult result = graphLens.lookupMethod(method, contextReference, type, codeLens);
-    if (result.getType() != type) {
-      assert (type == InvokeType.VIRTUAL && result.getType() == InvokeType.INTERFACE)
-          || (type == InvokeType.INTERFACE && result.getType() == InvokeType.VIRTUAL);
-      numberOfInvokeTypeChanges++;
-    } else {
-      // All non-type dependent mappings are just rewritten in the content pool.
-      addRewrittenMapping(method, result.getReference());
-    }
-  }
-
-  private void addRewrittenMapping(LirConstant item, LirConstant rewrittenItem) {
-    if (item == rewrittenItem) {
-      return;
-    }
-    if (constantPoolMapping == null) {
-      constantPoolMapping =
-          new IdentityHashMap<>(
-              // Avoid using initial capacity larger than the number of actual constants.
-              Math.min(getCode().getConstantPool().length, 32));
-    }
-    LirConstant old = constantPoolMapping.put(item, rewrittenItem);
-    if (old != null && old != rewrittenItem) {
-      throw new Unreachable(
-          "Unexpected rewriting of item: "
-              + item
-              + " to two distinct items: "
-              + rewrittenItem
-              + " and "
-              + old);
-    }
-  }
-
-  @Override
-  public void onInvokeDirect(DexMethod method, List<EV> arguments, boolean isInterface) {
-    onInvoke(method, InvokeType.DIRECT);
-  }
-
-  @Override
-  public void onInvokeSuper(DexMethod method, List<EV> arguments, boolean isInterface) {
-    onInvoke(method, InvokeType.SUPER);
-  }
-
-  @Override
-  public void onInvokeVirtual(DexMethod method, List<EV> arguments) {
-    onInvoke(method, InvokeType.VIRTUAL);
-  }
-
-  @Override
-  public void onInvokeStatic(DexMethod method, List<EV> arguments, boolean isInterface) {
-    onInvoke(method, InvokeType.STATIC);
-  }
-
-  @Override
-  public void onInvokeInterface(DexMethod method, List<EV> arguments) {
-    onInvoke(method, InvokeType.INTERFACE);
-  }
-
-  private InvokeType getInvokeTypeThatMayChange(int opcode) {
-    if (opcode == LirOpcodes.INVOKEVIRTUAL) {
-      return InvokeType.VIRTUAL;
-    }
-    if (opcode == LirOpcodes.INVOKEINTERFACE) {
-      return InvokeType.INTERFACE;
-    }
-    return null;
-  }
-
-  public LirCode<EV> rewrite() {
-    LirCode<EV> rewritten = rewriteConstantPoolAndScanForTypeChanges(getCode());
-    rewritten = rewriteInstructionsWithInvokeTypeChanges(rewritten);
-    return rewriteTryCatchTable(rewritten);
-  }
-
-  private LirCode<EV> rewriteConstantPoolAndScanForTypeChanges(LirCode<EV> code) {
-    // The code may need to be rewritten by the lens.
-    // First pass scans just the constant pool to see if any types change or if there are any
-    // fields/methods that need to be examined.
-    boolean hasPotentialRewrittenMethod = false;
-    for (LirConstant constant : code.getConstantPool()) {
-      // RecordFieldValuesPayload is lowered to NewArrayEmpty before lens code rewriting any LIR.
-      assert !(constant instanceof RecordFieldValuesPayload);
-      if (constant instanceof DexType) {
-        onTypeReference((DexType) constant);
-      } else if (constant instanceof DexField) {
-        onFieldReference((DexField) constant);
-      } else if (constant instanceof DexCallSite) {
-        onCallSiteReference((DexCallSite) constant);
-      } else if (constant instanceof DexMethodHandle) {
-        onMethodHandleReference((DexMethodHandle) constant);
-      } else if (constant instanceof DexProto) {
-        onProtoReference((DexProto) constant);
-      } else if (!hasPotentialRewrittenMethod && constant instanceof DexMethod) {
-        // We might be able to still fast-case this if we can guarantee the method is never
-        // rewritten. Say it is an java.lang.Object reference or if the lens can fast-check it.
-        hasPotentialRewrittenMethod = true;
-      }
-    }
-
-    // If there are potential method rewritings then we need to iterate the instructions as the
-    // rewriting is instruction-sensitive (i.e., may be dependent on the invoke type).
-    if (hasPotentialRewrittenMethod) {
-      for (LirInstructionView view : code) {
-        view.accept(this);
-      }
-    }
-
-    if (constantPoolMapping == null) {
-      return code;
-    }
-
-    return code.newCodeWithRewrittenConstantPool(
-        item -> constantPoolMapping.getOrDefault(item, item));
-  }
-
-  private LirCode<EV> rewriteInstructionsWithInvokeTypeChanges(LirCode<EV> code) {
-    if (numberOfInvokeTypeChanges == 0) {
-      return code;
-    }
-    // Build a small map from method refs to index in case the type-dependent methods are already
-    // in the constant pool.
-    Reference2IntMap<DexMethod> methodIndices = new Reference2IntOpenHashMap<>();
-    LirConstant[] rewrittenConstants = code.getConstantPool();
-    for (int i = 0, length = rewrittenConstants.length; i < length; i++) {
-      LirConstant constant = rewrittenConstants[i];
-      if (constant instanceof DexMethod) {
-        methodIndices.put((DexMethod) constant, i);
-      }
-    }
-
-    IRMetadata irMetadata = code.getMetadataForIR();
-    ByteArrayWriter byteWriter = new ByteArrayWriter();
-    LirWriter lirWriter = new LirWriter(byteWriter);
-    List<LirConstant> methodsToAppend = new ArrayList<>(numberOfInvokeTypeChanges);
-    for (LirInstructionView view : code) {
-      int opcode = view.getOpcode();
-      // Instructions that do not have an invoke-type change are just mapped via identity.
-      if (LirOpcodes.isOneByteInstruction(opcode)) {
-        lirWriter.writeOneByteInstruction(opcode);
-        continue;
-      }
-      InvokeType type = getInvokeTypeThatMayChange(opcode);
-      if (type == null) {
-        int size = view.getRemainingOperandSizeInBytes();
-        lirWriter.writeInstruction(opcode, size);
-        while (size-- > 0) {
-          lirWriter.writeOperand(view.getNextU1());
-        }
-        continue;
-      }
-      // This is potentially an invoke with a type change, in such cases the method is mapped with
-      // the instruction updated to the new type. The constant pool is amended with the mapped
-      // method if needed.
-      int constantIndex = view.getNextConstantOperand();
-      DexMethod method = (DexMethod) code.getConstantItem(constantIndex);
-      MethodLookupResult result =
-          graphLens.lookupMethod(method, context.getReference(), type, codeLens);
-      if (result.getType() != type) {
-        --numberOfInvokeTypeChanges;
-        if (result.getType().isVirtual()) {
-          opcode = LirOpcodes.INVOKEVIRTUAL;
-          irMetadata.record(Opcodes.INVOKE_VIRTUAL);
-        } else if (result.getType().isInterface()) {
-          opcode = LirOpcodes.INVOKEINTERFACE;
-          irMetadata.record(Opcodes.INVOKE_INTERFACE);
-        } else {
-          throw new Unreachable(
-              "Unexpected change of invoke that may need an interface bit set: "
-                  + result.getType());
-        }
-        constantIndex =
-            methodIndices.computeIfAbsent(
-                result.getReference(),
-                ref -> {
-                  methodsToAppend.add(ref);
-                  return rewrittenConstants.length + methodsToAppend.size() - 1;
-                });
-      }
-      int constantIndexSize = ByteUtils.intEncodingSize(constantIndex);
-      int remainingSize = view.getRemainingOperandSizeInBytes();
-      lirWriter.writeInstruction(opcode, constantIndexSize + remainingSize);
-      ByteUtils.writeEncodedInt(constantIndex, lirWriter::writeOperand);
-      while (remainingSize-- > 0) {
-        lirWriter.writeOperand(view.getNextU1());
-      }
-    }
-    assert numberOfInvokeTypeChanges == 0;
-    // Note that since we assume 'null' in the mapping is identity this may end up with a stale
-    // reference to a no longer used method. That is not an issue as it will be pruned when
-    // building IR again, it is just a small and size overhead.
-    LirCode<EV> newCode =
-        code.copyWithNewConstantsAndInstructions(
-            irMetadata,
-            ArrayUtils.appendElements(code.getConstantPool(), methodsToAppend),
-            byteWriter.toByteArray());
-    return newCode;
-  }
-
-  private LirCode<EV> rewriteTryCatchTable(LirCode<EV> code) {
-    TryCatchTable tryCatchTable = code.getTryCatchTable();
-    if (tryCatchTable == null) {
-      return code;
-    }
-    TryCatchTable newTryCatchTable = tryCatchTable.rewriteWithLens(graphLens, codeLens);
-    return code.newCodeWithRewrittenTryCatchTable(newTryCatchTable);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
index 5272500..d6bc517 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.InnerClassAttribute;
@@ -22,7 +21,6 @@
 import com.android.tools.r8.graph.ProgramPackageCollection;
 import com.android.tools.r8.graph.SortedProgramPackageCollection;
 import com.android.tools.r8.graph.fixup.TreeFixerBase;
-import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.naming.Minifier.MinificationPackageNamingStrategy;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.repackaging.RepackagingLens.Builder;
@@ -113,18 +111,7 @@
             assert false;
           }
         }.fixupClasses(appView.appInfo().classesWithDeterministicOrder());
-    NestedGraphLens emptyRepackagingLens =
-        new NestedGraphLens(appView) {
-          @Override
-          protected boolean isLegitimateToHaveEmptyMappings() {
-            return true;
-          }
-
-          @Override
-          public <T extends DexReference> boolean isSimpleRenaming(T from, T to) {
-            return getPrevious().isSimpleRenaming(from, to);
-          }
-        };
+    RepackagingLens emptyRepackagingLens = new RepackagingLens.Builder().buildEmpty(appView);
     DirectMappedDexApplication newApplication =
         appView
             .appInfo()
@@ -479,58 +466,4 @@
           || appView.appInfo().getMissingClasses().contains(type);
     }
   }
-
-  /** Testing only. */
-  public static class SuffixRenamingRepackagingConfiguration implements RepackagingConfiguration {
-
-    private final String classNameSuffix;
-    private final DexItemFactory dexItemFactory;
-
-    public SuffixRenamingRepackagingConfiguration(
-        String classNameSuffix, DexItemFactory dexItemFactory) {
-      this.classNameSuffix = classNameSuffix;
-      this.dexItemFactory = dexItemFactory;
-    }
-
-    @Override
-    public String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
-      // Don't change the package of classes.
-      return pkg.getPackageDescriptor();
-    }
-
-    @Override
-    public boolean isPackageInTargetLocation(ProgramPackage pkg) {
-      return true;
-    }
-
-    @Override
-    public DexType getRepackagedType(
-        DexProgramClass clazz,
-        DexProgramClass outerClass,
-        String newPackageDescriptor,
-        BiMap<DexType, DexType> mappings) {
-      DexType repackagedDexType = clazz.getType();
-      // Rename the class consistently with its outer class.
-      if (outerClass != null) {
-        String simpleName = clazz.getType().getSimpleName();
-        String outerClassSimpleName = outerClass.getType().getSimpleName();
-        if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
-          String newSimpleName =
-              mappings.get(outerClass.getType()).getSimpleName()
-                  + simpleName.substring(outerClassSimpleName.length());
-          repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
-        }
-      }
-      // Append the class name suffix to all classes.
-      repackagedDexType = repackagedDexType.addSuffix(classNameSuffix, dexItemFactory);
-      // Ensure that the generated name is unique.
-      DexType finalRepackagedDexType = repackagedDexType;
-      for (int i = 1; mappings.inverse().containsKey(finalRepackagedDexType); i++) {
-        finalRepackagedDexType =
-            repackagedDexType.addSuffix(
-                Character.toString(INNER_CLASS_SEPARATOR) + i, dexItemFactory);
-      }
-      return finalRepackagedDexType;
-    }
-  }
 }
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 0b40c72..acc7ed6 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
@@ -16,6 +16,7 @@
 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 java.util.Collections;
 import java.util.Map;
 
 public class RepackagingLens extends NestedGraphLens {
@@ -36,7 +37,8 @@
 
   @Override
   public String lookupPackageName(String pkg) {
-    return packageRenamings.getOrDefault(getPrevious().lookupPackageName(pkg), pkg);
+    String previousPkg = getPrevious().lookupPackageName(pkg);
+    return packageRenamings.getOrDefault(previousPkg, previousPkg);
   }
 
   @Override
@@ -101,5 +103,16 @@
       return new RepackagingLens(
           appView, newFieldSignatures, newMethodSignatures, newTypes, packageRenamings);
     }
+
+    public RepackagingLens buildEmpty(AppView<AppInfoWithLiveness> appView) {
+      return new RepackagingLens(
+          appView, EMPTY_FIELD_MAP, EMPTY_METHOD_MAP, EMPTY_TYPE_MAP, Collections.emptyMap()) {
+
+        @Override
+        protected boolean isLegitimateToHaveEmptyMappings() {
+          return true;
+        }
+      };
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 1210325..22d19f7 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -7,6 +7,8 @@
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.IntListIterator;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -208,6 +210,35 @@
     return list;
   }
 
+  public static <T> List<T> newArrayListWithoutIndices(List<T> list, IntList indicesToRemove) {
+    // Verify each index to remove is unique and in bounds.
+    assert indicesToRemove.stream().distinct().count() == indicesToRemove.size();
+    assert indicesToRemove.stream().allMatch(indexToRemove -> indexToRemove < list.size());
+    if (indicesToRemove.size() == list.size()) {
+      return new ArrayList<>();
+    }
+    List<T> result = new ArrayList<>(list.size() - indicesToRemove.size());
+    IntListIterator indicesToRemoveIterator = indicesToRemove.iterator();
+    int nextIndexToRemove = indicesToRemoveIterator.nextInt();
+    for (int i = 0; i < list.size(); i++) {
+      assert i <= nextIndexToRemove;
+      if (i == nextIndexToRemove) {
+        if (indicesToRemoveIterator.hasNext()) {
+          nextIndexToRemove = indicesToRemoveIterator.nextInt();
+          assert nextIndexToRemove > i;
+        } else {
+          for (int j = i + 1; j < list.size(); j++) {
+            result.add(list.get(j));
+          }
+          break;
+        }
+      } else {
+        result.add(list.get(i));
+      }
+    }
+    return result;
+  }
+
   public static <T> ArrayList<T> newInitializedArrayList(int size, T element) {
     ArrayList<T> list = new ArrayList<>(size);
     for (int i = 0; i < size; i++) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
index 18e8417..d5e839e 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -18,11 +18,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.ir.conversion.MethodConversionOptions;
-import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
-import com.android.tools.r8.ir.conversion.OneTimeMethodProcessor;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
+import com.android.tools.r8.ir.conversion.LirConverter;
 import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
@@ -248,46 +244,12 @@
     return lens;
   }
 
-  // TODO(b/320432664): For code objects where the rewriting is an alpha renaming we can rewrite the
-  //  LIR directly without building IR.
   private void rewriteCodeWithLens(ExecutorService executorService, Timing timing)
       throws ExecutionException {
     if (mode.isInitial()) {
       return;
     }
-
-    timing.begin("Rewrite code");
-    MethodProcessorEventConsumer eventConsumer = MethodProcessorEventConsumer.empty();
-    OneTimeMethodProcessor.Builder methodProcessorBuilder =
-        OneTimeMethodProcessor.builder(eventConsumer, appView);
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      clazz.forEachProgramMethodMatching(
-          method ->
-              method.hasCode()
-                  && !(method.getCode() instanceof IncompleteVerticalClassMergerBridgeCode),
-          methodProcessorBuilder::add);
-    }
-
-    IRConverter converter = new IRConverter(appView);
-    converter.clearEnumUnboxer();
-    converter.clearServiceLoaderRewriter();
-    OneTimeMethodProcessor methodProcessor = methodProcessorBuilder.build();
-    methodProcessor.forEachWaveWithExtension(
-        (method, methodProcessingContext) ->
-            converter.processDesugaredMethod(
-                method,
-                OptimizationFeedbackIgnore.getInstance(),
-                methodProcessor,
-                methodProcessingContext,
-                MethodConversionOptions.forLirPhase(appView)
-                    .disableStringSwitchConversion()
-                    .setFinalizeAfterLensCodeRewriter()),
-        options.getThreadingModule(),
-        executorService);
-
-    // Clear type elements created during IR processing.
-    dexItemFactory.clearTypeElementsCache();
-    timing.end();
+    LirConverter.rewriteLirWithLens(appView, timing, executorService);
   }
 
   private void updateArtProfiles(
@@ -410,7 +372,7 @@
       return;
     }
     timing.begin("Mark rewritten with lens");
-    appView.clearCodeRewritings(executorService);
+    appView.clearCodeRewritings(executorService, timing);
     timing.end();
   }
 
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index 7fa9f56..fcbf5eb 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -100,6 +100,10 @@
     this.staticizedMethods = staticizedMethods;
   }
 
+  public boolean hasInterfaceBeenMergedIntoClass(DexType type) {
+    return mergedClasses.hasInterfaceBeenMergedIntoClass(type);
+  }
+
   private boolean isMerged(DexMethod method) {
     return mergedMethods.contains(method);
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
index 31d25ab..dbe28ca 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
@@ -11,6 +11,7 @@
   private final InternalOptions options;
 
   private boolean enabled = true;
+  private boolean enableInitial = true;
 
   public VerticalClassMergerOptions(InternalOptions options) {
     this.options = options;
@@ -20,9 +21,18 @@
     setEnabled(false);
   }
 
+  public void disableInitial() {
+    enableInitial = false;
+  }
+
   public boolean isEnabled(ClassMergerMode mode) {
-    assert mode != null;
-    return enabled && options.isOptimizing() && options.isShrinking();
+    if (!enabled || !options.isOptimizing() || !options.isShrinking()) {
+      return false;
+    }
+    if (mode.isInitial() && !enableInitial) {
+      return false;
+    }
+    return true;
   }
 
   public void setEnabled(boolean enabled) {
diff --git a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
index 384043c..228536e 100644
--- a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
@@ -36,7 +36,7 @@
   @Parameters(name = "{0}: {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withDexRuntimes().withAllApiLevels().build(), tests.keySet());
+        getTestParameters().withDexRuntimesAndAllApiLevels().build(), tests.keySet());
   }
 
   private static final String SMALI_DIR = ToolHelper.SMALI_BUILD_DIR;
@@ -106,6 +106,8 @@
     tests = testsBuilder.build();
   }
 
+  private static Set<String> invokeErrors = ImmutableSet.of("bad-codegen", "illegal-invokes");
+
   private static Map<String, Set<String>> missingClasses =
       ImmutableMap.of(
           "try-catch", ImmutableSet.of("test.X"),
@@ -244,6 +246,10 @@
         .addProgramDexFileData(Files.readAllBytes(originalDexFile))
         .applyIf(testJarExists, p -> p.addProgramFiles(testJar))
         .addDontWarn(missingClasses.getOrDefault(directoryName, Collections.emptySet()))
+        .addOptionsModification(
+            options ->
+                options.getTestingOptions().allowInvokeErrors =
+                    invokeErrors.contains(directoryName))
         .setMinApi(parameters)
         .compile()
         .run(parameters.getRuntime(), "Test")
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
index 1ba7dcf..4025566 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
@@ -4,18 +4,19 @@
 
 package com.android.tools.r8.classmerging.vertical;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 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 com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.android.tools.r8.utils.codeinspector.TypeSubject;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.junit.Test;
@@ -28,13 +29,17 @@
 @RunWith(Parameterized.class)
 public class ExceptionTablesTest extends VerticalClassMergerTestBase {
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  private final boolean disableInitial;
+
+  @Parameters(name = "{1}, disable initial: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), TestBase.getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public ExceptionTablesTest(TestParameters parameters) {
+  public ExceptionTablesTest(boolean disableInitial, TestParameters parameters) {
     super(parameters);
+    this.disableInitial = disableInitial;
   }
 
   @Test
@@ -42,7 +47,14 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(ExceptionTablesTest.class)
         .addKeepMainRule(TestClass.class)
-        .addVerticallyMergedClassesInspector(this::inspectVerticallyMergedClasses)
+        .applyIf(
+            disableInitial,
+            testBuilder ->
+                testBuilder.addOptionsModification(
+                    options -> options.getVerticalClassMergerOptions().disableInitial()),
+            testBuilder ->
+                testBuilder.addVerticallyMergedClassesInspector(
+                    this::inspectVerticallyMergedClasses))
         .setMinApi(parameters)
         .compile()
         .inspect(this::inspect)
@@ -58,8 +70,8 @@
     assertThat(inspector.clazz(TestClass.class), isPresent());
     assertThat(inspector.clazz(ExceptionB.class), isPresent());
     assertThat(inspector.clazz(Exception2.class), isPresent());
-    assertThat(inspector.clazz(ExceptionA.class), not(isPresent()));
-    assertThat(inspector.clazz(Exception1.class), not(isPresent()));
+    assertThat(inspector.clazz(ExceptionA.class), isAbsent());
+    assertThat(inspector.clazz(Exception1.class), isAbsent());
 
     // Check that only two exception guard types remain.
     MethodSubject mainMethodSubject = inspector.clazz(TestClass.class).mainMethod();
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/InvokeStaticToInterfaceVerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/InvokeStaticToInterfaceVerticalClassMergerTest.java
new file mode 100644
index 0000000..ba6a83c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/InvokeStaticToInterfaceVerticalClassMergerTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.classmerging.vertical;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvokeStaticToInterfaceVerticalClassMergerTest extends TestBase {
+
+  @Parameter(0)
+  public boolean disableInitialRoundOfVerticalClassMerging;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, disable initial: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .applyIf(
+            disableInitialRoundOfVerticalClassMerging,
+            b ->
+                b.addOptionsModification(
+                    options -> options.getVerticalClassMergerOptions().disableInitial()))
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              // Check interface is removed due to class merging and the static interface method is
+              // present on the subclass except when moved as a result of static interface method
+              // desugaring.
+              ClassSubject iClassSubject = inspector.clazz(I.class);
+              assertThat(iClassSubject, isAbsent());
+
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              MethodSubject fooMethodSubject = aClassSubject.uniqueMethodWithOriginalName("hello");
+              assertThat(
+                  fooMethodSubject,
+                  isPresentIf(parameters.canUseDefaultAndStaticInterfaceMethods()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // After vertical class merging, invoke should have the is-interface bit set to false.
+      I.hello();
+      System.out.println(new A());
+    }
+  }
+
+  interface I {
+
+    @NeverInline
+    static void hello() {
+      System.out.print("Hello");
+    }
+  }
+
+  static class A implements I {
+
+    @Override
+    public String toString() {
+      return ", world!";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticInterfaceNestedTest.java b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticInterfaceNestedTest.java
index a15add8..f683135 100644
--- a/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticInterfaceNestedTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticInterfaceNestedTest.java
@@ -5,39 +5,43 @@
 package com.android.tools.r8.desugar.staticinterfacemethod;
 
 import static com.android.tools.r8.desugar.staticinterfacemethod.InvokeStaticInterfaceNestedTest.Library.foo;
-import static org.junit.Assert.assertThrows;
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilationIf;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DesugarTestConfiguration;
-import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class InvokeStaticInterfaceNestedTest extends TestBase {
 
-  private final TestParameters parameters;
-  private final String UNEXPECTED_SUCCESS = "Hello World!";
+  private static final String UNEXPECTED_SUCCESS = "Hello World!";
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  @Parameter(0)
+  public boolean allowInvokeErrors;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, allow invalid invokes: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
   }
 
-  public InvokeStaticInterfaceNestedTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  private void checkDexResult(TestRunResult<?> runResult, boolean isDesugared) {
+  private void inspectRunResult(TestRunResult<?> runResult, boolean isDesugared) {
     boolean didDesugarInterfaceMethods =
         isDesugared && !parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring();
     if (parameters.isCfRuntime()) {
@@ -67,6 +71,7 @@
 
   @Test
   public void testDesugar() throws Exception {
+    assumeFalse(allowInvokeErrors);
     testForDesugaring(parameters)
         .addProgramClassFileData(
             rewriteToUseNonInterfaceMethodReference(Main.class, "main"),
@@ -76,27 +81,27 @@
             result ->
                 result.applyIf(
                     DesugarTestConfiguration::isDesugared,
-                    r -> checkDexResult(r, true),
-                    r -> checkDexResult(r, false)));
+                    r -> inspectRunResult(r, true),
+                    r -> inspectRunResult(r, false)));
   }
 
   @Test
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
-    R8FullTestBuilder testBuilder =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(
-                rewriteToUseNonInterfaceMethodReference(Main.class, "main"),
-                rewriteToUseNonInterfaceMethodReference(Library.class, "foo"))
-            .addKeepAllClassesRule()
-            .setMinApi(parameters)
-            .addKeepMainRule(Main.class);
-    if (parameters.isDexRuntime()) {
-      checkDexResult(testBuilder.run(parameters.getRuntime(), Main.class), true);
-    } else {
-      // TODO(b/166213037): Should not throw an error.
-      assertThrows(CompilationFailedException.class, testBuilder::compile);
-    }
+    assertFailsCompilationIf(
+        !allowInvokeErrors,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClassFileData(
+                    rewriteToUseNonInterfaceMethodReference(Main.class, "main"),
+                    rewriteToUseNonInterfaceMethodReference(Library.class, "foo"))
+                .addKeepAllClassesRule()
+                .addOptionsModification(
+                    options -> options.getTestingOptions().allowInvokeErrors = allowInvokeErrors)
+                .setMinApi(parameters)
+                .compile()
+                .run(parameters.getRuntime(), Main.class)
+                .apply(runResult -> inspectRunResult(runResult, parameters.isDexRuntime())));
   }
 
   private byte[] rewriteToUseNonInterfaceMethodReference(Class<?> clazz, String methodName)
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
index e070f46..0e3f590 100644
--- a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialInterfaceWithBridge3Test.java
@@ -13,13 +13,13 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
@@ -27,25 +27,21 @@
 
   private static final String EXPECTED = StringUtils.lines("Hello World!");
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public InvokeSpecialInterfaceWithBridge3Test(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testRuntime() throws Exception {
-    TestRunResult<?> result =
-        testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
-            .addProgramClasses(I.class, A.class, Main.class)
-            .addProgramClassFileData(getClassWithTransformedInvoked())
-            .run(parameters.getRuntime(), Main.class)
-            .apply(this::inspectRunResult);
+    testForRuntime(parameters)
+        .addProgramClasses(I.class, A.class, Main.class)
+        .addProgramClassFileData(getClassWithTransformedInvoked())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::inspectRunResult);
   }
 
   private void inspectRunResult(SingleTestRunResult<?> runResult) {
@@ -77,6 +73,7 @@
         .addProgramClasses(I.class, A.class, Main.class)
         .addProgramClassFileData(getClassWithTransformedInvoked())
         .addKeepMainRule(Main.class)
+        .addOptionsModification(options -> options.getTestingOptions().allowInvokeErrors = true)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class)
         .applyIf(