Enable interface merging in second round of class merging

Bug: 173990042
Change-Id: Iaa9901fb499e795cb10eeb0e8c832fd2899649af
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index bf21997..4ef90be 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
-import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.LibraryModeledPredicate;
@@ -688,13 +687,26 @@
     }
 
     // Insert a member rebinding lens above the first unapplied lens.
-    MemberRebindingLens appliedMemberRebindingLens =
-        firstUnappliedLens.findPrevious(GraphLens::isMemberRebindingLens);
-    GraphLens newMemberRebindingLens =
-        appliedMemberRebindingLens != null
-            ? appliedMemberRebindingLens.toRewrittenFieldRebindingLens(
-                appView.dexItemFactory(), appliedLens)
-            : GraphLens.getIdentityLens();
+    // TODO(b/182129249): Once the member rebinding phase has been removed, the MemberRebindingLens
+    //  should be removed and all uses of FieldRebindingIdentityLens should be replaced by
+    //  MemberRebindingIdentityLens.
+    NonIdentityGraphLens appliedMemberRebindingLens =
+        firstUnappliedLens.findPrevious(
+            previous ->
+                previous.isMemberRebindingLens() || previous.isMemberRebindingIdentityLens());
+    GraphLens newMemberRebindingLens;
+    if (appliedMemberRebindingLens != null) {
+      newMemberRebindingLens =
+          appliedMemberRebindingLens.isMemberRebindingLens()
+              ? appliedMemberRebindingLens
+                  .asMemberRebindingLens()
+                  .toRewrittenFieldRebindingLens(appView, appliedLens)
+              : appliedMemberRebindingLens
+                  .asMemberRebindingIdentityLens()
+                  .toRewrittenMemberRebindingIdentityLens(appView, appliedLens);
+    } else {
+      newMemberRebindingLens = GraphLens.getIdentityLens();
+    }
 
     firstUnappliedLens.withAlternativeParentLens(
         newMemberRebindingLens,
diff --git a/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java b/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
index 1c7bf88..685188e 100644
--- a/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
+++ b/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
@@ -4,17 +4,19 @@
 
 package com.android.tools.r8.graph;
 
+import java.util.function.Function;
+
 public class BottomUpClassHierarchyTraversal<T extends DexClass>
     extends ClassHierarchyTraversal<T, BottomUpClassHierarchyTraversal<T>> {
 
-  private final SubtypingInfo subtypingInfo;
+  private final Function<DexType, Iterable<DexType>> immediateSubtypesProvider;
 
   private BottomUpClassHierarchyTraversal(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      SubtypingInfo subtypingInfo,
+      Function<DexType, Iterable<DexType>> immediateSubtypesProvider,
       Scope scope) {
     super(appView, scope);
-    this.subtypingInfo = subtypingInfo;
+    this.immediateSubtypesProvider = immediateSubtypesProvider;
   }
 
   /**
@@ -23,7 +25,8 @@
    */
   public static BottomUpClassHierarchyTraversal<DexClass> forAllClasses(
       AppView<? extends AppInfoWithClassHierarchy> appView, SubtypingInfo subtypingInfo) {
-    return new BottomUpClassHierarchyTraversal<>(appView, subtypingInfo, Scope.ALL_CLASSES);
+    return new BottomUpClassHierarchyTraversal<>(
+        appView, subtypingInfo::allImmediateSubtypes, Scope.ALL_CLASSES);
   }
 
   /**
@@ -32,8 +35,18 @@
    */
   public static BottomUpClassHierarchyTraversal<DexProgramClass> forProgramClasses(
       AppView<? extends AppInfoWithClassHierarchy> appView, SubtypingInfo subtypingInfo) {
+    return forProgramClasses(appView, subtypingInfo::allImmediateSubtypes);
+  }
+
+  /**
+   * Returns a visitor that can be used to visit all the program classes that are reachable from a
+   * given set of sources.
+   */
+  public static BottomUpClassHierarchyTraversal<DexProgramClass> forProgramClasses(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Function<DexType, Iterable<DexType>> immediateSubtypesProvider) {
     return new BottomUpClassHierarchyTraversal<>(
-        appView, subtypingInfo, Scope.ONLY_PROGRAM_CLASSES);
+        appView, immediateSubtypesProvider, Scope.ONLY_PROGRAM_CLASSES);
   }
 
   @Override
@@ -57,7 +70,7 @@
     worklist.addFirst(clazzWithTypeT);
 
     // Add subtypes to worklist.
-    for (DexType subtype : subtypingInfo.allImmediateSubtypes(clazz.type)) {
+    for (DexType subtype : immediateSubtypesProvider.apply(clazz.getType())) {
       DexClass definition = appView.definitionFor(subtype);
       if (definition != null) {
         if (scope != Scope.ONLY_PROGRAM_CLASSES || definition.isProgramClass()) {
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index ff2fd57..8abd2ec 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.desugar.itf.InterfaceProcessor.InterfaceProcessorNestedGraphLens;
+import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
+import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.IterableUtils;
@@ -457,6 +459,18 @@
     return false;
   }
 
+  public MemberRebindingLens asMemberRebindingLens() {
+    return null;
+  }
+
+  public boolean isMemberRebindingIdentityLens() {
+    return false;
+  }
+
+  public MemberRebindingIdentityLens asMemberRebindingIdentityLens() {
+    return null;
+  }
+
   public abstract boolean isNonIdentityLens();
 
   public NonIdentityGraphLens asNonIdentityLens() {
@@ -671,7 +685,8 @@
     }
 
     @SuppressWarnings("unchecked")
-    public final <T extends GraphLens> T findPrevious(Predicate<NonIdentityGraphLens> predicate) {
+    public final <T extends NonIdentityGraphLens> T findPrevious(
+        Predicate<NonIdentityGraphLens> predicate) {
       GraphLens current = getPrevious();
       while (current.isNonIdentityLens()) {
         NonIdentityGraphLens nonIdentityGraphLens = current.asNonIdentityLens();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index 2b65950..4b02558 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -270,15 +270,14 @@
   }
 
   private void mergeInterfaces() {
-    DexTypeList previousInterfaces = group.getTarget().getInterfaces();
-    Set<DexType> interfaces = Sets.newLinkedHashSet(previousInterfaces);
+    Set<DexType> interfaces = Sets.newLinkedHashSet();
     if (group.isInterfaceGroup()) {
       // Add all implemented interfaces from the merge group to the target class, ignoring
       // implemented interfaces that are part of the merge group.
       Set<DexType> groupTypes =
           SetUtils.newImmutableSet(
               builder -> group.forEach(clazz -> builder.accept(clazz.getType())));
-      group.forEachSource(
+      group.forEach(
           clazz -> {
             for (DexType itf : clazz.getInterfaces()) {
               if (!groupTypes.contains(itf)) {
@@ -288,7 +287,7 @@
           });
     } else {
       // Add all implemented interfaces from the merge group to the target class.
-      group.forEachSource(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
+      group.forEach(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
     }
     group.getTarget().setInterfaces(DexTypeList.create(interfaces));
   }
@@ -355,7 +354,8 @@
           target = current;
         }
       }
-      group.setTarget(appView.testing().horizontalClassMergingTarget.apply(candidates, target));
+      group.setTarget(
+          appView.testing().horizontalClassMergingTarget.apply(appView, candidates, target));
     }
 
     private ClassInitializerSynthesizedCode createClassInitializerMerger() {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index ebedb0b..c073a34 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -134,6 +134,10 @@
     return classes.isEmpty();
   }
 
+  public boolean isClassGroup() {
+    return !isInterfaceGroup();
+  }
+
   public boolean isInterfaceGroup() {
     assert !isEmpty();
     assert IterableUtils.allIdentical(getClasses(), DexClass::isInterface);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
index b091242..a2362bd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -22,11 +22,13 @@
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodCollisions.InterfaceInfo;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.Collection;
 import java.util.Collections;
@@ -34,6 +36,7 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 /**
  * This policy prevents that interface merging changes semantics of invoke-interface/invoke-virtual
@@ -153,7 +156,7 @@
     Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
         defaultMethodsInheritedBySubclassesPerClass =
             computeDefaultMethodsInheritedBySubclassesPerProgramClass(
-                classesOfInterest, inheritedDefaultMethodsPerClass, subtypingInfo);
+                classesOfInterest, inheritedDefaultMethodsPerClass, groups, subtypingInfo);
 
     // Store the computed information for each interface that is subject to merging.
     Map<DexType, InterfaceInfo> infos = new IdentityHashMap<>();
@@ -270,7 +273,16 @@
       computeDefaultMethodsInheritedBySubclassesPerProgramClass(
           Collection<DexProgramClass> classesOfInterest,
           Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerClass,
+          Collection<MergeGroup> groups,
           SubtypingInfo subtypingInfo) {
+    // Build a mapping from class types to their merge group.
+    Map<DexType, Iterable<DexProgramClass>> classGroupsByType =
+        MapUtils.newIdentityHashMap(
+            builder ->
+                Iterables.filter(groups, MergeGroup::isClassGroup)
+                    .forEach(
+                        group -> group.forEach(clazz -> builder.accept(clazz.getType(), group))));
+
     // Copy the map from classes to their inherited default methods.
     Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
         defaultMethodsInheritedBySubclassesPerClass =
@@ -279,7 +291,28 @@
                 new HashMap<>(),
                 outerValue ->
                     MapUtils.clone(outerValue, new HashMap<>(), SetUtils::newIdentityHashSet));
-    BottomUpClassHierarchyTraversal.forProgramClasses(appView, subtypingInfo)
+
+    // Propagate data upwards. If classes A and B are in a merge group, we need to push the state
+    // for A to all of B's supertypes, and the state for B to all of A's supertypes.
+    //
+    // Therefore, it is important that we don't process any of A's supertypes until B has been
+    // processed, since that would lead to inadequate upwards propagation. To achieve this, we
+    // simulate that both A and B are subtypes of A's and B's supertypes.
+    Function<DexType, Iterable<DexType>> immediateSubtypesProvider =
+        type -> {
+          Set<DexType> immediateSubtypesAfterClassMerging = Sets.newIdentityHashSet();
+          for (DexType immediateSubtype : subtypingInfo.allImmediateSubtypes(type)) {
+            Iterable<DexProgramClass> group = classGroupsByType.get(immediateSubtype);
+            if (group != null) {
+              group.forEach(member -> immediateSubtypesAfterClassMerging.add(member.getType()));
+            } else {
+              immediateSubtypesAfterClassMerging.add(immediateSubtype);
+            }
+          }
+          return immediateSubtypesAfterClassMerging;
+        };
+
+    BottomUpClassHierarchyTraversal.forProgramClasses(appView, immediateSubtypesProvider)
         .visit(
             classesOfInterest,
             clazz -> {
@@ -287,16 +320,20 @@
               Map<DexMethodSignature, Set<DexMethod>> defaultMethodsToPropagate =
                   defaultMethodsInheritedBySubclassesPerClass.getOrDefault(
                       clazz.getType(), emptyMap());
-              for (DexType supertype : clazz.allImmediateSupertypes()) {
-                Map<DexMethodSignature, Set<DexMethod>>
-                    defaultMethodsInheritedBySubclassesForSupertype =
-                        defaultMethodsInheritedBySubclassesPerClass.computeIfAbsent(
-                            supertype, ignore -> new HashMap<>());
-                defaultMethodsToPropagate.forEach(
-                    (signature, methods) ->
-                        defaultMethodsInheritedBySubclassesForSupertype
-                            .computeIfAbsent(signature, ignore -> Sets.newIdentityHashSet())
-                            .addAll(methods));
+              Iterable<DexProgramClass> group =
+                  classGroupsByType.getOrDefault(clazz.getType(), IterableUtils.singleton(clazz));
+              for (DexProgramClass member : group) {
+                for (DexType supertype : member.allImmediateSupertypes()) {
+                  Map<DexMethodSignature, Set<DexMethod>>
+                      defaultMethodsInheritedBySubclassesForSupertype =
+                          defaultMethodsInheritedBySubclassesPerClass.computeIfAbsent(
+                              supertype, ignore -> new HashMap<>());
+                  defaultMethodsToPropagate.forEach(
+                      (signature, methods) ->
+                          defaultMethodsInheritedBySubclassesForSupertype
+                              .computeIfAbsent(signature, ignore -> Sets.newIdentityHashSet())
+                              .addAll(methods));
+                }
               }
             });
     defaultMethodsInheritedBySubclassesPerClass
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
index d9e6cd1..fc457f7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
@@ -8,25 +8,26 @@
 
 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.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.IdentityHashMap;
-import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * This policy ensures that we do not create cycles in the class hierarchy as a result of interface
@@ -53,11 +54,15 @@
  *   interface J extends IK, ... {}
  * </pre>
  */
-public class OnlyDirectlyConnectedOrUnrelatedInterfaces extends MultiClassPolicy {
+public class OnlyDirectlyConnectedOrUnrelatedInterfaces
+    extends MultiClassPolicyWithPreprocessing<SubtypingInfo> {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final Mode mode;
 
+  // The interface merge groups that this policy has committed to so far.
+  private final Map<DexProgramClass, MergeGroup> committed = new IdentityHashMap<>();
+
   public OnlyDirectlyConnectedOrUnrelatedInterfaces(
       AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
     this.appView = appView;
@@ -65,116 +70,84 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<MergeGroup> apply(MergeGroup group, SubtypingInfo subtypingInfo) {
     if (!group.isInterfaceGroup()) {
       return ImmutableList.of(group);
     }
 
-    Set<DexProgramClass> classes = new LinkedHashSet<>(group.getClasses());
-    Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging =
-        computeIneligibleForMergingGraph(classes);
-    if (ineligibleForMerging.isEmpty()) {
-      return ImmutableList.of(group);
+    List<MergeGroupWithInfo> newGroupsWithInfo = new ArrayList<>();
+    for (DexProgramClass clazz : group) {
+      Set<DexProgramClass> superInterfaces = computeSuperInterfaces(clazz);
+      Set<DexProgramClass> subInterfaces = computeSubInterfaces(clazz, subtypingInfo);
+
+      MergeGroupWithInfo newGroup = null;
+      for (MergeGroupWithInfo candidateGroup : newGroupsWithInfo) {
+        // Check if adding `clazz` to `candidateGroup` would introduce a super interface that is
+        // also a sub interface. In that case we must abort since merging would lead to a cycle in
+        // the class hierarchy.
+        if (candidateGroup.isSafeToAddSubAndSuperInterfaces(
+            clazz, subInterfaces, superInterfaces)) {
+          newGroup = candidateGroup;
+          break;
+        }
+      }
+
+      if (newGroup != null) {
+        newGroup.add(clazz, superInterfaces, subInterfaces);
+      } else {
+        newGroupsWithInfo.add(new MergeGroupWithInfo(clazz, superInterfaces, subInterfaces));
+      }
     }
 
-    // Extract sub-merge groups from the graph in such a way that all pairs of interfaces in each
-    // merge group are not connected by an edge in the graph.
     List<MergeGroup> newGroups = new LinkedList<>();
-    while (!classes.isEmpty()) {
-      Iterator<DexProgramClass> iterator = classes.iterator();
-      MergeGroup newGroup = new MergeGroup(iterator.next());
-      Iterators.addAll(
-          newGroup,
-          Iterators.filter(
-              iterator,
-              candidate -> !isConnectedToGroup(candidate, newGroup, ineligibleForMerging)));
+    for (MergeGroupWithInfo newGroupWithInfo : newGroupsWithInfo) {
+      MergeGroup newGroup = newGroupWithInfo.getGroup();
       if (!newGroup.isTrivial()) {
         newGroups.add(newGroup);
+        newGroup.forEach(clazz -> committed.put(clazz, newGroup));
       }
-      classes.removeAll(newGroup.getClasses());
     }
     return newGroups;
   }
 
-  /**
-   * Computes an undirected graph, where the nodes are the interfaces from the merge group, and an
-   * edge I <-> J represents that I and J are not eligible for merging.
-   *
-   * <p>We will insert an edge I <-> J, if interface I inherits from interface J, and the path from
-   * I to J in the class hierarchy includes an interface K that is outside the merge group. Note
-   * that if I extends J directly we will not insert an edge I <-> J (unless there are multiple
-   * paths in the class hierarchy from I to J, and one of the paths goes through an interface
-   * outside the merge group).
-   */
-  private Map<DexProgramClass, Set<DexProgramClass>> computeIneligibleForMergingGraph(
-      Set<DexProgramClass> classes) {
-    Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging = new IdentityHashMap<>();
-    for (DexProgramClass clazz : classes) {
-      forEachIndirectlyReachableInterfaceInMergeGroup(
-          clazz,
-          classes,
-          other ->
-              ineligibleForMerging
-                  .computeIfAbsent(clazz, ignore -> Sets.newIdentityHashSet())
-                  .add(other));
-    }
-    return ineligibleForMerging;
+  private Set<DexProgramClass> computeSuperInterfaces(DexProgramClass clazz) {
+    return computeTransitiveSubOrSuperInterfaces(clazz, DexClass::getInterfaces);
   }
 
-  private void forEachIndirectlyReachableInterfaceInMergeGroup(
-      DexProgramClass clazz, Set<DexProgramClass> classes, Consumer<DexProgramClass> consumer) {
-    // First find the set of interfaces that can be reached via paths in the class hierarchy from
-    // the given interface, without visiting any interfaces outside the merge group.
-    WorkList<DexType> workList = WorkList.newIdentityWorkList(clazz.getInterfaces());
-    while (workList.hasNext()) {
-      DexProgramClass directlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(workList.next()));
-      if (directlyReachableInterface == null) {
-        continue;
-      }
-      // If the implemented interface is a member of the merge group, then include it's interfaces.
-      if (classes.contains(directlyReachableInterface)) {
-        workList.addIfNotSeen(directlyReachableInterface.getInterfaces());
-      }
-    }
-
-    // Initialize a new worklist with the first layer of indirectly reachable interface types.
-    Set<DexType> directlyReachableInterfaceTypes = workList.getSeenSet();
-    workList = WorkList.newIdentityWorkList();
-    for (DexType directlyReachableInterfaceType : directlyReachableInterfaceTypes) {
-      DexProgramClass directlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(directlyReachableInterfaceType));
-      if (directlyReachableInterface != null) {
-        workList.addIfNotSeen(directlyReachableInterface.getInterfaces());
-      }
-    }
-
-    // Report all interfaces from the merge group that are reachable in the class hierarchy from the
-    // worklist.
-    while (workList.hasNext()) {
-      DexProgramClass indirectlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(workList.next()));
-      if (indirectlyReachableInterface == null) {
-        continue;
-      }
-      if (classes.contains(indirectlyReachableInterface)) {
-        consumer.accept(indirectlyReachableInterface);
-      }
-      workList.addIfNotSeen(indirectlyReachableInterface.getInterfaces());
-    }
+  private Set<DexProgramClass> computeSubInterfaces(
+      DexProgramClass clazz, SubtypingInfo subtypingInfo) {
+    return computeTransitiveSubOrSuperInterfaces(
+        clazz, definition -> subtypingInfo.allImmediateExtendsSubtypes(definition.getType()));
   }
 
-  private boolean isConnectedToGroup(
+  private Set<DexProgramClass> computeTransitiveSubOrSuperInterfaces(
       DexProgramClass clazz,
-      MergeGroup group,
-      Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging) {
-    for (DexProgramClass member : group) {
-      if (ineligibleForMerging.getOrDefault(clazz, Collections.emptySet()).contains(member)
-          || ineligibleForMerging.getOrDefault(member, Collections.emptySet()).contains(clazz)) {
-        return true;
+      Function<DexProgramClass, Iterable<DexType>> immediateSubOrSuperInterfacesProvider) {
+    WorkList<DexProgramClass> workList = WorkList.newWorkList(new LinkedHashSet<>());
+    // Intentionally not marking `clazz` as seen, since we only want the strict sub/super types.
+    workList.addIgnoringSeenSet(clazz);
+    while (workList.hasNext()) {
+      DexProgramClass interfaceDefinition = workList.next();
+      MergeGroup group = committed.get(interfaceDefinition);
+      if (group != null) {
+        workList.addIfNotSeen(group);
+      }
+      for (DexType immediateSubOrSuperInterfaceType :
+          immediateSubOrSuperInterfacesProvider.apply(interfaceDefinition)) {
+        DexProgramClass immediateSubOrSuperInterface =
+            asProgramClassOrNull(appView.definitionFor(immediateSubOrSuperInterfaceType));
+        if (immediateSubOrSuperInterface != null) {
+          workList.addIfNotSeen(immediateSubOrSuperInterface);
+        }
       }
     }
-    return false;
+    assert !workList.isSeen(clazz);
+    return workList.getMutableSeenSet();
+  }
+
+  @Override
+  public void clear() {
+    committed.clear();
   }
 
   @Override
@@ -183,7 +156,80 @@
   }
 
   @Override
+  public SubtypingInfo preprocess(Collection<MergeGroup> groups) {
+    return new SubtypingInfo(appView);
+  }
+
+  @Override
   public boolean shouldSkipPolicy() {
     return !appView.options().horizontalClassMergerOptions().isInterfaceMergingEnabled(mode);
   }
+
+  static class MergeGroupWithInfo {
+
+    private final MergeGroup group;
+    private final Set<DexProgramClass> members;
+    private final Set<DexProgramClass> superInterfaces;
+    private final Set<DexProgramClass> subInterfaces;
+
+    MergeGroupWithInfo(
+        DexProgramClass clazz,
+        Set<DexProgramClass> superInterfaces,
+        Set<DexProgramClass> subInterfaces) {
+      this.group = new MergeGroup(clazz);
+      this.members = SetUtils.newIdentityHashSet(clazz);
+      this.superInterfaces = superInterfaces;
+      this.subInterfaces = subInterfaces;
+    }
+
+    void add(
+        DexProgramClass clazz,
+        Set<DexProgramClass> newSuperInterfaces,
+        Set<DexProgramClass> newSubInterfaces) {
+      group.add(clazz);
+      members.add(clazz);
+      Iterables.addAll(
+          superInterfaces,
+          Iterables.filter(
+              newSuperInterfaces, superInterface -> !members.contains(superInterface)));
+      superInterfaces.remove(clazz);
+      Iterables.addAll(
+          subInterfaces,
+          Iterables.filter(newSubInterfaces, subInterface -> !members.contains(subInterface)));
+      subInterfaces.remove(clazz);
+    }
+
+    MergeGroup getGroup() {
+      return group;
+    }
+
+    boolean isSafeToAddSubAndSuperInterfaces(
+        DexProgramClass clazz,
+        Set<DexProgramClass> newSubInterfaces,
+        Set<DexProgramClass> newSuperInterfaces) {
+      // Check that adding the new sub and super interfaces to the group is safe.
+      for (DexProgramClass newSubInterface : newSubInterfaces) {
+        if (!group.contains(newSubInterface) && superInterfaces.contains(newSubInterface)) {
+          return false;
+        }
+      }
+      for (DexProgramClass newSuperInterface : newSuperInterfaces) {
+        if (!group.contains(newSuperInterface) && subInterfaces.contains(newSuperInterface)) {
+          return false;
+        }
+      }
+      // Check that adding the sub and super interfaces of the group to the current class is safe.
+      for (DexProgramClass subInterface : subInterfaces) {
+        if (subInterface != clazz && newSuperInterfaces.contains(subInterface)) {
+          return false;
+        }
+      }
+      for (DexProgramClass superInterface : superInterfaces) {
+        if (superInterface != clazz && newSubInterfaces.contains(superInterface)) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 3f77dbb..0fdc499 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -41,7 +41,12 @@
   }
 
   public static Builder builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return new Builder(appView);
+    return builder(appView, appView.graphLens());
+  }
+
+  public static Builder builder(
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens previousLens) {
+    return new Builder(appView, previousLens);
   }
 
   @Override
@@ -129,16 +134,55 @@
     return getPrevious().isContextFreeForMethods();
   }
 
+  @Override
+  public boolean isMemberRebindingIdentityLens() {
+    return true;
+  }
+
+  @Override
+  public MemberRebindingIdentityLens asMemberRebindingIdentityLens() {
+    return this;
+  }
+
+  public MemberRebindingIdentityLens toRewrittenMemberRebindingIdentityLens(
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    Builder builder = builder(appView, getIdentityLens());
+    nonReboundFieldReferenceToDefinitionMap.forEach(
+        (nonReboundFieldReference, reboundFieldReference) -> {
+          DexField rewrittenReboundFieldReference = lens.lookupField(reboundFieldReference);
+          DexField rewrittenNonReboundFieldReference =
+              rewrittenReboundFieldReference.withHolder(
+                  lens.lookupType(nonReboundFieldReference.getHolderType()), dexItemFactory);
+          builder.recordNonReboundFieldAccess(
+              rewrittenNonReboundFieldReference, rewrittenReboundFieldReference);
+        });
+    nonReboundMethodReferenceToDefinitionMap.forEach(
+        (nonReboundMethodReference, reboundMethodReference) -> {
+          DexMethod rewrittenReboundMethodReference =
+              lens.getRenamedMethodSignature(reboundMethodReference);
+          DexMethod rewrittenNonReboundMethodReference =
+              rewrittenReboundMethodReference.withHolder(
+                  lens.lookupType(nonReboundMethodReference.getHolderType()), dexItemFactory);
+          builder.recordNonReboundMethodAccess(
+              rewrittenNonReboundMethodReference, rewrittenReboundMethodReference);
+        });
+    return builder.build();
+  }
+
   public static class Builder {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final GraphLens previousLens;
+
     private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap =
         new IdentityHashMap<>();
     private final Map<DexMethod, DexMethod> nonReboundMethodReferenceToDefinitionMap =
         new IdentityHashMap<>();
 
-    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens previousLens) {
       this.appView = appView;
+      this.previousLens = previousLens;
     }
 
     void recordNonReboundFieldAccesses(FieldAccessInfo fieldAccessInfo) {
@@ -152,6 +196,12 @@
       nonReboundFieldReferenceToDefinitionMap.put(nonReboundFieldReference, reboundFieldReference);
     }
 
+    private void recordNonReboundMethodAccess(
+        DexMethod nonReboundMethodReference, DexMethod reboundMethodReference) {
+      nonReboundMethodReferenceToDefinitionMap.put(
+          nonReboundMethodReference, reboundMethodReference);
+    }
+
     void recordMethodAccess(DexMethod reference) {
       if (reference.getHolderType().isArrayType()) {
         return;
@@ -161,7 +211,7 @@
         SingleResolutionResult resolutionResult =
             appView.appInfo().resolveMethodOn(holder, reference).asSingleResolution();
         if (resolutionResult != null && resolutionResult.getResolvedHolder() != holder) {
-          nonReboundMethodReferenceToDefinitionMap.put(
+          recordNonReboundMethodAccess(
               reference, resolutionResult.getResolvedMethod().getReference());
         }
       }
@@ -175,7 +225,7 @@
           nonReboundFieldReferenceToDefinitionMap,
           nonReboundMethodReferenceToDefinitionMap,
           appView.dexItemFactory(),
-          appView.graphLens());
+          previousLens);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 4310236..d202af8 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.graph.NestedGraphLens.mapVirtualInterfaceInvocationTypes;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -47,6 +48,11 @@
   }
 
   @Override
+  public MemberRebindingLens asMemberRebindingLens() {
+    return this;
+  }
+
+  @Override
   public DexType getOriginalType(DexType type) {
     return getPrevious().getOriginalType(type);
   }
@@ -131,7 +137,8 @@
   }
 
   public FieldRebindingIdentityLens toRewrittenFieldRebindingLens(
-      DexItemFactory dexItemFactory, GraphLens lens) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
     FieldRebindingIdentityLens.Builder builder = FieldRebindingIdentityLens.builder();
     nonReboundFieldReferenceToDefinitionMap.forEach(
         (nonReboundFieldReference, reboundFieldReference) -> {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 12126a4..73badd3 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -85,7 +85,6 @@
 import java.util.TreeSet;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -1215,7 +1214,7 @@
     private boolean enable =
         !Version.isDevelopmentVersion()
             || System.getProperty("com.android.tools.r8.disableHorizontalClassMerging") == null;
-    private boolean enableInterfaceMerging = false;
+    private boolean enableInterfaceMergingInInitial = false;
     private boolean enableSyntheticMerging = true;
     private boolean ignoreRuntimeTypeChecksForTesting = false;
     private boolean restrictToSynthetics = false;
@@ -1238,10 +1237,6 @@
       this.enable = enable;
     }
 
-    public void enableInterfaceMerging() {
-      enableInterfaceMerging = true;
-    }
-
     public int getMaxGroupSize() {
       return maxGroupSize;
     }
@@ -1265,23 +1260,26 @@
       return ignoreRuntimeTypeChecksForTesting;
     }
 
-    public boolean isInterfaceMergingEnabled() {
-      assert !isInterfaceMergingEnabled(HorizontalClassMerger.Mode.INITIAL);
-      return isInterfaceMergingEnabled(HorizontalClassMerger.Mode.FINAL);
-    }
-
     public boolean isSyntheticMergingEnabled() {
       return enableSyntheticMerging;
     }
 
     public boolean isInterfaceMergingEnabled(HorizontalClassMerger.Mode mode) {
-      return enableInterfaceMerging && mode.isFinal();
+      if (mode.isInitial()) {
+        return enableInterfaceMergingInInitial;
+      }
+      assert mode.isFinal();
+      return true;
     }
 
     public boolean isRestrictedToSynthetics() {
       return restrictToSynthetics || !isOptimizing() || !isShrinking();
     }
 
+    public void setEnableInterfaceMergingInInitial() {
+      enableInterfaceMergingInInitial = true;
+    }
+
     public void setIgnoreRuntimeTypeChecksForTesting() {
       ignoreRuntimeTypeChecksForTesting = true;
     }
@@ -1351,8 +1349,8 @@
 
     public BiConsumer<DexItemFactory, HorizontallyMergedClasses> horizontallyMergedClassesConsumer =
         ConsumerUtils.emptyBiConsumer();
-    public BiFunction<Iterable<DexProgramClass>, DexProgramClass, DexProgramClass>
-        horizontalClassMergingTarget = (candidates, target) -> target;
+    public TriFunction<AppView<?>, Iterable<DexProgramClass>, DexProgramClass, DexProgramClass>
+        horizontalClassMergingTarget = (appView, candidates, target) -> target;
 
     public BiConsumer<DexItemFactory, EnumDataMap> unboxedEnumsConsumer =
         ConsumerUtils.emptyBiConsumer();
diff --git a/src/main/java/com/android/tools/r8/utils/MapUtils.java b/src/main/java/com/android/tools/r8/utils/MapUtils.java
index 63e881e..fc39146 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.utils.StringUtils.BraceType;
+import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -50,6 +51,12 @@
     return result;
   }
 
+  public static <K, V> IdentityHashMap<K, V> newIdentityHashMap(BiForEachable<K, V> forEachable) {
+    IdentityHashMap<K, V> map = new IdentityHashMap<>();
+    forEachable.forEach(map::put);
+    return map;
+  }
+
   public static <T> void removeIdentityMappings(Map<T, T> map) {
     map.entrySet().removeIf(entry -> entry.getKey() == entry.getValue());
   }
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index 4279cb7..67e4148 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -48,6 +48,10 @@
     return workList;
   }
 
+  public static <T> WorkList<T> newWorkList(Set<T> seen) {
+    return new WorkList<>(seen);
+  }
+
   private WorkList(EqualityTest equalityTest) {
     this(equalityTest == EqualityTest.HASH ? new HashSet<>() : Sets.newIdentityHashSet());
   }
@@ -56,6 +60,10 @@
     this.seen = seen;
   }
 
+  public void addIgnoringSeenSet(T item) {
+    workingList.addLast(item);
+  }
+
   public void addAllIgnoringSeenSet(Iterable<T> items) {
     items.forEach(workingList::addLast);
   }
@@ -86,6 +94,10 @@
     return !hasNext();
   }
 
+  public boolean isSeen(T item) {
+    return seen.contains(item);
+  }
+
   public void markAsSeen(T item) {
     seen.add(item);
   }
@@ -103,6 +115,10 @@
     return Collections.unmodifiableSet(seen);
   }
 
+  public Set<T> getMutableSeenSet() {
+    return seen;
+  }
+
   public enum EqualityTest {
     HASH,
     IDENTITY
diff --git a/src/test/examples/classmerging/NoHorizontalClassMerging.java b/src/test/examples/classmerging/NoHorizontalClassMerging.java
new file mode 100644
index 0000000..3b7a831
--- /dev/null
+++ b/src/test/examples/classmerging/NoHorizontalClassMerging.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2021, 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+public @interface NoHorizontalClassMerging {}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
index 6fde90e..804b4ed 100644
--- a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -32,12 +32,14 @@
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
+  @NoHorizontalClassMerging
   public interface SimpleInterface {
 
     void foo();
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
+  @NoHorizontalClassMerging
   public interface OtherSimpleInterface {
 
     void bar();
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 64ce874..46fcb91 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -72,3 +72,4 @@
 -neverinline class * {
   @classmerging.NeverInline <methods>;
 }
+-nohorizontalclassmerging @classmerging.NoHorizontalClassMerging class *
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
index 8a80d6e..6fad862 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
@@ -41,7 +41,7 @@
         .addOptionsModification(
             options ->
                 options.testing.horizontalClassMergingTarget =
-                    (candidates, target) -> candidates.iterator().next())
+                    (appView, candidates, target) -> candidates.iterator().next())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("a", "b", "c", "d")
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
index bf50ab1..0844722 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
@@ -111,7 +111,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
@@ -164,7 +164,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
@@ -217,7 +217,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
index 3787201..955ff04 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -12,7 +13,6 @@
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.classmerging.horizontal.HorizontalClassMergingTestBase;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class OverrideAbstractMethodWithDefaultTest extends HorizontalClassMergingTestBase {
@@ -31,13 +31,14 @@
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(I.class, J.class).assertNoOtherClassesMerged())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("J", "B2")
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(codeInspector.clazz(J.class), isAbsent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B1.class), isPresent());
               assertThat(codeInspector.clazz(B2.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
index 580598b..0af1e85 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -36,9 +37,11 @@
               } else {
                 inspector
                     .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertIsCompleteMergeGroup(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
-                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
+                    .assertNoOtherClassesMerged();
               }
             })
         .run(parameters.getRuntime(), Main.class)
@@ -46,7 +49,9 @@
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(J.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isPresent());
               assertThat(codeInspector.clazz(C.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
index 65ca772..7b5437e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -42,9 +43,11 @@
               } else {
                 inspector
                     .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertIsCompleteMergeGroup(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
-                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
+                    .assertNoOtherClassesMerged();
               }
             })
         .run(parameters.getRuntime(), Main.class)
@@ -52,7 +55,9 @@
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(J.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(Parent.class), isPresent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
index 650d804..4d81f1a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
@@ -44,11 +44,6 @@
         // hierarchy.
         .addHorizontallyMergedClassesInspector(
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoHorizontalClassMergingAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
index 1c5d915..cc19dd2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -16,8 +15,9 @@
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -25,19 +25,21 @@
 @RunWith(Parameterized.class)
 public class CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest extends TestBase {
 
+  private final boolean enableInterfaceMergingInInitial;
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameterized.Parameters(name = "{1}, enableInterfaceMergingInInitial: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest(
-      TestParameters parameters) {
+      boolean enableInterfaceMergingInInitial, TestParameters parameters) {
+    this.enableInterfaceMergingInInitial = enableInterfaceMergingInInitial;
     this.parameters = parameters;
   }
 
-  // TODO(b/173990042): Disallow merging of A and B in the first round of class merging.
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
@@ -48,23 +50,22 @@
         // the default method J.m() to A.
         .addHorizontallyMergedClassesInspector(
             inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(A.class, B.class)
+                  .assertMergedInto(B.class, A.class);
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-                inspector
-                    .assertIsCompleteMergeGroup(A.class, B.class)
-                    .assertMergedInto(B.class, A.class)
-                    .assertClassesNotMerged(I.class, J.class, K.class);
+                inspector.assertClassesNotMerged(I.class, J.class, K.class);
               } else {
                 inspector
-                    .assertIsCompleteMergeGroup(A.class, B.class)
-                    .assertMergedInto(B.class, A.class)
                     .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertClassesNotMerged(K.class);
               }
             })
         .addOptionsModification(
             options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
+              if (enableInterfaceMergingInInitial) {
+                options.horizontalClassMergerOptions().setEnableInterfaceMergingInInitial();
+              }
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
@@ -80,10 +81,10 @@
               assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
               assertThat(aClassSubject, isImplementing(inspector.clazz(K.class)));
 
-              ClassSubject bClassSubject = inspector.clazz(C.class);
-              assertThat(bClassSubject, isPresent());
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
               assertThat(
-                  bClassSubject,
+                  cClassSubject,
                   isImplementing(
                       inspector.clazz(
                           parameters.canUseDefaultAndStaticInterfaceMethods()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
index d4f14bd..dca511f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
@@ -51,11 +51,6 @@
                 inspector.assertIsCompleteMergeGroup(I.class, J.class);
               }
             })
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
index f9e85ba..62cf256 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
@@ -52,12 +52,6 @@
                 inspector.assertNoClassesMerged();
               }
             })
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-              options.horizontalClassMergerOptions().setIgnoreRuntimeTypeChecksForTesting();
-            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
index 7ca9cc5..e8bd34f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
@@ -36,11 +36,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
index dad75a4..a88f6c1 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
@@ -36,11 +36,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
index e58ca16..dae6f36 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
@@ -38,11 +38,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
index a877121..e002263 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
@@ -38,11 +38,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
index 0807547..ef6ac67 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
@@ -42,11 +42,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
index 5f30eff..f33ec1c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
@@ -42,11 +42,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java
new file mode 100644
index 0000000..80160a4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java
@@ -0,0 +1,79 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyInterfaceChainMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EmptyInterfaceChainMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .assertIsCompleteMergeGroup(I.class, J.class, K.class)
+                    .assertNoOtherClassesMerged())
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccess();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(A.class);
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J extends I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface K extends J {}
+
+  static class A implements K {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
index 24e5de8..7cbdad2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
@@ -40,11 +40,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
index 2d264db..f13b7e0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
@@ -36,11 +36,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
index 23ed1ea..d529fc9 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
@@ -36,11 +36,6 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
index 6ad6ef1..e8630bb 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
@@ -50,11 +50,6 @@
                 inspector.assertIsCompleteMergeGroup(I.class, J.class);
               }
             })
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
index 1c33206..6905202 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
@@ -40,17 +40,17 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
-        // TODO(b/173990042): Extend horizontal class merging to interfaces.
         .addHorizontallyMergedClassesInspector(
             inspector -> {
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-                inspector.assertNoClassesMerged();
+                inspector.assertIsCompleteMergeGroup(I.class, J.class).assertNoOtherClassesMerged();
               } else {
                 inspector
+                    .assertClassesNotMerged(I.class, J.class)
                     .assertClassReferencesMerged(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
                         SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
-                    .assertClassesNotMerged(I.class, J.class);
+                    .assertNoOtherClassesMerged();
               }
             })
         .enableInliningAnnotations()
@@ -62,11 +62,9 @@
               // We do not allow horizontal class merging of interfaces and classes. Therefore, A
               // should remain in the output.
               assertThat(inspector.clazz(A.class), isPresent());
-
-              // TODO(b/173990042): I and J should be merged.
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
                 assertThat(inspector.clazz(I.class), isPresent());
-                assertThat(inspector.clazz(J.class), isPresent());
+                assertThat(inspector.clazz(J.class), isAbsent());
               } else {
                 assertThat(inspector.clazz(syntheticCompanionClass(I.class)), isPresent());
                 assertThat(inspector.clazz(syntheticCompanionClass(J.class)), isAbsent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 5468cae..2f157d0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -1052,7 +1052,8 @@
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class"),
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$1.class"),
-          CF_DIR.resolve("NeverInline.class")
+          CF_DIR.resolve("NeverInline.class"),
+          CF_DIR.resolve("NoHorizontalClassMerging.class")
         };
     // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl
     // is in a different package and is not public.
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
index 1bfbaac..71ddab8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
@@ -63,7 +63,7 @@
                 .addOptionsModification(
                     options ->
                         options.testing.horizontalClassMergingTarget =
-                            (candidates, target) -> candidates.iterator().next())
+                            (appView, candidates, target) -> candidates.iterator().next())
                 .addHorizontallyMergedClassesInspector(
                     inspector ->
                         inspector.assertMergedInto(BaseWithStatic.class, AFeatureWithStatic.class))
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
index 9d11655..12bc414 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
@@ -85,6 +85,7 @@
     void foo();
   }
 
+  @NoHorizontalClassMerging
   @NoVerticalClassMerging
   interface J {
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
index 768fbd6..6bc0862 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -42,6 +43,7 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -86,6 +88,7 @@
     void m();
   }
 
+  @NoHorizontalClassMerging
   @NoVerticalClassMerging
   interface J extends I {
 
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index 550356e..de5d7f0 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -12,21 +12,26 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
@@ -204,6 +209,7 @@
             .addKeepRules("-keepclassmembers class * { @" + PRESENT_ANNOTATION + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
             .compile()
             .graphInspector();
 
@@ -222,6 +228,7 @@
                     + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
             .compile()
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector);
@@ -241,6 +248,7 @@
                     + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
             .compile()
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifThenKeepClassesWithMembersInspector);
@@ -262,11 +270,27 @@
                     + " *** <2>(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
             .compile()
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector);
   }
 
+  private void configureHorizontalClassMerging(R8FullTestBuilder testBuilder) {
+    // Attempt to ensure similar class merging across different builds by choosing the merge target
+    // as the class with the lexicographically smallest original name.
+    testBuilder.addOptionsModification(
+        options ->
+            options.testing.horizontalClassMergingTarget =
+                (appView, candidates, target) -> {
+                  List<DexProgramClass> classes = Lists.newArrayList(candidates);
+                  classes.sort(
+                      Comparator.comparing(
+                          clazz -> appView.graphLens().getOriginalType(clazz.getType())));
+                  return ListUtils.first(classes);
+                });
+  }
+
   private void assertRetainedClassesEqual(
       GraphInspector referenceResult, GraphInspector conditionalResult) {
     assertRetainedClassesEqual(referenceResult, conditionalResult, false, false);
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index 59c649b..7f6323a 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -11,10 +11,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -33,16 +34,18 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+@NoHorizontalClassMerging
 interface B112452064SuperInterface1 {
   void foo();
 }
 
+@NoHorizontalClassMerging
 interface B112452064SuperInterface2 {
   void bar();
 }
 
-interface B112452064SubInterface extends B112452064SuperInterface1, B112452064SuperInterface2 {
-}
+@NoHorizontalClassMerging
+interface B112452064SubInterface extends B112452064SuperInterface1, B112452064SuperInterface2 {}
 
 class B112452064TestMain {
 
@@ -82,7 +85,8 @@
 
   @Parameters(name = "{1}, argument removal: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public ParameterTypeTest(boolean enableArgumentRemoval, TestParameters parameters) {
@@ -128,7 +132,8 @@
                   options.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
                   options.enableVerticalClassMerging = enableVerticalClassMerging;
                 })
-            .setMinApi(parameters.getRuntime())
+            .enableNoHorizontalClassMergingAnnotations()
+            .setMinApi(parameters.getApiLevel())
             .compile()
             .run(parameters.getRuntime(), B112452064TestMain.class)
             .assertSuccessWithOutput(javaResult.stdout)
@@ -286,7 +291,6 @@
         "return");
 
     final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClassName, false, false);
 
     // Run input program on java.
     Path outputDirectory = temp.newFolder().toPath();
@@ -296,36 +300,34 @@
     assertThat(javaResult.stdout, containsString(bar.name));
     assertEquals(-1, javaResult.stderr.indexOf("ClassNotFoundException"));
 
-    AndroidApp processedApp =
-        compileWithR8(
-            jasminBuilder.build(),
-            proguardConfig,
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClassName)
+        .addOptionsModification(
             options -> {
               // Disable inlining to avoid the (short) tested method from being inlined and removed.
               options.enableInlining = false;
               options.enableArgumentRemoval = enableArgumentRemoval;
-            });
-
-    // Run processed (output) program on ART
-    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    if (enableArgumentRemoval) {
-      assertEquals(0, artResult.exitCode);
-    } else {
-      assertNotEquals(0, artResult.exitCode);
-
-      DexVm.Version currentVersion = ToolHelper.getDexVm().getVersion();
-      String errorMessage =
-          currentVersion.isNewerThan(Version.V4_4_4)
-              ? "type Precise Reference: Foo[] but expected Reference: SubInterface[]"
-              : "[LFoo; is not instance of [LSubInterface;";
-      assertThat(artResult.stderr, containsString(errorMessage));
-    }
-
-    assertEquals(-1, artResult.stderr.indexOf("ClassNotFoundException"));
-
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject subSubject = inspector.clazz(sub.name);
-    assertNotEquals(enableArgumentRemoval, subSubject.isPresent());
+            })
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject subSubject = inspector.clazz(sub.name);
+              assertNotEquals(enableArgumentRemoval, subSubject.isPresent());
+            })
+        .run(parameters.getRuntime(), mainClassName)
+        .applyIf(
+            enableArgumentRemoval || parameters.isCfRuntime(),
+            SingleTestRunResult::assertSuccess,
+            result ->
+                result.assertFailureWithErrorThatMatches(
+                    containsString(
+                        parameters.getDexRuntimeVersion().isNewerThan(Version.V4_4_4)
+                            ? "type Precise Reference: Foo[] but expected Reference: SubInterface[]"
+                            : "[LFoo; is not instance of [LSubInterface;")))
+        .assertStderrMatches(not(containsString("ClassNotFoundException")));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
index c6e5eff..b427736 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
@@ -12,6 +12,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
@@ -43,6 +44,7 @@
         .addKeepMainRule(Main.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -79,6 +81,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   @NoUnusedInterfaceRemoval
   @NoVerticalClassMerging
   interface I {
@@ -94,6 +97,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   @NoUnusedInterfaceRemoval
   @NoVerticalClassMerging
   interface J extends I {}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index 0410bc0..b39fd97 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -32,6 +33,8 @@
   private final DexItemFactory dexItemFactory;
   private final HorizontallyMergedClasses horizontallyMergedClasses;
 
+  private final Set<ClassReference> seen = new HashSet<>();
+
   public HorizontallyMergedClassesInspector(
       DexItemFactory dexItemFactory, HorizontallyMergedClasses horizontallyMergedClasses) {
     this.dexItemFactory = dexItemFactory;
@@ -102,6 +105,7 @@
             + StringUtils.join(", ", unmerged, DexType::getTypeName),
         0,
         unmerged.size());
+    seen.addAll(types.stream().map(DexType::asClassReference).collect(Collectors.toList()));
     return this;
   }
 
@@ -117,6 +121,17 @@
     return this;
   }
 
+  public HorizontallyMergedClassesInspector assertNoOtherClassesMerged() {
+    horizontallyMergedClasses.forEachMergeGroup(
+        (sources, target) -> {
+          for (DexType source : sources) {
+            assertTrue(source.getTypeName(), seen.contains(source.asClassReference()));
+          }
+          assertTrue(target.getTypeName(), seen.contains(target.asClassReference()));
+        });
+    return this;
+  }
+
   public HorizontallyMergedClassesInspector assertClassesNotMerged(Class<?>... classes) {
     return assertClassesNotMerged(Arrays.asList(classes));
   }
@@ -145,6 +160,7 @@
       assertTrue(type.isClassType());
       assertFalse(horizontallyMergedClasses.hasBeenMergedOrIsMergeTarget(type));
     }
+    seen.addAll(types.stream().map(DexType::asClassReference).collect(Collectors.toList()));
     return this;
   }
 
@@ -204,6 +220,7 @@
         classReferences.size() - 1,
         sources.size());
     assertTrue(types.containsAll(sources));
+    seen.addAll(classReferences);
     return this;
   }