Mark interfaces as instantiated in compat mode.

Bug: 140471200
Change-Id: I3f9f4bca356a8f6d2b917ab37afcddda1eacd9d9
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 8cca825..e1650fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -261,11 +261,11 @@
   private final Set<DexType> instantiatedAppServices = Sets.newIdentityHashSet();
 
   /**
-   * Set of interface types for which a lambda expression can be reached. These never have a single
-   * interface implementation.
+   * Set of interface types for which there may be instantiations, such as lambda expressions or
+   * explicit keep rules.
    */
-  private final SetWithReason<DexProgramClass> instantiatedLambdas =
-      new SetWithReason<>(this::registerClass);
+  private final SetWithReason<DexProgramClass> instantiatedInterfaceTypes =
+      new SetWithReason<>(this::registerInterface);
 
   /** A queue of items that need processing. Different items trigger different actions. */
   private final EnqueuerWorklist workList;
@@ -435,8 +435,7 @@
       DexProgramClass clazz = item.asDexClass().asProgramClass();
       KeepReasonWitness witness = graphReporter.reportKeepClass(precondition, rules, clazz);
       if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) {
-        // TODO(zerny): This marking is due to a keep rule, so why add to the instantiatedLambdas?
-        markInterfaceAsInstantiatedByLambda(clazz, witness);
+        markInterfaceAsInstantiated(clazz, witness);
       } else {
         workList.enqueueMarkInstantiatedAction(clazz, witness);
         if (clazz.hasDefaultInitializer()) {
@@ -466,11 +465,10 @@
     pinnedItems.add(item.toReference());
   }
 
-  // TODO(zerny): Why is this "ByLambda"?
-  private void markInterfaceAsInstantiatedByLambda(DexProgramClass clazz, KeepReason reason) {
+  private void markInterfaceAsInstantiated(DexProgramClass clazz, KeepReason reason) {
     assert clazz.isInterface() && !clazz.accessFlags.isAnnotation();
 
-    if (!instantiatedLambdas.add(clazz, reason)) {
+    if (!instantiatedInterfaceTypes.add(clazz, reason)) {
       return;
     }
     populateInstantiatedTypesCache(clazz);
@@ -911,7 +909,7 @@
         if (clazz != null) {
           KeepReason reason = KeepReason.methodHandleReferencedIn(currentMethod);
           if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) {
-            markInterfaceAsInstantiatedByLambda(clazz, reason);
+            markInterfaceAsInstantiated(clazz, reason);
           } else {
             markInstantiated(clazz, reason);
           }
@@ -1020,7 +1018,7 @@
       DexType baseType = type.toBaseType(appView.dexItemFactory());
       if (baseType.isClassType()) {
         DexProgramClass baseClass = getProgramClassOrNull(baseType);
-        if (baseClass != null && !baseClass.isInterface()) {
+        if (baseClass != null) {
           // Don't require any constructor, see b/112386012.
           markClassAsInstantiatedWithCompatRule(baseClass);
         } else {
@@ -1692,7 +1690,8 @@
       return;
     }
     if (clazz.isProgramClass()) {
-      if (instantiatedLambdas.add(clazz.asProgramClass(), KeepReason.instantiatedIn(method))) {
+      KeepReason reason = KeepReason.instantiatedIn(method);
+      if (instantiatedInterfaceTypes.add(clazz.asProgramClass(), reason)) {
         populateInstantiatedTypesCache(clazz.asProgramClass());
       }
     }
@@ -1855,7 +1854,7 @@
     }
 
     if (instantiatedTypes.contains(clazz)
-        || instantiatedLambdas.contains(clazz)
+        || instantiatedInterfaceTypes.contains(clazz)
         || pinnedItems.contains(clazz.type)) {
       markVirtualMethodAsLive(
           clazz, encodedPossibleTarget, KeepReason.reachableFromLiveType(possibleTarget.holder));
@@ -2108,7 +2107,8 @@
             Collections.emptySet(),
             Collections.emptyMap(),
             Collections.emptyMap(),
-            SetUtils.mapIdentityHashSet(instantiatedLambdas.getItems(), DexProgramClass::getType));
+            SetUtils.mapIdentityHashSet(
+                instantiatedInterfaceTypes.getItems(), DexProgramClass::getType));
     appInfo.markObsolete();
     return appInfoWithLiveness;
   }
@@ -2436,6 +2436,10 @@
   private void markClassAsInstantiatedWithCompatRule(DexProgramClass clazz) {
     ProguardKeepRule rule = ProguardConfigurationUtils.buildDefaultInitializerKeepRule(clazz);
     KeepReason reason = KeepReason.dueToProguardCompatibilityKeepRule(rule);
+    if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) {
+      markInterfaceAsInstantiated(clazz, reason);
+      return;
+    }
     proguardCompatibilityWorkList.enqueueMarkInstantiatedAction(clazz, reason);
     if (clazz.hasDefaultInitializer()) {
       proguardCompatibilityWorkList.enqueueMarkReachableDirectAction(
@@ -3093,6 +3097,14 @@
     return registerEdge(getClassGraphNode(type), reason);
   }
 
+  private KeepReasonWitness registerInterface(DexProgramClass iface, KeepReason reason) {
+    assert iface.isInterface();
+    if (skipReporting(reason)) {
+      return KeepReasonWitness.INSTANCE;
+    }
+    return registerEdge(getClassGraphNode(iface.type), reason);
+  }
+
   private KeepReasonWitness registerClass(DexProgramClass clazz, KeepReason reason) {
     if (skipReporting(reason)) {
       return KeepReasonWitness.INSTANCE;
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/keepinterface/CompatKeepInterfaceAsInstantiatedTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/keepinterface/CompatKeepInterfaceAsInstantiatedTest.java
new file mode 100644
index 0000000..7f4606c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/keepinterface/CompatKeepInterfaceAsInstantiatedTest.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2019, 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.shaking.forceproguardcompatibility.keepinterface;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CompatKeepInterfaceAsInstantiatedTest extends TestBase {
+
+  public static class TestCast {
+
+    public static void main(String[] args) throws Exception {
+      Object bar =
+          Class.forName(
+                  TestCast.class.getPackage().getName()
+                      + ".CompatKeepInterfaceAsInstantiatedTest$Ba"
+                      + (args.length == 42 ? "" : "r"))
+              .getDeclaredConstructor()
+              .newInstance();
+      System.out.println(
+          "It was a Foo! "
+              // This cast should trigger a compatibility keep for Foo.
+              + ((Foo) bar).getClass().getName());
+    }
+  }
+
+  public static class TestInstanceOf {
+
+    public static void main(String[] args) throws Exception {
+      Object bar =
+          Class.forName(
+                  TestInstanceOf.class.getPackage().getName()
+                      + ".CompatKeepInterfaceAsInstantiatedTest$Ba"
+                      + (args.length == 42 ? "" : "r"))
+              .getDeclaredConstructor()
+              .newInstance();
+      // This instance-of causes Foo to be kept.
+      if (bar instanceof Foo) {
+        System.out.println("It was a Foo! " + bar.getClass().getName());
+      } else {
+        System.out.println("Who knows what it is...");
+      }
+    }
+  }
+
+  // Foo interface referenced in the above test classes.
+  public interface Foo {}
+
+  // Bar implementation of Foo, but not visible at compile time, reflectively created.
+  public static class Bar implements Foo {}
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntimes().build();
+  }
+
+  private static final String expected =
+      StringUtils.lines("It was a Foo! " + Bar.class.getTypeName());
+
+  private final TestParameters parameters;
+
+  public CompatKeepInterfaceAsInstantiatedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testCastReference() throws Exception {
+    testReference(TestCast.class);
+  }
+
+  @Test
+  public void testCastPG() throws Exception {
+    testCast(testForProguard()).assertSuccessWithOutput(expected);
+  }
+
+  @Test
+  public void testCastR8() throws Exception {
+    testCast(testForR8Compat(parameters.getBackend())).assertSuccessWithOutput(expected);
+  }
+
+  @Test
+  public void testInstanceOfReference() throws Exception {
+    testReference(TestInstanceOf.class);
+  }
+
+  @Test
+  public void testInstanceOfPG() throws Exception {
+    testInstanceOf(testForProguard()).assertSuccessWithOutput(expected);
+  }
+
+  @Test
+  public void testInstanceOfR8() throws Exception {
+    testInstanceOf(testForR8Compat(parameters.getBackend()))
+        // TODO(b/140471200): The compatibility behavior should keep Foo.
+        .assertFailureWithErrorThatMatches(containsString("NoClassDefFoundError"));
+  }
+
+  private TestRunResult<?> testCast(TestShrinkerBuilder<?, ?, ?, ?, ?> builder) throws Exception {
+    return testShrinker(builder, TestCast.class);
+  }
+
+  private TestRunResult<?> testInstanceOf(TestShrinkerBuilder<?, ?, ?, ?, ?> builder)
+      throws Exception {
+    return testShrinker(builder, TestInstanceOf.class);
+  }
+
+  private void testReference(Class<?> main) throws Exception {
+    testForJvm()
+        .addProgramClasses(main, Foo.class, Bar.class)
+        .run(parameters.getRuntime(), main)
+        .assertSuccessWithOutput(expected);
+  }
+
+  private TestRunResult<?> testShrinker(TestShrinkerBuilder<?, ?, ?, ?, ?> builder, Class<?> main)
+      throws Exception {
+    return builder
+        // Add -dontwarn to avoid PG failing since this test runner class is not present.
+        .addKeepRules("-dontwarn " + CompatKeepInterfaceAsInstantiatedTest.class.getTypeName())
+        .noMinification()
+        .addProgramClasses(main, Foo.class)
+        .addKeepMainRule(main)
+        .compile()
+        .addRunClasspathClasses(Bar.class)
+        .run(parameters.getRuntime(), main);
+  }
+}