Enum unboxing: support String#valueOf

Bug: 166397278
Change-Id: Ieee183d7676b0d933cd011d509dc5ccbd6b7be38
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 3d7d397..eea90f9 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
@@ -877,6 +877,10 @@
         if (singleTarget == factory.javaLangSystemMethods.identityHashCode) {
           return Reason.ELIGIBLE;
         }
+        if (singleTarget == factory.stringMembers.valueOf) {
+          requiredEnumInstanceFieldData(enumClass.type, factory.enumMembers.nameField);
+          return Reason.ELIGIBLE;
+        }
         if (singleTarget == factory.objectMembers.getClass
             && (!invokeMethod.hasOutValue() || !invokeMethod.outValue().hasAnyUsers())) {
           // This is a hidden null check.
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 ca106ab..b488c5b 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.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.ArrayAccess;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
@@ -205,15 +206,26 @@
             continue;
           }
         } else if (invokedMethod == factory.javaLangSystemMethods.identityHashCode) {
-          assert invokeStatic.inValues().size() == 1;
+          assert invokeStatic.arguments().size() == 1;
           Value argument = invokeStatic.getArgument(0);
           DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
           if (enumType != null) {
             invokeStatic.outValue().replaceUsers(argument);
             iterator.removeOrReplaceByDebugLocalRead();
           }
+        } else if (invokedMethod == factory.stringMembers.valueOf) {
+          assert invokeStatic.arguments().size() == 1;
+          Value argument = invokeStatic.getArgument(0);
+          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+          if (enumType != null) {
+            DexMethod stringValueOfMethod = computeStringValueOfUtilityMethod(enumType);
+            iterator.replaceCurrentInstruction(
+                new InvokeStatic(
+                    stringValueOfMethod, invokeStatic.outValue(), invokeStatic.arguments()));
+            continue;
+          }
         } else if (invokedMethod == factory.objectsMethods.requireNonNull) {
-          assert invokeStatic.inValues().size() == 1;
+          assert invokeStatic.arguments().size() == 1;
           Value argument = invokeStatic.getArgument(0);
           DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
           if (enumType != null) {
@@ -221,7 +233,7 @@
                 iterator, invokeStatic, zeroCheckMethod, m -> synthesizeZeroCheckMethod());
           }
         } else if (invokedMethod == factory.objectsMethods.requireNonNullWithMessage) {
-          assert invokeStatic.inValues().size() == 2;
+          assert invokeStatic.arguments().size() == 2;
           Value argument = invokeStatic.getArgument(0);
           DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
           if (enumType != null) {
@@ -407,7 +419,24 @@
             factory.createProto(field.type, factory.intType),
             methodName);
     utilityMethods.computeIfAbsent(
-        fieldMethod, m -> synthesizeInstanceFieldMethod(m, enumType, field));
+        fieldMethod, m -> synthesizeInstanceFieldMethod(m, enumType, field, null));
+    return fieldMethod;
+  }
+
+  private DexMethod computeStringValueOfUtilityMethod(DexType enumType) {
+    // TODO(b/167994636): remove duplication between instance field name read and this method.
+    assert enumsToUnbox.containsEnum(enumType);
+    String methodName = "string$valueOf$" + compatibleName(enumType);
+    DexMethod fieldMethod =
+        factory.createMethod(
+            factory.enumUnboxingUtilityType,
+            factory.createProto(factory.stringType, factory.intType),
+            methodName);
+    AbstractValue nullString =
+        appView.abstractValueFactory().createSingleStringValue(factory.createString("null"));
+    utilityMethods.computeIfAbsent(
+        fieldMethod,
+        m -> synthesizeInstanceFieldMethod(m, enumType, factory.enumMembers.nameField, nullString));
     return fieldMethod;
   }
 
@@ -489,7 +518,7 @@
   }
 
   private DexEncodedMethod synthesizeInstanceFieldMethod(
-      DexMethod method, DexType enumType, DexField field) {
+      DexMethod method, DexType enumType, DexField field, AbstractValue nullValue) {
     assert method.proto.returnType == field.type;
     assert unboxedEnumsInstanceFieldData.getInstanceFieldData(enumType, field).isMapping();
     CfCode cfCode =
@@ -500,7 +529,8 @@
                 enumsToUnbox.getEnumValueInfoMap(enumType),
                 unboxedEnumsInstanceFieldData
                     .getInstanceFieldData(enumType, field)
-                    .asEnumFieldMappingData())
+                    .asEnumFieldMappingData(),
+                nullValue)
             .generateCfCode();
     return synthesizeUtilityMethod(cfCode, method, false);
   }
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 4d15f0f..52cdb30 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
@@ -66,17 +66,20 @@
     private final DexType returnType;
     private final EnumValueInfoMap enumValueInfoMap;
     private final EnumInstanceFieldMappingData fieldDataMap;
+    private final AbstractValue nullValue;
 
     public EnumUnboxingInstanceFieldCfCodeProvider(
         AppView<?> appView,
         DexType holder,
         DexType returnType,
         EnumValueInfoMap enumValueInfoMap,
-        EnumInstanceFieldMappingData fieldDataMap) {
+        EnumInstanceFieldMappingData fieldDataMap,
+        AbstractValue nullValue) {
       super(appView, holder);
       this.returnType = returnType;
       this.enumValueInfoMap = enumValueInfoMap;
       this.fieldDataMap = fieldDataMap;
+      this.nullValue = nullValue;
     }
 
     @Override
@@ -113,9 +116,15 @@
             }
           });
 
-      // throw null;
-      instructions.add(new CfConstNull());
-      instructions.add(new CfThrow());
+      if (nullValue != null) {
+        // return "null"
+        addCfInstructionsForAbstractValue(instructions, nullValue, returnType);
+        instructions.add(new CfReturn(ValueType.fromDexType(returnType)));
+      } else {
+        // throw null;
+        instructions.add(new CfConstNull());
+        instructions.add(new CfThrow());
+      }
 
       return standardCfCodeFromInstructions(instructions);
     }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/StringValueOfEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/StringValueOfEnumUnboxingTest.java
new file mode 100644
index 0000000..1ab86b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/StringValueOfEnumUnboxingTest.java
@@ -0,0 +1,77 @@
+// 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.NeverInline;
+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 StringValueOfEnumUnboxingTest extends EnumUnboxingTestBase {
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public StringValueOfEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> classToTest = Main.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classToTest, MyEnum.class)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(MyEnum.class, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+      System.out.println(0);
+      System.out.println(getString(MyEnum.A));
+      System.out.println("A");
+      System.out.println(getString(null));
+      System.out.println("null");
+    }
+
+    @NeverInline
+    private static String getString(MyEnum e) {
+      return String.valueOf(e);
+    }
+  }
+}