Redirection of super calls to default interface methods

Change-Id: I08851eb9fd21cd293af8f803d049599877ecddbe
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 38473f3..4d072c4 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -895,50 +895,61 @@
     }
 
     private void redirectSuperCallsInTarget(DexMethod oldTarget, DexMethod newTarget) {
-      // If we merge class B into class C, and class C contains an invocation super.m(), then it
-      // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
-      // C.m$B denotes the direct method that has been created in C for B.m). In particular, there
-      // might be an instruction "invoke-super A.m()" in C that resolves to B.m at runtime (A is
-      // a superclass of B), which also needs to be rewritten to "invoke-direct C.m$B()".
-      //
-      // We handle this by adding a mapping for [target] and all of its supertypes.
-      DexClass holder = target;
-      while (holder != null && holder.isProgramClass()) {
-        DexMethod signatureInHolder =
-            application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
-        // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
-        boolean resolutionSucceeds =
-            holder.lookupVirtualMethod(signatureInHolder) != null
-                || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
-        if (resolutionSucceeds) {
-          deferredRenamings.mapVirtualMethodToDirectInType(
-              signatureInHolder, newTarget, target.type);
-        } else {
-          break;
-        }
+      if (source.accessFlags.isInterface()) {
+        // If we merge a default interface method from interface I to its subtype C, then we need
+        // to rewrite invocations on the form "invoke-super I.m()" to "invoke-direct C.m$I()".
+        //
+        // Unlike when we merge a class into its subclass (the else-branch below), we should *not*
+        // rewrite any invocations on the form "invoke-super J.m()" to "invoke-direct C.m$I()",
+        // if I has a supertype J. This is due to the fact that invoke-super instructions that
+        // resolve to a method on an interface never hit an implementation below that interface.
+        deferredRenamings.mapVirtualMethodToDirectInType(oldTarget, newTarget, target.type);
+      } else {
+        // If we merge class B into class C, and class C contains an invocation super.m(), then it
+        // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
+        // C.m$B denotes the direct method that has been created in C for B.m). In particular, there
+        // might be an instruction "invoke-super A.m()" in C that resolves to B.m at runtime (A is
+        // a superclass of B), which also needs to be rewritten to "invoke-direct C.m$B()".
+        //
+        // We handle this by adding a mapping for [target] and all of its supertypes.
+        DexClass holder = target;
+        while (holder != null && holder.isProgramClass()) {
+          DexMethod signatureInHolder =
+              application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
+          // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
+          boolean resolutionSucceeds =
+              holder.lookupVirtualMethod(signatureInHolder) != null
+                  || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+          if (resolutionSucceeds) {
+            deferredRenamings.mapVirtualMethodToDirectInType(
+                signatureInHolder, newTarget, target.type);
+          } else {
+            break;
+          }
 
-        // Consider that A gets merged into B and B's subclass C gets merged into D. Instructions
-        // on the form "invoke-super {B,C,D}.m()" in D are changed into "invoke-direct D.m$C()" by
-        // the code above. However, instructions on the form "invoke-super A.m()" should also be
-        // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
-        // that have been merged into [holder].
-        Set<DexType> mergedTypes = mergedClassesInverse.get(holder.type);
-        if (mergedTypes != null) {
-          for (DexType type : mergedTypes) {
-            DexMethod signatureInType =
-                application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
-            // Resolution would have succeeded if the method used to be in [type], or if one of
-            // its super classes declared the method.
-            boolean resolutionSucceededBeforeMerge =
-                renamedMembersLense.hasMappingForSignatureInContext(holder.type, signatureInType)
-                    || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
-            if (resolutionSucceededBeforeMerge) {
-              deferredRenamings.mapVirtualMethodToDirectInType(
-                  signatureInType, newTarget, target.type);
+          // Consider that A gets merged into B and B's subclass C gets merged into D. Instructions
+          // on the form "invoke-super {B,C,D}.m()" in D are changed into "invoke-direct D.m$C()" by
+          // the code above. However, instructions on the form "invoke-super A.m()" should also be
+          // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
+          // that have been merged into [holder].
+          Set<DexType> mergedTypes = mergedClassesInverse.get(holder.type);
+          if (mergedTypes != null) {
+            for (DexType type : mergedTypes) {
+              DexMethod signatureInType =
+                  application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
+              // Resolution would have succeeded if the method used to be in [type], or if one of
+              // its super classes declared the method.
+              boolean resolutionSucceededBeforeMerge =
+                  renamedMembersLense.hasMappingForSignatureInContext(holder.type, signatureInType)
+                      || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+              if (resolutionSucceededBeforeMerge) {
+                deferredRenamings.mapVirtualMethodToDirectInType(
+                    signatureInType, newTarget, target.type);
+              }
             }
           }
+          holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
         }
-        holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
       }
     }
 
diff --git a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
new file mode 100644
index 0000000..7a0e325
--- /dev/null
+++ b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
@@ -0,0 +1,37 @@
+// 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 classmerging;
+
+public class NestedDefaultInterfaceMethodsTest {
+
+  public static void main(String[] args) {
+    new C().m();
+  }
+
+  public interface A {
+
+    default void m() {
+      System.out.println("In A.m()");
+    }
+  }
+
+  public interface B extends A {
+
+    @Override
+    default void m() {
+      System.out.println("In B.m()");
+      A.super.m();
+    }
+  }
+
+  public static class C implements B {
+
+    @Override
+    public void m() {
+      System.out.println("In C.m()");
+      B.super.m();
+    }
+  }
+}
diff --git a/src/test/examplesAndroidO/classmerging/keep-rules.txt b/src/test/examplesAndroidO/classmerging/keep-rules.txt
index fc91808..4df182e 100644
--- a/src/test/examplesAndroidO/classmerging/keep-rules.txt
+++ b/src/test/examplesAndroidO/classmerging/keep-rules.txt
@@ -10,6 +10,9 @@
 -keep public class classmerging.MergeDefaultMethodIntoClassTest {
   public static void main(...);
 }
+-keep public class classmerging.NestedDefaultInterfaceMethodsTest {
+  public static void main(...);
+}
 
 # TODO(herhut): Consider supporting merging of inner-class attributes.
 # -keepattributes *
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index f28c86f..b6489b0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -251,6 +251,50 @@
   }
 
   @Test
+  public void testNestedDefaultInterfaceMethodsTest() throws Exception {
+    String main = "classmerging.NestedDefaultInterfaceMethodsTest";
+    Path[] programFiles =
+        new Path[] {
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest.class"),
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$A.class"),
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$B.class"),
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$C.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.NestedDefaultInterfaceMethodsTest",
+            "classmerging.NestedDefaultInterfaceMethodsTest$B",
+            "classmerging.NestedDefaultInterfaceMethodsTest$C");
+    runTest(
+        main, programFiles, preservedClassNames::contains, getProguardConfig(JAVA8_EXAMPLE_KEEP));
+  }
+
+  @Test
+  public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Exception {
+    String main = "classmerging.NestedDefaultInterfaceMethodsTest";
+    Path[] programFiles =
+        new Path[] {
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest.class"),
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$A.class"),
+          JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$B.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.NestedDefaultInterfaceMethodsTest",
+            "classmerging.NestedDefaultInterfaceMethodsTest$B",
+            "classmerging.NestedDefaultInterfaceMethodsTest$C");
+    runTestOnInput(
+        main,
+        AndroidApp.builder()
+            .addProgramFiles(programFiles)
+            .addClassProgramData(
+                NestedDefaultInterfaceMethodsTestDump.CDump.dump(), Origin.unknown())
+            .build(),
+        preservedClassNames::contains,
+        getProguardConfig(JAVA8_EXAMPLE_KEEP));
+  }
+
+  @Test
   public void testPinnedParameterTypes() throws Exception {
     String main = "classmerging.PinnedParameterTypesTest";
     Path[] programFiles =
diff --git a/src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java b/src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java
new file mode 100644
index 0000000..e55802d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java
@@ -0,0 +1,76 @@
+// 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.classmerging;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class NestedDefaultInterfaceMethodsTestDump {
+
+  // Generated by running ./tools/asmifier.py build/test/examplesAndroidO/classes/classmerging/-
+  // NestedDefaultInterfaceMethodsTest\$C.class, and changing "invoke-special B.m()" to "invoke-
+  // special A.m()".
+  public static class CDump implements Opcodes {
+
+    public static byte[] dump() {
+      ClassWriter cw = new ClassWriter(0);
+      FieldVisitor fv;
+      MethodVisitor mv;
+      AnnotationVisitor av0;
+
+      cw.visit(
+          V1_8,
+          ACC_PUBLIC + ACC_SUPER,
+          "classmerging/NestedDefaultInterfaceMethodsTest$C",
+          null,
+          "java/lang/Object",
+          new String[] {"classmerging/NestedDefaultInterfaceMethodsTest$B"});
+
+      cw.visitInnerClass(
+          "classmerging/NestedDefaultInterfaceMethodsTest$C",
+          "classmerging/NestedDefaultInterfaceMethodsTest",
+          "C",
+          ACC_PUBLIC + ACC_STATIC);
+
+      cw.visitInnerClass(
+          "classmerging/NestedDefaultInterfaceMethodsTest$B",
+          "classmerging/NestedDefaultInterfaceMethodsTest",
+          "B",
+          ACC_PUBLIC + ACC_STATIC + ACC_ABSTRACT + ACC_INTERFACE);
+
+      {
+        mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+        mv.visitCode();
+        mv.visitVarInsn(ALOAD, 0);
+        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        mv.visitInsn(RETURN);
+        mv.visitMaxs(1, 1);
+        mv.visitEnd();
+      }
+      {
+        mv = cw.visitMethod(ACC_PUBLIC, "m", "()V", null, null);
+        mv.visitCode();
+        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        mv.visitLdcInsn("In C.m()");
+        mv.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        mv.visitVarInsn(ALOAD, 0);
+        // The signature "classmerging/NestedDefaultInterfaceMethodsTest$B" has been changed to
+        // "classmerging/NestedDefaultInterfaceMethodsTest$A".
+        mv.visitMethodInsn(
+            INVOKESPECIAL, "classmerging/NestedDefaultInterfaceMethodsTest$A", "m", "()V", true);
+        mv.visitInsn(RETURN);
+        mv.visitMaxs(2, 1);
+        mv.visitEnd();
+      }
+      cw.visitEnd();
+
+      return cw.toByteArray();
+    }
+  }
+}