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());
+ }
+ }
+ }
+}