Dispatch classes for static library interface method calls

As described in b/76413326 presence of static calls to an interface
methods causes class verification failure on pre-L devices. This CL
solves this problem by creating a separate dispatch class for each
library interfaces we saw at least one static call for. This way
only dispatch class will be rejected by the verifier, but not the
app class making now indirect call.

Bug: 76413326
Change-Id: I5ccb419475411e6ded5cb64ce69151dd5c37a722
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 5dfef0c..3eda1b7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -84,6 +84,12 @@
     }
   }
 
+  public boolean isInvokeSuper(DexType clazz) {
+    return opcode == Opcodes.INVOKESPECIAL &&
+        method.holder != clazz &&
+        !method.name.toString().equals(Constants.INSTANCE_INITIALIZER_NAME);
+  }
+
   @Override
   public boolean canThrow() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index c540d24..91fefa9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -648,6 +648,9 @@
   }
 
   public static class OptimizationInfo {
+    public enum InlinePreference {
+      NeverInline, ForceInline, Default
+    }
 
     private int returnedArgument = -1;
     private boolean neverReturnsNull = false;
@@ -655,7 +658,7 @@
     private boolean returnsConstant = false;
     private long returnedConstant = 0;
     private boolean publicized = false;
-    private boolean forceInline = false;
+    private InlinePreference inlining = InlinePreference.Default;
     private boolean useIdentifierNameString = false;
     private boolean checksNullReceiverBeforeAnySideEffect = false;
     private boolean triggersClassInitBeforeAnySideEffect = false;
@@ -677,7 +680,7 @@
       returnsConstant = template.returnsConstant;
       returnedConstant = template.returnedConstant;
       publicized = template.publicized;
-      forceInline = template.forceInline;
+      inlining = template.inlining;
       useIdentifierNameString = template.useIdentifierNameString;
       checksNullReceiverBeforeAnySideEffect = template.checksNullReceiverBeforeAnySideEffect;
       trivialInitializerInfo = template.trivialInitializerInfo;
@@ -754,7 +757,11 @@
     }
 
     public boolean forceInline() {
-      return forceInline;
+      return inlining == InlinePreference.ForceInline;
+    }
+
+    public boolean neverInline() {
+      return inlining == InlinePreference.NeverInline;
     }
 
     public boolean useIdentifierNameString() {
@@ -790,11 +797,21 @@
     }
 
     private void markForceInline() {
-      forceInline = true;
+      // For concurrent scenarios we should allow the flag to be already set
+      assert inlining == InlinePreference.Default || inlining == InlinePreference.ForceInline;
+      inlining = InlinePreference.ForceInline;
     }
 
     private void unsetForceInline() {
-      forceInline = false;
+      // For concurrent scenarios we should allow the flag to be already unset
+      assert inlining == InlinePreference.Default || inlining == InlinePreference.ForceInline;
+      inlining = InlinePreference.Default;
+    }
+
+    private void markNeverInline() {
+      // For concurrent scenarios we should allow the flag to be already set
+      assert inlining == InlinePreference.Default || inlining == InlinePreference.NeverInline;
+      inlining = InlinePreference.NeverInline;
     }
 
     private void markPublicized() {
@@ -882,6 +899,10 @@
     ensureMutableOI().markForceInline();
   }
 
+  synchronized public void markNeverInline() {
+    ensureMutableOI().markNeverInline();
+  }
+
   public synchronized void unsetForceInline() {
     ensureMutableOI().unsetForceInline();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 3883f4d..9928f83 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -64,6 +65,7 @@
 public final class InterfaceMethodRewriter {
 
   // Public for testing.
+  public static final String DISPATCH_CLASS_NAME_SUFFIX = "$-DC";
   public static final String COMPANION_CLASS_NAME_SUFFIX = "$-CC";
   public static final String DEFAULT_METHOD_PREFIX = "$default$";
   public static final String PRIVATE_METHOD_PREFIX = "$private$";
@@ -74,15 +76,18 @@
 
   // All forwarding methods generated during desugaring. We don't synchronize access
   // to this collection since it is only filled in ClassProcessor running synchronously.
-  private final Set<DexEncodedMethod> forwardingMethods = Sets.newIdentityHashSet();
+  private final Set<DexEncodedMethod> synthesizedMethods = Sets.newIdentityHashSet();
 
   // Caches default interface method info for already processed interfaces.
   private final Map<DexType, DefaultMethodsHelper.Collection> cache = new ConcurrentHashMap<>();
 
+  /** Interfaces requiring dispatch classes created. */
+  private final Set<DexLibraryClass> requiredDispatchClasses = Sets.newConcurrentHashSet();
+
   /**
    * A set of dexitems we have reported missing to dedupe warnings.
    */
-  private final Set<DexItem> reportedMissing = Sets.newIdentityHashSet();
+  private final Set<DexItem> reportedMissing = Sets.newConcurrentHashSet();
 
   /**
    * Defines a minor variation in desugaring.
@@ -108,7 +113,7 @@
   // Rewrites the references to static and default interface methods.
   // NOTE: can be called for different methods concurrently.
   public void rewriteMethodReferences(DexEncodedMethod encodedMethod, IRCode code) {
-    if (forwardingMethods.contains(encodedMethod)) {
+    if (synthesizedMethods.contains(encodedMethod)) {
       return;
     }
 
@@ -142,15 +147,32 @@
             // exception but we can not report it as error since it can also be the intended
             // behavior.
             warnMissingType(encodedMethod.method, method.holder);
-          } else if (clazz.isInterface() && !clazz.isLibraryClass()) {
-            // NOTE: we intentionally don't desugar static calls into static interface
-            // methods coming from android.jar since it is only possible in case v24+
-            // version of android.jar is provided.
-            // WARNING: This may result in incorrect code on older platforms!
-            // Retarget call to an appropriate method of companion class.
-            instructions.replaceCurrentInstruction(
-                new InvokeStatic(staticAsMethodOfCompanionClass(method),
-                    invokeStatic.outValue(), invokeStatic.arguments()));
+          } else if (clazz.isInterface()) {
+            if (clazz.isLibraryClass()) {
+              // NOTE: we intentionally don't desugar static calls into static interface
+              // methods coming from android.jar since it is only possible in case v24+
+              // version of android.jar is provided.
+              //
+              // We assume such calls are properly guarded by if-checks like
+              //    'if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.XYZ) { ... }'
+              //
+              // WARNING: This may result in incorrect code on older platforms!
+              // Retarget call to an appropriate method of companion class.
+
+              if (!options.canLeaveStaticInterfaceMethodInvokes()) {
+                // On pre-L devices static calls to interface methods result in verifier
+                // rejecting the whole class. We have to create special dispatch classes,
+                // so the user class is not rejected because it make this call directly.
+                instructions.replaceCurrentInstruction(
+                    new InvokeStatic(staticAsMethodOfDispatchClass(method),
+                        invokeStatic.outValue(), invokeStatic.arguments()));
+                requiredDispatchClasses.add(clazz.asLibraryClass());
+              }
+            } else {
+              instructions.replaceCurrentInstruction(
+                  new InvokeStatic(staticAsMethodOfCompanionClass(method),
+                      invokeStatic.outValue(), invokeStatic.arguments()));
+            }
           }
           continue;
         }
@@ -168,6 +190,10 @@
             // NOTE: we intentionally don't desugar super calls into interface methods
             // coming from android.jar since it is only possible in case v24+ version
             // of android.jar is provided.
+            //
+            // We assume such calls are properly guarded by if-checks like
+            //    'if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.XYZ) { ... }'
+            //
             // WARNING: This may result in incorrect code on older platforms!
             // Retarget call to an appropriate method of companion class.
             DexMethod amendedMethod = amendDefaultMethod(
@@ -263,6 +289,15 @@
     return factory.createType(ccTypeDescriptor);
   }
 
+  // Gets the forwarding class for the interface `type`.
+  final DexType getDispatchClassType(DexType type) {
+    assert type.isClassType();
+    String descriptor = type.descriptor.toString();
+    String dcTypeDescriptor = descriptor.substring(0, descriptor.length() - 1)
+        + DISPATCH_CLASS_NAME_SUFFIX + ";";
+    return factory.createType(dcTypeDescriptor);
+  }
+
   // Checks if `type` is a companion class.
   private boolean isCompanionClassType(DexType type) {
     return type.descriptor.toString().endsWith(COMPANION_CLASS_NAME_SUFFIX + ";");
@@ -287,6 +322,16 @@
     return factory.createMethod(getCompanionClassType(method.holder), method.proto, method.name);
   }
 
+  // Represent a static interface method as a method of dispatch class.
+  final DexMethod staticAsMethodOfDispatchClass(DexMethod method) {
+    return factory.createMethod(getDispatchClassType(method.holder), method.proto, method.name);
+  }
+
+  // Checks if the type ends with dispatch class suffix.
+  public static boolean hasDispatchClassSuffix(DexType clazz) {
+    return clazz.getName().endsWith(DISPATCH_CLASS_NAME_SUFFIX);
+  }
+
   private DexMethod instanceAsMethodOfCompanionClass(DexMethod method, String prefix) {
     // Add an implicit argument to represent the receiver.
     DexType[] params = method.proto.parameters.values;
@@ -327,21 +372,21 @@
   public void desugarInterfaceMethods(Builder<?> builder, Flavor flavour) {
     // Process all classes first. Add missing forwarding methods to
     // replace desugared default interface methods.
-    forwardingMethods.addAll(processClasses(builder, flavour));
+    synthesizedMethods.addAll(processClasses(builder, flavour));
 
-    // Process interfaces, create companion class if needed, move static methods
-    // to companion class, copy default interface methods to companion classes,
-    // make original default methods abstract, remove bridge methods.
-    Map<DexProgramClass, DexProgramClass> companionClasses =
-        processInterfaces(builder, flavour);
+    // Process interfaces, create companion or dispatch class if needed, move static
+    // methods to companion class, copy default interface methods to companion classes,
+    // make original default methods abstract, remove bridge methods, create dispatch
+    // classes if needed.
+    Map<DexType, DexProgramClass> synthesizedClasses = processInterfaces(builder, flavour);
 
-    for (Map.Entry<DexProgramClass, DexProgramClass> entry : companionClasses.entrySet()) {
+    for (Map.Entry<DexType, DexProgramClass> entry : synthesizedClasses.entrySet()) {
       // Don't need to optimize synthesized class since all of its methods
       // are just moved from interfaces and don't need to be re-processed.
-      builder.addSynthesizedClass(entry.getValue(), isInMainDexList(entry.getKey().type));
+      builder.addSynthesizedClass(entry.getValue(), isInMainDexList(entry.getKey()));
     }
 
-    for (DexEncodedMethod method : forwardingMethods) {
+    for (DexEncodedMethod method : synthesizedMethods) {
       converter.optimizeSynthesizedMethod(method);
     }
 
@@ -351,7 +396,8 @@
 
   private void clear() {
     this.cache.clear();
-    this.forwardingMethods.clear();
+    this.synthesizedMethods.clear();
+    this.requiredDispatchClasses.clear();
   }
 
   private static boolean shouldProcess(
@@ -360,21 +406,23 @@
         && clazz.isInterface() == mustBeInterface;
   }
 
-  private Map<DexProgramClass, DexProgramClass> processInterfaces(
-      Builder<?> builder, Flavor flavour) {
+  private Map<DexType, DexProgramClass> processInterfaces(Builder<?> builder, Flavor flavour) {
     InterfaceProcessor processor = new InterfaceProcessor(this);
     for (DexProgramClass clazz : builder.getProgramClasses()) {
       if (shouldProcess(clazz, flavour, true)) {
         processor.process(clazz.asProgramClass());
       }
     }
+    for (DexLibraryClass iface : requiredDispatchClasses) {
+      synthesizedMethods.addAll(processor.process(iface));
+    }
     if (converter.enableWholeProgramOptimizations &&
         (!processor.methodsWithMovedCode.isEmpty() || !processor.movedMethods.isEmpty())) {
       converter.setGraphLense(
           new InterfaceMethodDesugaringLense(processor.movedMethods,
               processor.methodsWithMovedCode, converter.getGraphLense(), factory));
     }
-    return processor.companionClasses;
+    return processor.syntheticClasses;
   }
 
   private Set<DexEncodedMethod> processClasses(Builder<?> builder, Flavor flavour) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index e5900d9..608e430 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -4,6 +4,11 @@
 
 package com.android.tools.r8.ir.desugar;
 
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.InvokeSuper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.ClassAccessFlags;
@@ -13,11 +18,16 @@
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
+import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -39,8 +49,8 @@
 // Also moves static interface methods into a companion class.
 final class InterfaceProcessor {
   private final InterfaceMethodRewriter rewriter;
-  // All created companion classes indexed by interface classes.
-  final Map<DexProgramClass, DexProgramClass> companionClasses = new IdentityHashMap<>();
+  // All created companion and dispatch classes indexed by interface type.
+  final Map<DexType, DexProgramClass> syntheticClasses = new IdentityHashMap<>();
 
   final BiMap<DexMethod, DexMethod> movedMethods = HashBiMap.create();
   final Map<DexEncodedMethod, DexEncodedMethod> methodsWithMovedCode = new IdentityHashMap<>();
@@ -59,6 +69,11 @@
     List<DexEncodedMethod> remainingMethods = new ArrayList<>();
     for (DexEncodedMethod virtual : iface.virtualMethods()) {
       if (rewriter.isDefaultMethod(virtual)) {
+        if (!canMoveToCompanionClass(virtual)) {
+          throw new CompilationError("One or more instruction is preventing default interface "
+              + "method from being desugared: " + virtual.method.toSourceString(), iface.origin);
+        }
+
         // Create a new method in a companion class to represent default method implementation.
         DexMethod companionMethod = rewriter.defaultAsMethodOfCompanionClass(virtual.method);
 
@@ -193,7 +208,92 @@
             DexEncodedMethod.EMPTY_ARRAY,
             rewriter.factory.getSkipNameValidationForTesting(),
             Collections.singletonList(iface));
-    companionClasses.put(iface, companionClass);
+    syntheticClasses.put(iface.type, companionClass);
+  }
+
+  List<DexEncodedMethod> process(DexLibraryClass iface) {
+    assert iface.isInterface();
+
+    // The list of methods to be created in dispatch class.
+    // NOTE: we do NOT check static methods being actually called on the interface against
+    //       static methods actually existing in that interface. It is essential for supporting
+    //       D8 desugaring when each class may be dexed separately since it allows us to assume
+    //       that all synthesized dispatch classes have the same set of methods so we don't
+    //       need to merge them.
+    List<DexEncodedMethod> dispatchMethods = new ArrayList<>();
+
+    // Process public static methods, for each of them create a method dispatching the call to it.
+    for (DexEncodedMethod direct : iface.directMethods()) {
+      MethodAccessFlags originalAccessFlags = direct.accessFlags;
+      if (!originalAccessFlags.isStatic() || !originalAccessFlags.isPublic()) {
+        // We assume only public static methods of library interfaces can be called
+        // from program classes, since there should not be protected or package private
+        // static methods on interfaces.
+        assert !originalAccessFlags.isStatic() || originalAccessFlags.isPrivate();
+        continue;
+      }
+
+      assert !rewriter.factory.isClassConstructor(direct.method);
+
+      DexMethod origMethod = direct.method;
+      DexEncodedMethod newEncodedMethod = new DexEncodedMethod(
+          rewriter.staticAsMethodOfDispatchClass(origMethod),
+          MethodAccessFlags.fromSharedAccessFlags(
+              Constants.ACC_PUBLIC | Constants.ACC_STATIC | Constants.ACC_SYNTHETIC, false),
+          DexAnnotationSet.empty(),
+          ParameterAnnotationsList.empty(),
+          new SynthesizedCode(new ForwardMethodSourceCode(
+              null, origMethod.proto, null, origMethod, Type.STATIC)));
+      newEncodedMethod.markNeverInline();
+      dispatchMethods.add(newEncodedMethod);
+    }
+
+    ClassAccessFlags dispatchClassFlags =
+        ClassAccessFlags.fromSharedAccessFlags(
+            Constants.ACC_PUBLIC | Constants.ACC_FINAL | Constants.ACC_SYNTHETIC);
+
+    // Create dispatch class.
+    DexType dispatchClassType = rewriter.getDispatchClassType(iface.type);
+    DexProgramClass dispatchClass =
+        new DexProgramClass(
+            dispatchClassType,
+            null,
+            new SynthesizedOrigin("interface dispatch", getClass()),
+            dispatchClassFlags,
+            rewriter.factory.objectType,
+            DexTypeList.empty(),
+            iface.sourceFile,
+            null,
+            Collections.emptyList(),
+            DexAnnotationSet.empty(),
+            DexEncodedField.EMPTY_ARRAY,
+            DexEncodedField.EMPTY_ARRAY,
+            dispatchMethods.toArray(new DexEncodedMethod[dispatchMethods.size()]),
+            DexEncodedMethod.EMPTY_ARRAY,
+            rewriter.factory.getSkipNameValidationForTesting(),
+            Collections.emptyList());
+    syntheticClasses.put(iface.type, dispatchClass);
+    return dispatchMethods;
+  }
+
+  private boolean canMoveToCompanionClass(DexEncodedMethod method) {
+    Code code = method.getCode();
+    assert code != null;
+    if (code.isDexCode()) {
+      for (Instruction insn : code.asDexCode().instructions) {
+        if (insn instanceof InvokeSuper) {
+          return false;
+        }
+      }
+    } else {
+      assert code.isCfCode();
+      for (CfInstruction insn : code.asCfCode().getInstructions()) {
+        if (insn instanceof CfInvoke && ((CfInvoke) insn).isInvokeSuper(method.method.holder)) {
+          return false;
+        }
+      }
+    }
+    return true;
   }
 
   // Returns true if the given interface method must be kept on [iface] after moving its
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 823de39..eed13a8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -160,6 +160,10 @@
 
   private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
       Reason reason) {
+    if (candidate.getOptimizationInfo().neverInline()) {
+      return false;
+    }
+
     if (method == candidate) {
       // Cannot handle recursive inlining at this point.
       // Force inlined method should never be recursive.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index 07100fa..ca82166 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -49,6 +49,10 @@
     }
 
     assert method != info.target;
+    // Even though call to Inliner::performForcedInlining is supposed to be controlled by
+    // the caller, it's still suspicious if we want to force inline something that is marked
+    // with neverInline() flag.
+    assert !info.target.getOptimizationInfo().neverInline();
     return new InlineAction(info.target, invoke, Reason.FORCE);
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index d3c9e6d..9561d0e 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -482,6 +482,10 @@
     return hasMinApi(AndroidApiLevel.N);
   }
 
+  public boolean canLeaveStaticInterfaceMethodInvokes() {
+    return hasMinApi(AndroidApiLevel.L);
+  }
+
   public boolean canUsePrivateInterfaceMethods() {
     return hasMinApi(AndroidApiLevel.N);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
index 5184274..ee510dd 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -52,15 +53,24 @@
   }
 
   public static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
-    // Currently only allow collapsing synthetic lambda classes.
+    // Currently only allow collapsing synthetic lambda or dispatch classes.
     if (a.originatesFromDexResource()
         && b.originatesFromDexResource()
         && a.accessFlags.isSynthetic()
         && b.accessFlags.isSynthetic()
-        && LambdaRewriter.hasLambdaClassPrefix(a.type)
-        && LambdaRewriter.hasLambdaClassPrefix(b.type)) {
+        && (bothAreLambdaClasses(a.type, b.type) || bothAreDispatchClasses(a.type, b.type))) {
       return a;
     }
     throw new CompilationError("Program type already present: " + a.type.toSourceString());
   }
+
+  private static boolean bothAreLambdaClasses(DexType a, DexType b) {
+    return LambdaRewriter.hasLambdaClassPrefix(a) &&
+        LambdaRewriter.hasLambdaClassPrefix(b);
+  }
+
+  private static boolean bothAreDispatchClasses(DexType a, DexType b) {
+    return InterfaceMethodRewriter.hasDispatchClassSuffix(a) &&
+        InterfaceMethodRewriter.hasDispatchClassSuffix(b);
+  }
 }
diff --git a/src/test/examplesAndroidO/interfacedispatchclasses/Caller1.java b/src/test/examplesAndroidO/interfacedispatchclasses/Caller1.java
new file mode 100644
index 0000000..f254272
--- /dev/null
+++ b/src/test/examplesAndroidO/interfacedispatchclasses/Caller1.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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 interfacedispatchclasses;
+
+import java.util.Comparator;
+
+public class Caller1 {
+  public static synchronized void run(boolean doCall) {
+    System.out.println("Caller1::run(boolean)");
+    if (doCall) {
+      Comparator<String> comparator = Comparator.naturalOrder();
+      System.out.println(comparator.compare("A", "B"));
+      System.out.println(comparator.compare("B", "B"));
+      System.out.println(comparator.compare("B", "C"));
+    }
+  }
+}
diff --git a/src/test/examplesAndroidO/interfacedispatchclasses/Caller2.java b/src/test/examplesAndroidO/interfacedispatchclasses/Caller2.java
new file mode 100644
index 0000000..58a887f
--- /dev/null
+++ b/src/test/examplesAndroidO/interfacedispatchclasses/Caller2.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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 interfacedispatchclasses;
+
+import java.util.Comparator;
+
+public class Caller2 {
+  public static synchronized void run(boolean doCall) {
+    System.out.println("Caller2::run(boolean)");
+    if (doCall) {
+      Comparator<String> comparator = Comparator.naturalOrder();
+      System.out.println(comparator.compare("B", "C"));
+      System.out.println(comparator.compare("A", "B"));
+      System.out.println(comparator.compare("B", "B"));
+    }
+  }
+}
diff --git a/src/test/examplesAndroidO/interfacedispatchclasses/Caller3.java b/src/test/examplesAndroidO/interfacedispatchclasses/Caller3.java
new file mode 100644
index 0000000..003d258
--- /dev/null
+++ b/src/test/examplesAndroidO/interfacedispatchclasses/Caller3.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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 interfacedispatchclasses;
+
+import java.util.Comparator;
+
+public class Caller3 {
+  public static synchronized void run(boolean doCall) {
+    System.out.println("Caller3::run(boolean)");
+    if (doCall) {
+      Comparator<String> comparator = Comparator.naturalOrder();
+      System.out.println(comparator.compare("B", "B"));
+      System.out.println(comparator.compare("B", "C"));
+      System.out.println(comparator.compare("A", "B"));
+    }
+  }
+}
diff --git a/src/test/examplesAndroidO/interfacedispatchclasses/TestInterfaceDispatchClasses.java b/src/test/examplesAndroidO/interfacedispatchclasses/TestInterfaceDispatchClasses.java
new file mode 100644
index 0000000..98c12da
--- /dev/null
+++ b/src/test/examplesAndroidO/interfacedispatchclasses/TestInterfaceDispatchClasses.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2018, 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 interfacedispatchclasses;
+
+public class TestInterfaceDispatchClasses {
+  public static void main(String[] args) {
+    System.out.println("TestInterfaceDispatchClasses::main(String[])");
+    System.out.println(args[0]);
+    boolean doCall = args[0].equals("true");
+    Caller1.run(doCall);
+    Caller2.run(doCall);
+    Caller3.run(doCall);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 7a44516..525d0e6 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -6,6 +6,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
@@ -14,6 +15,8 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
@@ -35,14 +38,26 @@
 
   protected void ensureSameOutput(String main, AndroidApiLevel apiLevel, byte[]... classes)
       throws Exception {
+    ensureSameOutput(main, apiLevel, Collections.emptyList(), classes);
+  }
+
+  protected void ensureSameOutput(String main, AndroidApiLevel apiLevel,
+      List<String> args, byte[]... classes) throws Exception {
     AndroidApp app = buildAndroidApp(classes);
     Consumer<InternalOptions> setMinApiLevel = o -> o.minApiLevel = apiLevel.getLevel();
-    ProcessResult javaResult = runOnJavaRaw(main, classes);
-    ProcessResult d8Result = runOnArtRaw(compileWithD8(app, setMinApiLevel), main);
-    ProcessResult r8Result = runOnArtRaw(compileWithR8(app, setMinApiLevel), main);
+    ProcessResult javaResult = runOnJavaRaw(main, Arrays.asList(classes), args);
+    Consumer<ArtCommandBuilder> cmdBuilder = builder -> {
+      for (String arg : args) {
+        builder.appendProgramArgument(arg);
+      }
+    };
+    ProcessResult d8Result = runOnArtRaw(
+        compileWithD8(app, setMinApiLevel), main, cmdBuilder, null);
+    ProcessResult r8Result = runOnArtRaw(
+        compileWithR8(app, setMinApiLevel), main, cmdBuilder, null);
     ProcessResult r8ShakenResult = runOnArtRaw(
         compileWithR8(app, keepMainProguardConfiguration(main) + "-dontobfuscate\n",
-            setMinApiLevel), main);
+            setMinApiLevel), main, cmdBuilder, null);
     Assert.assertEquals(javaResult.stdout, d8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8ShakenResult.stdout);
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index caa1d48..37ffe9a 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -279,6 +279,29 @@
   }
 
   @Test
+  public void dexPerClassFileWithDispatchMethods() throws Throwable {
+    String testName = "dexPerClassFileWithDispatchMethods";
+    String testPackage = "interfacedispatchclasses";
+    String mainClass = "TestInterfaceDispatchClasses";
+
+    Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
+
+    D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
+    test.withInterfaceMethodDesugaring(OffOrAuto.Auto);
+
+    ProgramResource mergedFromCompiledSeparately =
+        test.mergeClassFiles(
+            Lists.newArrayList(test.compileClassesSeparately(inputJarFile).values()), null);
+    ProgramResource mergedFromCompiledTogether =
+        test.mergeClassFiles(
+            Lists.newArrayList(test.compileClassesTogether(inputJarFile, null).values()), null);
+
+    Assert.assertArrayEquals(
+        readResource(mergedFromCompiledSeparately),
+        readResource(mergedFromCompiledTogether));
+  }
+
+  @Test
   public void dexPerClassFileOutputFiles() throws Throwable {
     String testName = "dexPerClassFileNoDesugaring";
     String testPackage = "incremental";
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index f638575..1f0c464 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
@@ -33,6 +34,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -58,6 +60,7 @@
     final String testName;
     final String packageName;
     final String mainClass;
+    final List<String> args = new ArrayList<>();
 
     AndroidApiLevel androidJarVersion = null;
 
@@ -121,6 +124,11 @@
       return self();
     }
 
+    C withArg(String arg) {
+      args.add(arg);
+      return self();
+    }
+
     void combinedOptionConsumer(InternalOptions options) {
       for (Consumer<InternalOptions> consumer : optionConsumers) {
         consumer.accept(options);
@@ -161,7 +169,7 @@
         }
       }
 
-      execute(testName, qualifiedMainClass, new Path[]{inputFile}, new Path[]{out});
+      execute(testName, qualifiedMainClass, new Path[]{inputFile}, new Path[]{out}, args);
     }
 
     abstract C withMinApiLevel(AndroidApiLevel minApiLevel);
@@ -456,6 +464,15 @@
         "interfacemethods.I2");
   }
 
+  @Test
+  public void testInterfaceDispatchClasses() throws Throwable {
+    test("interfacedispatchclasses", "interfacedispatchclasses", "TestInterfaceDispatchClasses")
+        .withMinApiLevel(AndroidApiLevel.K) // K to create dispatch classes
+        .withAndroidJar(AndroidApiLevel.O)
+        .withArg(String.valueOf(ToolHelper.getMinApiLevelForDexVm().getLevel() >= 24))
+        .run();
+  }
+
   private void testIntermediateWithMainDexList(
       String packageName,
       int expectedMainDexListSize,
@@ -488,14 +505,13 @@
     full.build(input, fullDexes);
 
     // Builds with intermediate in both output mode.
-    Path dexesThroughIndexedIntermediate =
-        buildDexThroughIntermediate(packageName, input, OutputMode.DexIndexed, minApi, mainDexClasses);
-    Path dexesThroughFilePerInputClassIntermediate =
-        buildDexThroughIntermediate(packageName, input, OutputMode.DexFilePerClassFile, minApi,
-            mainDexClasses);
+    Path dexesThroughIndexedIntermediate = buildDexThroughIntermediate(
+        packageName, input, OutputMode.DexIndexed, minApi, mainDexClasses);
+    Path dexesThroughFilePerInputClassIntermediate = buildDexThroughIntermediate(
+        packageName, input, OutputMode.DexFilePerClassFile, minApi, mainDexClasses);
 
     // Collect main dex types.
-    CodeInspector fullInspector =  getMainDexInspector(fullDexes);
+    CodeInspector fullInspector = getMainDexInspector(fullDexes);
     CodeInspector indexedIntermediateInspector =
         getMainDexInspector(dexesThroughIndexedIntermediate);
     CodeInspector filePerInputClassIntermediateInspector =
@@ -552,10 +568,13 @@
   abstract RunExamplesAndroidOTest<B>.TestRunner<?> test(String testName, String packageName,
       String mainClass);
 
-  void execute(
-      String testName,
-      String qualifiedMainClass, Path[] jars, Path[] dexes)
-      throws IOException {
+  void execute(String testName,
+      String qualifiedMainClass, Path[] jars, Path[] dexes) throws IOException {
+    execute(testName, qualifiedMainClass, jars, dexes, Collections.emptyList());
+  }
+
+  void execute(String testName,
+      String qualifiedMainClass, Path[] jars, Path[] dexes, List<String> args) throws IOException {
 
     boolean expectedToFail = expectedToFail(testName);
     if (expectedToFail && !ToolHelper.compareAgaintsGoldenFiles()) {
@@ -564,10 +583,16 @@
     String output = ToolHelper.runArtNoVerificationErrors(
         Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
         qualifiedMainClass,
-        null);
+        builder -> {
+          for (String arg : args) {
+            builder.appendProgramArgument(arg);
+          }
+        });
     if (!expectedToFail && !skipRunningOnJvm(testName) && !ToolHelper.compareAgaintsGoldenFiles()) {
+      ArrayList<String> javaArgs = Lists.newArrayList(args);
+      javaArgs.add(0, qualifiedMainClass);
       ToolHelper.ProcessResult javaResult =
-          ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
+          ToolHelper.runJava(ImmutableList.copyOf(jars), javaArgs.toArray(new String[0]));
       assertEquals("JVM run failed", javaResult.exitCode, 0);
       assertTrue(
           "JVM output does not match art output.\n\tjvm: "
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index a622b6e..ebc278e 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.DataResourceProvider.Visitor;
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.code.Instruction;
@@ -486,11 +487,19 @@
   /**
    * Run application on the specified version of Art with the specified main class.
    */
-  protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass, DexVm version)
-      throws IOException {
+  protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass,
+      Consumer<ArtCommandBuilder> cmdBuilder, DexVm version) throws IOException {
     Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
     app.writeToZip(out, OutputMode.DexIndexed);
-    return ToolHelper.runArtRaw(ImmutableList.of(out.toString()), mainClass, null, version);
+    return ToolHelper.runArtRaw(ImmutableList.of(out.toString()), mainClass, cmdBuilder, version);
+  }
+
+  /**
+   * Run application on the specified version of Art with the specified main class.
+   */
+  protected ProcessResult runOnArtRaw(AndroidApp app, String mainClass, DexVm version)
+      throws IOException {
+    return runOnArtRaw(app, mainClass, null, version);
   }
 
   /**
@@ -590,12 +599,12 @@
   }
 
   protected ProcessResult runOnJavaRaw(String main, byte[]... classes) throws IOException {
-    return runOnJavaRaw(main, Arrays.asList(classes));
+    return runOnJavaRaw(main, Arrays.asList(classes), Collections.emptyList());
   }
 
-  protected ProcessResult runOnJavaRaw(String main, List<byte[]> classes) throws IOException {
-    Path file = writeToZip(classes);
-    return ToolHelper.runJavaNoVerify(file, main);
+  protected ProcessResult runOnJavaRaw(
+      String main, List<byte[]> classes, List<String> args) throws IOException {
+    return ToolHelper.runJavaNoVerify(Collections.singletonList(writeToZip(classes)), main, args);
   }
 
   private Path writeToZip(List<byte[]> classes) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 8337f6e..530518a 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -966,12 +966,6 @@
     return runJava(path, main);
   }
 
-  public static ProcessResult runJavaNoVerify(Class clazz) throws Exception {
-    String main = clazz.getCanonicalName();
-    Path path = getClassPathForTests();
-    return runJavaNoVerify(path, main);
-  }
-
   public static ProcessResult runJava(Path classpath, String... args) throws IOException {
     return runJava(ImmutableList.of(classpath), args);
   }
@@ -984,16 +978,24 @@
     return runProcess(builder);
   }
 
-  public static ProcessResult runJavaNoVerify(Path classpath, String mainClass)
-      throws IOException {
-    return runJavaNoVerify(ImmutableList.of(classpath), mainClass);
+  public static ProcessResult runJavaNoVerify(
+      Path classpath, String mainClass, String... args) throws IOException {
+    return runJavaNoVerify(
+        Collections.singletonList(classpath), mainClass, Lists.newArrayList(args));
   }
 
-  public static ProcessResult runJavaNoVerify(List<Path> classpath, String mainClass)
-      throws IOException {
+  public static ProcessResult runJavaNoVerify(
+      List<Path> classpath, String mainClass, String... args) throws IOException {
+    return runJavaNoVerify(classpath, mainClass, Lists.newArrayList(args));
+  }
+
+  public static ProcessResult runJavaNoVerify(
+      List<Path> classpath, String mainClass, List<String> args) throws IOException {
     String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
-    ProcessBuilder builder = new ProcessBuilder(
+    ArrayList<String> cmdline = Lists.newArrayList(
         getJavaExecutable(), "-cp", cp, "-noverify", mainClass);
+    cmdline.addAll(args);
+    ProcessBuilder builder = new ProcessBuilder(cmdline);
     return runProcess(builder);
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
index d2cf708..2b73eaa 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InterfaceMethodDesugaringTests.java
@@ -11,9 +11,22 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.VmTestRunner.IgnoreForRangeOfVmVersions;
+import com.android.tools.r8.desugaring.interfacemethods.default0.TestMainDefault0;
+import com.android.tools.r8.desugaring.interfacemethods.default1.Derived1;
+import com.android.tools.r8.desugaring.interfacemethods.default1.DerivedComparator1;
+import com.android.tools.r8.desugaring.interfacemethods.default1.TestMainDefault1;
+import com.android.tools.r8.desugaring.interfacemethods.default2.Derived2;
+import com.android.tools.r8.desugaring.interfacemethods.default2.DerivedComparator2;
+import com.android.tools.r8.desugaring.interfacemethods.default2.TestMainDefault2;
+import com.android.tools.r8.desugaring.interfacemethods.static0.TestMainStatic0;
+import com.android.tools.r8.desugaring.interfacemethods.static1.TestMainStatic1;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.objectweb.asm.ClassReader;
@@ -25,6 +38,11 @@
 @RunWith(VmTestRunner.class)
 public class InterfaceMethodDesugaringTests extends AsmTestBase {
 
+  private static List<String> getArgs(int startWith) {
+    return Collections.singletonList(
+        String.valueOf(ToolHelper.getMinApiLevelForDexVm().getLevel() >= startWith));
+  }
+
   @Test
   public void testInvokeSpecialToDefaultMethod() throws Exception {
     ensureSameOutput(
@@ -67,6 +85,94 @@
             com.android.tools.r8.desugaring.interfacemethods.test2.Test2.class));
   }
 
+  @Test
+  public void testInvokeStatic0a() throws Exception {
+    ensureSameOutput(
+        TestMainStatic0.class.getCanonicalName(),
+        AndroidApiLevel.K,
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainStatic0.class));
+  }
+
+  @Test
+  public void testInvokeStatic0b() throws Exception {
+    ensureSameOutput(
+        TestMainStatic0.class.getCanonicalName(),
+        ToolHelper.getMinApiLevelForDexVm(),
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainStatic0.class));
+  }
+
+  @Test
+  public void testInvokeStatic1a() throws Exception {
+    ensureSameOutput(
+        TestMainStatic1.class.getCanonicalName(),
+        AndroidApiLevel.K,
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainStatic1.class));
+  }
+
+  @Test
+  public void testInvokeStatic1b() throws Exception {
+    ensureSameOutput(
+        TestMainStatic1.class.getCanonicalName(),
+        ToolHelper.getMinApiLevelForDexVm(),
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainStatic1.class));
+  }
+
+  @Test
+  public void testInvokeDefault0a() throws Exception {
+    ensureSameOutput(
+        TestMainDefault0.class.getCanonicalName(),
+        AndroidApiLevel.K,
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainDefault0.class));
+  }
+
+  @Test
+  public void testInvokeDefault0b() throws Exception {
+    ensureSameOutput(
+        TestMainDefault0.class.getCanonicalName(),
+        ToolHelper.getMinApiLevelForDexVm(),
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainDefault0.class));
+  }
+
+  @Test(expected = CompilationError.class)
+  @IgnoreForRangeOfVmVersions(from = Version.V7_0_0, to = Version.DEFAULT) // No desugaring
+  public void testInvokeDefault1() throws Exception {
+    ensureSameOutput(
+        TestMainDefault1.class.getCanonicalName(),
+        ToolHelper.getMinApiLevelForDexVm(),
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainDefault1.class),
+        ToolHelper.getClassAsBytes(Derived1.class),
+        ToolHelper.getClassAsBytes(DerivedComparator1.class));
+  }
+
+  @Test()
+  public void testInvokeDefault2a() throws Exception {
+    ensureSameOutput(
+        TestMainDefault2.class.getCanonicalName(),
+        AndroidApiLevel.K,
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainDefault2.class),
+        ToolHelper.getClassAsBytes(Derived2.class),
+        ToolHelper.getClassAsBytes(DerivedComparator2.class));
+  }
+
+  @Test()
+  public void testInvokeDefault2b() throws Exception {
+    ensureSameOutput(
+        TestMainDefault2.class.getCanonicalName(),
+        ToolHelper.getMinApiLevelForDexVm(),
+        getArgs(AndroidApiLevel.N.getLevel()),
+        ToolHelper.getClassAsBytes(TestMainDefault2.class),
+        ToolHelper.getClassAsBytes(Derived2.class),
+        ToolHelper.getClassAsBytes(DerivedComparator2.class));
+  }
+
   private static class MutableInteger {
     int value;
   }
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default0/TestMainDefault0.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default0/TestMainDefault0.java
new file mode 100644
index 0000000..08b509c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default0/TestMainDefault0.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default0;
+
+import java.util.Comparator;
+
+public class TestMainDefault0 {
+  public static void main(String[] args) {
+    System.out.println("TestMainDefault0::test()");
+    System.out.println(args[0]);
+    if (args[0].equals("true")) {
+      Comparator<String> comparator = String::compareTo;
+      comparator = comparator.reversed();
+      System.out.println(comparator.compare("A", "B"));
+      System.out.println(comparator.compare("B", "B"));
+      System.out.println(comparator.compare("B", "C"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/Derived1.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/Derived1.java
new file mode 100644
index 0000000..2ecff49
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/Derived1.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default1;
+
+public class Derived1 implements DerivedComparator1<String> {
+  @Override
+  public int compare(String a, String b) {
+    return a.compareTo(b);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/DerivedComparator1.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/DerivedComparator1.java
new file mode 100644
index 0000000..035df9b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/DerivedComparator1.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default1;
+
+import java.util.Comparator;
+
+public interface DerivedComparator1<T> extends Comparator<T> {
+  @Override
+  default Comparator<T> reversed() {
+    return TestMainDefault1.shouldCallStaticOnInterface
+        ? Comparator.super.reversed().reversed() : this;
+  }
+
+  default Comparator<T> doubleReversed() {
+    return (TestMainDefault1.shouldCallStaticOnInterface
+        ? Comparator.super.reversed().reversed() : this).reversed();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/TestMainDefault1.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/TestMainDefault1.java
new file mode 100644
index 0000000..352cda0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default1/TestMainDefault1.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default1;
+
+public class TestMainDefault1 {
+  static boolean shouldCallStaticOnInterface;
+
+  public static void main(String[] args) {
+    System.out.println("TestMainDefault1::test()");
+    System.out.println(args[0]);
+    shouldCallStaticOnInterface = args[0].equals("true");
+    Derived1 comparator = new Derived1();
+    System.out.println(comparator.compare("A", "B"));
+    System.out.println(comparator.reversed().compare("B", "B"));
+    System.out.println(comparator.doubleReversed().compare("B", "C"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/Derived2.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/Derived2.java
new file mode 100644
index 0000000..1aa1acb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/Derived2.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default2;
+
+import java.util.Comparator;
+
+public class Derived2 implements DerivedComparator2<String> {
+  @Override
+  public int compare(String a, String b) {
+    return a.compareTo(b);
+  }
+
+  @Override
+  public Comparator<String> reversed() {
+    return TestMainDefault2.shouldCallStaticOnInterface
+        ? DerivedComparator2.super.reversed().reversed() : this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/DerivedComparator2.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/DerivedComparator2.java
new file mode 100644
index 0000000..f4eb949
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/DerivedComparator2.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default2;
+
+import java.util.Comparator;
+
+public interface DerivedComparator2<T> extends Comparator<T> {
+  @Override
+  default Comparator<T> reversed() {
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/TestMainDefault2.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/TestMainDefault2.java
new file mode 100644
index 0000000..6dfb014
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/default2/TestMainDefault2.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.default2;
+
+public class TestMainDefault2 {
+  static boolean shouldCallStaticOnInterface;
+
+  public static void main(String[] args) {
+    System.out.println("TestMainDefault2::test()");
+    System.out.println(args[0]);
+    shouldCallStaticOnInterface = args[0].equals("true");
+    Derived2 comparator = new Derived2();
+    System.out.println(comparator.compare("A", "B"));
+    System.out.println(comparator.reversed().compare("B", "B"));
+    System.out.println(comparator.reversed().compare("B", "C"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static0/TestMainStatic0.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static0/TestMainStatic0.java
new file mode 100644
index 0000000..5ca3cca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static0/TestMainStatic0.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.static0;
+
+import java.util.Comparator;
+
+public class TestMainStatic0 {
+  public static void main(String[] args) {
+    System.out.println("TestMainStatic0::test()");
+    System.out.println(args[0]);
+    if (args[0].equals("true")) {
+      Comparator<String> comparator = Comparator.naturalOrder();
+      System.out.println(comparator.compare("A", "B"));
+      System.out.println(comparator.compare("B", "B"));
+      System.out.println(comparator.compare("B", "C"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static1/TestMainStatic1.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static1/TestMainStatic1.java
new file mode 100644
index 0000000..fda5b6b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/static1/TestMainStatic1.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2018, 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.desugaring.interfacemethods.static1;
+
+import java.util.Comparator;
+import java.util.function.Supplier;
+
+public class TestMainStatic1 {
+  public static void main(String[] args) {
+    System.out.println("TestMainStatic1::test()");
+    System.out.println(args[0]);
+    if (args[0].equals("true")) {
+      Supplier<Comparator<String>> lambda = Comparator::naturalOrder;
+      Comparator<String> comparator = lambda.get();
+      System.out.println(comparator.compare("A", "B"));
+      System.out.println(comparator.compare("B", "B"));
+      System.out.println(comparator.compare("B", "C"));
+    }
+  }
+}