Replace calls to Enum.ordinal() on constant-like enum fields

Bug: 135667497
Test: tools/test.py --no-internal -v *EnumOptimizationTest*
Change-Id: I815668f17fc9c7f1c31ad578a77b45f09343b363
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index f6469b0..f427e0f 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -263,7 +263,7 @@
     assert !internal.enableHorizontalClassMerging;
     assert !internal.enableVerticalClassMerging;
     assert !internal.enableClassStaticizer;
-    assert !internal.enableSwitchMapRemoval;
+    assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableValuePropagation;
     assert !internal.enableLambdaMerging;
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index 5eb0151..a79167a 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -227,7 +227,7 @@
     internal.mainDexListConsumer = mainDexListConsumer;
     internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
     internal.minimalMainDex = internal.debug;
-    internal.enableSwitchMapRemoval = false;
+    internal.enableEnumValueOptimization = false;
     internal.enableInlining = false;
     return internal;
   }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index d907a81..a8afcfc 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -531,7 +531,7 @@
       assert appView.dexItemFactory().verifyNoCachedTypeLatticeElements();
 
       // Collect switch maps and ordinals maps.
-      if (options.enableSwitchMapRemoval) {
+      if (options.enableEnumValueOptimization) {
         appViewWithLiveness.setAppInfo(new SwitchMapCollector(appViewWithLiveness).run());
         appViewWithLiveness.setAppInfo(new EnumOrdinalMapCollector(appViewWithLiveness).run());
       }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 2bd69e5..9901957 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -655,6 +655,7 @@
   public class EnumMethods {
 
     public DexMethod valueOf;
+    public DexMethod ordinal;
 
     private EnumMethods() {
       valueOf =
@@ -663,6 +664,12 @@
               valueOfMethodName,
               enumDescriptor,
               new DexString[] {classDescriptor, stringDescriptor});
+      ordinal =
+          createMethod(
+              enumDescriptor,
+              ordinalMethodName,
+              intDescriptor,
+              DexString.EMPTY_ARRAY);
     }
   }
 
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 fb6edd2..26ea6c5 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
@@ -992,7 +992,7 @@
       memberValuePropagation.rewriteWithConstantValues(
           code, method.method.holder, isProcessedConcurrently);
     }
-    if (options.enableSwitchMapRemoval) {
+    if (options.enableEnumValueOptimization) {
       assert appView.enableWholeProgramOptimizations();
       codeRewriter.removeSwitchMaps(code);
     }
@@ -1052,6 +1052,11 @@
     assert code.verifyTypes(appView);
     codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(code);
 
+    if (options.enableEnumValueOptimization) {
+      assert appView.enableWholeProgramOptimizations();
+      codeRewriter.rewriteConstantEnumOrdinal(code);
+    }
+
     codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
     codeRewriter.commonSubexpressionElimination(code);
     codeRewriter.simplifyArrayConstruction(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 5c6240e..bb41249 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -64,6 +64,7 @@
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
@@ -3620,6 +3621,51 @@
     return true;
   }
 
+  public void rewriteConstantEnumOrdinal(IRCode code) {
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction current = iterator.next();
+
+      if (!current.isInvokeMethodWithReceiver()) {
+        continue;
+      }
+      InvokeMethodWithReceiver methodWithReceiver = current.asInvokeMethodWithReceiver();
+      if (methodWithReceiver.getInvokedMethod() != dexItemFactory.enumMethods.ordinal) {
+        continue;
+      }
+
+      Value receiver = methodWithReceiver.getReceiver().getAliasedValue();
+      if (receiver.isPhi()) {
+        continue;
+      }
+      Instruction definition = receiver.getDefinition();
+      if (!definition.isStaticGet()) {
+        continue;
+      }
+      DexField enumField = definition.asStaticGet().getField();
+
+      Reference2IntMap<DexField> ordinalMap =
+          appView.appInfo().withLiveness().getOrdinalsMapFor(enumField.type);
+      if (ordinalMap == null) {
+        continue;
+      }
+
+      // The receiver value is identified as being from a constant enum field lookup by the fact
+      // that it is a static-get to a field whose type is the same as the enclosing class (which
+      // is known to be an enum type). An enum may still define a static field using the enum type
+      // so ensure the field is present in the ordinal map for final validation.
+      if (!ordinalMap.containsKey(enumField)) {
+        continue;
+      }
+      int ordinalValue = ordinalMap.getInt(enumField);
+
+      Value outValue = methodWithReceiver.outValue();
+      iterator.replaceCurrentInstruction(new ConstNumber(outValue, ordinalValue));
+    }
+
+    assert code.isConsistentSSA();
+  }
+
   public void rewriteLongCompareAndRequireNonNull(IRCode code, InternalOptions options) {
     if (options.canUseJava7CompareAndObjectsOperations()) {
       return;
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 91da309..14a7ac1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -144,7 +144,7 @@
     enableUninstantiatedTypeOptimization = false;
     enableUnusedArgumentRemoval = false;
     outline.enabled = false;
-    enableSwitchMapRemoval = false;
+    enableEnumValueOptimization = false;
     enableValuePropagation = false;
     enableSideEffectAnalysis = false;
     enableTreeShakingOfLibraryMethodOverrides = false;
@@ -205,7 +205,7 @@
   // GMS Core.
   public int inliningControlFlowResolutionBlocksThreshold = 15;
   public boolean enableStringSwitchConversion = false;
-  public boolean enableSwitchMapRemoval = true;
+  public boolean enableEnumValueOptimization = true;
   public final OutlineOptions outline = new OutlineOptions();
   public boolean enableValuePropagation = true;
   public boolean enableUninstantiatedTypeOptimization = true;
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index ff5d0b5..374f91d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -41,7 +41,8 @@
     testForR8(parameters.getBackend())
         .addProgramFiles(Paths.get(ToolHelper.EXAMPLES_KOTLIN_BUILD_DIR, "enumswitch.jar"))
         .addKeepMainRule("enumswitch.EnumSwitchKt")
-        .addOptionsModification(options -> options.enableSwitchMapRemoval = enableSwitchMapRemoval)
+        .addOptionsModification(
+            options -> options.enableEnumValueOptimization = enableSwitchMapRemoval)
         .setMinApi(parameters.getRuntime())
         .noMinification()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
new file mode 100644
index 0000000..40afc97
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
@@ -0,0 +1,188 @@
+// Copyright (c) 2017, 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.rewrite.enums;
+
+import static com.android.tools.r8.ToolHelper.getDefaultAndroidJar;
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ForceInline;
+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.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 java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests checking that Enum.ordinal() calls on enum fields are rewritten into a constant integer.
+ */
+@RunWith(Parameterized.class)
+public class EnumOptimizationTest extends TestBase {
+  private final boolean enableOptimization;
+  private final TestParameters parameters;
+
+  @Parameters(name = "{1}, enable enum optimization: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+  }
+
+  public EnumOptimizationTest(boolean enableOptimization, TestParameters parameters) {
+    this.enableOptimization = enableOptimization;
+    this.parameters = parameters;
+  }
+
+  static class Ordinals {
+    enum Number {
+      ONE, TWO;
+
+      public static final Direction DOWN = Direction.DOWN;
+      public static final Number DEFAULT = TWO;
+    }
+
+    enum Direction {
+      UP, DOWN
+    }
+
+    @NeverInline
+    static long simple() {
+      return Number.TWO.ordinal();
+    }
+
+    @NeverInline
+    static long local() {
+      Number two = Number.TWO;
+      return two.ordinal();
+    }
+
+    @NeverInline
+    static String multipleUsages() {
+      Number two = Number.TWO;
+      return two.name() + two.ordinal();
+    }
+
+    @NeverInline
+    static long inlined() {
+      return inlined2(Number.TWO);
+    }
+    @ForceInline
+    private static long inlined2(Number number) {
+      return number.ordinal();
+    }
+
+    @NeverInline
+    static long libraryType() {
+      return TimeUnit.SECONDS.ordinal();
+    }
+
+    @NeverInline
+    static long wrongTypeStaticField() {
+      return Number.DOWN.ordinal();
+    }
+
+    @NeverInline
+    static long nonValueStaticField() {
+      return Number.DEFAULT.ordinal();
+    }
+
+    @NeverInline
+    static long phi(boolean value) {
+      Number number = Number.ONE;
+      if (value) {
+        number = Number.TWO;
+      }
+      return number.ordinal();
+    }
+
+    @NeverInline
+    static long nonStaticGet() {
+      return new Ordinals().two.ordinal();
+    }
+    private final Number two = Number.TWO;
+
+    public static void main(String[] args) {
+      System.out.println(simple());
+      System.out.println(local());
+      System.out.println(multipleUsages());
+      System.out.println(inlined());
+      System.out.println(libraryType());
+      System.out.println(wrongTypeStaticField());
+      System.out.println(nonValueStaticField());
+      System.out.println(phi(true));
+      System.out.println(nonStaticGet());
+    }
+  }
+
+  @Test public void ordinals() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryFiles(getDefaultAndroidJar())
+        .addProgramClassesAndInnerClasses(Ordinals.class)
+        .addKeepMainRule(Ordinals.class)
+        .noMinification()
+        .enableInliningAnnotations()
+        .addOptionsModification(options -> options.enableEnumValueOptimization = enableOptimization)
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspectOrdinals)
+        .run(parameters.getRuntime(), Ordinals.class)
+        .assertSuccessWithOutputLines("1", "1", "TWO1", "1", "3", "1", "1", "1", "1");
+  }
+
+  private void inspectOrdinals(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(Ordinals.class);
+    assertTrue(clazz.isPresent());
+
+    if (enableOptimization) {
+      assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("simple"), 1);
+      assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("local"), 1);
+      assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("multipleUsages"), 1);
+      assertOrdinalReplacedWithConst(clazz.uniqueMethodWithName("inlined"), 1);
+    } else {
+      assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("simple"));
+      assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("local"));
+      assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("multipleUsages"));
+      assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("inlined"));
+    }
+
+    assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("libraryType"));
+    assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("wrongTypeStaticField"));
+    assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("nonValueStaticField"));
+    assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("phi"));
+    assertOrdinalWasNotReplaced(clazz.uniqueMethodWithName("nonStaticGet"));
+  }
+
+  private static void assertOrdinalReplacedWithConst(MethodSubject method, int expectedConst) {
+    assertTrue(method.isPresent());
+    assertEquals(emptyList(), enumOrdinalInvokes(method));
+
+    long[] actualConst = method.streamInstructions()
+        .filter(InstructionSubject::isConstNumber)
+        .mapToLong(InstructionSubject::getConstNumber)
+        .toArray();
+    assertEquals(expectedConst, actualConst[0]);
+  }
+
+  private static void assertOrdinalWasNotReplaced(MethodSubject method) {
+    assertTrue(method.isPresent());
+    List<InstructionSubject> invokes = enumOrdinalInvokes(method);
+    assertEquals(invokes.toString(), 1, invokes.size());
+  }
+
+  private static List<InstructionSubject> enumOrdinalInvokes(MethodSubject method) {
+    return method.streamInstructions()
+        .filter(instruction -> instruction.isInvoke()
+            && instruction.getMethod().name.toString().equals("ordinal"))
+        .collect(toList());
+  }
+}
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 ff48000..d7ea3ae 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
@@ -122,8 +122,7 @@
 
   @Override
   public boolean isConstNumber(long value) {
-    return instruction instanceof CfConstNumber
-        && ((CfConstNumber) instruction).getRawValue() == value;
+    return isConstNumber() && getConstNumber() == value;
   }
 
   @Override
@@ -147,6 +146,11 @@
     return false;
   }
 
+  @Override public long getConstNumber() {
+    assert isConstNumber();
+    return ((CfConstNumber) instruction).getRawValue();
+  }
+
   @Override
   public String getConstString() {
     if (instruction instanceof CfConstString) {
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 32e3f00..53fb02c 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
@@ -222,20 +222,7 @@
 
   @Override
   public boolean isConstNumber(long value) {
-    if (!isConstNumber()) {
-      return false;
-    }
-    if (instruction instanceof Const
-        || instruction instanceof Const4
-        || instruction instanceof Const16
-        || instruction instanceof ConstHigh16) {
-      return ((SingleConstant) instruction).decodedValue() == value;
-    }
-    assert instruction instanceof ConstWide
-        || instruction instanceof ConstWide16
-        || instruction instanceof ConstWide32
-        || instruction instanceof ConstWideHigh16;
-    return ((WideConstant) instruction).decodedValue() == value;
+    return isConstNumber() && getConstNumber() == value;
   }
 
   @Override
@@ -263,6 +250,15 @@
     return instruction instanceof ConstStringJumbo;
   }
 
+  @Override public long getConstNumber() {
+    assert isConstNumber();
+    if (instruction instanceof SingleConstant) {
+      return ((SingleConstant) instruction).decodedValue();
+    }
+    assert instruction instanceof WideConstant;
+    return ((WideConstant) instruction).decodedValue();
+  }
+
   @Override
   public String getConstString() {
     if (instruction instanceof ConstString) {
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 722aa5f..25e10f4 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
@@ -50,6 +50,8 @@
 
   boolean isJumboString();
 
+  long getConstNumber();
+
   String getConstString();
 
   boolean isConstClass();