Refactor vertical class merger to use horizontal policy executor

Change-Id: I69665c5784245d21ba873b8b75126cb73adc2199
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 57ff66a..3bc0655 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -127,7 +127,8 @@
     List<Policy> policies =
         PolicyScheduler.getPolicies(appView, codeProvider, mode, runtimeTypeCheckInfo);
     Collection<MergeGroup> groups =
-        new PolicyExecutor().run(getInitialGroups(), policies, executorService, timing);
+        new HorizontalClassMergerPolicyExecutor()
+            .run(getInitialGroups(), policies, executorService, timing);
 
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
new file mode 100644
index 0000000..f207cbc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
@@ -0,0 +1,90 @@
+// 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.horizontalclassmerging;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class HorizontalClassMergerPolicyExecutor extends PolicyExecutor<MergeGroup> {
+
+  @Override
+  protected LinkedList<MergeGroup> apply(
+      Policy policy, LinkedList<MergeGroup> linkedGroups, ExecutorService executorService)
+      throws ExecutionException {
+    if (policy.isSingleClassPolicy()) {
+      applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
+    } else {
+      if (policy.isMultiClassPolicy()) {
+        linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
+      } else {
+        assert policy.isMultiClassPolicyWithPreprocessing();
+        linkedGroups =
+            applyMultiClassPolicyWithPreprocessing(
+                policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
+      }
+    }
+    return linkedGroups;
+  }
+
+  void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
+    Iterator<MergeGroup> i = groups.iterator();
+    while (i.hasNext()) {
+      MergeGroup group = i.next();
+      boolean isInterfaceGroup = group.isInterfaceGroup();
+      int previousGroupSize = group.size();
+      group.removeIf(clazz -> !policy.canMerge(clazz));
+      assert policy.recordRemovedClassesForDebugging(
+          isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
+      if (group.isTrivial()) {
+        i.remove();
+      }
+    }
+  }
+
+  // TODO(b/270398965): Replace LinkedList.
+  @SuppressWarnings("JdkObsolete")
+  private LinkedList<MergeGroup> applyMultiClassPolicy(
+      MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
+    // For each group apply the multi class policy and add all the new groups together.
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
+          Collection<MergeGroup> policyGroups = policy.apply(group);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
+  }
+
+  // TODO(b/270398965): Replace LinkedList.
+  @SuppressWarnings("JdkObsolete")
+  private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
+      MultiClassPolicyWithPreprocessing<T> policy,
+      LinkedList<MergeGroup> groups,
+      ExecutorService executorService)
+      throws ExecutionException {
+    // For each group apply the multi class policy and add all the new groups together.
+    T data = policy.preprocess(groups, executorService);
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
+          Collection<MergeGroup> policyGroups = policy.apply(group, data);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index 8062a16..06b3ab6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -26,7 +26,7 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
-public class MergeGroup implements Collection<DexProgramClass> {
+public class MergeGroup extends MergeGroupBase implements Collection<DexProgramClass> {
 
   public static class Metadata {}
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java
new file mode 100644
index 0000000..8d96410
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java
@@ -0,0 +1,9 @@
+// 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.horizontalclassmerging;
+
+public abstract class MergeGroupBase {
+
+  public abstract int size();
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
index 5bff09e..530a642 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.verticalclassmerging.VerticalClassMergerPolicy;
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -50,6 +51,14 @@
     return null;
   }
 
+  public boolean isVerticalClassMergerPolicy() {
+    return false;
+  }
+
+  public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+    return null;
+  }
+
   public boolean shouldSkipPolicy() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
index 28ec89d..0495ce2 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -5,9 +5,7 @@
 package com.android.tools.r8.horizontalclassmerging;
 
 import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableList;
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -17,64 +15,7 @@
  * primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
  * against which more efficient policy executors can be compared.
  */
-public class PolicyExecutor {
-
-  private void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
-    Iterator<MergeGroup> i = groups.iterator();
-    while (i.hasNext()) {
-      MergeGroup group = i.next();
-      boolean isInterfaceGroup = group.isInterfaceGroup();
-      int previousGroupSize = group.size();
-      group.removeIf(clazz -> !policy.canMerge(clazz));
-      assert policy.recordRemovedClassesForDebugging(
-          isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
-      if (group.isTrivial()) {
-        i.remove();
-      }
-    }
-  }
-
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private LinkedList<MergeGroup> applyMultiClassPolicy(
-      MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
-    // For each group apply the multi class policy and add all the new groups together.
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
-    groups.forEach(
-        group -> {
-          boolean isInterfaceGroup = group.isInterfaceGroup();
-          int previousGroupSize = group.size();
-          Collection<MergeGroup> policyGroups = policy.apply(group);
-          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
-          assert policy.recordRemovedClassesForDebugging(
-              isInterfaceGroup, previousGroupSize, policyGroups);
-          newGroups.addAll(policyGroups);
-        });
-    return newGroups;
-  }
-
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
-      MultiClassPolicyWithPreprocessing<T> policy,
-      LinkedList<MergeGroup> groups,
-      ExecutorService executorService)
-      throws ExecutionException {
-    // For each group apply the multi class policy and add all the new groups together.
-    T data = policy.preprocess(groups, executorService);
-    LinkedList<MergeGroup> newGroups = new LinkedList<>();
-    groups.forEach(
-        group -> {
-          boolean isInterfaceGroup = group.isInterfaceGroup();
-          int previousGroupSize = group.size();
-          Collection<MergeGroup> policyGroups = policy.apply(group, data);
-          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
-          assert policy.recordRemovedClassesForDebugging(
-              isInterfaceGroup, previousGroupSize, policyGroups);
-          newGroups.addAll(policyGroups);
-        });
-    return newGroups;
-  }
+public abstract class PolicyExecutor<MG extends MergeGroupBase> {
 
   /**
    * Given an initial collection of class groups which can potentially be merged, run all of the
@@ -83,16 +24,16 @@
    */
   // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
-  public Collection<MergeGroup> run(
-      Collection<MergeGroup> inputGroups,
+  public Collection<MG> run(
+      Collection<MG> inputGroups,
       Collection<Policy> policies,
       ExecutorService executorService,
       Timing timing)
       throws ExecutionException {
-    LinkedList<MergeGroup> linkedGroups;
+    LinkedList<MG> linkedGroups;
 
     if (inputGroups instanceof LinkedList) {
-      linkedGroups = (LinkedList<MergeGroup>) inputGroups;
+      linkedGroups = (LinkedList<MG>) inputGroups;
     } else {
       linkedGroups = new LinkedList<>(inputGroups);
     }
@@ -103,16 +44,7 @@
       }
 
       timing.begin(policy.getName());
-      if (policy.isSingleClassPolicy()) {
-        applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
-      } else if (policy.isMultiClassPolicy()) {
-        linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
-      } else {
-        assert policy.isMultiClassPolicyWithPreprocessing();
-        linkedGroups =
-            applyMultiClassPolicyWithPreprocessing(
-                policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
-      }
+      linkedGroups = apply(policy, linkedGroups, executorService);
       timing.end();
 
       policy.clear();
@@ -127,4 +59,8 @@
 
     return linkedGroups;
   }
+
+  protected abstract LinkedList<MG> apply(
+      Policy policy, LinkedList<MG> linkedGroups, ExecutorService executorService)
+      throws ExecutionException;
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
index 22316f3c..cb785f7 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
@@ -5,19 +5,18 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ListUtils;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
 public class ConnectedComponentVerticalClassMerger {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final Set<DexProgramClass> classesToMerge;
+  private final Collection<VerticalMergeGroup> classesToMerge;
 
   // The resulting graph lens that should be used after class merging.
   private final VerticalClassMergerGraphLens.Builder lensBuilder;
@@ -29,7 +28,7 @@
       VerticallyMergedClasses.builder();
 
   ConnectedComponentVerticalClassMerger(
-      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> classesToMerge) {
+      AppView<AppInfoWithLiveness> appView, Collection<VerticalMergeGroup> classesToMerge) {
     this.appView = appView;
     this.classesToMerge = classesToMerge;
     this.lensBuilder = new VerticalClassMergerGraphLens.Builder();
@@ -39,23 +38,19 @@
     return classesToMerge.isEmpty();
   }
 
-  public VerticalClassMergerResult.Builder run(ImmediateProgramSubtypingInfo immediateSubtypingInfo)
-      throws ExecutionException {
-    List<DexProgramClass> classesToMergeSorted =
-        ListUtils.sort(classesToMerge, Comparator.comparing(DexProgramClass::getType));
-    for (DexProgramClass clazz : classesToMergeSorted) {
-      mergeClassIfPossible(clazz, immediateSubtypingInfo);
+  public VerticalClassMergerResult.Builder run() throws ExecutionException {
+    List<VerticalMergeGroup> classesToMergeSorted =
+        ListUtils.sort(classesToMerge, Comparator.comparing(group -> group.getSource().getType()));
+    for (VerticalMergeGroup group : classesToMergeSorted) {
+      mergeClassIfPossible(group);
     }
     return VerticalClassMergerResult.builder(
         lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
   }
 
-  private void mergeClassIfPossible(
-      DexProgramClass sourceClass, ImmediateProgramSubtypingInfo immediateSubtypingInfo)
-      throws ExecutionException {
-    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
-    assert subclasses.size() == 1;
-    DexProgramClass targetClass = ListUtils.first(subclasses);
+  private void mergeClassIfPossible(VerticalMergeGroup group) throws ExecutionException {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
     if (verticallyMergedClassesBuilder.isMergeSource(targetClass)
         || verticallyMergedClassesBuilder.isMergeTarget(sourceClass)) {
       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 cb482b9..85a48f5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -224,8 +224,7 @@
     Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
         getConnectedComponentMergers(
             connectedComponents, immediateSubtypingInfo, executorService, timing);
-    return applyConnectedComponentMergers(
-        connectedComponentMergers, immediateSubtypingInfo, executorService, timing);
+    return applyConnectedComponentMergers(connectedComponentMergers, executorService, timing);
   }
 
   private Collection<ConnectedComponentVerticalClassMerger> getConnectedComponentMergers(
@@ -244,8 +243,9 @@
             connectedComponent -> {
               Timing threadTiming = Timing.create("Compute classes to merge in component", options);
               ConnectedComponentVerticalClassMerger connectedComponentMerger =
-                  new VerticalClassMergerPolicyExecutor(appView, pinnedClasses)
-                      .run(connectedComponent, immediateSubtypingInfo);
+                  new VerticalClassMergerPolicyExecutor(
+                          appView, immediateSubtypingInfo, pinnedClasses)
+                      .run(connectedComponent, executorService, threadTiming);
               if (!connectedComponentMerger.isEmpty()) {
                 synchronized (connectedComponentMergers) {
                   connectedComponentMergers.add(connectedComponentMerger);
@@ -263,7 +263,6 @@
 
   private VerticalClassMergerResult applyConnectedComponentMergers(
       Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       ExecutorService executorService,
       Timing timing)
       throws ExecutionException {
@@ -276,7 +275,7 @@
             connectedComponentMerger -> {
               Timing threadTiming = Timing.create("Merge classes in component", options);
               VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
-                  connectedComponentMerger.run(immediateSubtypingInfo);
+                  connectedComponentMerger.run();
               verticalClassMergerResult.merge(verticalClassMergerComponentResult);
               threadTiming.end();
               return threadTiming;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java
new file mode 100644
index 0000000..9635b3c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java
@@ -0,0 +1,441 @@
+// 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.verticalclassmerging;
+
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
+
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
+import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.Policy;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class VerticalClassMergerPolicy extends Policy {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final MainDexInfo mainDexInfo;
+  private final InternalOptions options;
+  private final Set<DexProgramClass> pinnedClasses;
+
+  VerticalClassMergerPolicy(
+      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+    this.appView = appView;
+    this.options = appView.options();
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.pinnedClasses = pinnedClasses;
+  }
+
+  @Override
+  public boolean isVerticalClassMergerPolicy() {
+    return true;
+  }
+
+  @Override
+  public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+    return this;
+  }
+
+  public boolean canMerge(VerticalMergeGroup group) {
+    DexProgramClass sourceClass = group.getSource();
+    DexProgramClass targetClass = group.getTarget();
+    if (!isMergeCandidate(sourceClass, targetClass)) {
+      return false;
+    }
+    if (!isStillMergeCandidate(sourceClass, targetClass)) {
+      return false;
+    }
+    if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
+        || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
+      return false;
+    }
+    return true;
+  }
+
+  // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
+  // method do not change in response to any class merges.
+  private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    assert targetClass != null;
+    ObjectAllocationInfoCollection allocationInfo =
+        appView.appInfo().getObjectAllocationInfoCollection();
+    if (allocationInfo.isInstantiatedDirectly(sourceClass)
+        || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
+        || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
+        || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
+        || pinnedClasses.contains(sourceClass)) {
+      return false;
+    }
+
+    assert sourceClass
+        .traverseProgramMembers(
+            member -> {
+              assert !appView.getKeepInfo(member).isPinned(options);
+              return TraversalContinuation.doContinue();
+            })
+        .shouldContinue();
+
+    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView)) {
+      return false;
+    }
+    if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+        sourceClass, targetClass, appView)) {
+      return false;
+    }
+    if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
+        && appView.getKeepInfo(targetClass).isPinned(options)) {
+      return false;
+    }
+    if (sourceClass.isAnnotation()) {
+      return false;
+    }
+    if (!sourceClass.isInterface()
+        && targetClass.isSerializable(appView)
+        && !appView.appInfo().isSerializable(sourceClass.getType())) {
+      // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
+      //   1.10 The Serializable Interface
+      //   ...
+      //   A Serializable class must do the following:
+      //   ...
+      //     * Have access to the no-arg constructor of its first non-serializable superclass
+      return false;
+    }
+
+    // If there is a constructor in the target, make sure that all source constructors can be
+    // inlined.
+    if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
+      TraversalContinuation<?, ?> result =
+          sourceClass.traverseProgramInstanceInitializers(
+              method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
+      if (result.shouldBreak()) {
+        return false;
+      }
+    }
+    if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
+      return false;
+    }
+    // We abort class merging when merging across nests or from a nest to non-nest.
+    // Without nest this checks null == null.
+    if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
+      return false;
+    }
+
+    // If there is an invoke-special to a default interface method and we are not merging into an
+    // interface, then abort, since invoke-special to a virtual class method requires desugaring.
+    if (sourceClass.isInterface() && !targetClass.isInterface()) {
+      TraversalContinuation<?, ?> result =
+          sourceClass.traverseProgramMethods(
+              method -> {
+                boolean foundInvokeSpecialToDefaultLibraryMethod =
+                    method.registerCodeReferencesWithResult(
+                        new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
+                return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
+              });
+      if (result.shouldBreak()) {
+        return false;
+      }
+    }
+
+    // Check with main dex classes to see if we are allowed to merge.
+    if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
+   * in this method may change in response to class merges. Therefore, this method should always be
+   * called before merging {@param sourceClass} into {@param targetClass}.
+   */
+  private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    // 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.
+    if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
+        || targetClass.classInitializationMayHaveSideEffects(
+            appView, type -> type.isIdenticalTo(sourceClass.getType()))
+        || (sourceClass.isInterface()
+            && sourceClass.classInitializationMayHaveSideEffects(appView))) {
+      return false;
+    }
+    boolean sourceCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(sourceClass)
+            || sourceClass.hasStaticSynchronizedMethods();
+    boolean targetCanBeSynchronizedOn =
+        appView.appInfo().isLockCandidate(targetClass)
+            || targetClass.hasStaticSynchronizedMethods();
+    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
+      return false;
+    }
+    if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
+      return false;
+    }
+    if (methodResolutionMayChange(sourceClass, targetClass)) {
+      return false;
+    }
+    // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+    // to the super class.
+    if (fieldResolutionMayChange(sourceClass, targetClass)) {
+      return false;
+    }
+    // Only merge if api reference level of source class is equal to target class. The check is
+    // somewhat expensive.
+    if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
+      AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
+      ComputedApiLevel sourceApiLevel =
+          getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
+      ComputedApiLevel targetApiLevel =
+          getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
+      if (!sourceApiLevel.equals(targetApiLevel)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
+    if (!appView.options().inlinerOptions().enableInlining) {
+      return true;
+    }
+    Code code = method.getDefinition().getCode();
+    if (code.isCfCode()) {
+      CfCode cfCode = code.asCfCode();
+      ConstraintWithTarget constraint =
+          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
+      if (constraint.isNever()) {
+        return true;
+      }
+      // Constructors can have references beyond the root main dex classes. This can increase the
+      // size of the main dex dependent classes and we should bail out.
+      if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
+        return true;
+      }
+      return false;
+    }
+    if (code.isDefaultInstanceInitializerCode()) {
+      return false;
+    }
+    return true;
+  }
+
+  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+    if (source.getType().isIdenticalTo(target.getSuperType())) {
+      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+      // Target implements an interface that declares a static final field f, this should yield an
+      // IncompatibleClassChangeError.
+      // TODO(christofferqa): In the following we only check if a static field from an interface
+      //  shadows an instance field from [source]. We could actually check if there is an iget/iput
+      //  instruction whose resolution would be affected by the merge. The situation where a static
+      //  field shadows an instance field is probably not widespread in practice, though.
+      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+      for (DexType interfaceType : target.getInterfaces()) {
+        DexClass clazz = appView.definitionFor(interfaceType);
+        for (DexEncodedField staticField : clazz.staticFields()) {
+          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
+        }
+      }
+      for (DexEncodedField instanceField : source.instanceFields()) {
+        if (staticFieldsInInterfacesOfTarget.contains(
+            equivalence.wrap(instanceField.getReference()))) {
+          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+          // interface would now hit an instance field from [source], so that an IncompatibleClass-
+          // ChangeError would no longer be thrown. Abort merge.
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
+    if (source.isSamePackage(target)) {
+      // When merging two classes from the same package, we only need to make sure that [source]
+      // does not get less visible, since that could make a valid access to [source] from another
+      // package illegal after [source] has been merged into [target].
+      assert source.getAccessFlags().isPackagePrivateOrPublic();
+      assert target.getAccessFlags().isPackagePrivateOrPublic();
+      // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
+      return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
+    }
+
+    // Check that all accesses to [source] and its members from inside the current package of
+    // [source] will continue to work. This is guaranteed if [target] is public and all members of
+    // [source] are either private or public.
+    //
+    // (Deliberately not checking all accesses to [source] since that would be expensive.)
+    if (!target.isPublic()) {
+      return true;
+    }
+    for (DexType sourceInterface : source.getInterfaces()) {
+      DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
+      if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
+        return true;
+      }
+    }
+    for (DexEncodedField field : source.fields()) {
+      if (!(field.isPublic() || field.isPrivate())) {
+        return true;
+      }
+    }
+    for (DexEncodedMethod method : source.methods()) {
+      if (!(method.isPublic() || method.isPrivate())) {
+        return true;
+      }
+      // Check if the target is overriding and narrowing the access.
+      if (method.isPublic()) {
+        DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
+        if (targetOverride != null && !targetOverride.isPublic()) {
+          return true;
+        }
+      }
+    }
+    // Check that all accesses from [source] to classes or members from the current package of
+    // [source] will continue to work. This is guaranteed if the methods of [source] do not access
+    // any private or protected classes or members from the current package of [source].
+    TraversalContinuation<?, ?> result =
+        source.traverseProgramMethods(
+            method -> {
+              boolean foundIllegalAccess =
+                  method.registerCodeReferencesWithResult(
+                      new IllegalAccessDetector(appView, method));
+              if (foundIllegalAccess) {
+                return TraversalContinuation.doBreak();
+              }
+              return TraversalContinuation.doContinue();
+            },
+            DexEncodedMethod::hasCode);
+    return result.shouldBreak();
+  }
+
+  // TODO: maybe skip this check if target does not implement any interfaces (directly or
+  // indirectly)?
+  private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
+    // This only returns true when an invoke-super instruction is found that targets a default
+    // interface method.
+    if (!options.canUseDefaultAndStaticInterfaceMethods()) {
+      return false;
+    }
+    // This problem may only arise when merging (non-interface) classes into classes.
+    if (source.isInterface() || target.isInterface()) {
+      return false;
+    }
+    return target
+        .traverseProgramMethods(
+            method -> {
+              MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
+                  new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
+              method.registerCodeReferencesWithResult(registry);
+              return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
+            },
+            DexEncodedMethod::hasCode)
+        .shouldBreak();
+  }
+
+  private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
+    for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
+      DexEncodedMethod directTargetMethod =
+          target.lookupDirectMethod(virtualSourceMethod.getReference());
+      if (directTargetMethod != null) {
+        // A private method shadows a virtual method. This situation is rare, since it is not
+        // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
+        // possible to rename the private method in the subclass, and then move the virtual method
+        // to the subclass without changing its name.)
+        return true;
+      }
+    }
+
+    // When merging an interface into a class, all instructions on the form "invoke-interface
+    // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
+    // transformation could hide IncompatibleClassChangeErrors.
+    if (source.isInterface() && !target.isInterface()) {
+      List<DexEncodedMethod> defaultMethods = new ArrayList<>();
+      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+        if (!virtualMethod.accessFlags.isAbstract()) {
+          defaultMethods.add(virtualMethod);
+        }
+      }
+
+      // For each of the default methods, the subclass [target] could inherit another default method
+      // with the same signature from another interface (i.e., there is a conflict). In such cases,
+      // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
+      // ClassChangeError.
+      //
+      // Example:
+      //   interface I1 { default void m() {} }
+      //   interface I2 { default void m() {} }
+      //   class C implements I1, I2 {
+      //     ... invoke-interface I1.m ... <- IncompatibleClassChangeError
+      //   }
+      for (DexEncodedMethod method : defaultMethods) {
+        // Conservatively find all possible targets for this method.
+        LookupResultSuccess lookupResult =
+            appView
+                .appInfo()
+                .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
+                .lookupVirtualDispatchTargets(target, appView)
+                .asLookupResultSuccess();
+        assert lookupResult != null;
+        if (lookupResult == null) {
+          return true;
+        }
+        if (lookupResult.contains(method)) {
+          Box<Boolean> found = new Box<>(false);
+          lookupResult.forEach(
+              interfaceTarget -> {
+                if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
+                  return;
+                }
+                DexClass enclosingClass = interfaceTarget.getHolder();
+                if (enclosingClass != null && enclosingClass.isInterface()) {
+                  // Found a default method that is different from the one in [source], aborting.
+                  found.set(true);
+                }
+              },
+              lambdaTarget -> {
+                // The merger should already have excluded lambda implemented interfaces.
+                assert false;
+              });
+          if (found.get()) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String getName() {
+    return "VerticalClassMergerPolicy";
+  }
+}
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 e2bfb17..e55dacb 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -3,436 +3,70 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.verticalclassmerging;
 
-import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
-
-import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
-import com.android.tools.r8.androidapi.ComputedApiLevel;
-import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
-import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.horizontalclassmerging.Policy;
+import com.android.tools.r8.horizontalclassmerging.PolicyExecutor;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.MainDexInfo;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.ObjectUtils;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.HashSet;
+import com.android.tools.r8.utils.Timing;
+import java.util.Collection;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
 // TODO(b/315252934): Parallelize policy execution over connected program components.
-public class VerticalClassMergerPolicyExecutor {
+public class VerticalClassMergerPolicyExecutor extends PolicyExecutor<VerticalMergeGroup> {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final InternalOptions options;
-  private final MainDexInfo mainDexInfo;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
   private final Set<DexProgramClass> pinnedClasses;
 
   VerticalClassMergerPolicyExecutor(
-      AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> pinnedClasses) {
     this.appView = appView;
-    this.options = appView.options();
-    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.pinnedClasses = pinnedClasses;
   }
 
   ConnectedComponentVerticalClassMerger run(
-      Set<DexProgramClass> connectedComponent,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    Set<DexProgramClass> mergeCandidates = Sets.newIdentityHashSet();
-    for (DexProgramClass sourceClass : connectedComponent) {
-      List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
-      if (subclasses.size() != 1) {
-        continue;
-      }
-      DexProgramClass targetClass = ListUtils.first(subclasses);
-      if (!isMergeCandidate(sourceClass, targetClass)) {
-        continue;
-      }
-      if (!isStillMergeCandidate(sourceClass, targetClass)) {
-        continue;
-      }
-      if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
-          || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
-        continue;
-      }
-      mergeCandidates.add(sourceClass);
-    }
-    return new ConnectedComponentVerticalClassMerger(appView, mergeCandidates);
+      Set<DexProgramClass> connectedComponent, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    Collection<VerticalMergeGroup> groups = createInitialMergeGroups(connectedComponent);
+    Collection<Policy> policies = List.of(new VerticalClassMergerPolicy(appView, pinnedClasses));
+    groups = run(groups, policies, executorService, timing);
+    return new ConnectedComponentVerticalClassMerger(appView, groups);
   }
 
-  // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
-  // method do not change in response to any class merges.
-  private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    assert targetClass != null;
-    ObjectAllocationInfoCollection allocationInfo =
-        appView.appInfo().getObjectAllocationInfoCollection();
-    if (allocationInfo.isInstantiatedDirectly(sourceClass)
-        || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
-        || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
-        || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
-        || pinnedClasses.contains(sourceClass)) {
-      return false;
-    }
-
-    assert sourceClass
-        .traverseProgramMembers(
-            member -> {
-              assert !appView.getKeepInfo(member).isPinned(options);
-              return TraversalContinuation.doContinue();
-            })
-        .shouldContinue();
-
-    if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
-        sourceClass, targetClass, appView)) {
-      return false;
-    }
-    if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
-        && appView.getKeepInfo(targetClass).isPinned(options)) {
-      return false;
-    }
-    if (sourceClass.isAnnotation()) {
-      return false;
-    }
-    if (!sourceClass.isInterface()
-        && targetClass.isSerializable(appView)
-        && !appView.appInfo().isSerializable(sourceClass.getType())) {
-      // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
-      //   1.10 The Serializable Interface
-      //   ...
-      //   A Serializable class must do the following:
-      //   ...
-      //     * Have access to the no-arg constructor of its first non-serializable superclass
-      return false;
-    }
-
-    // If there is a constructor in the target, make sure that all source constructors can be
-    // inlined.
-    if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
-      TraversalContinuation<?, ?> result =
-          sourceClass.traverseProgramInstanceInitializers(
-              method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
-      if (result.shouldBreak()) {
-        return false;
+  @SuppressWarnings("JdkObsolete")
+  private LinkedList<VerticalMergeGroup> createInitialMergeGroups(
+      Set<DexProgramClass> connectedComponent) {
+    LinkedList<VerticalMergeGroup> groups = new LinkedList<>();
+    for (DexProgramClass mergeCandidate : connectedComponent) {
+      List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
+      if (subclasses.size() == 1) {
+        groups.add(new VerticalMergeGroup(mergeCandidate, ListUtils.first(subclasses)));
       }
     }
-    if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
-      return false;
-    }
-    // We abort class merging when merging across nests or from a nest to non-nest.
-    // Without nest this checks null == null.
-    if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
-      return false;
-    }
-
-    // If there is an invoke-special to a default interface method and we are not merging into an
-    // interface, then abort, since invoke-special to a virtual class method requires desugaring.
-    if (sourceClass.isInterface() && !targetClass.isInterface()) {
-      TraversalContinuation<?, ?> result =
-          sourceClass.traverseProgramMethods(
-              method -> {
-                boolean foundInvokeSpecialToDefaultLibraryMethod =
-                    method.registerCodeReferencesWithResult(
-                        new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
-                return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
-              });
-      if (result.shouldBreak()) {
-        return false;
-      }
-    }
-
-    // Check with main dex classes to see if we are allowed to merge.
-    if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
-      return false;
-    }
-
-    return true;
+    return groups;
   }
 
-  /**
-   * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
-   * in this method may change in response to class merges. Therefore, this method should always be
-   * called before merging {@param sourceClass} into {@param targetClass}.
-   */
-  boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    // 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.
-    if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
-        || targetClass.classInitializationMayHaveSideEffects(
-            appView, type -> type.isIdenticalTo(sourceClass.getType()))
-        || (sourceClass.isInterface()
-            && sourceClass.classInitializationMayHaveSideEffects(appView))) {
-      return false;
-    }
-    boolean sourceCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(sourceClass)
-            || sourceClass.hasStaticSynchronizedMethods();
-    boolean targetCanBeSynchronizedOn =
-        appView.appInfo().isLockCandidate(targetClass)
-            || targetClass.hasStaticSynchronizedMethods();
-    if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
-      return false;
-    }
-    if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
-      return false;
-    }
-    if (methodResolutionMayChange(sourceClass, targetClass)) {
-      return false;
-    }
-    // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
-    // to the super class.
-    if (fieldResolutionMayChange(sourceClass, targetClass)) {
-      return false;
-    }
-    // Only merge if api reference level of source class is equal to target class. The check is
-    // somewhat expensive.
-    if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
-      AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
-      ComputedApiLevel sourceApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
-      ComputedApiLevel targetApiLevel =
-          getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
-      if (!sourceApiLevel.equals(targetApiLevel)) {
-        return false;
-      }
-    }
-    return true;
+  @Override
+  protected LinkedList<VerticalMergeGroup> apply(
+      Policy policy, LinkedList<VerticalMergeGroup> linkedGroups, ExecutorService executorService)
+      throws ExecutionException {
+    assert policy.isVerticalClassMergerPolicy();
+    return apply(policy.asVerticalClassMergerPolicy(), linkedGroups);
   }
 
-  private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
-    if (!appView.options().inlinerOptions().enableInlining) {
-      return true;
-    }
-    Code code = method.getDefinition().getCode();
-    if (code.isCfCode()) {
-      CfCode cfCode = code.asCfCode();
-      ConstraintWithTarget constraint =
-          cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
-      if (constraint.isNever()) {
-        return true;
-      }
-      // Constructors can have references beyond the root main dex classes. This can increase the
-      // size of the main dex dependent classes and we should bail out.
-      if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
-        return true;
-      }
-      return false;
-    }
-    if (code.isDefaultInstanceInitializerCode()) {
-      return false;
-    }
-    return true;
-  }
-
-  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
-    if (source.getType().isIdenticalTo(target.getSuperType())) {
-      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
-      // Target implements an interface that declares a static final field f, this should yield an
-      // IncompatibleClassChangeError.
-      // TODO(christofferqa): In the following we only check if a static field from an interface
-      //  shadows an instance field from [source]. We could actually check if there is an iget/iput
-      //  instruction whose resolution would be affected by the merge. The situation where a static
-      //  field shadows an instance field is probably not widespread in practice, though.
-      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
-      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
-      for (DexType interfaceType : target.getInterfaces()) {
-        DexClass clazz = appView.definitionFor(interfaceType);
-        for (DexEncodedField staticField : clazz.staticFields()) {
-          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
-        }
-      }
-      for (DexEncodedField instanceField : source.instanceFields()) {
-        if (staticFieldsInInterfacesOfTarget.contains(
-            equivalence.wrap(instanceField.getReference()))) {
-          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
-          // interface would now hit an instance field from [source], so that an IncompatibleClass-
-          // ChangeError would no longer be thrown. Abort merge.
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
-    if (source.isSamePackage(target)) {
-      // When merging two classes from the same package, we only need to make sure that [source]
-      // does not get less visible, since that could make a valid access to [source] from another
-      // package illegal after [source] has been merged into [target].
-      assert source.getAccessFlags().isPackagePrivateOrPublic();
-      assert target.getAccessFlags().isPackagePrivateOrPublic();
-      // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
-      return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
-    }
-
-    // Check that all accesses to [source] and its members from inside the current package of
-    // [source] will continue to work. This is guaranteed if [target] is public and all members of
-    // [source] are either private or public.
-    //
-    // (Deliberately not checking all accesses to [source] since that would be expensive.)
-    if (!target.isPublic()) {
-      return true;
-    }
-    for (DexType sourceInterface : source.getInterfaces()) {
-      DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
-      if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
-        return true;
-      }
-    }
-    for (DexEncodedField field : source.fields()) {
-      if (!(field.isPublic() || field.isPrivate())) {
-        return true;
-      }
-    }
-    for (DexEncodedMethod method : source.methods()) {
-      if (!(method.isPublic() || method.isPrivate())) {
-        return true;
-      }
-      // Check if the target is overriding and narrowing the access.
-      if (method.isPublic()) {
-        DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
-        if (targetOverride != null && !targetOverride.isPublic()) {
-          return true;
-        }
-      }
-    }
-    // Check that all accesses from [source] to classes or members from the current package of
-    // [source] will continue to work. This is guaranteed if the methods of [source] do not access
-    // any private or protected classes or members from the current package of [source].
-    TraversalContinuation<?, ?> result =
-        source.traverseProgramMethods(
-            method -> {
-              boolean foundIllegalAccess =
-                  method.registerCodeReferencesWithResult(
-                      new IllegalAccessDetector(appView, method));
-              if (foundIllegalAccess) {
-                return TraversalContinuation.doBreak();
-              }
-              return TraversalContinuation.doContinue();
-            },
-            DexEncodedMethod::hasCode);
-    return result.shouldBreak();
-  }
-
-  // TODO: maybe skip this check if target does not implement any interfaces (directly or
-  // indirectly)?
-  private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
-    // This only returns true when an invoke-super instruction is found that targets a default
-    // interface method.
-    if (!options.canUseDefaultAndStaticInterfaceMethods()) {
-      return false;
-    }
-    // This problem may only arise when merging (non-interface) classes into classes.
-    if (source.isInterface() || target.isInterface()) {
-      return false;
-    }
-    return target
-        .traverseProgramMethods(
-            method -> {
-              MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
-                  new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
-              method.registerCodeReferencesWithResult(registry);
-              return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
-            },
-            DexEncodedMethod::hasCode)
-        .shouldBreak();
-  }
-
-  private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
-    for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
-      DexEncodedMethod directTargetMethod =
-          target.lookupDirectMethod(virtualSourceMethod.getReference());
-      if (directTargetMethod != null) {
-        // A private method shadows a virtual method. This situation is rare, since it is not
-        // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
-        // possible to rename the private method in the subclass, and then move the virtual method
-        // to the subclass without changing its name.)
-        return true;
-      }
-    }
-
-    // When merging an interface into a class, all instructions on the form "invoke-interface
-    // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
-    // transformation could hide IncompatibleClassChangeErrors.
-    if (source.isInterface() && !target.isInterface()) {
-      List<DexEncodedMethod> defaultMethods = new ArrayList<>();
-      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
-        if (!virtualMethod.accessFlags.isAbstract()) {
-          defaultMethods.add(virtualMethod);
-        }
-      }
-
-      // For each of the default methods, the subclass [target] could inherit another default method
-      // with the same signature from another interface (i.e., there is a conflict). In such cases,
-      // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
-      // ClassChangeError.
-      //
-      // Example:
-      //   interface I1 { default void m() {} }
-      //   interface I2 { default void m() {} }
-      //   class C implements I1, I2 {
-      //     ... invoke-interface I1.m ... <- IncompatibleClassChangeError
-      //   }
-      for (DexEncodedMethod method : defaultMethods) {
-        // Conservatively find all possible targets for this method.
-        LookupResultSuccess lookupResult =
-            appView
-                .appInfo()
-                .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
-                .lookupVirtualDispatchTargets(target, appView)
-                .asLookupResultSuccess();
-        assert lookupResult != null;
-        if (lookupResult == null) {
-          return true;
-        }
-        if (lookupResult.contains(method)) {
-          Box<Boolean> found = new Box<>(false);
-          lookupResult.forEach(
-              interfaceTarget -> {
-                if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
-                  return;
-                }
-                DexClass enclosingClass = interfaceTarget.getHolder();
-                if (enclosingClass != null && enclosingClass.isInterface()) {
-                  // Found a default method that is different from the one in [source], aborting.
-                  found.set(true);
-                }
-              },
-              lambdaTarget -> {
-                // The merger should already have excluded lambda implemented interfaces.
-                assert false;
-              });
-          if (found.get()) {
-            return true;
-          }
-        }
-      }
-    }
-    return false;
+  private LinkedList<VerticalMergeGroup> apply(
+      VerticalClassMergerPolicy policy, LinkedList<VerticalMergeGroup> linkedGroups) {
+    linkedGroups.removeIf(group -> !policy.canMerge(group));
+    return linkedGroups;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
new file mode 100644
index 0000000..28b2a82
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
@@ -0,0 +1,31 @@
+// 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.verticalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroupBase;
+
+public class VerticalMergeGroup extends MergeGroupBase {
+
+  private final DexProgramClass source;
+  private final DexProgramClass target;
+
+  VerticalMergeGroup(DexProgramClass source, DexProgramClass target) {
+    this.source = source;
+    this.target = target;
+  }
+
+  public DexProgramClass getSource() {
+    return source;
+  }
+
+  public DexProgramClass getTarget() {
+    return target;
+  }
+
+  @Override
+  public int size() {
+    return 2;
+  }
+}