Canonicalize constant classes.

Bug: 129381548
Change-Id: I99e83466ef35a9890e03b5ee95cce36313816bf3
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index a7a7bbc..a643c9a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -1047,7 +1047,7 @@
 
     // TODO(mkroghj) Test if shorten live ranges is worth it.
     if (!options.isGeneratingClassFiles()) {
-      ConstantCanonicalizer.canonicalize(code);
+      ConstantCanonicalizer.canonicalize(appView, code);
       codeRewriter.useDedicatedConstantForLitInstruction(code);
       codeRewriter.shortenLiveRanges(code);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 53da564..4a2e847 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstInstruction;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
@@ -27,18 +30,23 @@
 
   private static final int MAX_CANONICALIZED_CONSTANT = 15;
 
-  public static void canonicalize(IRCode code) {
+  public static void canonicalize(AppView<? extends AppInfo> appView, IRCode code) {
     Object2ObjectLinkedOpenCustomHashMap<ConstInstruction, List<Value>> valuesDefinedByConstant =
         new Object2ObjectLinkedOpenCustomHashMap<>(
             new Strategy<ConstInstruction>() {
               @Override
               public int hashCode(ConstInstruction constInstruction) {
-                assert constInstruction.isConstNumber() || constInstruction.isConstString();
+                assert constInstruction.isConstNumber()
+                    || constInstruction.isConstString()
+                    || constInstruction.isConstClass();
                 if (constInstruction.isConstNumber()) {
                   return Long.hashCode(constInstruction.asConstNumber().getRawValue())
                       + 13 * constInstruction.outType().hashCode();
                 }
-                return constInstruction.asConstString().getValue().hashCode();
+                if (constInstruction.isConstString()) {
+                  return constInstruction.asConstString().getValue().hashCode();
+                }
+                return constInstruction.asConstClass().getValue().hashCode();
               }
 
               @Override
@@ -56,8 +64,14 @@
       InstructionListIterator it = block.listIterator();
       while (it.hasNext()) {
         Instruction current = it.next();
-        // Interested in ConstNumber and ConstString.
-        if (!current.isConstNumber() && !current.isConstString()) {
+        // Interested in ConstNumber, ConstString, and ConstClass
+        if (!current.isConstNumber() && !current.isConstString() && !current.isConstClass()) {
+          continue;
+        }
+        // Do not canonicalize ConstClass that may have side effects. Its original instructions
+        // will not be removed by dead code remover due to the side effects.
+        if (current.isConstClass()
+            && current.instructionMayHaveSideEffects(appView, code.method.method.holder)) {
           continue;
         }
         // Do not canonicalize ConstString instructions if there are monitor operations in the code.
@@ -102,14 +116,19 @@
         .limit(MAX_CANONICALIZED_CONSTANT)
         .forEach((entry) -> {
           ConstInstruction canonicalizedConstant = entry.getKey().asConstInstruction();
-          assert canonicalizedConstant.isConstNumber() || canonicalizedConstant.isConstString();
+          assert canonicalizedConstant.isConstNumber()
+              || canonicalizedConstant.isConstString()
+              || canonicalizedConstant.isConstClass();
           ConstInstruction newConst;
           if (canonicalizedConstant.isConstNumber()) {
             ConstNumber canonicalizedConstantNumber = canonicalizedConstant.asConstNumber();
             newConst = ConstNumber.copyOf(code, canonicalizedConstantNumber);
-          } else {
+          } else if (canonicalizedConstant.isConstString()) {
             ConstString canonicalizedConstantString = canonicalizedConstant.asConstString();
             newConst = ConstString.copyOf(code, canonicalizedConstantString);
+          } else {
+            ConstClass canonicalizedConstClass = canonicalizedConstant.asConstClass();
+            newConst = ConstClass.copyOf(code, canonicalizedConstClass);
           }
           newConst.setPosition(firstNonNonePosition);
           insertCanonicalizedConstant(code, newConst);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java
new file mode 100644
index 0000000..4e8fdc8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstClassCanonicalizationTest.java
@@ -0,0 +1,219 @@
+// 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.R8TestRunResult;
+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.ir.optimize.ConstClassMain.Outer;
+import com.android.tools.r8.ir.optimize.ConstClassMain.Outer.Inner;
+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.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class IncrementalA {
+}
+
+class IncrementalMain {
+  public static void main(String... args) {
+    System.out.println(IncrementalA.class.getSimpleName());
+    System.out.println(IncrementalA.class.getSimpleName());
+    System.out.println(IncrementalA.class.getSimpleName());
+  }
+}
+
+class ConstClassMain {
+
+  static class Outer {
+    Outer() {
+      System.out.println("Outer::<init>");
+    }
+
+    static class Inner {
+      Inner() {
+        System.out.println("Inner::<init>");
+      }
+    }
+  }
+
+  public static void main(String... args) {
+    Class outer = Inner.class.getDeclaringClass();
+    System.out.println(outer.getSimpleName());
+    for (Class<?> inner : outer.getDeclaredClasses()) {
+      System.out.println(inner.getSimpleName());
+    }
+    System.out.println(ConstClassMain.class.getSimpleName());
+    System.out.println(Outer.class.getSimpleName());
+    System.out.println(Inner.class.getSimpleName());
+    System.out.println(ConstClassMain.class.getSimpleName().length());
+    System.out.println(Outer.class.getSimpleName().length());
+    System.out.println(Inner.class.getSimpleName().length());
+  }
+}
+
+@RunWith(Parameterized.class)
+public class ConstClassCanonicalizationTest extends TestBase {
+  private static final String INCREMENTAL_OUTPUT = StringUtils.lines(
+      "IncrementalA",
+      "IncrementalA",
+      "IncrementalA"
+  );
+
+  private static final Class<?> MAIN = ConstClassMain.class;
+  private static final String JAVA_OUTPUT = StringUtils.lines(
+      "Outer",
+      "Inner",
+      "ConstClassMain",
+      "Outer",
+      "Inner",
+      "14",
+      "5",
+      "5"
+  );
+  private static final int ORIGINAL_MAIN_COUNT = 2;
+  private static final int ORIGINAL_OUTER_COUNT = 2;
+  private static final int ORIGINAL_INNER_COUNT = 3;
+  private static final int CANONICALIZED_MAIN_COUNT = 1;
+  private static final int CANONICALIZED_OUTER_COUNT = 1;
+  private static final int CANONICALIZED_INNER_COUNT = 1;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  private final TestParameters parameters;
+
+  public ConstClassCanonicalizationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJVMOutput() throws Exception {
+    assumeTrue(
+        "Only run JVM reference once (for CF backend)",
+        parameters.getBackend() == Backend.CF);
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testD8_incremental() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+
+    Path zipA = temp.newFile("a.zip").toPath().toAbsolutePath();
+    testForD8()
+        .release()
+        .addProgramClasses(IncrementalA.class)
+        .setMinApi(parameters.getRuntime())
+        .setProgramConsumer(new ArchiveConsumer(zipA))
+        .compile();
+    testForD8()
+        .release()
+        .addProgramClasses(IncrementalMain.class)
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .addRunClasspathFiles(zipA)
+        .run(parameters.getRuntime(), IncrementalMain.class)
+        .assertSuccessWithOutput(INCREMENTAL_OUTPUT)
+        .inspect(inspector -> {
+          ClassSubject main = inspector.clazz(IncrementalMain.class);
+          assertThat(main, isPresent());
+          MethodSubject mainMethod = main.mainMethod();
+          assertThat(mainMethod, isPresent());
+          assertEquals(
+              3,
+              Streams.stream(
+                  mainMethod.iterateInstructions(InstructionSubject::isConstClass))
+              .count());
+        });
+  }
+
+  private void test(
+      TestRunResult result, int mainCount, int outerCount, int innerCount) throws Exception {
+    CodeInspector codeInspector = result.inspector();
+    ClassSubject mainClass = codeInspector.clazz(MAIN);
+    MethodSubject mainMethod = mainClass.mainMethod();
+    assertThat(mainMethod, isPresent());
+    assertEquals(
+        mainCount,
+        Streams.stream(
+            mainMethod.iterateInstructions(i -> i.isConstClass(MAIN.getTypeName())))
+            .count());
+    assertEquals(
+        outerCount,
+        Streams.stream(
+            mainMethod.iterateInstructions(i -> i.isConstClass(Outer.class.getTypeName())))
+            .count());
+    assertEquals(
+        innerCount,
+        Streams.stream(
+            mainMethod.iterateInstructions(i -> i.isConstClass(Inner.class.getTypeName())))
+            .count());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.getBackend() == Backend.DEX);
+
+    D8TestRunResult result =
+        testForD8()
+            .debug()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, CANONICALIZED_MAIN_COUNT, ORIGINAL_OUTER_COUNT, ORIGINAL_INNER_COUNT);
+
+    result =
+        testForD8()
+            .release()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, CANONICALIZED_MAIN_COUNT, ORIGINAL_OUTER_COUNT, ORIGINAL_INNER_COUNT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClassesAndInnerClasses(MAIN)
+            .addKeepMainRule(MAIN)
+            .addKeepAllAttributes()
+            .noMinification()
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    // The number of expected const-class instructions differs because constant canonicalization is
+    // only enabled for the DEX backend.
+    int expectedMainCount =
+        parameters.getBackend() == Backend.CF ? ORIGINAL_MAIN_COUNT : CANONICALIZED_MAIN_COUNT;
+    int expectedOuterCount =
+        parameters.getBackend() == Backend.CF ? ORIGINAL_OUTER_COUNT : CANONICALIZED_OUTER_COUNT;
+    int expectedInnerCount =
+        parameters.getBackend() == Backend.CF ? ORIGINAL_INNER_COUNT : CANONICALIZED_INNER_COUNT;
+    test(result, expectedMainCount, expectedOuterCount, expectedInnerCount);
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
index 27cd963..7bb6c66 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
@@ -111,7 +111,10 @@
     assumeTrue(
         "Only run JVM reference once (for CF backend)",
         parameters.getBackend() == Backend.CF);
-    testForJvm().addTestClasspath().run(MAIN).assertSuccessWithOutput(JAVA_OUTPUT);
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
   private static boolean isValueOf(DexMethod method, String descriptor) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
index 8854018..c78f4b9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
@@ -232,6 +232,10 @@
             .run(parameters.getRuntime(), MAIN);
     test(result, 5, 1, 1, 0);
 
+    // The number of expected const-class instructions differs because constant canonicalization is
+    // only enabled for the DEX backend.
+    int expectedConstClassCount = parameters.getBackend() == Backend.CF ? 7 : 5;
+
     // R8 release, no minification.
     result =
         testForR8(parameters.getBackend())
@@ -243,7 +247,7 @@
             .setMinApi(parameters.getRuntime())
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, 0, 7, 0, 1);
+    test(result, 0, expectedConstClassCount, 0, 1);
 
     // R8 release, minification.
     result =
@@ -255,7 +259,7 @@
             .setMinApi(parameters.getRuntime())
             // We are not checking output because it can't be matched due to minification. Just run.
             .run(parameters.getRuntime(), MAIN);
-    test(result, 0, 7, 0, 1);
+    test(result, 0, expectedConstClassCount, 0, 1);
   }
 
 }