Add test for dynamic type optimization

Bug: 127461806
Change-Id: I02f38db6fe358234e0d838e1112a7a6703d74f65
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 3291692..f0591ee 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -256,6 +256,7 @@
     internal.passthroughDexCode = true;
 
     // Assert some of R8 optimizations are disabled.
+    assert !internal.enableDynamicTypeOptimization;
     assert !internal.enableInlining;
     assert !internal.enableClassInlining;
     assert !internal.enableHorizontalClassMerging;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 5980ac3..f000ece 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -676,6 +676,7 @@
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
 
+    assert internal.enableDynamicTypeOptimization || !proguardConfiguration.isOptimizing();
     assert internal.enableHorizontalClassMerging || !proguardConfiguration.isOptimizing();
     assert internal.enableVerticalClassMerging || !proguardConfiguration.isOptimizing();
     if (internal.debug) {
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 3a5bd94..15ee862 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -112,6 +112,7 @@
 
   public void disableGlobalOptimizations() {
     enableArgumentRemoval = false;
+    enableDynamicTypeOptimization = false;
     enableInlining = false;
     enableClassInlining = false;
     enableClassStaticizer = false;
@@ -133,6 +134,7 @@
   public boolean passthroughDexCode = false;
 
   // Optimization-related flags. These should conform to -dontoptimize and disableAllOptimizations.
+  public boolean enableDynamicTypeOptimization = true;
   public boolean enableHorizontalClassMerging = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableArgumentRemoval = true;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/dynamictype/DynamicTypeOptimizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/dynamictype/DynamicTypeOptimizationTest.java
new file mode 100644
index 0000000..849927a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/dynamictype/DynamicTypeOptimizationTest.java
@@ -0,0 +1,199 @@
+// 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.ir.optimize.dynamictype;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DynamicTypeOptimizationTest extends TestBase {
+
+  private final boolean enableDynamicTypeOptimization;
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{1}, enable dynamic type optimization: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+  }
+
+  public DynamicTypeOptimizationTest(
+      boolean enableDynamicTypeOptimization, TestParameters parameters) {
+    this.enableDynamicTypeOptimization = enableDynamicTypeOptimization;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(DynamicTypeOptimizationTest.class)
+        .addKeepMainRule(TestClass.class)
+        // Keep B to ensure that we will treat it as being instantiated.
+        .addKeepClassRulesWithAllowObfuscation(B.class)
+        .addOptionsModification(
+            options -> options.enableDynamicTypeOptimization = enableDynamicTypeOptimization)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(StringUtils.lines("Hello world!"));
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject mainClassSubject = inspector.clazz(TestClass.class);
+    assertThat(mainClassSubject, isPresent());
+
+    ClassSubject interfaceSubject = inspector.clazz(I.class);
+    assertThat(interfaceSubject, isPresent());
+
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    // Verify that the check-cast instruction is still present in testCheckCastRemoval().
+    MethodSubject testCheckCastRemovalMethod =
+        mainClassSubject.uniqueMethodWithName("testCheckCastRemoval");
+    assertThat(testCheckCastRemovalMethod, isPresent());
+    assertTrue(
+        testCheckCastRemovalMethod
+            .streamInstructions()
+            .anyMatch(instruction -> instruction.isCheckCast(aClassSubject.getFinalName())));
+
+    // Verify that the instance-of instruction is only present in testInstanceOfRemoval() if the
+    // dynamic type optimization is disabled.
+    MethodSubject testInstanceOfRemovalMethod =
+        mainClassSubject.uniqueMethodWithName("testInstanceOfRemoval");
+    assertThat(testInstanceOfRemovalMethod, isPresent());
+    // TODO(b/127461806): Update expectation.
+    assertTrue(
+        testInstanceOfRemovalMethod
+            .streamInstructions()
+            .anyMatch(instruction -> instruction.isInstanceOf(aClassSubject.getFinalName())));
+
+    // Verify that world() has been inlined() into testMethodInlining() unless the dynamic type
+    // optimization is disabled.
+    MethodSubject testMethodInliningMethod =
+        mainClassSubject.uniqueMethodWithName("testMethodInlining");
+    assertThat(testMethodInliningMethod, isPresent());
+    // TODO(b/127461806): Update expectation.
+    assertThat(
+        testMethodInliningMethod, invokesMethod(interfaceSubject.uniqueMethodWithName("world")));
+
+    // Verify that exclamationMark() has been rebound in testMethodRebinding() unless the dynamic
+    // type optimization is disabled.
+    MethodSubject testMethodRebindingMethod =
+        mainClassSubject.uniqueMethodWithName("testMethodRebinding");
+    assertThat(testMethodRebindingMethod, isPresent());
+    // TODO(b/127461806): Update expectation.
+    assertThat(
+        testMethodRebindingMethod,
+        invokesMethod(interfaceSubject.uniqueMethodWithName("exclamationMark")));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      testCheckCastRemoval();
+      testInstanceOfRemoval();
+      testMethodInlining();
+      testMethodRebinding();
+    }
+
+    @NeverInline
+    private static void testCheckCastRemoval() {
+      // Used to verify that we do not remove check-cast instructions, since these are sometimes
+      // required for the program to pass the static type checker.
+      A obj = (A) get();
+      obj.hello();
+    }
+
+    @NeverInline
+    private static void testInstanceOfRemoval() {
+      // Used to verify that we remove trivial instance-of instructions.
+      if (get() instanceof A) {
+        System.out.print(" ");
+      }
+    }
+
+    @NeverInline
+    private static void testMethodInlining() {
+      // Used to verify that we identify a single target despite of the imprecise static type.
+      get().world();
+    }
+
+    @NeverInline
+    private static void testMethodRebinding() {
+      // Used to verify that we rebind to the most specific target when inlining is not possible.
+      get().exclamationMark();
+    }
+
+    @NeverInline
+    private static I get() {
+      return new A();
+    }
+  }
+
+  interface I {
+
+    void hello();
+
+    void world();
+
+    @NeverInline
+    void exclamationMark();
+  }
+
+  static class A implements I {
+
+    @Override
+    public void hello() {
+      System.out.print("Hello");
+    }
+
+    @Override
+    public void world() {
+      System.out.print("world");
+    }
+
+    @NeverInline
+    @Override
+    public void exclamationMark() {
+      System.out.println("!");
+    }
+  }
+
+  static class B implements I {
+
+    @Override
+    public void hello() {
+      System.out.println("Unreachable");
+    }
+
+    @Override
+    public void world() {
+      System.out.println("Unreachable");
+    }
+
+    @NeverInline
+    @Override
+    public void exclamationMark() {
+      System.out.println("Unreachable");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index 5f1b405..ded4f14 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -222,6 +222,11 @@
   }
 
   @Override
+  public boolean isInstanceOf(String type) {
+    return isInstanceOf() && ((CfInstanceOf) instruction).getType().toString().equals(type);
+  }
+
+  @Override
   public boolean isIf() {
     return instruction instanceof CfIf || instruction instanceof CfIfCmp;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 7d5381a..5dacc5a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -31,7 +31,7 @@
 
       @Override
       public void describeTo(Description description) {
-        description.appendText(" does not invoke `" + target.toSourceString() + "`");
+        description.appendText("invokes method `" + target.toSourceString() + "`");
       }
 
       @Override
@@ -42,6 +42,6 @@
   }
 
   public static Predicate<InstructionSubject> isInvokeWithTarget(DexMethod target) {
-    return instruction -> instruction.isInvokeStatic() && instruction.getMethod() == target;
+    return instruction -> instruction.isInvoke() && instruction.getMethod() == target;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index 93faa64..3b04382 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -340,6 +340,11 @@
     return instruction instanceof InstanceOf;
   }
 
+  @Override
+  public boolean isInstanceOf(String type) {
+    return isInstanceOf() && ((InstanceOf) instruction).getType().toString().equals(type);
+  }
+
   public boolean isConst4() {
     return instruction instanceof Const4;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index f2eb753..d297a3b 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -78,6 +78,8 @@
 
   boolean isInstanceOf();
 
+  boolean isInstanceOf(String type);
+
   boolean isIf(); // Also include CF/if_cmp* instructions.
 
   boolean isPackedSwitch();