Enum unboxing: support toString and name

Bug: 157112269
Change-Id: I1204eb15e85c4f15cb98878399a49ceb5296e6de
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index c179446..3461a0f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -452,14 +452,12 @@
         if (singleTarget == factory.enumMethods.compareTo) {
           return Reason.COMPARE_TO_INVOKE;
         }
-        if (singleTarget == factory.enumMethods.name) {
-          return Reason.NAME_INVOKE;
-        }
-        if (singleTarget == factory.enumMethods.toString) {
-          return Reason.TO_STRING_INVOKE;
-        }
       }
-      if (singleTarget == factory.enumMethods.ordinal) {
+      if (singleTarget == factory.enumMethods.name) {
+        return Reason.ELIGIBLE;
+      } else if (singleTarget == factory.enumMethods.toString) {
+        return Reason.ELIGIBLE;
+      } else if (singleTarget == factory.enumMethods.ordinal) {
         return Reason.ELIGIBLE;
       } else if (singleTarget == factory.enumMethods.constructor) {
         // Enum constructor call is allowed only if first call of an enum initializer.
@@ -664,8 +662,6 @@
     VALUE_OF_INVOKE,
     VALUES_INVOKE,
     COMPARE_TO_INVOKE,
-    TO_STRING_INVOKE,
-    NAME_INVOKE,
     UNSUPPORTED_LIBRARY_CALL,
     MISSING_INFO_MAP,
     INVALID_FIELD_PUT,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 6f6fba1..e417ad9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ArrayAccess;
+import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -46,6 +47,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -104,6 +106,7 @@
       return Sets.newIdentityHashSet();
     }
     assert code.isConsistentSSABeforeTypesAreCorrect();
+    Map<Instruction, DexType> convertedEnums = new IdentityHashMap<>();
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
     InstructionListIterator iterator = code.instructionListIterator();
     while (iterator.hasNext()) {
@@ -113,14 +116,22 @@
       if (instruction.isInvokeMethodWithReceiver()) {
         InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
         DexMethod invokedMethod = invokeMethod.getInvokedMethod();
-        if (invokedMethod == factory.enumMethods.ordinal
-            && isEnumToUnboxOrInt(invokeMethod.getReceiver().getType())) {
-          instruction =
-              new InvokeStatic(
-                  ordinalUtilityMethod, invokeMethod.outValue(), invokeMethod.inValues());
-          iterator.replaceCurrentInstruction(instruction);
-          requiresOrdinalUtilityMethod = true;
-          continue;
+        DexType enumType = getEnumTypeOrNull(invokeMethod.getReceiver(), convertedEnums);
+        if (enumType != null) {
+          if (invokedMethod == factory.enumMethods.ordinal) {
+            instruction =
+                new InvokeStatic(
+                    ordinalUtilityMethod, invokeMethod.outValue(), invokeMethod.inValues());
+            iterator.replaceCurrentInstruction(instruction);
+            requiresOrdinalUtilityMethod = true;
+            continue;
+          } else if (invokedMethod == factory.enumMethods.name
+              || invokedMethod == factory.enumMethods.toString) {
+            DexMethod toStringMethod = computeDefaultToStringUtilityMethod(enumType);
+            iterator.replaceCurrentInstruction(
+                new InvokeStatic(toStringMethod, invokeMethod.outValue(), invokeMethod.inValues()));
+            continue;
+          }
         }
         // TODO(b/147860220): rewrite also other enum methods.
       } else if (instruction.isInvokeStatic()) {
@@ -138,11 +149,13 @@
               rewrittenOutValue = code.createValue(TypeElement.getInt());
               affectedPhis.addAll(outValue.uniquePhiUsers());
             }
-            iterator.replaceCurrentInstruction(
+            InvokeStatic invoke =
                 new InvokeStatic(
                     valueOfMethod,
                     rewrittenOutValue,
-                    Collections.singletonList(invokeStatic.inValues().get(1))));
+                    Collections.singletonList(invokeStatic.inValues().get(1)));
+            iterator.replaceCurrentInstruction(invoke);
+            convertedEnums.put(invoke, enumType);
             continue;
           }
         }
@@ -172,23 +185,28 @@
             Value rewrittenOutValue =
                 code.createValue(
                     ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
-            iterator.replaceCurrentInstruction(
-                new InvokeStatic(methodValues, rewrittenOutValue, ImmutableList.of()));
+            InvokeStatic invoke =
+                new InvokeStatic(methodValues, rewrittenOutValue, ImmutableList.of());
+            iterator.replaceCurrentInstruction(invoke);
+            convertedEnums.put(invoke, holder);
           } else {
             // Replace by ordinal + 1 for null check (null is 0).
             assert enumValueInfo != null
                 : "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
-            iterator.replaceCurrentInstruction(
-                code.createIntConstant(enumValueInfo.convertToInt()));
+            ConstNumber intConstant = code.createIntConstant(enumValueInfo.convertToInt());
+            iterator.replaceCurrentInstruction(intConstant);
+            convertedEnums.put(intConstant, holder);
           }
         }
       }
       // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
       if (instruction.isArrayAccess()) {
         ArrayAccess arrayAccess = instruction.asArrayAccess();
-        if (shouldRewriteArrayAccess(arrayAccess)) {
+        DexType enumType = getEnumTypeOrNull(arrayAccess);
+        if (enumType != null) {
           instruction = arrayAccess.withMemberType(MemberType.INT);
           iterator.replaceCurrentInstruction(instruction);
+          convertedEnums.put(instruction, enumType);
         }
         assert validateArrayAccess(arrayAccess);
       }
@@ -209,14 +227,16 @@
     return true;
   }
 
-  private boolean isEnumToUnboxOrInt(TypeElement type) {
+  private DexType getEnumTypeOrNull(Value receiver, Map<Instruction, DexType> convertedEnums) {
+    TypeElement type = receiver.getType();
     if (type.isInt()) {
-      return true;
+      return convertedEnums.get(receiver.definition);
     }
     if (!type.isClassType()) {
-      return false;
+      return null;
     }
-    return enumsToUnbox.containsEnum(type.asClassType().getClassType());
+    DexType enumType = type.asClassType().getClassType();
+    return enumsToUnbox.containsEnum(enumType) ? enumType : null;
   }
 
   public String compatibleName(DexType type) {
@@ -270,18 +290,32 @@
     return valueOf;
   }
 
-  private boolean shouldRewriteArrayAccess(ArrayAccess arrayAccess) {
+  private DexMethod computeDefaultToStringUtilityMethod(DexType type) {
+    assert enumsToUnbox.containsEnum(type);
+    DexMethod toString =
+        factory.createMethod(
+            factory.enumUnboxingUtilityType,
+            factory.createProto(factory.stringType, factory.intType),
+            "toString" + compatibleName(type));
+    extraUtilityMethods.computeIfAbsent(toString, m -> synthesizeToStringUtilityMethod(m, type));
+    return toString;
+  }
+
+  private DexType getEnumTypeOrNull(ArrayAccess arrayAccess) {
     ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
     if (arrayType == null) {
       assert arrayAccess.array().getType().isNullType();
-      return false;
+      return null;
     }
     if (arrayType.getNesting() != 1) {
-      return false;
+      return null;
     }
     TypeElement baseType = arrayType.getBaseType();
-    return baseType.isClassType()
-        && enumsToUnbox.containsEnum(baseType.asClassType().getClassType());
+    if (!baseType.isClassType()) {
+      return null;
+    }
+    DexType classType = baseType.asClassType().getClassType();
+    return enumsToUnbox.containsEnum(classType) ? classType : null;
   }
 
   void synthesizeEnumUnboxingUtilityMethods(
@@ -340,6 +374,17 @@
         DexProgramClass::checksumFromType);
   }
 
+  private DexEncodedMethod synthesizeToStringUtilityMethod(DexMethod method, DexType enumType) {
+    CfCode cfCode =
+        new EnumUnboxingCfCodeProvider.EnumUnboxingDefaultToStringCfCodeProvider(
+                appView,
+                factory.enumUnboxingUtilityType,
+                enumType,
+                enumsToUnbox.getEnumValueInfoMap(enumType))
+            .generateCfCode();
+    return synthesizeUtilityMethod(cfCode, method, false);
+  }
+
   private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
     CfCode cfCode =
         new EnumUnboxingCfCodeProvider.EnumUnboxingValueOfCfCodeProvider(
@@ -348,14 +393,7 @@
                 enumType,
                 enumsToUnbox.getEnumValueInfoMap(enumType))
             .generateCfCode();
-    return new DexEncodedMethod(
-        method,
-        synthesizedMethodAccessFlags(false),
-        DexAnnotationSet.empty(),
-        ParameterAnnotationsList.empty(),
-        cfCode,
-        REQUIRED_CLASS_FILE_VERSION,
-        true);
+    return synthesizeUtilityMethod(cfCode, method, false);
   }
 
   // TODO(b/150178516): Add a test for this case.
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
index f9f66e8..61bdbbc 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.synthetic;
 
+import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
@@ -36,6 +37,49 @@
     super(appView, holder);
   }
 
+  public static class EnumUnboxingDefaultToStringCfCodeProvider extends EnumUnboxingCfCodeProvider {
+
+    private DexType enumType;
+    private EnumValueInfoMap map;
+
+    public EnumUnboxingDefaultToStringCfCodeProvider(
+        AppView<?> appView, DexType holder, DexType enumType, EnumValueInfoMap map) {
+      super(appView, holder);
+      this.enumType = enumType;
+      this.map = map;
+    }
+
+    @Override
+    public CfCode generateCfCode() {
+      // Generated static method, for class com.x.MyEnum {A,B} would look like:
+      // String UtilityClass#com.x.MyEnum_toString(int i) {
+      // if (i == 1) { return "A";}
+      // if (i == 2) { return "B";}
+      // throw null;
+      DexItemFactory factory = appView.dexItemFactory();
+      List<CfInstruction> instructions = new ArrayList<>();
+
+      // if (i == 1) { return "A";}
+      // if (i == 2) { return "B";}
+      map.forEach(
+          (field, enumValueInfo) -> {
+            CfLabel dest = new CfLabel();
+            instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
+            instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
+            instructions.add(new CfIf(If.Type.EQ, ValueType.INT, dest));
+            instructions.add(new CfConstString(field.name));
+            instructions.add(new CfReturn(ValueType.OBJECT));
+            instructions.add(dest);
+          });
+
+      // throw null;
+      instructions.add(new CfConstNull());
+      instructions.add(new CfThrow());
+
+      return standardCfCodeFromInstructions(instructions);
+    }
+  }
+
   public static class EnumUnboxingValueOfCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
     private DexType enumType;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ToStringEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ToStringEnumUnboxingTest.java
new file mode 100644
index 0000000..d8d781e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ToStringEnumUnboxingTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2020, 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.enumunboxing;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import java.util.List;
+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 ToStringEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final KeepRule enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public ToStringEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, KeepRule enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> success = EnumNameToString.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(ToStringEnumUnboxingTest.class)
+            .addKeepMainRule(EnumNameToString.class)
+            .enableNeverClassInliningAnnotations()
+            .addKeepRules(enumKeepRules.getKeepRule())
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m ->
+                    assertEnumIsUnboxed(
+                        success.getDeclaredClasses()[0], success.getSimpleName(), m))
+            .run(parameters.getRuntime(), success)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  static class EnumNameToString {
+
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.toString());
+      System.out.println(MyEnum.A.name());
+      System.out.println(MyEnum.B.toString());
+      System.out.println(MyEnum.B.name());
+      try {
+        System.out.println(((MyEnum) null).toString());
+      } catch (NullPointerException e) {
+        System.out.println("npeToString " + e.getMessage());
+        System.out.println("npeToString " + e.getMessage());
+      }
+      try {
+        System.out.println(((MyEnum) null).name());
+      } catch (NullPointerException e) {
+        System.out.println("npeName " + e.getMessage());
+        System.out.println("npeName " + e.getMessage());
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ToStringOverrideEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ToStringOverrideEnumUnboxingTest.java
new file mode 100644
index 0000000..b7643a0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ToStringOverrideEnumUnboxingTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2020, 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.enumunboxing;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import java.util.List;
+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 ToStringOverrideEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final KeepRule enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public ToStringOverrideEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, KeepRule enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> success = EnumNameToString.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(ToStringOverrideEnumUnboxingTest.class)
+            .addKeepMainRule(EnumNameToString.class)
+            .enableNeverClassInliningAnnotations()
+            .addKeepRules(enumKeepRules.getKeepRule())
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsBoxed(success.getDeclaredClasses()[0], success.getSimpleName(), m))
+            .run(parameters.getRuntime(), success)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  static class EnumNameToString {
+
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B {
+        @Override
+        public String toString() {
+          return "bezinga";
+        }
+      }
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.toString());
+      System.out.println(MyEnum.A.name());
+      System.out.println(MyEnum.B.toString());
+      System.out.println("bezinga");
+      System.out.println(MyEnum.B.name());
+      System.out.println("B");
+      try {
+        System.out.println(((MyEnum) null).toString());
+      } catch (NullPointerException e) {
+        System.out.println("npeToString " + e.getMessage());
+        System.out.println("npeToString " + e.getMessage());
+      }
+      try {
+        System.out.println(((MyEnum) null).name());
+      } catch (NullPointerException e) {
+        System.out.println("npeName " + e.getMessage());
+        System.out.println("npeName " + e.getMessage());
+      }
+    }
+  }
+}