Optimize Class#isEnum()

- This reduces the code size of the enum comparison
  in type switches

Change-Id: I129c72d9e3025f2013740c829b2b39cc98e17407
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 1dda2a0..5189d88 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1968,6 +1968,7 @@
   public class ClassMethods {
 
     public final DexMethod desiredAssertionStatus;
+    public final DexMethod isEnum = createMethod(classType, createProto(booleanType), "isEnum");
     public final DexMethod forName;
     public final DexMethod forName3;
     public final DexMethod getClassLoader =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
index b166b9e..91d8474 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
@@ -5,41 +5,44 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.AffectedValues;
-import com.android.tools.r8.utils.InternalOptions;
 import java.util.Set;
 
 public class ClassOptimizer extends StatelessLibraryMethodModelCollection {
 
-  private final InternalOptions options;
-  private final DexItemFactory dexItemFactory;
+  private final AppView<?> appView;
+  private final DexItemFactory factory;
   private final DexMethod getConstructor;
   private final DexMethod getDeclaredConstructor;
   private final DexMethod getMethod;
   private final DexMethod getDeclaredMethod;
+  private final DexMethod isEnum;
 
   ClassOptimizer(AppView<?> appView) {
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
-    this.options = appView.options();
-    this.dexItemFactory = dexItemFactory;
-    getConstructor = dexItemFactory.classMethods.getConstructor;
-    getDeclaredConstructor = dexItemFactory.classMethods.getDeclaredConstructor;
-    getMethod = dexItemFactory.classMethods.getMethod;
-    getDeclaredMethod = dexItemFactory.classMethods.getDeclaredMethod;
+    this.appView = appView;
+    this.factory = appView.dexItemFactory();
+    getConstructor = factory.classMethods.getConstructor;
+    getDeclaredConstructor = factory.classMethods.getDeclaredConstructor;
+    getMethod = factory.classMethods.getMethod;
+    getDeclaredMethod = factory.classMethods.getDeclaredMethod;
+    isEnum = factory.classMethods.isEnum;
   }
 
   @Override
   public DexType getType() {
-    return dexItemFactory.classType;
+    return factory.classType;
   }
 
   @Override
@@ -57,9 +60,30 @@
         || singleTargetReference.isIdenticalTo(getMethod)
         || singleTargetReference.isIdenticalTo(getDeclaredMethod)) {
       EmptyVarargsUtil.replaceWithNullIfEmptyArray(
-          invoke, invoke.arguments().size() - 1, code, instructionIterator, options);
+          invoke, invoke.arguments().size() - 1, code, instructionIterator, appView.options());
       assert instructionIterator.peekPrevious() == invoke;
     }
+    if (singleTargetReference.isIdenticalTo(isEnum)) {
+      Value receiver = invoke.getFirstArgument();
+      if (invoke.hasUnusedOutValue()) {
+        if (receiver.getType().isDefinitelyNotNull()) {
+          instructionIterator.removeOrReplaceByDebugLocalRead();
+        }
+        return instructionIterator;
+      }
+      Value aliasedReceiver = receiver.getAliasedValue();
+      if (aliasedReceiver.isConstClass()) {
+        ConstClass constClass = aliasedReceiver.getDefinition().asConstClass();
+        // An enum must both directly extend java.lang.Enum and have the ENUM bit set;
+        // classes for specialized enum constants don't do the former.
+        DexClass dexClass = appView.definitionFor(constClass.getType());
+        if (dexClass != null) {
+          boolean isEnumResult =
+              dexClass.isEnum() && dexClass.getSuperType().isIdenticalTo(factory.enumType);
+          instructionIterator.replaceCurrentInstructionWithConstBoolean(code, isEnumResult);
+        }
+      }
+    }
     return instructionIterator;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/IsEnumRemovalTest.java b/src/test/java/com/android/tools/r8/shaking/enums/IsEnumRemovalTest.java
new file mode 100644
index 0000000..d0bab3a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/IsEnumRemovalTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2025, 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.enums;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Set;
+import org.gradle.internal.impldep.org.testng.Assert;
+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 IsEnumRemovalTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String EXPECTED = StringUtils.lines("true", "false", "false");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public IsEnumRemovalTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(IsEnumRemovalTest.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(IsEnumRemovalTest.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::inspectR8);
+  }
+
+  private void inspectR8(CodeInspector inspector) {
+    ClassSubject main = inspector.clazz(Main.class);
+    assertThat(main, isPresent());
+    Assert.assertEquals(
+        0,
+        main.uniqueMethod()
+            .streamInstructions()
+            .filter(i -> i.isInvoke() && i.getMethod().getName().toString().equals("isEnum"))
+            .count());
+  }
+
+  public enum MyEnum {
+    A {
+      void foo() {
+        System.out.println("aa");
+      }
+    },
+    B {
+      void foo() {
+        System.out.println("bb");
+      }
+    };
+
+    abstract void foo();
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.class.isEnum());
+      System.out.println(MyEnum.B.getClass().isEnum());
+      System.out.println(Set.class.isEnum());
+    }
+  }
+}