Introduce another round of vertical class merging

Bug: b/315445393
Bug: b/320431939
Change-Id: Ic9bb6ade2a73fca214882750c7a5df8c0c96e6cb
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 65ff14d..b1b7241 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -105,10 +105,18 @@
     return getParameter(argumentIndex - 1);
   }
 
+  public DexType getArgumentTypeForNonStaticMethod(int argumentIndex) {
+    return getArgumentType(argumentIndex, false);
+  }
+
   public int getNumberOfArguments(boolean isStatic) {
     return getArity() + BooleanUtils.intValue(!isStatic);
   }
 
+  public int getNumberOfArgumentsForNonStaticMethod() {
+    return getNumberOfArguments(false);
+  }
+
   public DexType getParameter(int index) {
     return proto.getParameter(index);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodCollection.java b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
index cc69b8e..0f10f5a 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -335,6 +335,9 @@
   }
 
   public void removeMethods(Set<DexEncodedMethod> methods) {
+    if (methods.isEmpty()) {
+      return;
+    }
     backing.removeMethods(methods);
     resetDirectMethodCaches();
     resetVirtualMethodCaches();
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 9fb677b..b0f61a0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -423,6 +423,13 @@
           || invokedMethod.mustBeInlinedIntoInstanceInitializer(appView)) {
         return false;
       }
+    } else if (toBeReplaced.isInvokeVirtual()) {
+      // TODO(b/321171043): This is only needed as long as we change constructors in vertical class
+      //  merging to non-constructors methods.
+      DexMethod invokedMethod = toBeReplaced.asInvokeVirtual().getInvokedMethod();
+      if (invokedMethod.mustBeInlinedIntoInstanceInitializer(appView)) {
+        return false;
+      }
     }
     if (toBeReplaced.instructionMayHaveSideEffects(
         appView, context, Instruction.SideEffectAssumption.RECEIVER_NOT_NULL)) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 365708f..534c482 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -197,6 +197,10 @@
     this.type = type;
   }
 
+  public static Value createNoDebugLocal(int number, TypeElement type) {
+    return new Value(number, type, null);
+  }
+
   public boolean isFixedRegisterValue() {
     return false;
   }
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 2dc1a3f..db19bb2 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
@@ -126,7 +126,7 @@
   private final StringSwitchRemover stringSwitchRemover;
   private final TypeChecker typeChecker;
   protected ServiceLoaderRewriter serviceLoaderRewriter;
-  protected final EnumUnboxer enumUnboxer;
+  protected EnumUnboxer enumUnboxer;
   protected final NumberUnboxer numberUnboxer;
   protected final RemoveVerificationErrorForUnknownReturnedValues
       removeVerificationErrorForUnknownReturnedValues;
@@ -299,6 +299,14 @@
     this(AppView.createForD8(appInfo));
   }
 
+  public void clearEnumUnboxer() {
+    enumUnboxer = EnumUnboxer.empty();
+  }
+
+  public void clearServiceLoaderRewriter() {
+    serviceLoaderRewriter = null;
+  }
+
   public Inliner getInliner() {
     return inliner;
   }
@@ -515,13 +523,14 @@
       feedback.markProcessed(method.getDefinition(), ConstraintWithTarget.NEVER);
       return Timing.empty();
     }
-    return optimize(code, feedback, methodProcessor, methodProcessingContext);
+    return optimize(code, feedback, conversionOptions, methodProcessor, methodProcessingContext);
   }
 
   // TODO(b/140766440): Convert all sub steps an implementer of CodeOptimization
   Timing optimize(
       IRCode code,
       OptimizationFeedback feedback,
+      MethodConversionOptions methodConversionOptions,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     ProgramMethod context = code.context();
@@ -595,6 +604,14 @@
       return timing;
     }
 
+    if (methodConversionOptions.shouldFinalizeAfterLensCodeRewriter()) {
+      deadCodeRemover.run(code, timing);
+      timing.begin("Finalize IR");
+      finalizeIR(code, feedback, BytecodeMetadataProvider.empty(), timing);
+      timing.end();
+      return timing;
+    }
+
     if (options.canHaveArtStringNewInitBug()) {
       timing.begin("Check for new-init issue");
       TrivialPhiSimplifier.ensureDirectStringNewToInit(appView, code);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
index ceed103..7732f47 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
@@ -84,11 +84,14 @@
 
   public abstract boolean isStringSwitchConversionEnabled();
 
+  public abstract boolean shouldFinalizeAfterLensCodeRewriter();
+
   public static class MutableMethodConversionOptions extends MethodConversionOptions {
 
     private Target target;
     private boolean enablePeepholeOptimizations = true;
     private boolean enableStringSwitchConversion;
+    private boolean finalizeAfterLensCodeRewriter;
 
     private MutableMethodConversionOptions(Target target, boolean enableStringSwitchConversion) {
       this.target = target;
@@ -109,6 +112,11 @@
       return this;
     }
 
+    public MutableMethodConversionOptions setFinalizeAfterLensCodeRewriter() {
+      finalizeAfterLensCodeRewriter = true;
+      return this;
+    }
+
     @Override
     public boolean isGeneratingLir() {
       return target == Target.LIR;
@@ -133,6 +141,11 @@
     public boolean isStringSwitchConversionEnabled() {
       return enableStringSwitchConversion;
     }
+
+    @Override
+    public boolean shouldFinalizeAfterLensCodeRewriter() {
+      return finalizeAfterLensCodeRewriter;
+    }
   }
 
   public static class ThrowingMethodConversionOptions extends MutableMethodConversionOptions {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OneTimeMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/OneTimeMethodProcessor.java
index 6fed019..b75e95f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OneTimeMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OneTimeMethodProcessor.java
@@ -32,6 +32,10 @@
     this.wave = wave;
   }
 
+  public static Builder builder(MethodProcessorEventConsumer eventConsumer, AppView<?> appView) {
+    return builder(eventConsumer, appView.createProcessorContext());
+  }
+
   public static Builder builder(
       MethodProcessorEventConsumer eventConsumer, ProcessorContext processorContext) {
     return new Builder(eventConsumer, processorContext);
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 5a1d5b0..b6e87c2 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
@@ -59,7 +59,7 @@
       while (!worklist.isEmpty()) {
         BasicBlock block = worklist.removeLast();
         removeDeadInstructions(worklist, code, block, affectedValues, valueIsDeadAnalysis);
-        removeDeadPhis(worklist, block, valueIsDeadAnalysis);
+        removeDeadAndTrivialPhis(worklist, block, valueIsDeadAnalysis);
       }
       affectedValues.narrowingWithAssumeRemoval(appView, code);
     } while (branchSimplifier
@@ -117,7 +117,7 @@
     }
   }
 
-  private void removeDeadPhis(
+  private void removeDeadAndTrivialPhis(
       Queue<BasicBlock> worklist, BasicBlock block, ValueIsDeadAnalysis valueIsDeadAnalysis) {
     Iterator<Phi> phiIt = block.getPhis().iterator();
     while (phiIt.hasNext()) {
@@ -128,6 +128,9 @@
           operand.removePhiUser(phi);
           updateWorklist(worklist, operand);
         }
+      } else if (phi.isTrivialPhi()) {
+        phiIt.remove();
+        phi.removeTrivialPhi();
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index 1a6c122..321c554 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -48,7 +48,6 @@
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ArrayUtils;
-import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
@@ -508,10 +507,6 @@
     }
 
     private boolean verifyWasInstanceInitializer() {
-      VerticallyMergedClasses verticallyMergedClasses = appView.getVerticallyMergedClasses();
-      assert verticallyMergedClasses != null;
-      assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
-          || appView.horizontallyMergedClasses().isMergeTarget(method.getHolderType());
       assert appView
           .dexItemFactory()
           .isConstructor(appView.graphLens().getOriginalMethodSignature(method.getReference()));
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 63e6bbb..5279f1d 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -5025,11 +5025,15 @@
     }
     DexReference referencedItem = identifierLookupResult.getReference();
     if (referencedItem.isDexType()) {
+      DexType referencedType = referencedItem.asDexType();
+      if (!referencedType.isClassType()
+          || appView.allMergedClasses().isMergeSource(referencedType)) {
+        return;
+      }
       assert identifierLookupResult.isTypeResult();
       IdentifierNameStringTypeLookupResult identifierTypeLookupResult =
           identifierLookupResult.asTypeResult();
-      DexProgramClass clazz =
-          getProgramClassOrNullFromReflectiveAccess(referencedItem.asDexType(), method);
+      DexProgramClass clazz = getProgramClassOrNullFromReflectiveAccess(referencedType, method);
       if (clazz == null) {
         return;
       }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
index bf712ad..f93292d 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.CompilationState;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMember;
@@ -44,6 +45,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import java.util.Collection;
@@ -193,7 +195,7 @@
                     candidate ->
                         availableMethodSignatures.test(candidate)
                             && source.lookupVirtualMethod(candidate) == null);
-            add(directMethods, resultingConstructor, MethodSignatureEquivalence.get());
+            add(virtualMethods, resultingConstructor, MethodSignatureEquivalence.get());
             blockRedirectionOfSuperCalls(resultingConstructor);
           } else {
             DexEncodedMethod resultingDirectMethod =
@@ -264,7 +266,7 @@
         // unique name, such that relevant invoke-super instructions can be rewritten to target
         // this method directly.
         resultingMethod = renameMethod(virtualMethod, availableMethodSignatures, Rename.ALWAYS);
-        makePublicFinal(resultingMethod);
+        makePublicFinal(resultingMethod.getAccessFlags());
       }
 
       add(
@@ -664,13 +666,28 @@
     } while (!availableMethodSignatures.test(newSignature));
 
     DexEncodedMethod result =
-        method.toTypeSubstitutedMethodAsInlining(newSignature, dexItemFactory);
+        method.toTypeSubstitutedMethodAsInlining(
+            newSignature,
+            dexItemFactory,
+            // Set the compilation state to satisfy the inliner in the final round of vertical class
+            // merging, which checks that the method is processed.
+            methodBuilder -> {
+              methodBuilder
+                  .modifyAccessFlags(
+                      accessFlags -> {
+                        // TODO(b/321171043): Do not change constructors to non-constructors as this
+                        //  would avoid the need for force inlining (though we would then need to
+                        //  find a fresh constructor signature).
+                        // Renamed constructors turn into ordinary non-constructor methods.
+                        accessFlags.unsetConstructor();
+                        makePublicFinal(accessFlags);
+                      })
+                  .setCompilationState(CompilationState.PROCESSED_INLINING_CANDIDATE_ANY)
+                  .setIsLibraryMethodOverride(OptionalBool.FALSE);
+            });
+    // TODO(b/321171043): Do not mark force inlining.
     result.getMutableOptimizationInfo().markForceInline();
     lensBuilder.recordMove(method, result);
-    // Renamed constructors turn into ordinary private functions. They can be private, as
-    // they are only references from their direct subclass, which they were merged into.
-    result.getAccessFlags().unsetConstructor();
-    makePrivate(result);
     return result;
   }
 
@@ -738,16 +755,7 @@
     return field.toTypeSubstitutedField(appView, newSignature);
   }
 
-  private static void makePrivate(DexEncodedMethod method) {
-    MethodAccessFlags accessFlags = method.getAccessFlags();
-    assert !accessFlags.isAbstract();
-    accessFlags.unsetPublic();
-    accessFlags.unsetProtected();
-    accessFlags.setPrivate();
-  }
-
-  private static void makePublicFinal(DexEncodedMethod method) {
-    MethodAccessFlags accessFlags = method.getAccessFlags();
+  private static void makePublicFinal(MethodAccessFlags accessFlags) {
     assert !accessFlags.isAbstract();
     accessFlags.unsetPrivate();
     accessFlags.unsetProtected();
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/IncompleteVerticalClassMergerBridgeCode.java b/src/main/java/com/android/tools/r8/verticalclassmerging/IncompleteVerticalClassMergerBridgeCode.java
index 60694ae..f13c3c6 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/IncompleteVerticalClassMergerBridgeCode.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/IncompleteVerticalClassMergerBridgeCode.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging;
 
+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.CfCode;
@@ -11,18 +14,29 @@
 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.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
+import com.android.tools.r8.lightir.LirBuilder;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirEncodingStrategy;
+import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
- * A short-lived piece of code that will be converted into {@link CfCode} using the method {@link
- * IncompleteVerticalClassMergerBridgeCode#toCfCode(DexItemFactory, VerticalClassMergerGraphLens)}.
+ * A short-lived piece of code that will be converted into {@link CfCode} using {@link
+ * #toCfCode(DexItemFactory)} or {@link LirCode} using {@link #toLirCode(AppView)}.
  */
 public class IncompleteVerticalClassMergerBridgeCode extends Code {
 
@@ -86,6 +100,48 @@
         .build();
   }
 
+  public LirCode<?> toLirCode(AppView<AppInfoWithLiveness> appView) {
+    boolean isD8R8Synthesized = true;
+    LirEncodingStrategy<Value, Integer> strategy =
+        LirStrategy.getDefaultStrategy().getEncodingStrategy();
+    LirBuilder<Value, Integer> lirBuilder =
+        LirCode.builder(method, isD8R8Synthesized, strategy, appView.options())
+            .setMetadata(IRMetadata.unknown());
+
+    // Add all arguments.
+    List<Value> argumentValues = new ArrayList<>();
+    int instructionIndex = 0;
+    for (; instructionIndex < method.getNumberOfArgumentsForNonStaticMethod(); instructionIndex++) {
+      DexType argumentType = method.getArgumentTypeForNonStaticMethod(instructionIndex);
+      TypeElement argumentTypeElement =
+          argumentType.toTypeElement(
+              appView, instructionIndex == 0 ? definitelyNotNull() : maybeNull());
+      Value argumentValue = Value.createNoDebugLocal(instructionIndex, argumentTypeElement);
+      argumentValues.add(argumentValue);
+      strategy.defineValue(argumentValue, argumentValue.getNumber());
+      lirBuilder.addArgument(instructionIndex, argumentType.isBooleanType());
+    }
+
+    if (type.isStatic()) {
+      lirBuilder.addInvokeStatic(invocationTarget, argumentValues, isInterface);
+    } else if (isInterface) {
+      lirBuilder.addInvokeInterface(invocationTarget, argumentValues);
+    } else {
+      lirBuilder.addInvokeVirtual(invocationTarget, argumentValues);
+    }
+
+    if (method.getReturnType().isVoidType()) {
+      lirBuilder.addReturnVoid();
+    } else {
+      Value returnValue =
+          Value.createNoDebugLocal(instructionIndex, method.getReturnType().toTypeElement(appView));
+      strategy.defineValue(returnValue, returnValue.getNumber());
+      lirBuilder.addReturn(returnValue);
+    }
+
+    return lirBuilder.build();
+  }
+
   // Implement Code.
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java
index d365ddd..53ee29c 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java
@@ -49,7 +49,8 @@
     if (previousLens != null
         && previousLens != codeLens
         && previousLens.isVerticalClassMergerLens()) {
-      return new InterfaceTypeToClassTypeLensCodeRewriterHelperImpl(appView, code);
+      return new InterfaceTypeToClassTypeLensCodeRewriterHelperImpl(
+          appView, code, graphLens, codeLens);
     }
     return new EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper();
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java
index 3087061..56010ad 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java
@@ -12,6 +12,7 @@
 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.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -38,13 +39,20 @@
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final IRCode code;
+  private final GraphLens graphLens;
+  private final GraphLens codeLens;
 
   private final Map<Instruction, Deque<WorklistItem>> worklist = new IdentityHashMap<>();
 
   public InterfaceTypeToClassTypeLensCodeRewriterHelperImpl(
-      AppView<? extends AppInfoWithClassHierarchy> appView, IRCode code) {
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCode code,
+      GraphLens graphLens,
+      GraphLens codeLens) {
     this.appView = appView;
     this.code = code;
+    this.graphLens = graphLens;
+    this.codeLens = codeLens;
   }
 
   @Override
@@ -91,7 +99,7 @@
       InstructionListIterator instructionIterator) {
     assert !rewrittenReturn.isReturnVoid();
     DexMethod originalMethodSignature =
-        appView.graphLens().getOriginalMethodSignature(code.context().getReference());
+        graphLens.getOriginalMethodSignature(code.context().getReference(), codeLens);
     DexType originalReturnType = originalMethodSignature.getReturnType();
     DexType rewrittenReturnType = code.context().getReturnType();
     if (needsCastForOperand(
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
index 8c76978..9dc57d5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSpecialToDefaultLibraryMethodUseRegistry.java
@@ -1,5 +1,6 @@
 package com.android.tools.r8.verticalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -10,14 +11,30 @@
 public class InvokeSpecialToDefaultLibraryMethodUseRegistry
     extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
+  private final ClassMergerMode mode;
+
   public InvokeSpecialToDefaultLibraryMethodUseRegistry(
-      AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
+      AppView<AppInfoWithLiveness> appView, ProgramMethod context, ClassMergerMode mode) {
     super(appView, context, false);
+    this.mode = mode;
     assert context.getHolder().isInterface();
   }
 
   @Override
   public void registerInvokeSpecial(DexMethod method) {
+    assert mode.isInitial();
+    handleInvokeSpecial(method);
+  }
+
+  // Handle invoke-super callbacks in the final round of class merging where we have LIR instead of
+  // CF.
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    assert mode.isFinal();
+    handleInvokeSpecial(method);
+  }
+
+  private void handleInvokeSpecial(DexMethod method) {
     ProgramMethod context = getContext();
     if (!method.getHolderType().isIdenticalTo(context.getHolderType())) {
       return;
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 d791b84..68f4dbd 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -12,10 +12,16 @@
 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.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 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.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
@@ -26,6 +32,7 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.Timing.TimingMerger;
+import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -113,11 +120,29 @@
         ProfileCollectionAdditions.create(appView);
     VerticalClassMergerGraphLens lens =
         runFixup(profileCollectionAdditions, verticalClassMergerResult, executorService, timing);
-    updateKeepInfoForMergedClasses(verticalClassMergerResult);
     assert verifyGraphLens(lens, verticalClassMergerResult);
+
+    // Update keep info and art profiles.
+    updateKeepInfoForMergedClasses(verticalClassMergerResult);
     updateArtProfiles(profileCollectionAdditions, lens, verticalClassMergerResult);
+
+    // Remove merged classes and rewrite AppView with the new lens.
     appView.rewriteWithLens(lens, executorService, timing);
+
+    // The code must be rewritten before we remove the merged classes from the app. Otherwise we
+    // can't build IR.
+    rewriteCodeWithLens(executorService);
+
+    // Remove force inlined constructors.
+    removeForceInlinedConstructors(executorService);
+    removeMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses());
+
+    // Convert the (incomplete) synthesized bridges to CF or LIR.
     finalizeSynthesizedBridges(verticalClassMergerResult.getSynthesizedBridges(), lens);
+
+    // Finally update the code lens to signal that the code is fully up to date.
+    markRewrittenWithLens(executorService);
+
     appView.notifyOptimizationFinishedForTesting();
   }
 
@@ -148,7 +173,7 @@
             connectedComponent -> {
               Timing threadTiming = Timing.create("Compute classes to merge in component", options);
               ConnectedComponentVerticalClassMerger connectedComponentMerger =
-                  new VerticalClassMergerPolicyExecutor(appView, immediateSubtypingInfo)
+                  new VerticalClassMergerPolicyExecutor(appView, immediateSubtypingInfo, mode)
                       .run(connectedComponent, executorService, threadTiming);
               if (!connectedComponentMerger.isEmpty()) {
                 synchronized (connectedComponentMergers) {
@@ -217,19 +242,61 @@
     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) throws ExecutionException {
+    if (mode.isInitial()) {
+      return;
+    }
+
+    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,
+                // TODO(b/321171043): Set setFinalizeAfterLensCodeRewriter() on the
+                //  MethodConversionOptions to improve build speed (no need to run all
+                //  optimizations!). A prerequisite for this is that we remove all uses of force
+                //  inlining in the vertical class merger.
+                MethodConversionOptions.forLirPhase(appView)),
+        options.getThreadingModule(),
+        executorService);
+
+    // Clear type elements created during IR processing.
+    dexItemFactory.clearTypeElementsCache();
+  }
+
   private void updateArtProfiles(
       ProfileCollectionAdditions profileCollectionAdditions,
       VerticalClassMergerGraphLens verticalClassMergerLens,
       VerticalClassMergerResult verticalClassMergerResult) {
     // Include bridges in art profiles.
-    if (!profileCollectionAdditions.isNop()) {
-      List<IncompleteVerticalClassMergerBridgeCode> synthesizedBridges =
-          verticalClassMergerResult.getSynthesizedBridges();
-      for (IncompleteVerticalClassMergerBridgeCode synthesizedBridge : synthesizedBridges) {
-        profileCollectionAdditions.applyIfContextIsInProfile(
-            verticalClassMergerLens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
-            additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
-      }
+    if (profileCollectionAdditions.isNop()) {
+      return;
+    }
+    List<IncompleteVerticalClassMergerBridgeCode> synthesizedBridges =
+        verticalClassMergerResult.getSynthesizedBridges();
+    for (IncompleteVerticalClassMergerBridgeCode synthesizedBridge : synthesizedBridges) {
+      profileCollectionAdditions.applyIfContextIsInProfile(
+          verticalClassMergerLens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
+          additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
     }
     profileCollectionAdditions.commit(appView);
   }
@@ -247,6 +314,55 @@
         });
   }
 
+  // TODO(b/321171043): No need to forcefully remove these force inlining constructors if we don't
+  //  use force inlining (though it may be desirable to apply inlining also in the final round of
+  //  vertical class merging if a merged constructor has a single caller inside the target class).
+  private void removeForceInlinedConstructors(ExecutorService executorService)
+      throws ExecutionException {
+    if (mode.isInitial()) {
+      return;
+    }
+    PrunedItems.Builder prunedItemsBuilder =
+        PrunedItems.concurrentBuilder().setPrunedApp(appView.app());
+    ThreadUtils.<DexProgramClass, Exception>processItems(
+        consumer -> {
+          for (DexProgramClass clazz : appView.appInfo().classes()) {
+            if (!clazz.isInterface()) {
+              consumer.accept(clazz);
+            }
+          }
+        },
+        clazz -> {
+          Set<DexEncodedMethod> methodsToRemove = Sets.newIdentityHashSet();
+          clazz.forEachProgramMethodMatching(
+              method -> method.willBeInlinedIntoInstanceInitializer(dexItemFactory),
+              method -> methodsToRemove.add(method.getDefinition()));
+          clazz.getMethodCollection().removeMethods(methodsToRemove);
+          methodsToRemove.forEach(
+              removedMethod -> prunedItemsBuilder.addRemovedMethod(removedMethod.getReference()));
+        },
+        options.getThreadingModule(),
+        executorService);
+    PrunedItems prunedItems = prunedItemsBuilder.build();
+    appView.pruneItems(prunedItems, executorService, Timing.empty());
+    appView.appInfo().getMethodAccessInfoCollection().withoutPrunedItems(prunedItems);
+  }
+
+  private void removeMergedClasses(VerticallyMergedClasses verticallyMergedClasses) {
+    if (mode.isInitial()) {
+      return;
+    }
+
+    DirectMappedDexApplication newApplication =
+        appView
+            .app()
+            .asDirect()
+            .builder()
+            .removeProgramClasses(clazz -> verticallyMergedClasses.isMergeSource(clazz.getType()))
+            .build();
+    appView.setAppInfo(appView.appInfo().rebuildWithLiveness(newApplication));
+  }
+
   private void finalizeSynthesizedBridges(
       List<IncompleteVerticalClassMergerBridgeCode> bridges, VerticalClassMergerGraphLens lens) {
     KeepInfoCollection keepInfo = appView.getKeepInfo();
@@ -258,7 +374,11 @@
       assert target != null;
 
       // Finalize code.
-      bridge.setCode(code.toCfCode(dexItemFactory, lens), appView);
+      assert mode.isInitial() == appView.testing().isPreLirPhase();
+      assert mode.isFinal() == appView.testing().isSupportedLirPhase();
+      bridge.setCode(
+          mode.isInitial() ? code.toCfCode(dexItemFactory, lens) : code.toLirCode(appView),
+          appView);
 
       // Copy keep info to newly synthesized methods.
       keepInfo.mutate(
@@ -267,6 +387,13 @@
     }
   }
 
+  private void markRewrittenWithLens(ExecutorService executorService) throws ExecutionException {
+    if (mode.isInitial()) {
+      return;
+    }
+    appView.clearCodeRewritings(executorService);
+  }
+
   private boolean verifyGraphLens(
       VerticalClassMergerGraphLens graphLens, VerticalClassMergerResult verticalClassMergerResult) {
     // Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
@@ -297,12 +424,13 @@
     // pinned, because this rewriting does not affect A.method() in any way.
     assert graphLens.assertPinnedNotModified(appView);
 
+    GraphLens previousLens = graphLens.getPrevious();
     VerticallyMergedClasses mergedClasses = verticalClassMergerResult.getVerticallyMergedClasses();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       for (DexEncodedMethod encodedMethod : clazz.methods()) {
         DexMethod method = encodedMethod.getReference();
-        DexMethod originalMethod = graphLens.getOriginalMethodSignature(method);
-        DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod);
+        DexMethod originalMethod = graphLens.getOriginalMethodSignature(method, previousLens);
+        DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod, previousLens);
 
         // Must be able to map back and forth.
         if (encodedMethod.hasCode()
@@ -315,7 +443,7 @@
           DexMethod implementationMethod =
               ((IncompleteVerticalClassMergerBridgeCode) encodedMethod.getCode()).getTarget();
           DexMethod originalImplementationMethod =
-              graphLens.getOriginalMethodSignature(implementationMethod);
+              graphLens.getOriginalMethodSignature(implementationMethod, previousLens);
           assert originalMethod.isIdenticalTo(originalImplementationMethod);
           assert implementationMethod.isIdenticalTo(renamedMethod);
         } else {
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 994795d..20bb15f 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -283,6 +283,12 @@
   @Override
   protected InvokeType mapInvocationType(
       DexMethod newMethod, DexMethod previousMethod, InvokeType type) {
+    // TODO(b/321171043): Remove the need to map constructor calls to invoke-virtual.
+    if (dexItemFactory().isConstructor(previousMethod)
+        && !dexItemFactory().isConstructor(newMethod)) {
+      assert newMethod.getName().startsWith(dexItemFactory().temporaryConstructorMethodPrefix);
+      return InvokeType.VIRTUAL;
+    }
     if (isStaticized(newMethod)) {
       return InvokeType.STATIC;
     }
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 b7f435c..0accf81 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
@@ -21,7 +21,14 @@
   }
 
   public boolean isEnabled(ClassMergerMode mode) {
-    return enabled && options.isOptimizing() && options.isShrinking() && mode.isInitial();
+    if (!enabled || !options.isOptimizing() || !options.isShrinking()) {
+      return false;
+    }
+    // TODO(b/320431939): Enable final round of vertical class merging for desugared library.
+    if (mode.isFinal() && !options.synthesizedClassPrefix.isEmpty()) {
+      return false;
+    }
+    return true;
   }
 
   public void setEnabled(boolean enabled) {
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
index 6b6f314..2691d18 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.classmerging.PolicyExecutor;
 import com.android.tools.r8.graph.AppView;
@@ -17,6 +18,7 @@
 import com.android.tools.r8.verticalclassmerging.policies.NoDirectlyInstantiatedClassesPolicy;
 import com.android.tools.r8.verticalclassmerging.policies.NoEnclosingMethodAttributesPolicy;
 import com.android.tools.r8.verticalclassmerging.policies.NoFieldResolutionChangesPolicy;
+import com.android.tools.r8.verticalclassmerging.policies.NoFinalSourceInstanceFieldsWithConstructorInliningPolicy;
 import com.android.tools.r8.verticalclassmerging.policies.NoIllegalAccessesPolicy;
 import com.android.tools.r8.verticalclassmerging.policies.NoInnerClassAttributesPolicy;
 import com.android.tools.r8.verticalclassmerging.policies.NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy;
@@ -49,11 +51,15 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+  private final ClassMergerMode mode;
 
   VerticalClassMergerPolicyExecutor(
-      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      ClassMergerMode mode) {
     this.appView = appView;
     this.immediateSubtypingInfo = immediateSubtypingInfo;
+    this.mode = mode;
   }
 
   ConnectedComponentVerticalClassMerger run(
@@ -64,6 +70,7 @@
     Collection<Policy> policies =
         List.of(
             new NoDirectlyInstantiatedClassesPolicy(appView),
+            new NoFinalSourceInstanceFieldsWithConstructorInliningPolicy(appView, mode),
             new NoInterfacesWithUnknownSubtypesPolicy(appView),
             new NoKeptClassesPolicy(appView),
             new SameFeatureSplitPolicy(appView),
@@ -82,7 +89,7 @@
             new NoMethodResolutionChangesPolicy(appView),
             new NoIllegalAccessesPolicy(appView),
             new NoClassInitializationChangesPolicy(appView),
-            new NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy(appView),
+            new NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy(appView, mode),
             new NoInvokeSuperNoSuchMethodErrorsPolicy(appView),
             new SuccessfulVirtualMethodResolutionInTargetPolicy(appView),
             new NoAbstractMethodsOnAbstractClassesPolicy(appView),
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java
index d8b6933..8d137ee 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoClassInitializationChangesPolicy.java
@@ -3,12 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging.policies;
 
+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.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
 
-public class NoClassInitializationChangesPolicy extends VerticalClassMergerPolicy {
+public class NoClassInitializationChangesPolicy
+    extends VerticalClassMergerPolicyWithPreprocessing<Map<DexProgramClass, Set<DexProgramClass>>> {
 
   private final AppView<AppInfoWithLiveness> appView;
 
@@ -17,21 +25,47 @@
   }
 
   @Override
-  public boolean canMerge(VerticalMergeGroup group) {
+  public boolean canMerge(
+      VerticalMergeGroup group,
+      Map<DexProgramClass, Set<DexProgramClass>> sourcesWithClassInitializers) {
     // For interface types, this is more complicated, see:
     // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
     // We basically can't move the clinit, since it is not called when implementing classes have
     // their clinit called - except when the interface has a default method.
     DexProgramClass sourceClass = group.getSource();
     DexProgramClass targetClass = group.getTarget();
-    return (!sourceClass.hasClassInitializer() || !targetClass.hasClassInitializer())
-        && !targetClass.classInitializationMayHaveSideEffects(
+    // TODO(b/320433836): Add support for concatenating <clinit>s.
+    if (sourceClass.hasClassInitializer()) {
+      if (targetClass.hasClassInitializer()
+          || sourcesWithClassInitializers.get(targetClass).size() > 1) {
+        boolean removed = sourcesWithClassInitializers.get(targetClass).remove(sourceClass);
+        assert removed;
+        return false;
+      }
+    }
+    assert !sourceClass.hasClassInitializer() || !targetClass.hasClassInitializer();
+    return !targetClass.classInitializationMayHaveSideEffects(
             appView, type -> type.isIdenticalTo(sourceClass.getType()))
         && (!sourceClass.isInterface()
             || !sourceClass.classInitializationMayHaveSideEffects(appView));
   }
 
   @Override
+  public Map<DexProgramClass, Set<DexProgramClass>> preprocess(
+      Collection<VerticalMergeGroup> groups) {
+    Map<DexProgramClass, Set<DexProgramClass>> sourcesWithClassInitializers =
+        new IdentityHashMap<>();
+    for (VerticalMergeGroup group : groups) {
+      if (group.getSource().hasClassInitializer()) {
+        sourcesWithClassInitializers
+            .computeIfAbsent(group.getTarget(), ignoreKey(Sets::newIdentityHashSet))
+            .add(group.getSource());
+      }
+    }
+    return sourcesWithClassInitializers;
+  }
+
+  @Override
   public String getName() {
     return "NoClassInitializationChangesPolicy";
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFinalSourceInstanceFieldsWithConstructorInliningPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFinalSourceInstanceFieldsWithConstructorInliningPolicy.java
new file mode 100644
index 0000000..14b9ece
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoFinalSourceInstanceFieldsWithConstructorInliningPolicy.java
@@ -0,0 +1,42 @@
+// 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.verticalclassmerging.policies;
+
+import com.android.tools.r8.classmerging.ClassMergerMode;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.verticalclassmerging.VerticalMergeGroup;
+import com.google.common.collect.Iterables;
+
+public class NoFinalSourceInstanceFieldsWithConstructorInliningPolicy
+    extends VerticalClassMergerPolicy {
+
+  private final ClassMergerMode mode;
+  private final InternalOptions options;
+
+  public NoFinalSourceInstanceFieldsWithConstructorInliningPolicy(
+      AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
+    this.mode = mode;
+    this.options = appView.options();
+  }
+
+  @Override
+  public boolean canMerge(VerticalMergeGroup group) {
+    return group.getSource().isInterface()
+        || !options.canInitNewInstanceUsingSuperclassConstructor()
+        || Iterables.isEmpty(group.getSource().instanceFields(DexEncodedField::isFinal));
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return mode.isInitial();
+  }
+
+  @Override
+  public String getName() {
+    return "NoFinalSourceInstanceFieldsWithConstructorInliningPolicy";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java
index 13b1342..8f36d05 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/policies/NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging.policies;
 
+import com.android.tools.r8.classmerging.ClassMergerMode;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -14,10 +15,12 @@
     extends VerticalClassMergerPolicy {
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final ClassMergerMode mode;
 
   public NoInterfacesWithInvokeSpecialToDefaultMethodIntoClassPolicy(
-      AppView<AppInfoWithLiveness> appView) {
+      AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
     this.appView = appView;
+    this.mode = mode;
   }
 
   @Override
@@ -34,7 +37,7 @@
             method -> {
               boolean foundInvokeSpecialToDefaultLibraryMethod =
                   method.registerCodeReferencesWithResult(
-                      new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
+                      new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method, mode));
               return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
             });
     return result.shouldContinue();
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b64e03a..f803d78 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -669,6 +669,10 @@
     return self();
   }
 
+  public T addNoVerticalClassMergingRule(String clazz) {
+    return addInternalKeepRules("-noverticalclassmerging class " + clazz);
+  }
+
   public T enableMemberValuePropagationAnnotations() {
     return enableMemberValuePropagationAnnotations(true);
   }
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
index a73b895..52fd6e9 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
@@ -28,8 +28,7 @@
   @Parameters(name = "{1}, minification: {0}")
   public static List<Object[]> data() {
     return buildParameters(
-        BooleanUtils.values(),
-        getTestParameters().withDexRuntimes().withAllRuntimesAndApiLevels().build());
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public KeepNonVisibilityBridgeMethodsTest(boolean minification, TestParameters parameters) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
index 81396ab..313937a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
@@ -35,7 +35,7 @@
         .assertSuccessWithOutputLines("bar", "foo y", "foo z")
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(I.class), isPresent());
+              assertThat(codeInspector.clazz(I.class), isAbsent());
               assertThat(codeInspector.clazz(X.class), isPresent());
               assertThat(codeInspector.clazz(Y.class), isAbsent());
               assertThat(codeInspector.clazz(Z.class), isAbsent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
index 5858809..f356c0b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
@@ -31,7 +31,7 @@
         .assertSuccessWithOutputLines("a", "foo a", "b", "foo")
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(Parent.class), isPresent());
+              assertThat(codeInspector.clazz(Parent.class), isAbsent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isAbsent());
               assertThat(codeInspector.clazz(C.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
index 3cd0ac6..1aade71 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
@@ -35,7 +35,7 @@
         .assertSuccess()
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(Parent.class), isPresent());
+              assertThat(codeInspector.clazz(Parent.class), isAbsent());
 
               ClassSubject aClassSubject = codeInspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
index 293646e..1d4b66a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -32,9 +33,11 @@
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(A.class), isPresent());
-              assertThat(codeInspector.clazz(C.class), isPresent());
-              assertThat(codeInspector.clazz(B.class), not(isPresent()));
-              assertThat(codeInspector.clazz(D.class), not(isPresent()));
+              assertThat(
+                  codeInspector.clazz(C.class),
+                  isAbsentIf(parameters.canHaveNonReboundConstructorInvoke()));
+              assertThat(codeInspector.clazz(B.class), isAbsent());
+              assertThat(codeInspector.clazz(D.class), isAbsent());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
index 50d3724..abff6e4 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
@@ -6,7 +6,7 @@
 
 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.onlyIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -47,11 +47,13 @@
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
               assertThat(codeInspector.clazz(J.class), isAbsent());
-              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(A.class),
+                  isPresentIf(parameters.canUseDefaultAndStaticInterfaceMethods()));
               assertThat(codeInspector.clazz(B1.class), isPresent());
               assertThat(
                   codeInspector.clazz(B2.class),
-                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
+                  isPresentIf(parameters.canUseDefaultAndStaticInterfaceMethods()));
               assertThat(codeInspector.clazz(C1.class), isPresent());
               assertThat(codeInspector.clazz(C2.class), isPresent());
             });
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java
index 7bbb17e..4a2eab5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java
@@ -47,6 +47,8 @@
     testForR8(parameters.getBackend())
         .addProgramClasses(Main.class, A.class, B.class)
         .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
         .setMinApi(parameters)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
@@ -73,7 +75,7 @@
 
   public static class Main {
 
-    private static final String className =
+    private static String className =
         "com.android.tools.r8.classmerging.vertical.VerticalClassMergerReflectiveNameTest$A";
 
     static {
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 0e87692..edda012 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -388,7 +388,6 @@
     Set<String> preservedClassNames =
         ImmutableSet.of(
             "classmerging.NestedDefaultInterfaceMethodsTest",
-            "classmerging.NestedDefaultInterfaceMethodsTest$A",
             "classmerging.NestedDefaultInterfaceMethodsTest$C");
     runTest(
         testForR8(parameters.getBackend())
@@ -411,10 +410,14 @@
           JAVA8_CF_DIR.resolve("NeverInline.class")
         };
     Set<String> preservedClassNames =
-        ImmutableSet.of(
-            "classmerging.NestedDefaultInterfaceMethodsTest",
-            "classmerging.NestedDefaultInterfaceMethodsTest$A",
-            "classmerging.NestedDefaultInterfaceMethodsTest$C");
+        parameters.isCfRuntime()
+            ? ImmutableSet.of(
+                "classmerging.NestedDefaultInterfaceMethodsTest",
+                "classmerging.NestedDefaultInterfaceMethodsTest$A",
+                "classmerging.NestedDefaultInterfaceMethodsTest$C")
+            : ImmutableSet.of(
+                "classmerging.NestedDefaultInterfaceMethodsTest",
+                "classmerging.NestedDefaultInterfaceMethodsTest$C");
     runTestOnInput(
         testForR8(parameters.getBackend())
             .addKeepRules(getProguardConfig(JAVA8_EXAMPLE_KEEP))
@@ -850,7 +853,6 @@
         ImmutableSet.of(
             "classmerging.SuperCallToMergedClassIsRewrittenTest",
             "classmerging.A",
-            "classmerging.B",
             "classmerging.D",
             "classmerging.F");
 
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationTestBase.java
index 8a15c91..5b9b506 100644
--- a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationTestBase.java
@@ -38,7 +38,8 @@
 
   public YouTubeCompilationTestBase(int majorVersion, int minorVersion, AndroidApiLevel apiLevel) {
     this.base =
-        "third_party/youtube/youtube.android_"
+        ToolHelper.THIRD_PARTY_DIR
+            + "youtube/youtube.android_"
             + majorVersion
             + "."
             + String.format("%02d", minorVersion)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
index 83c7f4f..b0b7907 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
@@ -110,6 +110,7 @@
   interface J extends I {}
 
   @NeverClassInline
+  @NoVerticalClassMerging
   static class A implements J {
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index d2bf214..867f9c9 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -154,6 +154,11 @@
               public boolean isStringSwitchConversionEnabled() {
                 return false;
               }
+
+              @Override
+              public boolean shouldFinalizeAfterLensCodeRewriter() {
+                return false;
+              }
             }));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index ba27997..30be52f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -143,7 +143,8 @@
                     // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
                     .addKeepRules("-neverinline class * { void test*State*(...); }")
                     .addNoHorizontalClassMergingRule(
-                        "class_inliner_lambda_j_style.SamIface$Consumer"))
+                        "class_inliner_lambda_j_style.SamIface$Consumer")
+                    .addNoVerticalClassMergingRule("class_inliner_lambda_j_style.SamIface"))
         .inspect(
             inspector -> {
               if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
index 7e3ba53..205d483 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
@@ -92,7 +92,10 @@
 
               MethodSubject virtualMethodSubject =
                   mergeTarget.uniqueMethodThatMatches(
-                      method -> method.isVirtual() && !method.isSynthetic());
+                      method ->
+                          method.isVirtual()
+                              && !method.isSynthetic()
+                              && method.getOriginalName(false).equals("invoke"));
               assertThat(virtualMethodSubject, isPresent());
 
               int found = 0;
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
index 0704294..43808b8 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -8,8 +8,9 @@
 import static com.android.tools.r8.ir.desugar.records.RecordDesugaring.GET_FIELDS_AS_OBJECTS_METHOD_NAME;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.ifThen;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
@@ -193,23 +194,24 @@
     assertThat(mainMethodSubject, isPresent());
 
     ClassSubject recordTagClassSubject = inspector.clazz(recordClassReference);
-    assertThat(recordTagClassSubject, notIf(isPresent(), canUseRecords));
-    if (!canUseRecords) {
+    assertThat(
+        recordTagClassSubject, isAbsentIf(canHaveNonReboundConstructorInvoke || canUseRecords));
+    if (recordTagClassSubject.isPresent()) {
       assertEquals(
           canHaveNonReboundConstructorInvoke ? 0 : 1, recordTagClassSubject.allMethods().size());
     }
 
     MethodSubject recordTagInstanceInitializerSubject = recordTagClassSubject.init();
-    assertThat(
-        recordTagInstanceInitializerSubject,
-        notIf(isPresent(), canHaveNonReboundConstructorInvoke || canUseRecords));
+    assertThat(recordTagInstanceInitializerSubject, isPresentIf(recordTagClassSubject.isPresent()));
 
     ClassSubject personRecordClassSubject = inspector.clazz(PERSON_REFERENCE);
     assertThat(personRecordClassSubject, isPresent());
     assertEquals(
-        canUseRecords
-            ? inspector.getTypeSubject(RECORD_REFERENCE.getTypeName())
-            : recordTagClassSubject.asTypeSubject(),
+        canHaveNonReboundConstructorInvoke
+            ? inspector.getTypeSubject(Object.class.getTypeName())
+            : canUseRecords
+                ? inspector.getTypeSubject(RECORD_REFERENCE.getTypeName())
+                : recordTagClassSubject.asTypeSubject(),
         personRecordClassSubject.getSuperType());
     assertEquals(canUseRecords ? 6 : 10, personRecordClassSubject.allMethods().size());
 
@@ -226,7 +228,7 @@
             SyntheticItemsTestUtils.syntheticNestInstanceFieldGetter(
                     Reference.field(PERSON_REFERENCE, "name", STRING_REFERENCE))
                 .getMethodName());
-    assertThat(nameNestAccessorMethodSubject, notIf(isPresent(), canUseNestBasedAccesses));
+    assertThat(nameNestAccessorMethodSubject, isAbsentIf(canUseNestBasedAccesses));
 
     // Age getters.
     MethodSubject ageMethodSubject = personRecordClassSubject.uniqueMethodWithOriginalName("age");
@@ -237,16 +239,16 @@
             SyntheticItemsTestUtils.syntheticNestInstanceFieldGetter(
                     Reference.field(PERSON_REFERENCE, "age", Reference.INT))
                 .getMethodName());
-    assertThat(ageNestAccessorMethodSubject, notIf(isPresent(), canUseNestBasedAccesses));
+    assertThat(ageNestAccessorMethodSubject, isAbsentIf(canUseNestBasedAccesses));
 
     // boolean equals(Object)
     MethodSubject getFieldsAsObjectsMethodSubject =
         personRecordClassSubject.uniqueMethodWithOriginalName(GET_FIELDS_AS_OBJECTS_METHOD_NAME);
-    assertThat(getFieldsAsObjectsMethodSubject, notIf(isPresent(), canUseRecords));
+    assertThat(getFieldsAsObjectsMethodSubject, isAbsentIf(canUseRecords));
 
     MethodSubject equalsHelperMethodSubject =
         personRecordClassSubject.uniqueMethodWithOriginalName(EQUALS_RECORD_METHOD_NAME);
-    assertThat(equalsHelperMethodSubject, notIf(isPresent(), canUseRecords));
+    assertThat(equalsHelperMethodSubject, isAbsentIf(canUseRecords));
 
     MethodSubject equalsMethodSubject =
         personRecordClassSubject.uniqueMethodWithOriginalName("equals");
@@ -257,10 +259,10 @@
     // int hashCode()
     ClassSubject hashCodeHelperClassSubject =
         inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 1));
-    assertThat(hashCodeHelperClassSubject, notIf(isPresent(), canUseRecords));
+    assertThat(hashCodeHelperClassSubject, isAbsentIf(canUseRecords));
 
     MethodSubject hashCodeHelperMethodSubject = hashCodeHelperClassSubject.uniqueMethod();
-    assertThat(hashCodeHelperMethodSubject, notIf(isPresent(), canUseRecords));
+    assertThat(hashCodeHelperMethodSubject, isAbsentIf(canUseRecords));
 
     MethodSubject hashCodeMethodSubject =
         personRecordClassSubject.uniqueMethodWithOriginalName("hashCode");
@@ -274,10 +276,10 @@
     // String toString()
     ClassSubject toStringHelperClassSubject =
         inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 0));
-    assertThat(toStringHelperClassSubject, notIf(isPresent(), canUseRecords));
+    assertThat(toStringHelperClassSubject, isAbsentIf(canUseRecords));
 
     MethodSubject toStringHelperMethodSubject = toStringHelperClassSubject.uniqueMethod();
-    assertThat(toStringHelperMethodSubject, notIf(isPresent(), canUseRecords));
+    assertThat(toStringHelperMethodSubject, isAbsentIf(canUseRecords));
 
     MethodSubject toStringMethodSubject =
         personRecordClassSubject.uniqueMethodWithOriginalName("toString");
@@ -306,10 +308,7 @@
         .applyIf(
             !canUseRecords,
             i ->
-                i.assertContainsClassRules(
-                        recordTagClassSubject,
-                        hashCodeHelperClassSubject,
-                        toStringHelperClassSubject)
+                i.assertContainsClassRules(hashCodeHelperClassSubject, toStringHelperClassSubject)
                     .assertContainsMethodRules(
                         equalsHelperMethodSubject,
                         getFieldsAsObjectsMethodSubject,
@@ -317,7 +316,9 @@
                         toStringHelperMethodSubject)
                     .applyIf(
                         !canHaveNonReboundConstructorInvoke,
-                        j -> j.assertContainsMethodRule(recordTagInstanceInitializerSubject)))
+                        j ->
+                            j.assertContainsClassRules(recordTagClassSubject)
+                                .assertContainsMethodRule(recordTagInstanceInitializerSubject)))
         .assertContainsNoOtherRules();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
index 9b344c5..fe575ed 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
@@ -4,36 +4,77 @@
 
 package com.android.tools.r8.rewrite.enums;
 
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase.EnumKeepRules;
+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;
 
+// This is testing various edge cases of the Enum.valueOf(Class, String) optimization, such as
+// passing an enum subclass constant as the first argument to Enum.valueOf. The implementation of
+// Enum.valueOf reflects on the user program and keep rules are therefore needed for the program
+// to produce the expected result after shrinking.
 @RunWith(Parameterized.class)
 public class EnumValueOfOptimizationTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public boolean enableNoVerticalClassMergingAnnotations;
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
+  @Parameter(1)
+  public EnumKeepRules enumKeepRules;
 
-  public EnumValueOfOptimizationTest(TestParameters parameters) {
-    this.parameters = parameters;
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameters(name = "{2}, annotations: {0}, keep: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        EnumKeepRules.values(),
+        getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   @Test
   public void testValueOf() throws Exception {
     testForR8(parameters.getBackend())
-        .addKeepMainRule(Main.class)
         .addInnerClasses(EnumValueOfOptimizationTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .applyIf(
+            enableNoVerticalClassMergingAnnotations,
+            R8TestBuilder::enableNoVerticalClassMergingAnnotations,
+            TestShrinkerBuilder::addNoVerticalClassMergingAnnotations)
         .setMinApi(parameters)
         .compile()
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("npe OK", "iae1 OK", "iae2 OK", "iae3 OK", "iae4 OK");
+        .applyIf(
+            parameters.isCfRuntime()
+                || enableNoVerticalClassMergingAnnotations
+                || enumKeepRules.isStudio(),
+            runResult ->
+                runResult.assertSuccessWithOutputLines(
+                    "npe OK", "iae1 OK", "iae2 OK", "iae3 OK", "iae4 OK"),
+            parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik(),
+            runResult ->
+                runResult.assertSuccessWithOutputLines(
+                    "npe OK", "iae1 OK", "iae2 OK", "iae3 OK", "npe OK"),
+            parameters.isDexRuntime()
+                && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V10_0_0),
+            runResult ->
+                runResult.assertSuccessWithOutputLines(
+                    "npe OK", "iae1 OK", "iae2 OK", "iae3 OK", "nsme->re OK"),
+            runResult ->
+                runResult.assertSuccessWithOutputLines(
+                    "npe OK", "iae1 OK", "iae2 OK", "iae3 OK", "nsme->ae OK"));
   }
 
   enum MyEnum {
@@ -41,6 +82,7 @@
     B
   }
 
+  @NoVerticalClassMerging
   enum ComplexEnum {
     A {
       @Override
@@ -62,9 +104,25 @@
       Enum<?> e = null;
       try {
         e = subtypeError();
-        System.out.println("iae4 KO");
+      } catch (AssertionError ae) {
+        if (ae.getCause() instanceof NoSuchMethodException) {
+          System.out.println("nsme->ae OK");
+        } else {
+          System.out.println(ae.getClass().getName());
+          System.out.println(ae.getCause().getClass().getName());
+        }
       } catch (IllegalArgumentException iae) {
         System.out.println("iae4 OK");
+      } catch (NullPointerException npe) {
+        System.out.println("npe OK");
+      } catch (RuntimeException re) {
+        if (re.getClass() == RuntimeException.class
+            && re.getCause() instanceof NoSuchMethodException) {
+          System.out.println("nsme->re OK");
+        } else {
+          System.out.println(re.getClass().getName());
+          System.out.println(re.getCause().getClass().getName());
+        }
       }
       if (e != null) {
         throw new Error("enum set");
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index 0fe9536..da8944c 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -148,8 +148,7 @@
     assertThat(foo, isAbsent());
 
     ClassSubject superInterface2 = inspector.clazz(B112452064SuperInterface2.class);
-    assertThat(
-        superInterface2, isAbsentIf(enableUnusedInterfaceRemoval && enableVerticalClassMerging));
+    assertThat(superInterface2, isAbsentIf(enableVerticalClassMerging));
 
     MethodSubject bar = superInterface2.uniqueMethodWithOriginalName("bar");
     assertThat(bar, isAbsent());
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index b573cd5..6ae395d 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -58,7 +58,7 @@
 
   protected abstract String getMainClass();
 
-  private final TestParameters parameters;
+  protected final TestParameters parameters;
   private final MinifyMode minify;
 
   public TestParameters getParameters() {
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking9Test.java b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking9Test.java
index 0b89761..3b80981 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking9Test.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/TreeShaking9Test.java
@@ -3,13 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking.examples;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbstract;
+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.TestParameters;
 import com.android.tools.r8.shaking.TreeShakingTest;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,7 +45,7 @@
   @Test
   public void testKeeprules() throws Exception {
     runTest(
-        TreeShaking9Test::shaking9OnlySuperMethodsKept,
+        this::shaking9OnlySuperMethodsKept,
         null,
         null,
         ImmutableList.of("src/test/examples/shaking9/keep-rules.txt"));
@@ -52,11 +57,19 @@
         null, null, null, ImmutableList.of("src/test/examples/shaking9/keep-rules-printusage.txt"));
   }
 
-  private static void shaking9OnlySuperMethodsKept(CodeInspector inspector) {
+  private void shaking9OnlySuperMethodsKept(CodeInspector inspector) {
     ClassSubject superclass = inspector.clazz("shaking9.Superclass");
-    Assert.assertTrue(superclass.isAbstract());
-    Assert.assertTrue(superclass.method("void", "aMethod", ImmutableList.of()).isPresent());
+    if (parameters.canHaveNonReboundConstructorInvoke()) {
+      assertThat(superclass, isAbsent());
+    } else {
+      assertThat(superclass, isAbstract());
+      assertThat(superclass.method("void", "aMethod", ImmutableList.of()), isPresent());
+    }
+
     ClassSubject subclass = inspector.clazz("shaking9.Subclass");
-    Assert.assertFalse(subclass.method("void", "aMethod", ImmutableList.of()).isPresent());
+    assertThat(subclass, isPresent());
+    assertThat(
+        subclass.method("void", "aMethod", ImmutableList.of()),
+        isPresentIf(parameters.canHaveNonReboundConstructorInvoke() && !getMinify().isMinify()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/interfaces/RedundantImplementsClauseTest.java b/src/test/java/com/android/tools/r8/shaking/interfaces/RedundantImplementsClauseTest.java
index 97f8d03..943e497 100644
--- a/src/test/java/com/android/tools/r8/shaking/interfaces/RedundantImplementsClauseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/interfaces/RedundantImplementsClauseTest.java
@@ -86,6 +86,7 @@
     }
   }
 
+  @NoVerticalClassMerging
   interface I {
 
     void m();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 04202fd..be77daa 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -341,22 +341,27 @@
     };
   }
 
-  public static Matcher<MethodSubject> isAbstract() {
-    return new TypeSafeMatcher<MethodSubject>() {
+  public static Matcher<ClassOrMemberSubject> isAbstract() {
+    return new TypeSafeMatcher<ClassOrMemberSubject>() {
       @Override
-      public boolean matchesSafely(final MethodSubject method) {
-        return method.isPresent() && method.isAbstract();
+      public boolean matchesSafely(ClassOrMemberSubject subject) {
+        if (subject instanceof FoundClassSubject) {
+          return ((FoundClassSubject) subject).isAbstract();
+        }
+        if (subject instanceof FoundMethodSubject) {
+          return ((FoundMethodSubject) subject).isAbstract();
+        }
+        return false;
       }
 
       @Override
-      public void describeTo(final Description description) {
-        description.appendText("method abstract");
+      public void describeTo(Description description) {
+        description.appendText("abstract");
       }
 
       @Override
-      public void describeMismatchSafely(final MethodSubject method, Description description) {
-        description
-            .appendText("method ").appendValue(method.getOriginalName()).appendText(" was not");
+      public void describeMismatchSafely(ClassOrMemberSubject subject, Description description) {
+        description.appendValue(subject.getOriginalName()).appendText(" was not");
       }
     };
   }