Extend interface method resolution to account for multiple maximal specific

Bug: b/230289235
Bug: b/214382176
Bug: b/226170842
Change-Id: I3fe50eeb47d61b7c59f774743cde02e1a2e60b9e
diff --git a/src/main/java/com/android/tools/r8/graph/LookupResult.java b/src/main/java/com/android/tools/r8/graph/LookupResult.java
index b8bc3c8..a23a5a6 100644
--- a/src/main/java/com/android/tools/r8/graph/LookupResult.java
+++ b/src/main/java/com/android/tools/r8/graph/LookupResult.java
@@ -5,11 +5,13 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.LookupResult.LookupResultSuccess.LookupResultCollectionState;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
 
 public abstract class LookupResult {
@@ -163,6 +165,7 @@
       private final Map<DexMethod, LookupMethodTarget> methodTargets = new IdentityHashMap<>();
       private final List<LookupLambdaTarget> lambdaTargets = new ArrayList<>();
       private final List<DexEncodedMethod> methodsCausingFailure = new ArrayList<>();
+      private final Set<DexType> typesCausingFailure = Sets.newIdentityHashSet();
       private LookupResultCollectionState state;
 
       public Builder addMethodTarget(LookupMethodTarget methodTarget) {
@@ -181,6 +184,11 @@
         return this;
       }
 
+      public Builder addTypeCausingFailure(DexType typeCausingFailure) {
+        typesCausingFailure.add(typeCausingFailure);
+        return this;
+      }
+
       public Builder setState(LookupResultCollectionState state) {
         this.state = state;
         return this;
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolution.java b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
index a680b25..990f432 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolution.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.errors.Unreachable;
+import static com.android.tools.r8.graph.MethodResolution.UniquePathOracle.SplitToken.NO_SPLIT_TOKEN;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
 import com.android.tools.r8.graph.MethodResolutionResult.ArrayCloneMethodResult;
 import com.android.tools.r8.graph.MethodResolutionResult.ClassNotFoundResult;
 import com.android.tools.r8.graph.MethodResolutionResult.IllegalAccessOrNoSuchMethodResult;
@@ -13,11 +15,17 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SetUtils;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.function.Function;
 
 /**
@@ -31,30 +39,36 @@
   private final Function<DexType, ClassResolutionResult> definitionFor;
   private final DexItemFactory factory;
   private final boolean escapeIfLibraryHasProgramSuperType;
+  private final boolean canHaveIncompletePaths;
 
   private MethodResolution(
       Function<DexType, ClassResolutionResult> definitionFor,
       DexItemFactory factory,
-      boolean escapeIfLibraryHasProgramSuperType) {
+      boolean escapeIfLibraryHasProgramSuperType,
+      boolean canHaveIncompletePaths) {
     this.definitionFor = definitionFor;
     this.factory = factory;
     this.escapeIfLibraryHasProgramSuperType = escapeIfLibraryHasProgramSuperType;
+    this.canHaveIncompletePaths = canHaveIncompletePaths;
   }
 
+  @Deprecated
   public static MethodResolution createLegacy(
       Function<DexType, DexClass> definitionFor, DexItemFactory factory) {
+    // TODO(b/230289235): Remove this when R8/D8 can handle multiple definitions.
     return new MethodResolution(
         type -> {
           DexClass clazz = definitionFor.apply(type);
           return clazz == null ? ClassResolutionResult.NoResolutionResult.noResult() : clazz;
         },
         factory,
+        false,
         false);
   }
 
   public static MethodResolution create(
       Function<DexType, ClassResolutionResult> definitionFor, DexItemFactory factory) {
-    return new MethodResolution(definitionFor, factory, true);
+    return new MethodResolution(definitionFor, factory, true, true);
   }
 
   private ClassResolutionResult definitionFor(DexType type) {
@@ -246,6 +260,128 @@
     return builder;
   }
 
+  /**
+   * UniquePathOracle will compute for all parent definitions in the hierarchy if a class is visited
+   * by all paths or not depending on class resolution resolving a type to either library or
+   * program/classpath definitions. It does so by visiting all paths upwards in the hierarchy and
+   * when a split point is seen it will create a "left" and "right" half rooted by the split type.
+   * If a definition is only seen by a single token it will always be on an incomplete path
+   * otherwise if the class is seen by both the left and right token it is visited by all:
+   *
+   * <pre>
+   *                     C_Library { left_B, right_B }
+   *                    /                     \
+   *     B_Program extends C { left_B }     B_Library extends C { right_B }
+   *                    \                     /
+   *                      A_Program extends B { }
+   * </pre>
+   */
+  static class UniquePathOracle {
+
+    static class SplitToken {
+
+      static final SplitToken NO_SPLIT_TOKEN = new SplitToken(null);
+
+      private final DexType split;
+
+      private SplitToken(DexType split) {
+        this.split = split;
+      }
+
+      boolean isSplitToken() {
+        return this != NO_SPLIT_TOKEN;
+      }
+    }
+
+    private final Function<DexType, ClassResolutionResult> definitionFor;
+    private final boolean escapeIfLibraryHasProgramSuperType;
+    private final Map<DexClass, Map<DexType, SplitToken>> incompletePaths = new IdentityHashMap<>();
+    private final Set<DexType> seenTypes = Sets.newIdentityHashSet();
+
+    public UniquePathOracle(
+        Function<DexType, ClassResolutionResult> definitionFor,
+        boolean escapeIfLibraryHasProgramSuperType) {
+      this.definitionFor = definitionFor;
+      this.escapeIfLibraryHasProgramSuperType = escapeIfLibraryHasProgramSuperType;
+    }
+
+    public boolean onIncompletePath(DexClass definition) {
+      Map<DexType, SplitToken> tokenMap = incompletePaths.get(definition);
+      assert tokenMap != null;
+      for (SplitToken value : tokenMap.values()) {
+        if (value.isSplitToken()) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    void lookupPath(DexType type, DexClass previousClass) {
+      lookupPath(type, previousClass, Collections.emptySet());
+    }
+
+    private void lookupPath(DexType type, DexClass previousClass, Set<SplitToken> splitTokens) {
+      if (splitTokens.isEmpty() && !seenTypes.add(type)) {
+        return;
+      }
+      ClassResolutionResult resolutionResult = definitionFor.apply(type);
+      resolutionResult.forEachClassResolutionResult(
+          clazz -> {
+            if (escapeIfLibraryHasProgramSuperType
+                && previousClass.isLibraryClass()
+                && !clazz.isLibraryClass()) {
+              return;
+            }
+            Set<SplitToken> currentSplitTokens;
+            if (resolutionResult.isMultipleClassResolutionResult()) {
+              currentSplitTokens = SetUtils.newIdentityHashSet(splitTokens);
+              currentSplitTokens.add(new SplitToken(type));
+            } else {
+              currentSplitTokens = splitTokens;
+            }
+            Map<DexType, SplitToken> paths =
+                incompletePaths.computeIfAbsent(clazz, ignoreKey(IdentityHashMap::new));
+            currentSplitTokens.forEach(
+                splitToken -> {
+                  SplitToken otherSplitToken = paths.get(splitToken.split);
+                  if (otherSplitToken == null) {
+                    paths.put(splitToken.split, splitToken);
+                  } else if (otherSplitToken != splitToken && otherSplitToken.isSplitToken()) {
+                    // We have not seen this half of the split token, so it is visited on all paths.
+                    paths.put(splitToken.split, NO_SPLIT_TOKEN);
+                  }
+                });
+            clazz.interfaces.forEach(iface -> lookupPath(iface, clazz, currentSplitTokens));
+            if (clazz.superType != null) {
+              lookupPath(clazz.superType, clazz, currentSplitTokens);
+            }
+          });
+    }
+  }
+
+  static class AllUniquePathsOracle extends UniquePathOracle {
+
+    private static final AllUniquePathsOracle INSTANCE = new AllUniquePathsOracle();
+
+    static AllUniquePathsOracle getInstance() {
+      return INSTANCE;
+    }
+
+    private AllUniquePathsOracle() {
+      super(null, false);
+    }
+
+    @Override
+    void lookupPath(DexType type, DexClass previousClass) {
+      // Intentionally empty
+    }
+
+    @Override
+    public boolean onIncompletePath(DexClass definition) {
+      return false;
+    }
+  }
+
   /** Helper method that builds the set of maximally specific methods. */
   private void resolveMethodStep3Helper(
       DexProto methodProto,
@@ -268,25 +404,59 @@
       MaximallySpecificMethodsBuilder builder,
       DexType superType,
       List<DexType> interfaces) {
+    UniquePathOracle uniquePathOracle;
+    if (canHaveIncompletePaths) {
+      uniquePathOracle = new UniquePathOracle(definitionFor, escapeIfLibraryHasProgramSuperType);
+      interfaces.forEach(iFace -> uniquePathOracle.lookupPath(iFace, clazz));
+      if (superType != null) {
+        uniquePathOracle.lookupPath(superType, clazz);
+      }
+    } else {
+      uniquePathOracle = AllUniquePathsOracle.getInstance();
+    }
+    resolveMethodStep3Helper(
+        methodProto, methodName, clazz, builder, superType, interfaces, uniquePathOracle);
+  }
+
+  private void resolveMethodStep3Helper(
+      DexProto methodProto,
+      DexString methodName,
+      DexClass clazz,
+      MaximallySpecificMethodsBuilder builder,
+      DexType superType,
+      List<DexType> interfaces,
+      UniquePathOracle uniquePathOracle) {
     for (DexType iface : interfaces) {
       ClassResolutionResult classResolutionResult = definitionFor(iface);
-      if (classResolutionResult.isMultipleClassResolutionResult()) {
-        // TODO(b/214382176, b/226170842): Compute maximal specific set in precense of
-        //   multiple results.
-        throw new Unreachable(
-            "MethodResolution should not be passed definition with multiple results");
-      }
       classResolutionResult.forEachClassResolutionResult(
           definition -> {
+            // Guard against going back into the program for resolution.
+            if (escapeIfLibraryHasProgramSuperType
+                && clazz != null
+                && clazz.isLibraryClass()
+                && !definition.isLibraryClass()) {
+              return;
+            }
+            if (classResolutionResult.isMultipleClassResolutionResult()) {
+              builder.addTypeWithMultipleDefinitions(iface);
+            }
             assert definition.isInterface();
             DexEncodedMethod result = definition.lookupMethod(methodProto, methodName);
             if (isMaximallySpecificCandidate(result)) {
               // The candidate is added and doing so will prohibit shadowed methods from being
               // in the set.
-              builder.addCandidate(definition, result);
+              builder.addCandidate(
+                  definition, result, uniquePathOracle.onIncompletePath(definition));
             } else {
               // Look at the super-interfaces of this class and keep searching.
-              resolveMethodStep3Helper(methodProto, methodName, definition, builder);
+              resolveMethodStep3Helper(
+                  methodProto,
+                  methodName,
+                  definition,
+                  builder,
+                  definition.superType,
+                  Arrays.asList(definition.interfaces.values),
+                  uniquePathOracle);
             }
           });
     }
@@ -302,7 +472,14 @@
                     && !superClass.isLibraryClass()) {
                   return;
                 }
-                resolveMethodStep3Helper(methodProto, methodName, superClass, builder);
+                resolveMethodStep3Helper(
+                    methodProto,
+                    methodName,
+                    superClass,
+                    builder,
+                    superClass.superType,
+                    Arrays.asList(superClass.interfaces.values),
+                    uniquePathOracle);
               });
     }
   }
@@ -386,9 +563,12 @@
     // in the case that a type has a candidate which is shadowed by a subinterface, the map will
     // map the class to a null entry, thus any addition to the map must check for key containment
     // prior to writing.
-    private final LinkedHashMap<DexClass, DexEncodedMethod> maximallySpecificMethods =
-        new LinkedHashMap<>();
+    private final LinkedHashMap<DexClass, DexEncodedMethod>
+        maximallySpecificMethodsOnCompletePaths = new LinkedHashMap<>();
+    private final LinkedHashMap<DexClass, DexEncodedMethod>
+        maximallySpecificMethodsOnIncompletePaths = new LinkedHashMap<>();
     private final Function<DexType, ClassResolutionResult> definitionFor;
+    private final Set<DexType> typesWithMultipleDefinitions = Sets.newIdentityHashSet();
     private final DexItemFactory factory;
 
     private MaximallySpecificMethodsBuilder(
@@ -397,12 +577,28 @@
       this.factory = factory;
     }
 
-    void addCandidate(DexClass holder, DexEncodedMethod method) {
+    void addTypeWithMultipleDefinitions(DexType type) {
+      typesWithMultipleDefinitions.add(type);
+    }
+
+    void addCandidate(DexClass holder, DexEncodedMethod method, boolean isIncompletePath) {
       // If this candidate is already a candidate or it is shadowed, then no need to continue.
-      if (maximallySpecificMethods.containsKey(holder)) {
+      if (isIncompletePath) {
+        if (!maximallySpecificMethodsOnIncompletePaths.containsKey(holder)) {
+          maximallySpecificMethodsOnIncompletePaths.put(holder, method);
+        }
+        // Incomplete paths can shadow complete path results if all partial paths is
+        // shadowing.
+        //       I_L { f(); }
+        //     /            \
+        // J_L { f(); }  // J_P { f(); }
+        // One way to track them is to count all coverings are equal to the splits. For now
+        // we bail out.
+        return;
+      } else if (maximallySpecificMethodsOnCompletePaths.containsKey(holder)) {
         return;
       }
-      maximallySpecificMethods.put(holder, method);
+      maximallySpecificMethodsOnCompletePaths.put(holder, method);
       // Prune exiting candidates and prohibit future candidates in the super hierarchy.
       assert holder.isInterface();
       assert holder.superType == factory.objectType;
@@ -415,29 +611,26 @@
       if (type == null) {
         return;
       }
-      ClassResolutionResult classResolutionResult = definitionFor.apply(type);
-      if (classResolutionResult.isMultipleClassResolutionResult()) {
-        // TODO(b/214382176, b/226170842): Compute maximal specific set in precense of
-        //   multiple results.
-        throw new Unreachable(
-            "MethodResolution should not be passed definition with multiple results");
-      }
-      classResolutionResult.forEachClassResolutionResult(
-          clazz -> {
-            assert clazz.isInterface();
-            assert clazz.superType == factory.objectType;
-            // A null entry signifies that the candidate is shadowed blocking future candidates.
-            // If the candidate is already shadowed at this type there is no need to shadow
-            // further up.
-            if (maximallySpecificMethods.containsKey(clazz)
-                && maximallySpecificMethods.get(clazz) == null) {
-              return;
-            }
-            maximallySpecificMethods.put(clazz, null);
-            for (DexType iface : clazz.interfaces.values) {
-              markShadowed(iface);
-            }
-          });
+      definitionFor
+          .apply(type)
+          .forEachClassResolutionResult(
+              clazz -> {
+                assert clazz.isInterface();
+                assert clazz.superType == factory.objectType;
+                // A null entry signifies that the candidate is shadowed blocking future candidates.
+                // If the candidate is already shadowed at this type there is no need to shadow
+                // further up.
+                if (maximallySpecificMethodsOnCompletePaths.getOrDefault(
+                        clazz, DexEncodedMethod.SENTINEL)
+                    == null) {
+                  return;
+                }
+                maximallySpecificMethodsOnCompletePaths.put(clazz, null);
+                maximallySpecificMethodsOnIncompletePaths.put(clazz, null);
+                for (DexType iface : clazz.interfaces.values) {
+                  markShadowed(iface);
+                }
+              });
     }
 
     DexClassAndMethod lookup() {
@@ -450,40 +643,64 @@
     }
 
     private MethodResolutionResult internalResolve(DexClass initialResolutionHolder) {
-      if (maximallySpecificMethods.isEmpty()) {
+      if (maximallySpecificMethodsOnCompletePaths.isEmpty()
+          && maximallySpecificMethodsOnIncompletePaths.isEmpty()) {
         return NoSuchMethodResult.INSTANCE;
       }
-      // Fast path in the common case of a single method.
-      if (maximallySpecificMethods.size() == 1) {
-        return singleResultHelper(
-            initialResolutionHolder, maximallySpecificMethods.entrySet().iterator().next());
+      List<Entry<DexClass, DexEncodedMethod>> nonAbstractOnComplete =
+          getNonAbstractMethods(maximallySpecificMethodsOnCompletePaths);
+      if (nonAbstractOnComplete.size() > 1) {
+        return IncompatibleClassResult.create(
+            ListUtils.map(nonAbstractOnComplete, Entry::getValue));
       }
-      Entry<DexClass, DexEncodedMethod> firstMaximallySpecificMethod = null;
-      List<Entry<DexClass, DexEncodedMethod>> nonAbstractMethods =
-          new ArrayList<>(maximallySpecificMethods.size());
-      for (Entry<DexClass, DexEncodedMethod> entry : maximallySpecificMethods.entrySet()) {
+      List<Entry<DexClass, DexEncodedMethod>> nonAbstractOnIncomplete =
+          getNonAbstractMethods(maximallySpecificMethodsOnIncompletePaths);
+      if (nonAbstractOnIncomplete.isEmpty()) {
+        // If there are no non-abstract methods, then any candidate on a complete path will suffice
+        // as a target. For deterministic resolution, we return the first mapped method (of the
+        // linked map).
+        if (nonAbstractOnComplete.isEmpty()) {
+          return singleResultHelper(
+              initialResolutionHolder, firstNonNullEntry(maximallySpecificMethodsOnCompletePaths));
+        } else {
+          // If there is exactly one non-abstract method (a default method) it is the resolution
+          // target.
+          return singleResultHelper(initialResolutionHolder, nonAbstractOnComplete.get(0));
+        }
+      } else {
+        // We cannot guarantee a single target or an error, so we have to report all on incomplete
+        // paths.
+        MethodResolutionResult.Builder builder =
+            MethodResolutionResult.builder().allowMultipleProgramResults();
+        if (nonAbstractOnComplete.isEmpty()) {
+          assert !typesWithMultipleDefinitions.isEmpty();
+          builder.addResolutionResult(new NoSuchMethodResult(typesWithMultipleDefinitions));
+        } else {
+          nonAbstractOnComplete.forEach(
+              entry ->
+                  builder.addResolutionResult(singleResultHelper(initialResolutionHolder, entry)));
+        }
+        nonAbstractOnIncomplete.forEach(
+            entry ->
+                builder.addResolutionResult(singleResultHelper(initialResolutionHolder, entry)));
+        return builder.buildOrIfEmpty(NoSuchMethodResult.INSTANCE);
+      }
+    }
+
+    private List<Entry<DexClass, DexEncodedMethod>> getNonAbstractMethods(
+        Map<DexClass, DexEncodedMethod> candidates) {
+      List<Entry<DexClass, DexEncodedMethod>> nonAbstractMethods = new ArrayList<>();
+      for (Entry<DexClass, DexEncodedMethod> entry : candidates.entrySet()) {
         DexEncodedMethod method = entry.getValue();
         if (method == null) {
           // Ignore shadowed candidates.
           continue;
         }
-        if (firstMaximallySpecificMethod == null) {
-          firstMaximallySpecificMethod = entry;
-        }
         if (method.isNonAbstractVirtualMethod()) {
           nonAbstractMethods.add(entry);
         }
       }
-      // If there are no non-abstract methods, then any candidate will suffice as a target.
-      // For deterministic resolution, we return the first mapped method (of the linked map).
-      if (nonAbstractMethods.isEmpty()) {
-        return singleResultHelper(initialResolutionHolder, firstMaximallySpecificMethod);
-      }
-      // If there is exactly one non-abstract method (a default method) it is the resolution target.
-      if (nonAbstractMethods.size() == 1) {
-        return singleResultHelper(initialResolutionHolder, nonAbstractMethods.get(0));
-      }
-      return IncompatibleClassResult.create(ListUtils.map(nonAbstractMethods, Entry::getValue));
+      return nonAbstractMethods;
     }
 
     private static SingleResolutionResult<?> singleResultHelper(
@@ -493,5 +710,17 @@
           entry.getKey(),
           entry.getValue());
     }
+
+    private static Entry<DexClass, DexEncodedMethod> firstNonNullEntry(
+        Map<DexClass, DexEncodedMethod> candidates) {
+      for (Entry<DexClass, DexEncodedMethod> entry : candidates.entrySet()) {
+        DexEncodedMethod method = entry.getValue();
+        if (method != null) {
+          return entry;
+        }
+      }
+      assert false : "Should not be called on a collection without any non-null candidates";
+      return null;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
index 65ff279..a2dd885 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.ConsumerUtils;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
@@ -83,6 +84,14 @@
     return false;
   }
 
+  public boolean isMultiMethodResolutionResult() {
+    return false;
+  }
+
+  public final void forEachMethodResolutionResult(Consumer<MethodResolutionResult> resultConsumer) {
+    visitMethodResolutionResults(resultConsumer, resultConsumer, resultConsumer, resultConsumer);
+  }
+
   /** Returns non-null if isFailedResolution() is true, otherwise null. */
   public FailedResolutionResult asFailedResolution() {
     return null;
@@ -162,14 +171,23 @@
   public abstract LookupTarget lookupVirtualDispatchTarget(
       LambdaDescriptor lambdaInstance,
       AppInfoWithClassHierarchy appInfo,
+      Consumer<DexType> typeCausingFailureConsumer,
       Consumer<? super DexEncodedMethod> methodCausingFailureConsumer);
 
   public abstract void visitMethodResolutionResults(
-      Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+      Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+          programOrClasspathConsumer,
       Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
       Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
       Consumer<? super FailedResolutionResult> failedResolutionConsumer);
 
+  public void visitMethodResolutionResults(
+      Consumer<? super MethodResolutionResult> resultConsumer,
+      Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
+    visitMethodResolutionResults(
+        resultConsumer, resultConsumer, resultConsumer, failedResolutionConsumer);
+  }
+
   public boolean hasProgramResult() {
     return false;
   }
@@ -502,7 +520,11 @@
             incompleteness.checkClass(subClass);
             LookupMethodTarget lookupTarget =
                 lookupVirtualDispatchTarget(
-                    subClass, appInfo, resolvedHolder.type, resultBuilder::addMethodCausingFailure);
+                    subClass,
+                    appInfo,
+                    resolvedHolder.type,
+                    resultBuilder::addTypeCausingFailure,
+                    resultBuilder::addMethodCausingFailure);
             if (lookupTarget != null) {
               incompleteness.checkDexClassAndMethod(lookupTarget);
               addVirtualDispatchTarget(lookupTarget, resolvedHolder.isInterface(), resultBuilder);
@@ -513,7 +535,10 @@
                 || resolvedHolder.type == appInfo.dexItemFactory().objectType;
             LookupTarget target =
                 lookupVirtualDispatchTarget(
-                    lambda, appInfo, resultBuilder::addMethodCausingFailure);
+                    lambda,
+                    appInfo,
+                    resultBuilder::addTypeCausingFailure,
+                    resultBuilder::addMethodCausingFailure);
             if (target != null) {
               if (target.isLambdaTarget()) {
                 resultBuilder.addLambdaTarget(target.asLambdaTarget());
@@ -639,20 +664,22 @@
         InstantiatedObject instance, AppInfoWithClassHierarchy appInfo) {
       return instance.isClass()
           ? lookupVirtualDispatchTarget(instance.asClass(), appInfo)
-          : lookupVirtualDispatchTarget(instance.asLambda(), appInfo, emptyConsumer());
+          : lookupVirtualDispatchTarget(
+              instance.asLambda(), appInfo, emptyConsumer(), emptyConsumer());
     }
 
     @Override
     public LookupMethodTarget lookupVirtualDispatchTarget(
         DexClass dynamicInstance, AppInfoWithClassHierarchy appInfo) {
       return lookupVirtualDispatchTarget(
-          dynamicInstance, appInfo, initialResolutionHolder.type, emptyConsumer());
+          dynamicInstance, appInfo, initialResolutionHolder.type, emptyConsumer(), emptyConsumer());
     }
 
     @Override
     public LookupTarget lookupVirtualDispatchTarget(
         LambdaDescriptor lambdaInstance,
         AppInfoWithClassHierarchy appInfo,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       if (lambdaInstance.getMainMethod().match(resolvedMethod)) {
         DexMethod methodReference = lambdaInstance.implHandle.asMethod();
@@ -665,13 +692,14 @@
         return new LookupLambdaTarget(lambdaInstance, method);
       }
       return lookupMaximallySpecificDispatchTarget(
-          lambdaInstance, appInfo, methodCausingFailureConsumer);
+          lambdaInstance, appInfo, typeCausingFailureConsumer, methodCausingFailureConsumer);
     }
 
     private LookupMethodTarget lookupVirtualDispatchTarget(
         DexClass dynamicInstance,
         AppInfoWithClassHierarchy appInfo,
         DexType resolutionHolder,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       assert appInfo.isSubtype(dynamicInstance.type, resolutionHolder)
           : dynamicInstance.type + " is not a subtype of " + resolutionHolder;
@@ -714,12 +742,13 @@
         return null;
       }
       return lookupMaximallySpecificDispatchTarget(
-          dynamicInstance, appInfo, methodCausingFailureConsumer);
+          dynamicInstance, appInfo, typeCausingFailureConsumer, methodCausingFailureConsumer);
     }
 
     private DexClassAndMethod lookupMaximallySpecificDispatchTarget(
         DexClass dynamicInstance,
         AppInfoWithClassHierarchy appInfo,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       MethodResolutionResult maximallySpecificResolutionResult =
           appInfo.resolveMaximallySpecificTarget(dynamicInstance, resolvedMethod.getReference());
@@ -729,7 +758,7 @@
       if (maximallySpecificResolutionResult.isFailedResolution()) {
         maximallySpecificResolutionResult
             .asFailedResolution()
-            .forEachFailureDependency(methodCausingFailureConsumer);
+            .forEachFailureDependency(typeCausingFailureConsumer, methodCausingFailureConsumer);
         return null;
       }
       assert maximallySpecificResolutionResult.isArrayCloneMethodResult();
@@ -739,6 +768,7 @@
     private DexClassAndMethod lookupMaximallySpecificDispatchTarget(
         LambdaDescriptor lambdaDescriptor,
         AppInfoWithClassHierarchy appInfo,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       MethodResolutionResult maximallySpecificResolutionResult =
           appInfo.resolveMaximallySpecificTarget(lambdaDescriptor, resolvedMethod.getReference());
@@ -748,7 +778,7 @@
       if (maximallySpecificResolutionResult.isFailedResolution()) {
         maximallySpecificResolutionResult
             .asFailedResolution()
-            .forEachFailureDependency(methodCausingFailureConsumer);
+            .forEachFailureDependency(typeCausingFailureConsumer, methodCausingFailureConsumer);
         return null;
       }
       assert maximallySpecificResolutionResult.isArrayCloneMethodResult();
@@ -833,7 +863,8 @@
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
@@ -872,7 +903,8 @@
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
@@ -906,7 +938,8 @@
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
@@ -974,6 +1007,7 @@
     public LookupTarget lookupVirtualDispatchTarget(
         LambdaDescriptor lambdaInstance,
         AppInfoWithClassHierarchy appInfo,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       return null;
     }
@@ -1007,7 +1041,8 @@
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
@@ -1023,6 +1058,12 @@
   /** Base class for all types of failed resolutions. */
   public abstract static class FailedResolutionResult extends EmptyResult {
 
+    protected final Collection<DexType> typesCausingError;
+
+    private FailedResolutionResult(Collection<DexType> typesCausingError) {
+      this.typesCausingError = typesCausingError;
+    }
+
     @Override
     public boolean isFailedResolution() {
       return true;
@@ -1034,8 +1075,11 @@
     }
 
     public void forEachFailureDependency(
+        Consumer<DexType> typesCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
-      // Default failure has no dependencies.
+      if (typesCausingError != null) {
+        typesCausingError.forEach(typesCausingFailureConsumer);
+      }
     }
 
     @Override
@@ -1061,7 +1105,8 @@
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
@@ -1073,7 +1118,7 @@
     static final ClassNotFoundResult INSTANCE = new ClassNotFoundResult();
 
     private ClassNotFoundResult() {
-      // Intentionally left empty.
+      super(null);
     }
 
     @Override
@@ -1087,12 +1132,15 @@
     private final Collection<DexEncodedMethod> methodsCausingError;
 
     private FailedResolutionWithCausingMethods(Collection<DexEncodedMethod> methodsCausingError) {
+      super(ListUtils.map(methodsCausingError, DexEncodedMember::getHolderType));
       this.methodsCausingError = methodsCausingError;
     }
 
     @Override
     public void forEachFailureDependency(
+        Consumer<DexType> typesCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
+      super.forEachFailureDependency(typesCausingFailureConsumer, methodCausingFailureConsumer);
       this.methodsCausingError.forEach(methodCausingFailureConsumer);
     }
 
@@ -1124,7 +1172,11 @@
 
   public static class NoSuchMethodResult extends FailedResolutionResult {
 
-    static final NoSuchMethodResult INSTANCE = new NoSuchMethodResult();
+    static final NoSuchMethodResult INSTANCE = new NoSuchMethodResult(null);
+
+    public NoSuchMethodResult(Collection<DexType> typesCausingError) {
+      super(typesCausingError);
+    }
 
     @Override
     public boolean isNoSuchMethodErrorResult(DexClass context, AppInfoWithClassHierarchy appInfo) {
@@ -1155,9 +1207,21 @@
       }
       BooleanBox seenNoAccess = new BooleanBox(false);
       forEachFailureDependency(
+          type -> {
+            appInfo
+                .contextIndependentDefinitionForWithResolutionResult(type)
+                .forEachClassResolutionResult(
+                    clazz -> {
+                      AccessControl.isClassAccessible(
+                          clazz,
+                          context,
+                          appInfo.getClassToFeatureSplitMap(),
+                          appInfo.getSyntheticItems());
+                    });
+          },
           method -> {
-            DexClassAndMethod classAndMethod =
-                DexClassAndMethod.create(appInfo.definitionFor(method.getHolderType()), method);
+            DexClass holder = appInfo.definitionFor(method.getHolderType());
+            DexClassAndMethod classAndMethod = DexClassAndMethod.create(holder, method);
             seenNoAccess.or(
                 AccessControl.isMemberAccessible(
                         classAndMethod, initialResolutionHolder, context, appInfo)
@@ -1184,6 +1248,9 @@
     private boolean verifyInvalidSymbolicReference() {
       BooleanBox invalidSymbolicReference = new BooleanBox(true);
       forEachFailureDependency(
+          type -> {
+            // Intentionally empty
+          },
           method -> {
             invalidSymbolicReference.and(
                 method.getHolderType() != initialResolutionHolder.getType());
@@ -1197,14 +1264,18 @@
       extends MethodResolutionResult {
 
     protected final T programOrClasspathResult;
+    protected final List<SingleResolutionResult<? extends ProgramOrClasspathClass>>
+        otherProgramOrClasspathResults;
     protected final List<SingleLibraryResolutionResult> libraryResolutionResults;
     protected final List<FailedResolutionResult> failedResolutionResults;
 
-    public MultipleMethodResolutionResult(
+    protected MultipleMethodResolutionResult(
         T programOrClasspathResult,
+        List<SingleResolutionResult<? extends ProgramOrClasspathClass>> programOrClasspathResults,
         List<SingleLibraryResolutionResult> libraryResolutionResults,
         List<FailedResolutionResult> failedResolutionResults) {
       this.programOrClasspathResult = programOrClasspathResult;
+      this.otherProgramOrClasspathResults = programOrClasspathResults;
       this.libraryResolutionResults = libraryResolutionResults;
       this.failedResolutionResults = failedResolutionResults;
     }
@@ -1284,22 +1355,32 @@
     public LookupTarget lookupVirtualDispatchTarget(
         LambdaDescriptor lambdaInstance,
         AppInfoWithClassHierarchy appInfo,
+        Consumer<DexType> typeCausingFailureConsumer,
         Consumer<? super DexEncodedMethod> methodCausingFailureConsumer) {
       throw new Unreachable("Should not be called on MultipleFieldResolutionResult");
     }
 
     @Override
     public void visitMethodResolutionResults(
-        Consumer<? super SingleResolutionResult<?>> programOrClasspathConsumer,
+        Consumer<? super SingleResolutionResult<? extends ProgramOrClasspathClass>>
+            programOrClasspathConsumer,
         Consumer<? super SingleLibraryResolutionResult> libraryResultConsumer,
         Consumer<? super ArrayCloneMethodResult> cloneResultConsumer,
         Consumer<? super FailedResolutionResult> failedResolutionConsumer) {
       if (programOrClasspathResult != null) {
         programOrClasspathConsumer.accept(programOrClasspathResult);
       }
+      if (otherProgramOrClasspathResults != null) {
+        otherProgramOrClasspathResults.forEach(programOrClasspathConsumer);
+      }
       libraryResolutionResults.forEach(libraryResultConsumer);
       failedResolutionResults.forEach(failedResolutionConsumer);
     }
+
+    @Override
+    public boolean isMultiMethodResolutionResult() {
+      return true;
+    }
   }
 
   public static class MultipleProgramWithLibraryResolutionResult
@@ -1309,7 +1390,11 @@
         SingleProgramResolutionResult programOrClasspathResult,
         List<SingleLibraryResolutionResult> libraryResolutionResults,
         List<FailedResolutionResult> failedOrUnknownResolutionResults) {
-      super(programOrClasspathResult, libraryResolutionResults, failedOrUnknownResolutionResults);
+      super(
+          programOrClasspathResult,
+          null,
+          libraryResolutionResults,
+          failedOrUnknownResolutionResults);
     }
   }
 
@@ -1320,7 +1405,11 @@
         SingleClasspathResolutionResult programOrClasspathResult,
         List<SingleLibraryResolutionResult> libraryResolutionResults,
         List<FailedResolutionResult> failedOrUnknownResolutionResults) {
-      super(programOrClasspathResult, libraryResolutionResults, failedOrUnknownResolutionResults);
+      super(
+          programOrClasspathResult,
+          null,
+          libraryResolutionResults,
+          failedOrUnknownResolutionResults);
     }
   }
 
@@ -1330,7 +1419,18 @@
     public MultipleLibraryMethodResolutionResult(
         List<SingleLibraryResolutionResult> libraryResolutionResults,
         List<FailedResolutionResult> failedOrUnknownResolutionResults) {
-      super(null, libraryResolutionResults, failedOrUnknownResolutionResults);
+      super(null, null, libraryResolutionResults, failedOrUnknownResolutionResults);
+    }
+  }
+
+  public static class MultipleMaximallySpecificResolutionResult
+      extends MultipleMethodResolutionResult<DexProgramClass, SingleProgramResolutionResult> {
+
+    public MultipleMaximallySpecificResolutionResult(
+        List<SingleResolutionResult<? extends ProgramOrClasspathClass>> programOrClasspathResult,
+        List<SingleLibraryResolutionResult> libraryResolutionResults,
+        List<FailedResolutionResult> failedResolutionResults) {
+      super(null, programOrClasspathResult, libraryResolutionResults, failedResolutionResults);
     }
   }
 
@@ -1342,6 +1442,7 @@
 
     private MethodResolutionResult possiblySingleResult = null;
     private List<MethodResolutionResult> allResults = null;
+    private boolean allowMultipleProgramResults = false;
 
     private Builder() {}
 
@@ -1357,26 +1458,29 @@
       allResults.add(result);
     }
 
+    public Builder allowMultipleProgramResults() {
+      allowMultipleProgramResults = true;
+      return this;
+    }
+
     public MethodResolutionResult buildOrIfEmpty(MethodResolutionResult emptyResult) {
       if (possiblySingleResult == null) {
         return emptyResult;
       } else if (allResults == null) {
         return possiblySingleResult;
       }
-      Box<SingleResolutionResult<?>> singleResult = new Box<>();
+      List<SingleResolutionResult<? extends ProgramOrClasspathClass>> programOrClasspathResults =
+          new ArrayList<>();
       List<SingleLibraryResolutionResult> libraryResults = new ArrayList<>();
       List<FailedResolutionResult> failedResults = new ArrayList<>();
       allResults.forEach(
           otherResult -> {
             otherResult.visitMethodResolutionResults(
                 otherProgramOrClasspathResult -> {
-                  if (singleResult.isSet()) {
+                  if (!programOrClasspathResults.isEmpty() && !allowMultipleProgramResults) {
                     assert false : "Unexpected multiple results between program and classpath";
-                    if (singleResult.get().hasProgramResult()) {
-                      return;
-                    }
                   }
-                  singleResult.set(otherProgramOrClasspathResult);
+                  programOrClasspathResults.add(otherProgramOrClasspathResult);
                 },
                 newLibraryResult -> {
                   if (!Iterables.any(
@@ -1396,7 +1500,7 @@
                   }
                 });
           });
-      if (!singleResult.isSet()) {
+      if (programOrClasspathResults.isEmpty()) {
         if (libraryResults.size() == 1 && failedResults.isEmpty()) {
           return libraryResults.get(0);
         } else if (libraryResults.isEmpty() && failedResults.size() == 1) {
@@ -1404,17 +1508,27 @@
         } else {
           return new MultipleLibraryMethodResolutionResult(libraryResults, failedResults);
         }
-      } else if (libraryResults.isEmpty() && failedResults.isEmpty()) {
-        return singleResult.get();
-      } else if (singleResult.get().hasProgramResult()) {
-        return new MultipleProgramWithLibraryResolutionResult(
-            singleResult.get().asSingleProgramResolutionResult(), libraryResults, failedResults);
+      } else if (libraryResults.isEmpty()
+          && failedResults.isEmpty()
+          && programOrClasspathResults.size() == 1) {
+        return programOrClasspathResults.get(0);
+      } else if (programOrClasspathResults.size() == 1) {
+        SingleResolutionResult<?> singleResult = programOrClasspathResults.get(0);
+        if (singleResult.hasProgramResult()) {
+          return new MultipleProgramWithLibraryResolutionResult(
+              singleResult.asSingleProgramResolutionResult(), libraryResults, failedResults);
+        } else {
+          SingleClasspathResolutionResult classpathResult =
+              singleResult.asSingleClasspathResolutionResult();
+          assert classpathResult != null;
+          return new MultipleClasspathWithLibraryResolutionResult(
+              classpathResult, libraryResults, failedResults);
+        }
       } else {
-        SingleClasspathResolutionResult classpathResult =
-            singleResult.get().asSingleClasspathResolutionResult();
-        assert classpathResult != null;
-        return new MultipleClasspathWithLibraryResolutionResult(
-            classpathResult, libraryResults, failedResults);
+        // This must be a maximally specific result since we have multiple program or classpath
+        // values.
+        return new MultipleMaximallySpecificResolutionResult(
+            programOrClasspathResults, libraryResults, failedResults);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
index 0806685..2d360c0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
@@ -32,6 +32,7 @@
 import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.OptionalBool;
@@ -696,7 +697,8 @@
       if (resolutionResult.isFailedResolution()) {
         resolutionResult
             .asFailedResolution()
-            .forEachFailureDependency(target -> staticTarget.and(target.isStatic()));
+            .forEachFailureDependency(
+                ConsumerUtils.emptyConsumer(), target -> staticTarget.and(target.isStatic()));
       } else if (resolutionResult.isSuccessfulMemberResolutionResult()) {
         staticTarget.set(
             resolutionResult.asSuccessfulMemberResolutionResult().getResolvedMember().isStatic());
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index 242b6e1..3c53944 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -403,7 +404,9 @@
     assert resolutionResult.isFailedResolution();
 
     List<DexEncodedMethod> targets = new ArrayList<>();
-    resolutionResult.asFailedResolution().forEachFailureDependency(targets::add);
+    resolutionResult
+        .asFailedResolution()
+        .forEachFailureDependency(ConsumerUtils.emptyConsumer(), targets::add);
     if (!targets.isEmpty()) {
       DexString newName = renaming.get(targets.get(0).getReference());
       assert targets.stream().allMatch(target -> renaming.get(target.getReference()) == newName);
diff --git a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
index ddd882c..d4b20fe 100644
--- a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
@@ -137,6 +137,9 @@
     resolution
         .asFailedResolution()
         .forEachFailureDependency(
+            type -> {
+              assert type.descriptor == lookupDescriptor(type);
+            },
             failureDependence -> {
               assert lookupName(method) == lookupName(failureDependence.getReference());
             });
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index a2c0d26..05ad88c 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3354,6 +3354,7 @@
       KeepReason reason) {
     failedMethodResolutionTargets.add(symbolicMethod);
     failedResolution.forEachFailureDependency(
+        type -> recordTypeReference(type, context),
         method -> {
           DexProgramClass clazz = getProgramClassOrNull(method.getHolderType(), context);
           if (clazz != null) {
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 42dc619..a17a299 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -363,9 +363,9 @@
           resolutionResult
               .asFailedResolution()
               .forEachFailureDependency(
-                  methodCausingFailure -> {
-                    handleRewrittenMethodReference(method, methodCausingFailure);
-                  });
+                  type -> addType(type, referencedFrom),
+                  methodCausingFailure ->
+                      handleRewrittenMethodReference(method, methodCausingFailure));
           return;
         }
         handleRewrittenMethodReference(method, resolutionResult.getResolutionPair());
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificDifferentParentHierarchyTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificDifferentParentHierarchyTest.java
index 856d3fe..d609491 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificDifferentParentHierarchyTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificDifferentParentHierarchyTest.java
@@ -4,22 +4,27 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -74,12 +79,23 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Library: void " + typeName(I.class) + ".foo()",
+            "Program: void " + typeName(J.class) + ".foo()"),
+        methodResults);
   }
 
   @Test
@@ -96,8 +112,12 @@
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    runTest(testForD8(parameters.getBackend()))
-        // TODO(b/214382176): Extend resolution to support multiple definition results.
+    testForD8(parameters.getBackend())
+        .apply(this::setupTestBuilder)
+        .compile()
+        .addBootClasspathFiles(buildOnDexRuntime(parameters, libraryClasses))
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/230289235): Extend to support multiple definition results.
         .assertFailureWithErrorThatThrowsIf(
             !parameters.canUseDefaultAndStaticInterfaceMethods(),
             IncompatibleClassChangeError.class)
@@ -107,23 +127,24 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class))
+    // TODO(b/230289235): Extend to support multiple definition results.
+    R8FullTestBuilder testBuilder = testForR8(parameters.getBackend()).addKeepMainRule(Main.class);
+    testBuilder
+        .apply(this::setupTestBuilder)
+        .compile()
+        .addBootClasspathFiles(buildOnDexRuntime(parameters, libraryClasses))
+        .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
   }
 
-  private TestRunResult<?> runTest(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder)
-      throws Exception {
-    return testBuilder
+  private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+    testBuilder
         .addProgramClasses(Main.class)
         .addProgramClassFileData(getJProgram())
         .addDefaultRuntimeLibrary(parameters)
         .addLibraryFiles(libraryClasses)
         .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(options -> options.loadAllClassDefinitions = true)
-        .compile()
-        .addBootClasspathFiles(buildOnDexRuntime(parameters, libraryClasses))
-        .run(parameters.getRuntime(), Main.class);
+        .addOptionsModification(options -> options.loadAllClassDefinitions = true);
   }
 
   private byte[] getJProgram() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultipleOnCompleteTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultipleOnCompleteTest.java
new file mode 100644
index 0000000..0ba0078
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultipleOnCompleteTest.java
@@ -0,0 +1,177 @@
+// Copyright (c) 2022, 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.resolution.duplicatedefinitions;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+/**
+ * This is testing resolving Main.f for:
+ *
+ * <pre>
+ * I: I_L { f }
+ * J: J_L extends I { f }, J_P extends I { f }
+ * K: K_P extends J { f }
+ * L: L_L { f }
+ * M: M_L extends L { }, M_P extends L { }
+ * class Main implements I,J,K,M
+ * </pre>
+ */
+public class MaximallySpecificMultipleOnCompleteTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private Path libraryClasses;
+
+  @Before
+  public void setup() throws Exception {
+    libraryClasses = temp.newFile("lib.jar").toPath();
+    ZipBuilder.builder(libraryClasses)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(),
+            ToolHelper.getClassFileForTestClass(J.class),
+            ToolHelper.getClassFileForTestClass(I.class),
+            ToolHelper.getClassFileForTestClass(L.class),
+            ToolHelper.getClassFileForTestClass(M.class))
+        .build();
+  }
+
+  @Test
+  public void testResolution() throws Exception {
+    assumeTrue(parameters.useRuntimeAsNoneRuntime());
+    AndroidApp.Builder builder = AndroidApp.builder();
+    builder
+        .addProgramFiles(
+            ToolHelper.getClassFileForTestClass(K.class),
+            ToolHelper.getClassFileForTestClass(M.class))
+        .addClassProgramData(ImmutableList.of(getJOnProgram(), getMainWithAllImplements()));
+    builder.addLibraryFiles(parameters.getDefaultRuntimeLibrary(), libraryClasses);
+    AppView<AppInfoWithClassHierarchy> appView =
+        computeAppViewWithClassHierarchy(
+            builder.build(), null, options -> options.loadAllClassDefinitions = true);
+    AppInfoWithClassHierarchy appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isIncompatibleClassChangeErrorResult());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addRunClasspathFiles(libraryClasses)
+        .addProgramClasses(K.class, M.class)
+        .addProgramClassFileData(getJOnProgram(), getMainWithAllImplements())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    runTest(testForD8(parameters.getBackend()));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class));
+  }
+
+  private void runTest(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+    testBuilder
+        .addProgramClasses(K.class, M.class)
+        .addProgramClassFileData(getJOnProgram(), getMainWithAllImplements())
+        .addDefaultRuntimeLibrary(parameters)
+        .addLibraryFiles(libraryClasses)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(options -> options.loadAllClassDefinitions = true)
+        .compile()
+        .addBootClasspathFiles(buildOnDexRuntime(parameters, libraryClasses))
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+  }
+
+  private byte[] getJOnProgram() throws Exception {
+    return transformer(JProgram.class).setClassDescriptor(descriptor(J.class)).transform();
+  }
+
+  private byte[] getMainWithAllImplements() throws Exception {
+    return transformer(Main.class).setImplements(I.class, J.class, K.class, M.class).transform();
+  }
+
+  public interface I {
+    default void foo() {
+      System.out.println("I::foo");
+    }
+  }
+
+  /* Present on both library and program */
+  public interface JProgram extends I {
+    @Override
+    default void foo() {
+      System.out.println("J_Program::foo");
+      ;
+    }
+  }
+
+  public interface J extends I {
+    @Override
+    default void foo() {
+      System.out.println("J_Library::foo");
+      ;
+    }
+  }
+
+  public interface K extends J {
+
+    @Override
+    default void foo() {
+      System.out.println("J::foo");
+    }
+  }
+
+  public interface L {
+
+    default void foo() {
+      System.out.println("L::foo");
+    }
+  }
+
+  public interface M extends L {}
+
+  public static class Main implements I, J, K /*, M */ {
+
+    public static void main(String[] args) {
+      new Main().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsICCETest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsICCETest.java
index 2e57f9f..2b291f6 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsICCETest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsICCETest.java
@@ -4,21 +4,27 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -73,12 +79,25 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Library: void " + typeName(I.class) + ".foo()",
+            "Program: void " + typeName(I.class) + ".foo()",
+            "Library: void " + typeName(J.class) + ".foo()",
+            "Program: void " + typeName(J.class) + ".foo()"),
+        methodResults);
   }
 
   @Test
@@ -95,20 +114,19 @@
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    runTest(testForD8(parameters.getBackend()), IncompatibleClassChangeError.class);
+    runTest(testForD8(parameters.getBackend()))
+        .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
   }
 
   @Test
   public void testR8() throws Exception {
-    runTest(
-        testForR8(parameters.getBackend()).addKeepMainRule(Main.class),
-        IncompatibleClassChangeError.class);
+    runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class))
+        .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
   }
 
-  private void runTest(
-      TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder, Class<? extends Throwable> errorClass)
+  private TestRunResult<?> runTest(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder)
       throws Exception {
-    testBuilder
+    return testBuilder
         .addProgramClasses(I.class, J.class)
         .addProgramClassFileData(getMainWithInterfacesIAndJ())
         .addDefaultRuntimeLibrary(parameters)
@@ -117,8 +135,7 @@
         .addOptionsModification(options -> options.loadAllClassDefinitions = true)
         .compile()
         .addBootClasspathFiles(buildOnDexRuntime(parameters, libraryClasses))
-        .run(parameters.getRuntime(), Main.class)
-        .assertFailureWithErrorThatThrows(errorClass);
+        .run(parameters.getRuntime(), Main.class);
   }
 
   private byte[] getMainWithInterfacesIAndJ() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsSuccessTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsSuccessTest.java
index c743ac4..a70390c 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsSuccessTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsSuccessTest.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -13,13 +14,17 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -40,7 +45,7 @@
 public class MaximallySpecificMultiplePathsSuccessTest extends TestBase {
 
   private static final String EXPECTED = "I::foo";
-  // TODO(b/214382176): Extend resolution to support multiple definition results.
+  // TODO(b/230289235): Extend resolution to support multiple definition results.
   private static final String D8_R8_RESULT = "J::foo";
 
   @Parameter() public TestParameters parameters;
@@ -77,12 +82,23 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Library: void " + typeName(I.class) + ".foo()",
+            "Program: void " + typeName(J.class) + ".foo()"),
+        methodResults);
   }
 
   @Test
@@ -105,14 +121,14 @@
         .assertSuccessWithOutputLinesIf(isDalvik, D8_R8_RESULT)
         .assertSuccessWithOutputLinesIf(
             !isDalvik && !parameters.canUseDefaultAndStaticInterfaceMethods(), D8_R8_RESULT)
-        // TODO(b/214382176): Extend resolution to support multiple definition results.
+        // TODO(b/230289235): Extend resolution to support multiple definition results.
         .assertSuccessWithOutputLinesIf(
             parameters.canUseDefaultAndStaticInterfaceMethods(), EXPECTED);
   }
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
+    // TODO(b/230289235): Extend resolution to support multiple definition results.
     runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class))
         .assertFailureWithErrorThatThrowsIf(
             parameters.canUseDefaultAndStaticInterfaceMethods(), NoSuchMethodError.class)
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsThroughClassTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsThroughClassTest.java
index a14d33a..65487b4 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsThroughClassTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificMultiplePathsThroughClassTest.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -12,13 +13,17 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -73,12 +78,24 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Library: void " + typeName(I.class) + ".foo()",
+            "Program: void " + typeName(I.class) + ".foo()",
+            "Program: void " + typeName(J.class) + ".foo()"),
+        methodResults);
   }
 
   @Test
@@ -100,6 +117,7 @@
 
   @Test
   public void testR8() throws Exception {
+    // TODO(b/230289235): Extend to support multiple definition results.
     runTest(
         testForR8(parameters.getBackend()).addKeepMainRule(Main.class),
         IncompatibleClassChangeError.class);
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingAfterJoinTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingAfterJoinTest.java
index 3e3b85b..eb02ca8 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingAfterJoinTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingAfterJoinTest.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -12,13 +13,17 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -75,12 +80,19 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isSingleResolution());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(ImmutableSet.of("Library: void " + typeName(J.class) + ".foo()"), methodResults);
   }
 
   @Test
@@ -125,7 +137,7 @@
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLinesIf(
             parameters.canUseDefaultAndStaticInterfaceMethods(), EXPECTED)
-        // TODO(b/214382176): Extend resolution to support multiple definition results.
+        // TODO(b/230289235): Extend to support multiple definition results.
         .assertFailureWithErrorThatThrowsIf(
             !parameters.canUseDefaultAndStaticInterfaceMethods(),
             errorIfNotSupportingDefaultMethods);
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingSubTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingSubTest.java
index 630c284..3381b31 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingSubTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleDominatingSubTest.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -12,14 +13,18 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,7 +40,7 @@
  * I: I_L { f }
  * J: J_L extends I { f }, J_P extends I { f }
  * K: K_P extends J { f }
- * class Main implements I,K
+ * class Main implements I,J,K
  * </pre>
  */
 public class MaximallySpecificSingleDominatingSubTest extends TestBase {
@@ -77,12 +82,19 @@
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
     DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isSingleResolution());
+    Set<String> methodResults = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          assertTrue(result.isSingleResolution());
+          SingleResolutionResult<?> resolution = result.asSingleResolution();
+          methodResults.add(
+              (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                  + resolution.getResolvedMethod().getReference().toString());
         });
+    assertEquals(ImmutableSet.of("Program: void " + typeName(K.class) + ".foo()"), methodResults);
   }
 
   @Test
@@ -156,7 +168,7 @@
     }
   }
 
-  public static class Main implements I, K {
+  public static class Main implements I, J, K {
 
     public static void main(String[] args) {
       new Main().foo();
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleLibraryPartialTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleLibraryPartialTest.java
index c840cc2..bc40110 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleLibraryPartialTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleLibraryPartialTest.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -13,13 +15,20 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 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.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,13 +79,34 @@
         computeAppViewWithClassHierarchy(
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
-    DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    DexItemFactory factory = appInfo.dexItemFactory();
+    DexMethod method = buildNullaryVoidMethod(Main.class, "foo", factory);
+    DexClass mainClass = appInfo.definitionFor(factory.createType(descriptor(Main.class)));
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    Set<DexType> failingTypesResult = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          if (result.isSingleResolution()) {
+            SingleResolutionResult<?> resolution = result.asSingleResolution();
+            methodResults.add(
+                (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                    + resolution.getResolvedMethod().getReference().toString());
+          } else {
+            assertTrue(result.isNoSuchMethodErrorResult(mainClass, appInfo));
+            methodResults.add(typeName(NoSuchMethodError.class));
+            result
+                .asFailedResolution()
+                .forEachFailureDependency(failingTypesResult::add, failingMethod -> fail());
+          }
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Library: void " + typeName(I.class) + ".foo()", typeName(NoSuchMethodError.class)),
+        methodResults);
+    assertEquals(ImmutableSet.of(factory.createType(descriptor(I.class))), failingTypesResult);
   }
 
   @Test
@@ -93,7 +123,7 @@
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
+    // TODO(b/230289235): Extend resolution to support multiple definition results.
     runTest(testForD8(parameters.getBackend()))
         .assertFailureWithErrorThatThrowsIf(
             !parameters.canUseDefaultAndStaticInterfaceMethods(),
@@ -106,7 +136,7 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
+    // TODO(b/230289235): Extend resolution to support multiple definition results.
     runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class))
         .assertFailureWithErrorThatThrows(NoSuchMethodError.class);
   }
diff --git a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleProgramPartialTest.java b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleProgramPartialTest.java
index de9876c..ad45b89 100644
--- a/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleProgramPartialTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/duplicatedefinitions/MaximallySpecificSingleProgramPartialTest.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.resolution.duplicatedefinitions;
 
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -13,13 +15,20 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.errors.Unreachable;
 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.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -68,13 +77,34 @@
         computeAppViewWithClassHierarchy(
             builder.build(), null, options -> options.loadAllClassDefinitions = true);
     AppInfoWithClassHierarchy appInfo = appView.appInfo();
-    DexMethod method = buildNullaryVoidMethod(Main.class, "foo", appInfo.dexItemFactory());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
-    assertThrows(
-        Unreachable.class,
-        () -> {
-          appInfo.unsafeResolveMethodDueToDexFormat(method);
+    DexItemFactory factory = appInfo.dexItemFactory();
+    DexMethod method = buildNullaryVoidMethod(Main.class, "foo", factory);
+    DexClass mainClass = appInfo.definitionFor(factory.createType(descriptor(Main.class)));
+    MethodResolutionResult methodResolutionResult =
+        appInfo.unsafeResolveMethodDueToDexFormat(method);
+    assertTrue(methodResolutionResult.isMultiMethodResolutionResult());
+    Set<String> methodResults = new HashSet<>();
+    Set<DexType> failingTypesResult = new HashSet<>();
+    methodResolutionResult.forEachMethodResolutionResult(
+        result -> {
+          if (result.isSingleResolution()) {
+            SingleResolutionResult<?> resolution = result.asSingleResolution();
+            methodResults.add(
+                (resolution.getResolvedHolder().isProgramClass() ? "Program: " : "Library: ")
+                    + resolution.getResolvedMethod().getReference().toString());
+          } else {
+            assertTrue(result.isNoSuchMethodErrorResult(mainClass, appInfo));
+            methodResults.add(typeName(NoSuchMethodError.class));
+            result
+                .asFailedResolution()
+                .forEachFailureDependency(failingTypesResult::add, failingMethod -> fail());
+          }
         });
+    assertEquals(
+        ImmutableSet.of(
+            "Program: void " + typeName(I.class) + ".foo()", typeName(NoSuchMethodError.class)),
+        methodResults);
+    assertEquals(ImmutableSet.of(factory.createType(descriptor(I.class))), failingTypesResult);
   }
 
   @Test
@@ -90,7 +120,7 @@
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
+    // TODO(b/230289235): Extend to support multiple definition results.
     runTest(testForD8(parameters.getBackend()))
         .assertFailureWithErrorThatThrowsIf(
             parameters.canUseDefaultAndStaticInterfaceMethods(), NoSuchMethodError.class)
@@ -100,7 +130,7 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/214382176): Extend resolution to support multiple definition results.
+    // TODO(b/230289235): Extend to support multiple definition results.
     runTest(testForR8(parameters.getBackend()).addKeepMainRule(Main.class))
         .assertFailureWithErrorThatThrowsIf(
             parameters.canUseDefaultAndStaticInterfaceMethods(), NoSuchMethodError.class)
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
index 31e9e9e..7e76e3e 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
@@ -5,6 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestAppViewBuilder;
@@ -57,7 +58,8 @@
     Set<String> holders = new HashSet<>();
     resolutionResult
         .asFailedResolution()
-        .forEachFailureDependency(target -> holders.add(target.getHolderType().toSourceString()));
+        .forEachFailureDependency(
+            type -> fail(), target -> holders.add(target.getHolderType().toSourceString()));
     assertEquals(ImmutableSet.of(L.class.getTypeName(), R.class.getTypeName()), holders);
   }
 
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
index d5aefd7..b3b1f8e 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
@@ -6,6 +6,7 @@
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestAppViewBuilder;
@@ -81,7 +82,8 @@
         Set<String> holders = new HashSet<>();
         resolutionResult
             .asFailedResolution()
-            .forEachFailureDependency(m -> holders.add(m.getHolderType().toSourceString()));
+            .forEachFailureDependency(
+                type -> fail(), m -> holders.add(m.getHolderType().toSourceString()));
         assertEquals(ImmutableSet.of(I.class.getTypeName(), J.class.getTypeName()), holders);
       }
     }
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java b/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java
index d0c2f5c..8494f39 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java
@@ -71,6 +71,7 @@
             .setMinApi(parameters.getApiLevel())
             .addDontWarn(A.class, B.class)
             .addKeepMainRule(Main.class)
+            .addOptionsModification(options -> options.loadAllClassDefinitions = true)
             .noMinification()
             .allowUnusedDontWarnPatterns();
     byte[] libraryA = getAFromClassTestParam(this.aInLibrary);
@@ -89,10 +90,10 @@
     R8TestRunResult runResult = compileResult.run(parameters.getRuntime(), Main.class);
     if (isExpectedToFailWithNoClassDefError()) {
       runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
-    } else if (isExpectedToFailWithNoSuchMethodError()) {
-      runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
     } else if (isExpectedToFailWithICCE()) {
       runResult.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+    } else if (isExpectedToFailWithNoSuchMethodError()) {
+      runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
     } else if (isDefinedOnAProgram() || (isDefinedOnALibrary() && !isAInProgram())) {
       runResult.assertSuccessWithOutputLines("A::foo");
     } else {
@@ -144,7 +145,7 @@
       return true;
     }
     if (notDefinedInProgram) {
-      // TODO(b/214382176): Currently, a program definition will shadow the library definition and
+      // TODO(b/230289235): Currently, a program definition will shadow the library definition and
       //  R8 will optimize the interfaces away.
       if (isDefinedOnALibrary() && isDefinedOnBLibrary()) {
         return isAInProgram() && isBInProgram();
@@ -154,6 +155,15 @@
         assert isDefinedOnBLibrary();
         return isBInProgram();
       }
+    } else {
+      // if the library definition is overriding the program definition and there is no definition
+      // then we also fail.
+      if (isDefinedOnAProgram() && isAInLibrary() && !isDefinedOnALibrary()) {
+        return true;
+      }
+      if (isDefinedOnBProgram() && isBInLibrary() && !isDefinedOnBLibrary()) {
+        return true;
+      }
     }
     return false;
   }