Parallelize vertical class merging

Change-Id: I7301960537f59beae1a6961e6a9ac5411723de21
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
index aa03ea2..2a3da47 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
@@ -6,18 +6,15 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMember;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
-import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
@@ -25,16 +22,12 @@
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.TriFunction;
-import com.android.tools.r8.verticalclassmerging.SingleTypeMapperGraphLens;
 
 // Computes the inlining constraint for a given instruction.
 public class InliningConstraints {
 
   private AppView<AppInfoWithLiveness> appView;
 
-  private boolean allowStaticInterfaceMethodCalls = true;
-
   // Currently used only by the vertical class merger (in all other cases this is the identity).
   //
   // When merging a type A into its subtype B we need to inline A.<init>() into B.<init>().
@@ -64,10 +57,6 @@
     allowStaticInterfaceMethodCalls = false;
   }
 
-  private boolean isVerticalClassMerging() {
-    return graphLens instanceof SingleTypeMapperGraphLens;
-  }
-
   public ConstraintWithTarget forAlwaysMaterializingUser() {
     return ConstraintWithTarget.ALWAYS;
   }
@@ -176,15 +165,17 @@
     if (lookup.holder.isArrayType()) {
       return ConstraintWithTarget.ALWAYS;
     }
-    MethodResolutionResult resolutionResult =
-        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup);
-    DexClassAndMethod target =
-        singleTargetWhileVerticalClassMerging(
-            resolutionResult, context, MethodResolutionResult::lookupInvokeDirectTarget);
-    if (target != null) {
-      return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
+    SingleResolutionResult<?> resolutionResult =
+        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup).asSingleResolution();
+    if (resolutionResult == null) {
+      return ConstraintWithTarget.NEVER;
     }
-    return ConstraintWithTarget.NEVER;
+    DexClassAndMethod target =
+        resolutionResult.lookupInvokeDirectTarget(context.getHolder(), appView);
+    if (target == null) {
+      return ConstraintWithTarget.NEVER;
+    }
+    return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
   }
 
   public ConstraintWithTarget forInvokeInterface(DexMethod method, ProgramMethod context) {
@@ -211,55 +202,20 @@
     if (lookup.holder.isArrayType()) {
       return ConstraintWithTarget.ALWAYS;
     }
-    MethodResolutionResult resolutionResult =
-        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup);
+    SingleResolutionResult<?> resolutionResult =
+        appView.appInfo().unsafeResolveMethodDueToDexFormatLegacy(lookup).asSingleResolution();
+    if (resolutionResult == null) {
+      return ConstraintWithTarget.NEVER;
+    }
     DexClassAndMethod target =
-        singleTargetWhileVerticalClassMerging(
-            resolutionResult, context, MethodResolutionResult::lookupInvokeStaticTarget);
-    if (!allowStaticInterfaceMethodCalls && target != null) {
-      // See b/120121170.
-      DexClass methodClass = appView.definitionFor(graphLens.lookupType(target.getHolderType()));
-      if (methodClass != null && methodClass.isInterface() && target.getDefinition().hasCode()) {
-        return ConstraintWithTarget.NEVER;
-      }
+        resolutionResult.lookupInvokeStaticTarget(context.getHolder(), appView);
+    if (target == null) {
+      return ConstraintWithTarget.NEVER;
     }
     if (target != null) {
       return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
     }
-    return ConstraintWithTarget.NEVER;
-  }
-
-  @SuppressWarnings({"ConstantConditions", "ReferenceEquality"})
-  private DexClassAndMethod singleTargetWhileVerticalClassMerging(
-      MethodResolutionResult resolutionResult,
-      ProgramMethod context,
-      TriFunction<
-              MethodResolutionResult,
-              DexProgramClass,
-              AppView<? extends AppInfoWithClassHierarchy>,
-              DexClassAndMethod>
-          lookup) {
-    if (!resolutionResult.isSingleResolution()) {
-      return null;
-    }
-    DexClassAndMethod lookupResult = lookup.apply(resolutionResult, context.getHolder(), appView);
-    if (!isVerticalClassMerging() || lookupResult != null) {
-      return lookupResult;
-    }
-    assert isVerticalClassMerging();
-    assert graphLens.lookupType(context.getHolder().superType) == context.getHolderType();
-    DexProgramClass superContext =
-        appView.programDefinitionFor(context.getHolder().superType, context);
-    if (superContext == null) {
-      return null;
-    }
-    DexClassAndMethod alternativeDexEncodedMethod =
-        lookup.apply(resolutionResult, superContext, appView);
-    if (alternativeDexEncodedMethod != null
-        && alternativeDexEncodedMethod.getHolderType() == superContext.type) {
-      return alternativeDexEncodedMethod;
-    }
-    return null;
+    return forResolvedMember(resolutionResult.getInitialResolutionHolder(), context, target);
   }
 
   public ConstraintWithTarget forInvokeSuper(DexMethod method, ProgramMethod context) {
@@ -394,10 +350,6 @@
       DexClass initialResolutionHolder,
       ProgramMethod context,
       DexClassAndMember<?, ?> resolvedMember) {
-    if (resolvedMember == null) {
-      // This will fail at runtime.
-      return ConstraintWithTarget.NEVER;
-    }
     ConstraintWithTarget featureSplitInliningConstraint =
         FeatureSplitBoundaryOptimizationUtils.getInliningConstraintForResolvedMember(
             context, resolvedMember, appView);
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 44b2d2c..5fb872d 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -47,7 +47,6 @@
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.ObjectUtils;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Streams;
@@ -79,7 +78,7 @@
   private final VerticalClassMergerGraphLens.Builder deferredRenamings;
   private final DexItemFactory dexItemFactory;
   private final VerticalClassMergerGraphLens.Builder lensBuilder;
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
+  private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
 
   private final DexProgramClass source;
   private final DexProgramClass target;
@@ -91,15 +90,14 @@
   ClassMerger(
       AppView<AppInfoWithLiveness> appView,
       VerticalClassMergerGraphLens.Builder lensBuilder,
-      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
+      VerticallyMergedClasses.Builder verticallyMergedClassesBuilder,
       DexProgramClass source,
       DexProgramClass target) {
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
     this.appView = appView;
-    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
-    this.dexItemFactory = dexItemFactory;
+    this.deferredRenamings = new VerticalClassMergerGraphLens.Builder(appView);
+    this.dexItemFactory = appView.dexItemFactory();
     this.lensBuilder = lensBuilder;
-    this.mergedClasses = mergedClasses;
+    this.verticallyMergedClassesBuilder = verticallyMergedClassesBuilder;
     this.source = source;
     this.target = target;
   }
@@ -585,7 +583,7 @@
         // the code above. However, instructions on the form "invoke-super A.m()" should also be
         // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
         // that have been merged into [holder].
-        Set<DexType> mergedTypes = mergedClasses.getKeys(holder.getType());
+        Set<DexType> mergedTypes = verticallyMergedClassesBuilder.getSourcesFor(holder);
         for (DexType type : mergedTypes) {
           DexMethod signatureInType = oldTargetReference.withHolder(type, dexItemFactory);
           // Resolution would have succeeded if the method used to be in [type], or if one of
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java b/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
index ca22d3d..961f719 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/CollisionDetector.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -26,7 +25,6 @@
 
   private final DexItemFactory dexItemFactory;
   private final Collection<DexMethod> invokes;
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
 
   private final DexType source;
   private final Reference2IntMap<DexProto> sourceProtoCache;
@@ -39,12 +37,10 @@
   CollisionDetector(
       AppView<AppInfoWithLiveness> appView,
       Collection<DexMethod> invokes,
-      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
       DexType source,
       DexType target) {
     this.dexItemFactory = appView.dexItemFactory();
     this.invokes = invokes;
-    this.mergedClasses = mergedClasses;
     this.source = source;
     this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
     this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
@@ -115,7 +111,7 @@
     int accumulator = 0;
     for (DexType parameterBaseType : proto.getParameterBaseTypes(dexItemFactory)) {
       // Substitute the type with the already merged class to estimate what it will look like.
-      DexType mappedType = mergedClasses.getOrDefault(parameterBaseType, parameterBaseType);
+      DexType mappedType = parameterBaseType;
       accumulator <<= 1;
       bitsUsed++;
       if (mappedType.isIdenticalTo(type)) {
@@ -130,7 +126,7 @@
     }
     // We also take the return type into account for potential conflicts.
     DexType returnBaseType = proto.getReturnType().toBaseType(dexItemFactory);
-    DexType mappedReturnType = mergedClasses.getOrDefault(returnBaseType, returnBaseType);
+    DexType mappedReturnType = returnBaseType;
     accumulator <<= 1;
     if (mappedReturnType.isIdenticalTo(type)) {
       accumulator |= 1;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
new file mode 100644
index 0000000..a823a1a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
@@ -0,0 +1,239 @@
+// 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.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+public class ConnectedComponentVerticalClassMerger {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final MainDexInfo mainDexInfo;
+
+  private Collection<DexMethod> invokes;
+
+  // The resulting graph lens that should be used after class merging.
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+
+  // All the bridge methods that have been synthesized during vertical class merging.
+  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
+
+  private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder =
+      VerticallyMergedClasses.builder();
+
+  ConnectedComponentVerticalClassMerger(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.lensBuilder = new VerticalClassMergerGraphLens.Builder(appView);
+  }
+
+  public VerticalClassMergerResult.Builder run(
+      Set<DexProgramClass> connectedComponent,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> pinnedClasses,
+      Timing timing)
+      throws ExecutionException {
+    // Visit the program classes in a top-down order according to the class hierarchy.
+    VerticalClassMergerPolicyExecutor policyExecutor =
+        new VerticalClassMergerPolicyExecutor(
+            appView, pinnedClasses, verticallyMergedClassesBuilder);
+    Set<DexProgramClass> mergeCandidates =
+        policyExecutor.run(connectedComponent, immediateSubtypingInfo);
+    List<DexProgramClass> mergeCandidatesSorted =
+        ListUtils.sort(mergeCandidates, Comparator.comparing(DexProgramClass::getType));
+    TopDownClassHierarchyTraversal.forProgramClasses(appView)
+        .visit(
+            mergeCandidatesSorted,
+            clazz ->
+                mergeClassIfPossible(
+                    clazz, immediateSubtypingInfo, mergeCandidates, policyExecutor, timing));
+    return VerticalClassMergerResult.builder(
+        lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
+  }
+
+  private void mergeClassIfPossible(
+      DexProgramClass clazz,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> mergeCandidates,
+      VerticalClassMergerPolicyExecutor policyExecutor,
+      Timing timing)
+      throws ExecutionException {
+    if (!mergeCandidates.contains(clazz)) {
+      return;
+    }
+    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(clazz);
+    if (subclasses.size() != 1) {
+      return;
+    }
+    DexProgramClass targetClass = ListUtils.first(subclasses);
+    assert !verticallyMergedClassesBuilder.isMergeSource(targetClass);
+    if (verticallyMergedClassesBuilder.isMergeTarget(clazz)) {
+      return;
+    }
+    if (verticallyMergedClassesBuilder.isMergeTarget(targetClass)) {
+      if (!policyExecutor.isStillMergeCandidate(clazz, targetClass)) {
+        return;
+      }
+    } else {
+      assert policyExecutor.isStillMergeCandidate(clazz, targetClass);
+    }
+
+    // Guard against the case where we have two methods that may get the same signature
+    // if we replace types. This is rare, so we approximate and err on the safe side here.
+    CollisionDetector collisionDetector =
+        new CollisionDetector(
+            appView,
+            getInvokes(immediateSubtypingInfo, mergeCandidates),
+            clazz.getType(),
+            targetClass.getType());
+    if (collisionDetector.mayCollide(timing)) {
+      return;
+    }
+
+    // Check with main dex classes to see if we are allowed to merge.
+    if (!mainDexInfo.canMerge(clazz, targetClass, appView.getSyntheticItems())) {
+      return;
+    }
+
+    ClassMerger merger =
+        new ClassMerger(appView, lensBuilder, verticallyMergedClassesBuilder, clazz, targetClass);
+    if (merger.merge()) {
+      verticallyMergedClassesBuilder.add(clazz, targetClass);
+      // Commit the changes to the graph lens.
+      lensBuilder.merge(merger.getRenamings());
+      synthesizedBridges.addAll(merger.getSynthesizedBridges());
+    }
+  }
+
+  private Collection<DexMethod> getInvokes(
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo, Set<DexProgramClass> mergeCandidates) {
+    if (invokes == null) {
+      invokes =
+          new OverloadedMethodSignaturesRetriever(immediateSubtypingInfo, mergeCandidates)
+              .get(mergeCandidates);
+    }
+    return invokes;
+  }
+
+  // Collects all potentially overloaded method signatures that reference at least one type that
+  // may be the source or target of a merge operation.
+  private class OverloadedMethodSignaturesRetriever {
+    private final Reference2BooleanOpenHashMap<DexProto> cache =
+        new Reference2BooleanOpenHashMap<>();
+    private final Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+    private final Set<DexType> mergeeCandidates = new HashSet<>();
+
+    public OverloadedMethodSignaturesRetriever(
+        ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+        Set<DexProgramClass> mergeCandidates) {
+      for (DexProgramClass mergeCandidate : mergeCandidates) {
+        List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
+        if (subclasses.size() == 1) {
+          mergeeCandidates.add(ListUtils.first(subclasses).getType());
+        }
+      }
+    }
+
+    public Collection<DexMethod> get(Set<DexProgramClass> mergeCandidates) {
+      Map<DexString, DexProto> overloadingInfo = new HashMap<>();
+
+      // Find all signatures that may reference a type that could be the source or target of a
+      // merge operation.
+      Set<Wrapper<DexMethod>> filteredSignatures = new HashSet<>();
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        for (DexEncodedMethod encodedMethod : clazz.methods()) {
+          DexMethod method = encodedMethod.getReference();
+          DexClass definition = appView.definitionFor(method.getHolderType());
+          if (definition != null
+              && definition.isProgramClass()
+              && protoMayReferenceMergedSourceOrTarget(method.getProto(), mergeCandidates)) {
+            filteredSignatures.add(equivalence.wrap(method));
+
+            // Record that we have seen a method named [signature.name] with the proto
+            // [signature.proto]. If at some point, we find a method with the same name, but a
+            // different proto, it could be the case that a method with the given name is
+            // overloaded.
+            DexProto existing =
+                overloadingInfo.computeIfAbsent(method.getName(), key -> method.getProto());
+            if (existing.isNotIdenticalTo(DexProto.SENTINEL)
+                && !existing.equals(method.getProto())) {
+              // Mark that this signature is overloaded by mapping it to SENTINEL.
+              overloadingInfo.put(method.getName(), DexProto.SENTINEL);
+            }
+          }
+        }
+      }
+
+      List<DexMethod> result = new ArrayList<>();
+      for (Wrapper<DexMethod> wrappedSignature : filteredSignatures) {
+        DexMethod signature = wrappedSignature.get();
+
+        // Ignore those method names that are definitely not overloaded since they cannot lead to
+        // any collisions.
+        if (overloadingInfo.get(signature.getName()).isIdenticalTo(DexProto.SENTINEL)) {
+          result.add(signature);
+        }
+      }
+      return result;
+    }
+
+    private boolean protoMayReferenceMergedSourceOrTarget(
+        DexProto proto, Set<DexProgramClass> mergeCandidates) {
+      boolean result;
+      if (cache.containsKey(proto)) {
+        result = cache.getBoolean(proto);
+      } else {
+        result =
+            Iterables.any(
+                proto.getTypes(),
+                type -> typeMayReferenceMergedSourceOrTarget(type, mergeCandidates));
+        cache.put(proto, result);
+      }
+      return result;
+    }
+
+    private boolean typeMayReferenceMergedSourceOrTarget(
+        DexType type, Set<DexProgramClass> mergeCandidates) {
+      type = type.toBaseType(appView.dexItemFactory());
+      if (type.isClassType()) {
+        if (mergeeCandidates.contains(type)) {
+          return true;
+        }
+        DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+        if (clazz != null) {
+          return mergeCandidates.contains(clazz.asProgramClass());
+        }
+      }
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java
deleted file mode 100644
index 55a1a0a..0000000
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/SingleTypeMapperGraphLens.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.verticalclassmerging;
-
-import static com.android.tools.r8.ir.code.InvokeType.VIRTUAL;
-
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.lens.FieldLookupResult;
-import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.lens.MethodLookupResult;
-import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
-import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
-import com.android.tools.r8.ir.code.InvokeType;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
-
-public class SingleTypeMapperGraphLens extends NonIdentityGraphLens {
-
-  private final VerticalClassMergerGraphLens.Builder lensBuilder;
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses;
-
-  private final DexProgramClass source;
-  private final DexProgramClass target;
-
-  public SingleTypeMapperGraphLens(
-      AppView<AppInfoWithLiveness> appView,
-      VerticalClassMergerGraphLens.Builder lensBuilder,
-      MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses,
-      DexProgramClass source,
-      DexProgramClass target) {
-    super(appView, GraphLens.getIdentityLens());
-    this.lensBuilder = lensBuilder;
-    this.mergedClasses = mergedClasses;
-    this.source = source;
-    this.target = target;
-  }
-
-  @Override
-  public Iterable<DexType> getOriginalTypes(DexType type) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public DexType getPreviousClassType(DexType type) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public final DexType getNextClassType(DexType type) {
-    return type.isIdenticalTo(source.getType())
-        ? target.getType()
-        : mergedClasses.getOrDefault(type, type);
-  }
-
-  @Override
-  public DexField getPreviousFieldSignature(DexField field) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public DexField getNextFieldSignature(DexField field) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public DexMethod getPreviousMethodSignature(DexMethod method) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public DexMethod getNextMethodSignature(DexMethod method) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public MethodLookupResult lookupMethod(
-      DexMethod method, DexMethod context, InvokeType type, GraphLens codeLens) {
-    // First look up the method using the existing graph lens (for example, the type will have
-    // changed if the method was publicized by ClassAndMemberPublicizer).
-    MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type, codeLens);
-    // Then check if there is a renaming due to the vertical class merger.
-    DexMethod newMethod = lensBuilder.methodMap.get(lookup.getReference());
-    if (newMethod == null) {
-      return lookup;
-    }
-    MethodLookupResult.Builder methodLookupResultBuilder =
-        MethodLookupResult.builder(this)
-            .setReference(newMethod)
-            .setPrototypeChanges(lookup.getPrototypeChanges())
-            .setType(lookup.getType());
-    if (lookup.getType() == InvokeType.INTERFACE) {
-      // If an interface has been merged into a class, invoke-interface needs to be translated
-      // to invoke-virtual.
-      DexClass clazz = appView.definitionFor(newMethod.holder);
-      if (clazz != null && !clazz.accessFlags.isInterface()) {
-        assert appView.definitionFor(method.holder).accessFlags.isInterface();
-        methodLookupResultBuilder.setType(VIRTUAL);
-      }
-    }
-    return methodLookupResultBuilder.build();
-  }
-
-  @Override
-  protected MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
-    // This is unreachable since we override the implementation of lookupMethod() above.
-    throw new Unreachable();
-  }
-
-  @Override
-  public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
-      DexMethod method, GraphLens codeLens) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public DexField lookupField(DexField field, GraphLens codeLens) {
-    return lensBuilder.fieldMap.getOrDefault(field, field);
-  }
-
-  @Override
-  protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    // This is unreachable since we override the implementation of lookupField() above.
-    throw new Unreachable();
-  }
-
-  @Override
-  public boolean isContextFreeForMethods(GraphLens codeLens) {
-    return true;
-  }
-}
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 a9132b3..ef70058 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -5,63 +5,34 @@
 
 import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-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.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
-import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.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.graph.PrunedItems;
-import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+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;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
-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.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
+import com.android.tools.r8.utils.Timing.TimingMerger;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import com.google.common.collect.Streams;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -79,74 +50,31 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory dexItemFactory;
   private final InternalOptions options;
-  private Collection<DexMethod> invokes;
-
-  // Set of merge candidates. Note that this must have a deterministic iteration order.
-  private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
-
-  // Map from source class to target class.
-  private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
-      BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
-
-  private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedInterfaces =
-      BidirectionalManyToOneHashMap.newIdentityHashMap();
-
-  // Set of types that must not be merged into their subtype.
-  private final Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
-
-  // The resulting graph lens that should be used after class merging.
-  private final VerticalClassMergerGraphLens.Builder lensBuilder;
-
-  // All the bridge methods that have been synthesized during vertical class merging.
-  private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
-
-  private final MainDexInfo mainDexInfo;
 
   public VerticalClassMerger(AppView<AppInfoWithLiveness> appView) {
-    AppInfoWithLiveness appInfo = appView.appInfo();
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
     this.options = appView.options();
-    this.mainDexInfo = appInfo.getMainDexInfo();
-    this.lensBuilder = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
-  }
-
-  private void initializeMergeCandidates(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    for (DexProgramClass sourceClass : appView.appInfo().classesWithDeterministicOrder()) {
-      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)) {
-        continue;
-      }
-      mergeCandidates.add(sourceClass);
-    }
   }
 
   // Returns a set of types that must not be merged into other types.
-  private void initializePinnedTypes() {
+  private Set<DexProgramClass> getPinnedClasses() {
+    Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
+
     // For all pinned fields, also pin the type of the field (because changing the type of the field
     // implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
     // the return type and the parameter types of the method.
     // TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
-    List<DexReference> pinnedItems = new ArrayList<>();
+    List<DexReference> pinnedReferences = new ArrayList<>();
     KeepInfoCollection keepInfo = appView.getKeepInfo();
-    keepInfo.forEachPinnedType(pinnedItems::add, options);
-    keepInfo.forEachPinnedMethod(pinnedItems::add, options);
-    keepInfo.forEachPinnedField(pinnedItems::add, options);
-    extractPinnedItems(pinnedItems);
+    keepInfo.forEachPinnedType(pinnedReferences::add, options);
+    keepInfo.forEachPinnedMethod(pinnedReferences::add, options);
+    keepInfo.forEachPinnedField(pinnedReferences::add, options);
+    extractPinnedClasses(pinnedReferences, pinnedClasses);
 
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (Iterables.any(clazz.methods(), method -> method.getAccessFlags().isNative())) {
-        markClassAsPinned(clazz);
+        markClassAsPinned(clazz, pinnedClasses);
       }
     }
 
@@ -155,39 +83,42 @@
     // SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
     // Therefore, we disallow merging such interfaces into their subtypes.
     for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
-      markTypeAsPinned(signature.getHolderType());
+      markTypeAsPinned(signature.getHolderType(), pinnedClasses);
     }
 
     // The set of targets that must remain for proper resolution error cases should not be merged.
     // TODO(b/192821424): Can be removed if handled.
-    extractPinnedItems(appView.appInfo().getFailedMethodResolutionTargets());
+    extractPinnedClasses(appView.appInfo().getFailedMethodResolutionTargets(), pinnedClasses);
+
+    return pinnedClasses;
   }
 
-  private <T extends DexReference> void extractPinnedItems(Iterable<T> items) {
+  private <T extends DexReference> void extractPinnedClasses(
+      Iterable<T> items, Set<DexProgramClass> pinnedClasses) {
     for (DexReference item : items) {
       if (item.isDexType()) {
-        markTypeAsPinned(item.asDexType());
+        markTypeAsPinned(item.asDexType(), pinnedClasses);
       } else if (item.isDexField()) {
         // Pin the holder and the type of the field.
         DexField field = item.asDexField();
-        markTypeAsPinned(field.getHolderType());
-        markTypeAsPinned(field.getType());
+        markTypeAsPinned(field.getHolderType(), pinnedClasses);
+        markTypeAsPinned(field.getType(), pinnedClasses);
       } else {
         assert item.isDexMethod();
         // Pin the holder, the return type and the parameter types of the method. If we were to
         // merge any of these types into their sub classes, then we would implicitly change the
         // signature of this method.
         DexMethod method = item.asDexMethod();
-        markTypeAsPinned(method.getHolderType());
-        markTypeAsPinned(method.getReturnType());
+        markTypeAsPinned(method.getHolderType(), pinnedClasses);
+        markTypeAsPinned(method.getReturnType(), pinnedClasses);
         for (DexType parameterType : method.getParameters()) {
-          markTypeAsPinned(parameterType);
+          markTypeAsPinned(parameterType, pinnedClasses);
         }
       }
     }
   }
 
-  private void markTypeAsPinned(DexType type) {
+  private void markTypeAsPinned(DexType type, Set<DexProgramClass> pinnedClasses) {
     DexType baseType = type.toBaseType(dexItemFactory);
     if (!baseType.isClassType() || appView.appInfo().isPinnedWithDefinitionLookup(baseType)) {
       // We check for the case where the type is pinned according to appInfo.isPinned,
@@ -197,316 +128,14 @@
 
     DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(baseType));
     if (clazz != null) {
-      markClassAsPinned(clazz);
+      markClassAsPinned(clazz, pinnedClasses);
     }
   }
 
-  private void markClassAsPinned(DexProgramClass clazz) {
+  private void markClassAsPinned(DexProgramClass clazz, Set<DexProgramClass> pinnedClasses) {
     pinnedClasses.add(clazz);
   }
 
-  // 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 (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.getEnclosingMethodAttribute() != null
-        || !sourceClass.getInnerClasses().isEmpty()) {
-      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
-      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;
-      }
-    }
-    return true;
-  }
-
-  // Returns true if [clazz] 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 [clazz] into its subtype.
-  private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
-    assert isMergeCandidate(sourceClass, targetClass);
-    assert !mergedClasses.containsValue(sourceClass.getType());
-    // 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))) {
-      // TODO(herhut): Handle class initializers.
-      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.getEnclosingMethodAttribute() != null
-        || !targetClass.getInnerClasses().isEmpty()) {
-      // TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
-      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 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();
-            });
-    return result.shouldBreak();
-  }
-
-  private Collection<DexMethod> getInvokes(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    if (invokes == null) {
-      invokes = new OverloadedMethodSignaturesRetriever(immediateSubtypingInfo).get();
-    }
-    return invokes;
-  }
-
-  // Collects all potentially overloaded method signatures that reference at least one type that
-  // may be the source or target of a merge operation.
-  private class OverloadedMethodSignaturesRetriever {
-    private final Reference2BooleanOpenHashMap<DexProto> cache =
-        new Reference2BooleanOpenHashMap<>();
-    private final Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
-    private final Set<DexType> mergeeCandidates = new HashSet<>();
-
-    public OverloadedMethodSignaturesRetriever(
-        ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-      for (DexProgramClass mergeCandidate : mergeCandidates) {
-        List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
-        if (subclasses.size() == 1) {
-          mergeeCandidates.add(ListUtils.first(subclasses).getType());
-        }
-      }
-    }
-
-    public Collection<DexMethod> get() {
-      Map<DexString, DexProto> overloadingInfo = new HashMap<>();
-
-      // Find all signatures that may reference a type that could be the source or target of a
-      // merge operation.
-      Set<Wrapper<DexMethod>> filteredSignatures = new HashSet<>();
-      for (DexProgramClass clazz : appView.appInfo().classes()) {
-        for (DexEncodedMethod encodedMethod : clazz.methods()) {
-          DexMethod method = encodedMethod.getReference();
-          DexClass definition = appView.definitionFor(method.getHolderType());
-          if (definition != null
-              && definition.isProgramClass()
-              && protoMayReferenceMergedSourceOrTarget(method.getProto())) {
-            filteredSignatures.add(equivalence.wrap(method));
-
-            // Record that we have seen a method named [signature.name] with the proto
-            // [signature.proto]. If at some point, we find a method with the same name, but a
-            // different proto, it could be the case that a method with the given name is
-            // overloaded.
-            DexProto existing =
-                overloadingInfo.computeIfAbsent(method.getName(), key -> method.getProto());
-            if (existing.isNotIdenticalTo(DexProto.SENTINEL)
-                && !existing.equals(method.getProto())) {
-              // Mark that this signature is overloaded by mapping it to SENTINEL.
-              overloadingInfo.put(method.getName(), DexProto.SENTINEL);
-            }
-          }
-        }
-      }
-
-      List<DexMethod> result = new ArrayList<>();
-      for (Wrapper<DexMethod> wrappedSignature : filteredSignatures) {
-        DexMethod signature = wrappedSignature.get();
-
-        // Ignore those method names that are definitely not overloaded since they cannot lead to
-        // any collisions.
-        if (overloadingInfo.get(signature.getName()).isIdenticalTo(DexProto.SENTINEL)) {
-          result.add(signature);
-        }
-      }
-      return result;
-    }
-
-    private boolean protoMayReferenceMergedSourceOrTarget(DexProto proto) {
-      boolean result;
-      if (cache.containsKey(proto)) {
-        result = cache.getBoolean(proto);
-      } else {
-        result = false;
-        if (typeMayReferenceMergedSourceOrTarget(proto.getReturnType())) {
-          result = true;
-        } else {
-          for (DexType type : proto.getParameters()) {
-            if (typeMayReferenceMergedSourceOrTarget(type)) {
-              result = true;
-              break;
-            }
-          }
-        }
-        cache.put(proto, result);
-      }
-      return result;
-    }
-
-    private boolean typeMayReferenceMergedSourceOrTarget(DexType type) {
-      type = type.toBaseType(dexItemFactory);
-      if (type.isClassType()) {
-        if (mergeeCandidates.contains(type)) {
-          return true;
-        }
-        DexClass clazz = appView.definitionFor(type);
-        if (clazz != null && clazz.isProgramClass()) {
-          return mergeCandidates.contains(clazz.asProgramClass());
-        }
-      }
-      return false;
-    }
-  }
-
   public static void runIfNecessary(
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
@@ -527,59 +156,103 @@
   }
 
   private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Setup");
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
-
-    initializePinnedTypes(); // Must be initialized prior to mergeCandidates.
-    initializeMergeCandidates(immediateSubtypingInfo);
-
-    timing.begin("merge");
-    // Visit the program classes in a top-down order according to the class hierarchy.
-    TopDownClassHierarchyTraversal.forProgramClasses(appView)
-        .visit(
-            mergeCandidates, clazz -> mergeClassIfPossible(clazz, immediateSubtypingInfo, timing));
+    List<Set<DexProgramClass>> connectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
+    Set<DexProgramClass> pinnedClasses = getPinnedClasses();
     timing.end();
 
-    VerticallyMergedClasses verticallyMergedClasses =
-        new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
-    appView.setVerticallyMergedClasses(verticallyMergedClasses);
-    if (verticallyMergedClasses.isEmpty()) {
+    // Apply class merging concurrently in disjoint class hierarchies.
+    VerticalClassMergerResult verticalClassMergerResult =
+        mergeClassesInConnectedComponents(
+            connectedComponents, immediateSubtypingInfo, pinnedClasses, executorService, timing);
+    appView.setVerticallyMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses());
+    if (verticalClassMergerResult.isEmpty()) {
       return;
     }
 
     timing.begin("fixup");
     VerticalClassMergerGraphLens lens =
-        new VerticalClassMergerTreeFixer(
-                appView, lensBuilder, verticallyMergedClasses, synthesizedBridges)
-            .fixupTypeReferences();
-    KeepInfoCollection keepInfo = appView.getKeepInfo();
-    keepInfo.mutate(
-        mutator ->
-            mutator.removeKeepInfoForMergedClasses(
-                PrunedItems.builder().setRemovedClasses(mergedClasses.keySet()).build()));
-    timing.end();
+        new VerticalClassMergerTreeFixer(appView, verticalClassMergerResult).run(timing);
+    updateKeepInfoForMergedClasses(verticalClassMergerResult);
+    assert verifyGraphLens(lens, verticalClassMergerResult);
+    updateArtProfiles(lens, verticalClassMergerResult);
+    appView.rewriteWithLens(lens, executorService, timing);
+    updateKeepInfoForSynthesizedBridges(verticalClassMergerResult);
+    appView.notifyOptimizationFinishedForTesting();
+  }
 
-    assert lens != null;
-    assert verifyGraphLens(lens);
+  private VerticalClassMergerResult mergeClassesInConnectedComponents(
+      List<Set<DexProgramClass>> connectedComponents,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> pinnedClasses,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    VerticalClassMergerResult.Builder verticalClassMergerResult =
+        VerticalClassMergerResult.builder(appView);
+    TimingMerger merger = timing.beginMerger("Merge classes", executorService);
+    Collection<Timing> timings =
+        ThreadUtils.processItemsWithResults(
+            connectedComponents,
+            connectedComponent -> {
+              Timing threadTiming = Timing.create("Merge classes in component", options);
+              VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
+                  new ConnectedComponentVerticalClassMerger(appView)
+                      .run(connectedComponent, immediateSubtypingInfo, pinnedClasses, threadTiming);
+              verticalClassMergerResult.merge(verticalClassMergerComponentResult);
+              threadTiming.end();
+              return threadTiming;
+            },
+            appView.options().getThreadingModule(),
+            executorService);
+    merger.add(timings);
+    merger.end();
+    return verticalClassMergerResult.build();
+  }
 
+  private void updateArtProfiles(
+      VerticalClassMergerGraphLens verticalClassMergerLens,
+      VerticalClassMergerResult verticalClassMergerResult) {
     // Include bridges in art profiles.
     ProfileCollectionAdditions profileCollectionAdditions =
         ProfileCollectionAdditions.create(appView);
     if (!profileCollectionAdditions.isNop()) {
+      List<SynthesizedBridgeCode> synthesizedBridges =
+          verticalClassMergerResult.getSynthesizedBridges();
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         profileCollectionAdditions.applyIfContextIsInProfile(
-            lens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
+            verticalClassMergerLens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
             additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
       }
     }
     profileCollectionAdditions.commit(appView);
+  }
 
-    // Rewrite collections using the lens.
-    appView.rewriteWithLens(lens, executorService, timing);
-
-    // Copy keep info to newly synthesized methods.
+  private void updateKeepInfoForMergedClasses(VerticalClassMergerResult verticalClassMergerResult) {
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
     keepInfo.mutate(
         mutator -> {
+          VerticallyMergedClasses verticallyMergedClasses =
+              verticalClassMergerResult.getVerticallyMergedClasses();
+          mutator.removeKeepInfoForMergedClasses(
+              PrunedItems.builder()
+                  .setRemovedClasses(verticallyMergedClasses.getSources())
+                  .build());
+        });
+  }
+
+  private void updateKeepInfoForSynthesizedBridges(
+      VerticalClassMergerResult verticalClassMergerResult) {
+    // Copy keep info to newly synthesized methods.
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    keepInfo.mutate(
+        mutator -> {
+          List<SynthesizedBridgeCode> synthesizedBridges =
+              verticalClassMergerResult.getSynthesizedBridges();
           for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
             ProgramMethod bridge =
                 asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getMethod()));
@@ -592,11 +265,10 @@
             assert false;
           }
         });
-
-    appView.notifyOptimizationFinishedForTesting();
   }
 
-  private boolean verifyGraphLens(VerticalClassMergerGraphLens graphLens) {
+  private boolean verifyGraphLens(
+      VerticalClassMergerGraphLens graphLens, VerticalClassMergerResult verticalClassMergerResult) {
     // Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
     // getRenamedMethodSignature() instead of lookupField() and lookupMethod(). This is important
     // for this check to succeed, since it is not guaranteed that calling lookupMethod() with a
@@ -625,6 +297,7 @@
     // pinned, because this rewriting does not affect A.method() in any way.
     assert graphLens.assertPinnedNotModified(appView);
 
+    VerticallyMergedClasses mergedClasses = verticalClassMergerResult.getVerticallyMergedClasses();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       for (DexEncodedMethod encodedMethod : clazz.methods()) {
         DexMethod method = encodedMethod.getReference();
@@ -650,195 +323,10 @@
 
         // Verify that all types are up-to-date. After vertical class merging, there should be no
         // more references to types that have been merged into another type.
-        assert !mergedClasses.containsKey(method.getReturnType());
-        assert Arrays.stream(method.getParameters().getBacking())
-            .noneMatch(mergedClasses::containsKey);
+        assert Streams.stream(method.getReferencedBaseTypes(dexItemFactory))
+            .noneMatch(mergedClasses::hasBeenMergedIntoSubtype);
       }
     }
     return true;
   }
-
-  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 void mergeClassIfPossible(
-      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo, Timing timing)
-      throws ExecutionException {
-    if (!mergeCandidates.contains(clazz)) {
-      return;
-    }
-    List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(clazz);
-    if (subclasses.size() != 1) {
-      return;
-    }
-    DexProgramClass targetClass = ListUtils.first(subclasses);
-    assert !mergedClasses.containsKey(targetClass.getType());
-    if (mergedClasses.containsValue(clazz.getType())) {
-      return;
-    }
-    assert isMergeCandidate(clazz, targetClass);
-    if (mergedClasses.containsValue(targetClass.getType())) {
-      if (!isStillMergeCandidate(clazz, targetClass)) {
-        return;
-      }
-    } else {
-      assert isStillMergeCandidate(clazz, targetClass);
-    }
-
-    // Guard against the case where we have two methods that may get the same signature
-    // if we replace types. This is rare, so we approximate and err on the safe side here.
-    CollisionDetector collisionDetector =
-        new CollisionDetector(
-            appView,
-            getInvokes(immediateSubtypingInfo),
-            mergedClasses,
-            clazz.getType(),
-            targetClass.getType());
-    if (collisionDetector.mayCollide(timing)) {
-      return;
-    }
-
-    // Check with main dex classes to see if we are allowed to merge.
-    if (!mainDexInfo.canMerge(clazz, targetClass, appView.getSyntheticItems())) {
-      return;
-    }
-
-    ClassMerger merger = new ClassMerger(appView, lensBuilder, mergedClasses, clazz, targetClass);
-    if (merger.merge()) {
-      mergedClasses.put(clazz.getType(), targetClass.getType());
-      if (clazz.isInterface()) {
-        mergedInterfaces.put(clazz.getType(), targetClass.getType());
-      }
-      // Commit the changes to the graph lens.
-      lensBuilder.merge(merger.getRenamings());
-      synthesizedBridges.addAll(merger.getSynthesizedBridges());
-    }
-  }
-
-  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 disallowInlining(ProgramMethod method, DexProgramClass context) {
-    if (appView.options().inlinerOptions().enableInlining) {
-      Code code = method.getDefinition().getCode();
-      if (code.isCfCode()) {
-        CfCode cfCode = code.asCfCode();
-        SingleTypeMapperGraphLens lens =
-            new SingleTypeMapperGraphLens(
-                appView, lensBuilder, mergedClasses, method.getHolder(), context);
-        ConstraintWithTarget constraint = cfCode.computeInliningConstraint(appView, lens, 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;
-      } else if (code.isDefaultInstanceInitializerCode()) {
-        return false;
-      }
-      // For non-jar/cf code we currently cannot guarantee that markForceInline() will succeed.
-    }
-    return true;
-  }
-
 }
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 eebae8e..f4d2461 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
@@ -58,15 +59,15 @@
 // For the invocation "invoke-virtual A.m()" in B.m2, this graph lens will return the method B.m.
 public class VerticalClassMergerGraphLens extends NestedGraphLens {
 
-  interface GraphLensLookupResultProvider {
+  public interface GraphLensLookupResultProvider {
 
     MethodLookupResult get(RewrittenPrototypeDescription prototypeChanges);
   }
 
-  private VerticallyMergedClasses mergedClasses;
+  private final VerticallyMergedClasses mergedClasses;
   private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
       contextualVirtualToDirectMethodMaps;
-  private Set<DexMethod> mergedMethods;
+  private final Set<DexMethod> mergedMethods;
   private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges;
   private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges;
 
@@ -198,6 +199,7 @@
 
   public static class Builder {
 
+    private final AppView<AppInfoWithLiveness> appView;
     private final DexItemFactory dexItemFactory;
 
     protected final MutableBidirectionalOneToOneMap<DexField, DexField> fieldMap =
@@ -216,13 +218,17 @@
 
     private final Map<DexProto, DexProto> cache = new IdentityHashMap<>();
 
-    Builder(DexItemFactory dexItemFactory) {
-      this.dexItemFactory = dexItemFactory;
+    Builder(AppView<AppInfoWithLiveness> appView) {
+      this.appView = appView;
+      this.dexItemFactory = appView.dexItemFactory();
     }
 
     @SuppressWarnings("ReferenceEquality")
-    static Builder createBuilderForFixup(Builder builder, VerticallyMergedClasses mergedClasses) {
-      Builder newBuilder = new Builder(builder.dexItemFactory);
+    static Builder createBuilderForFixup(VerticalClassMergerResult verticalClassMergerResult) {
+      Builder builder = verticalClassMergerResult.getLensBuilder();
+      VerticallyMergedClasses mergedClasses =
+          verticalClassMergerResult.getVerticallyMergedClasses();
+      Builder newBuilder = new Builder(builder.appView);
       builder.fieldMap.forEach(
           (key, value) ->
               newBuilder.map(
@@ -279,8 +285,7 @@
       return newBuilder;
     }
 
-    public VerticalClassMergerGraphLens build(
-        AppView<?> appView, VerticallyMergedClasses mergedClasses) {
+    public VerticalClassMergerGraphLens build(VerticallyMergedClasses mergedClasses) {
       if (mergedClasses.isEmpty()) {
         return null;
       }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
new file mode 100644
index 0000000..e69e96e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -0,0 +1,406 @@
+// 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.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.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 java.util.List;
+import java.util.Set;
+
+// TODO(b/315252934): Parallelize policy execution over connected program components.
+public class VerticalClassMergerPolicyExecutor {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
+  private final MainDexInfo mainDexInfo;
+  private final Set<DexProgramClass> pinnedClasses;
+  private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
+
+  VerticalClassMergerPolicyExecutor(
+      AppView<AppInfoWithLiveness> appView,
+      Set<DexProgramClass> pinnedClasses,
+      VerticallyMergedClasses.Builder verticallyMergedClassesInComponentBuilder) {
+    this.appView = appView;
+    this.options = appView.options();
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.pinnedClasses = pinnedClasses;
+    this.verticallyMergedClassesBuilder = verticallyMergedClassesInComponentBuilder;
+  }
+
+  Set<DexProgramClass> 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)) {
+        continue;
+      }
+      mergeCandidates.add(sourceClass);
+    }
+    return mergeCandidates;
+  }
+
+  // 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 (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;
+      }
+    }
+    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}.
+   */
+  boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+    assert !verticallyMergedClassesBuilder.isMergeTarget(sourceClass);
+    // 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();
+            });
+    return result.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;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java
new file mode 100644
index 0000000..a98e6c3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerResult.java
@@ -0,0 +1,87 @@
+// 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.AppView;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VerticalClassMergerResult {
+
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
+  private final List<SynthesizedBridgeCode> synthesizedBridges;
+  private final VerticallyMergedClasses verticallyMergedClasses;
+
+  public VerticalClassMergerResult(
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      List<SynthesizedBridgeCode> synthesizedBridges,
+      VerticallyMergedClasses verticallyMergedClasses) {
+    this.lensBuilder = lensBuilder;
+    this.synthesizedBridges = synthesizedBridges;
+    this.verticallyMergedClasses = verticallyMergedClasses;
+  }
+
+  public static Builder builder(AppView<AppInfoWithLiveness> appView) {
+    return new Builder(appView);
+  }
+
+  public static Builder builder(
+      VerticalClassMergerGraphLens.Builder lensBuilder,
+      List<SynthesizedBridgeCode> synthesizedBridges,
+      VerticallyMergedClasses.Builder verticallyMergedClassesBuilder) {
+    return new Builder(lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
+  }
+
+  VerticalClassMergerGraphLens.Builder getLensBuilder() {
+    return lensBuilder;
+  }
+
+  List<SynthesizedBridgeCode> getSynthesizedBridges() {
+    return synthesizedBridges;
+  }
+
+  VerticallyMergedClasses getVerticallyMergedClasses() {
+    return verticallyMergedClasses;
+  }
+
+  boolean isEmpty() {
+    return verticallyMergedClasses.isEmpty();
+  }
+
+  public static class Builder {
+
+    private final VerticalClassMergerGraphLens.Builder lensBuilder;
+    private final List<SynthesizedBridgeCode> synthesizedBridges;
+    private final VerticallyMergedClasses.Builder verticallyMergedClassesBuilder;
+
+    Builder(AppView<AppInfoWithLiveness> appView) {
+      this(
+          new VerticalClassMergerGraphLens.Builder(appView),
+          new ArrayList<>(),
+          VerticallyMergedClasses.builder());
+    }
+
+    Builder(
+        VerticalClassMergerGraphLens.Builder lensBuilder,
+        List<SynthesizedBridgeCode> synthesizedBridges,
+        VerticallyMergedClasses.Builder verticallyMergedClassesBuilder) {
+      this.lensBuilder = lensBuilder;
+      this.synthesizedBridges = synthesizedBridges;
+      this.verticallyMergedClassesBuilder = verticallyMergedClassesBuilder;
+    }
+
+    synchronized void merge(VerticalClassMergerResult.Builder other) {
+      lensBuilder.merge(other.lensBuilder);
+      synthesizedBridges.addAll(other.synthesizedBridges);
+      verticallyMergedClassesBuilder.merge(other.verticallyMergedClassesBuilder);
+    }
+
+    VerticalClassMergerResult build() {
+      VerticallyMergedClasses verticallyMergedClasses = verticallyMergedClassesBuilder.build();
+      return new VerticalClassMergerResult(
+          lensBuilder, synthesizedBridges, verticallyMergedClasses);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
index 832e38e..8358aac 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerTreeFixer.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.shaking.AnnotationFixer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
 import java.util.List;
 
 class VerticalClassMergerTreeFixer extends TreeFixerBase {
@@ -23,18 +24,16 @@
   private final List<SynthesizedBridgeCode> synthesizedBridges;
 
   VerticalClassMergerTreeFixer(
-      AppView<AppInfoWithLiveness> appView,
-      VerticalClassMergerGraphLens.Builder lensBuilder,
-      VerticallyMergedClasses mergedClasses,
-      List<SynthesizedBridgeCode> synthesizedBridges) {
+      AppView<AppInfoWithLiveness> appView, VerticalClassMergerResult verticalClassMergerResult) {
     super(appView);
     this.lensBuilder =
-        VerticalClassMergerGraphLens.Builder.createBuilderForFixup(lensBuilder, mergedClasses);
-    this.mergedClasses = mergedClasses;
-    this.synthesizedBridges = synthesizedBridges;
+        VerticalClassMergerGraphLens.Builder.createBuilderForFixup(verticalClassMergerResult);
+    this.mergedClasses = verticalClassMergerResult.getVerticallyMergedClasses();
+    this.synthesizedBridges = verticalClassMergerResult.getSynthesizedBridges();
   }
 
-  VerticalClassMergerGraphLens fixupTypeReferences() {
+  VerticalClassMergerGraphLens run(Timing timing) {
+    timing.begin("Fixup");
     // Globally substitute merged class types in protos and holders.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       clazz.getMethodCollection().replaceMethods(this::fixupMethod);
@@ -46,10 +45,11 @@
     for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
       synthesizedBridge.updateMethodSignatures(this::fixupMethodReference);
     }
-    VerticalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
+    VerticalClassMergerGraphLens lens = lensBuilder.build(mergedClasses);
     if (lens != null) {
       new AnnotationFixer(lens, appView.graphLens()).run(appView.appInfo().classes());
     }
+    timing.end();
     return lens;
   }
 
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
index d6f3e64..8fb557a 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticallyMergedClasses.java
@@ -5,14 +5,17 @@
 package com.android.tools.r8.verticalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import java.util.Collection;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -28,6 +31,10 @@
     this.mergedInterfaces = mergedInterfaces;
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   public static VerticallyMergedClasses empty() {
     EmptyBidirectionalOneToOneMap<DexType, DexType> emptyMap =
         new EmptyBidirectionalOneToOneMap<>();
@@ -43,8 +50,8 @@
     return mergedClasses;
   }
 
-  public Map<DexType, DexType> getForwardMap() {
-    return mergedClasses.getForwardMap();
+  public Set<DexType> getSources() {
+    return mergedClasses.keySet();
   }
 
   public Collection<DexType> getSourcesFor(DexType type) {
@@ -90,4 +97,41 @@
     }
     return true;
   }
+
+  public static class Builder {
+
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
+        BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
+
+    private final BidirectionalManyToOneHashMap<DexType, DexType> mergedInterfaces =
+        BidirectionalManyToOneHashMap.newIdentityHashMap();
+
+    void add(DexProgramClass source, DexProgramClass target) {
+      mergedClasses.put(source.getType(), target.getType());
+      if (source.isInterface()) {
+        mergedInterfaces.put(source.getType(), target.getType());
+      }
+    }
+
+    Set<DexType> getSourcesFor(DexProgramClass target) {
+      return mergedClasses.getKeys(target.getType());
+    }
+
+    boolean isMergeSource(DexProgramClass clazz) {
+      return mergedClasses.containsKey(clazz.getType());
+    }
+
+    boolean isMergeTarget(DexProgramClass clazz) {
+      return mergedClasses.containsValue(clazz.getType());
+    }
+
+    void merge(VerticallyMergedClasses.Builder other) {
+      mergedClasses.putAll(other.mergedClasses);
+      mergedInterfaces.putAll(other.mergedInterfaces);
+    }
+
+    VerticallyMergedClasses build() {
+      return new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
+    }
+  }
 }