Account for default interface methods in subclasses in bridge hoisting

When hoisting a virtual bridge m() from a class B to its superclass A, the virtual method is implicitly added to all subclasses of A. If there exists a subclass C of A, which implements an interface I that declares a default interface method m(), then bridge hoisting would change semantics since virtual class methods take precedence over default interface methods in resolution.

Bug: b/369040938
Change-Id: I5b425f2cde9c48e0a298af02f67eb83951473006
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index e8bc0b7..c9a5078 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -569,6 +569,10 @@
     return methodCollection.getMethod(method);
   }
 
+  public DexEncodedMethod lookupMethod(DexMethodSignature method) {
+    return lookupMethod(method.getProto(), method.getName());
+  }
+
   public DexEncodedMethod lookupMethod(DexProto methodProto, DexString methodName) {
     return methodCollection.getMethod(methodProto, methodName);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
index 617a1ed..a7203b9 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.bridgehoisting;
 
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
 import com.android.tools.r8.graph.DexClass;
@@ -11,6 +10,7 @@
 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.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -19,8 +19,11 @@
 import com.android.tools.r8.ir.optimize.info.bridge.VirtualBridgeInfo;
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.DepthFirstSearchWorkList;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
@@ -29,12 +32,14 @@
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
 
 /**
  * An optimization pass that hoists bridges upwards with the purpose of sharing redundant bridge
@@ -66,6 +71,16 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
+  // Cache that for a given non-interface class stores the default interface methods present on the
+  // class and all of its subclasses.
+  private final Map<DexProgramClass, DexMethodSignatureSet>
+      defaultInterfaceMethodsInClassAndSubclassesCache = new IdentityHashMap<>();
+
+  // Cache that for a given interface stores the default methods on that interface and all of its
+  // transitive superinterfaces.
+  private final Map<DexClass, DexMethodSignatureSet> inheritedDefaultInterfaceMethodsCache =
+      new IdentityHashMap<>();
+
   // Structure that keeps track of the changes for construction of the Proguard map and
   // AppInfoWithLiveness maintenance.
   private final BridgeHoistingResult result;
@@ -103,7 +118,7 @@
       }
     }
     for (ProgramMethod candidate : getCandidatesForHoisting(eligibleSubclasses)) {
-      hoistBridgeIfPossible(candidate, clazz, eligibleSubclasses);
+      hoistBridgeIfPossible(candidate, clazz, eligibleSubclasses, immediateSubtypingInfo);
     }
   }
 
@@ -125,7 +140,10 @@
   }
 
   private void hoistBridgeIfPossible(
-      ProgramMethod method, DexProgramClass clazz, List<DexProgramClass> subclasses) {
+      ProgramMethod method,
+      DexProgramClass clazz,
+      List<DexProgramClass> subclasses,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
     // If the method is defined on the parent class, we cannot hoist the bridge.
     // TODO(b/153147967): If the declared method is abstract, we could replace it by the bridge.
     //  Add a test.
@@ -160,23 +178,38 @@
                 .appInfo()
                 .resolveMethodOnClassLegacy(subclass, methodReference)
                 .getSingleTarget();
-        if (resolutionTarget == null || resolutionTarget.isAbstract()) {
-          // The fact that this class does not declare the bridge (or the bridge is abstract) should
-          // not prevent us from hoisting the bridge.
-          //
-          // Strictly speaking, there could be an invoke instruction that targets the bridge on this
-          // subclass and fails with an AbstractMethodError or a NoSuchMethodError in the input
-          // program. After hoisting the bridge to the superclass such an instruction would no
-          // longer fail with an error in the generated program.
-          //
-          // If this ever turns out be an issue, it would be possible to track if there is an invoke
-          // instruction targeting the bridge on this subclass that fails in the Enqueuer, but this
-          // should never be the case in practice.
-          continue;
+        if (resolutionTarget != null && !resolutionTarget.isAbstract()) {
+          // Hoisting would change the program behavior.
+          return;
         }
 
-        // Hoisting would change the program behavior.
-        return;
+        if (appView.options().canUseDefaultAndStaticInterfaceMethods()) {
+          DexMethodSignatureSet defaultInterfaceMethodsBelowSubclass =
+              getOrComputeDefaultInterfaceMethodsOnClassAndSubclasses(
+                  subclass, immediateSubtypingInfo);
+          if (defaultInterfaceMethodsBelowSubclass.contains(method)) {
+            // Hoisting would change the program behavior. Virtual methods on classes takes
+            // precedence over default interface methods. By hoisting the current bridge method to
+            // the superclass, we may change virtual calls that would previously have dispatched to
+            // a default interface method into calling the hoisted bridge method.
+            //
+            // See also b/369040938.
+            return;
+          }
+        }
+
+        // The fact that this class does not declare the bridge (or the bridge is abstract) should
+        // not prevent us from hoisting the bridge.
+        //
+        // Strictly speaking, there could be an invoke instruction that targets the bridge on this
+        // subclass and fails with an AbstractMethodError or a NoSuchMethodError in the input
+        // program. After hoisting the bridge to the superclass such an instruction would no
+        // longer fail with an error in the generated program.
+        //
+        // If this ever turns out be an issue, it would be possible to track if there is an invoke
+        // instruction targeting the bridge on this subclass that fails in the Enqueuer, but this
+        // should never be the case in practice.
+        continue;
       }
 
       BridgeInfo currentBridgeInfo = definition.getOptimizationInfo().getBridgeInfo();
@@ -320,4 +353,104 @@
           return item;
         });
   }
+
+  private DexMethodSignatureSet getOrComputeDefaultInterfaceMethodsOnClassAndSubclasses(
+      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    if (!defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(clazz)) {
+      new DefaultInterfaceMethodsTraversal(immediateSubtypingInfo).run(List.of(clazz));
+    }
+    DexMethodSignatureSet cachedResult =
+        defaultInterfaceMethodsInClassAndSubclassesCache.get(clazz);
+    assert cachedResult != null;
+    return cachedResult;
+  }
+
+  // A depth-first traversal over the class hierarchy that will collect and cache the default
+  // interface method present on a given class and its subclasses.
+  private class DefaultInterfaceMethodsTraversal
+      extends DepthFirstSearchWorkList<DexProgramClass, Void, Void> {
+
+    private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+
+    DefaultInterfaceMethodsTraversal(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+      this.immediateSubtypingInfo = immediateSubtypingInfo;
+    }
+
+    @Override
+    protected TraversalContinuation<Void, Void> process(
+        DFSNode<DexProgramClass> node,
+        Function<DexProgramClass, DFSNode<DexProgramClass>> childNodeConsumer) {
+      // Enqueue unseen subclasses for processing.
+      DexProgramClass currentClass = node.getNode();
+      assert !currentClass.isInterface();
+      assert !defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(currentClass);
+      for (DexProgramClass subclass : immediateSubtypingInfo.getSubclasses(currentClass)) {
+        if (!defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(subclass)) {
+          DFSNode<DexProgramClass> unusedChildNode = childNodeConsumer.apply(subclass);
+        }
+      }
+      return TraversalContinuation.doContinue();
+    }
+
+    @Override
+    public TraversalContinuation<Void, Void> joiner(DFSNode<DexProgramClass> node) {
+      // All subclasses of the current class have now been processed.
+      DexProgramClass currentClass = node.getNode();
+      assert !currentClass.isInterface();
+      assert !defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(currentClass);
+      // Compute the default interface methods that this class inherits from its implemented
+      // interfaces.
+      DexMethodSignatureSet defaultInterfaceMethods = DexMethodSignatureSet.create();
+      for (DexType implementedType : currentClass.getInterfaces()) {
+        DexClass implementedInterface = appView.definitionFor(implementedType);
+        if (implementedInterface != null) {
+          defaultInterfaceMethods.addAll(
+              getOrComputeInheritedDefaultInterfaceMethodsForInterface(implementedInterface));
+        }
+      }
+      // Include the default interface methods that are present on the class' subclasses.
+      for (DexProgramClass subclass : immediateSubtypingInfo.getSubclasses(currentClass)) {
+        DexMethodSignatureSet defaultInterfaceMethodsInSubclass =
+            defaultInterfaceMethodsInClassAndSubclassesCache.get(subclass);
+        assert defaultInterfaceMethodsInSubclass != null;
+        defaultInterfaceMethods.addAll(defaultInterfaceMethodsInSubclass);
+      }
+      // Remove all virtual methods declared by the class itself, since we are only interested in
+      // the occurrence of default interface method that are not overridden by a non-default
+      // interface method.
+      defaultInterfaceMethods.removeIf(m -> currentClass.lookupMethod(m) != null);
+      // Cache the computed result.
+      defaultInterfaceMethodsInClassAndSubclassesCache.put(currentClass, defaultInterfaceMethods);
+      return TraversalContinuation.doContinue();
+    }
+
+    // For a given interface, returns the default interface methods present on that interface and
+    // all transitive superinterfaces.
+    private DexMethodSignatureSet getOrComputeInheritedDefaultInterfaceMethodsForInterface(
+        DexClass itf) {
+      assert itf.isInterface();
+      // Check if we have already computed the default interface methods in the current interface
+      // and its superinterfaces.
+      DexMethodSignatureSet cachedResult = inheritedDefaultInterfaceMethodsCache.get(itf);
+      if (cachedResult != null) {
+        return cachedResult;
+      }
+      DexMethodSignatureSet inheritedDefaultInterfaceMethods = DexMethodSignatureSet.create();
+      // First add the default interface methods that are present on the interface directly.
+      for (DexEncodedMethod defaultInterfaceMethod :
+          itf.virtualMethods(DexEncodedMethod::hasCode)) {
+        inheritedDefaultInterfaceMethods.add(defaultInterfaceMethod);
+      }
+      // Then add the default interface methods that are present on the superinterfaces.
+      for (DexType implementedType : itf.getInterfaces()) {
+        DexClass implementedInterface = appView.definitionFor(implementedType);
+        if (implementedInterface != null) {
+          inheritedDefaultInterfaceMethods.addAll(
+              getOrComputeInheritedDefaultInterfaceMethodsForInterface(implementedInterface));
+        }
+      }
+      inheritedDefaultInterfaceMethodsCache.put(itf, inheritedDefaultInterfaceMethods);
+      return inheritedDefaultInterfaceMethods;
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java
index e44992d..7861ac8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java
@@ -50,11 +50,7 @@
         .enableNoVerticalClassMergingAnnotations()
         .compile()
         .run(parameters.getRuntime(), Main.class)
-        .applyIf(
-            parameters.canUseDefaultAndStaticInterfaceMethods(),
-            // TODO(b/369040938): Disallow bridge hoisting of B.m().
-            rr -> rr.assertSuccessWithOutputLines("A.bridgeTarget()", "A.bridgeTarget()"),
-            rr -> rr.assertSuccessWithOutputLines("A.bridgeTarget()", "I.m()"));
+        .assertSuccessWithOutputLines("A.bridgeTarget()", "I.m()");
   }
 
   static class Main {