Fix dispatch for toString
Bug: b/271385332
Change-Id: I20a8be42185325707515b8eb281cd8a85ea6f5ef
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 63595f7..da8ac44 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -108,7 +108,7 @@
if (superEnum != previousContext.getHolderType()) {
DexMethod reference = previous.getReference();
if (reference.getHolderType() != superEnum) {
- // Rebind the reference to the superEnum if that is not the case.
+ // We are in an enum subtype where super-invokes are rebound differently.
reference = reference.withHolder(superEnum, dexItemFactory());
}
result = newMethodSignatures.getRepresentativeValue(reference);
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 94470b9..4aa7645 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
@@ -208,21 +208,31 @@
rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
continue;
} else if (invokedMethod.match(factory.enumMembers.toString)) {
+ DexMethod reboundMethod =
+ invokedMethod.withHolder(unboxedEnumsData.representativeType(enumType), factory);
DexMethod lookupMethod =
enumUnboxingLens
.lookupMethod(
- invokedMethod,
+ reboundMethod,
context.getReference(),
invoke.getType(),
enumUnboxingLens.getPrevious())
.getReference();
- // If the lookupMethod is different, then a toString method was on the enumType
- // class, which was moved, and the lens code rewriter will rewrite the invoke to
- // that method.
- if (invoke.isInvokeSuper() || lookupMethod == invokedMethod) {
+ // If the SuperEnum had declared a toString() override, then the unboxer moves it to
+ // the local utility class method corresponding to that override.
+ // If a SubEnum had declared a toString() override, then the unboxer records a
+ // synthetic move from SuperEnum.toString() to the dispatch method on the local
+ // utility class.
+ // When they are the same, then there are no overrides of toString().
+ if (lookupMethod == reboundMethod) {
rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
- continue;
+ } else {
+ DexClassAndMethod dexClassAndMethod = appView.definitionFor(lookupMethod);
+ assert dexClassAndMethod != null;
+ assert dexClassAndMethod.isProgramMethod();
+ replaceEnumInvoke(iterator, invoke, dexClassAndMethod.asProgramMethod());
}
+ continue;
} else if (invokedMethod == factory.objectMembers.getClass) {
rewriteNullCheck(iterator, invoke, context, eventConsumer);
continue;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 92f6d70..8d04a32 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -10,12 +10,14 @@
import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
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.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedField.Builder;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
@@ -77,7 +79,6 @@
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
import java.util.function.Predicate;
class EnumUnboxingTreeFixer {
@@ -529,6 +530,25 @@
}));
}
+ private void processMethod(
+ ProgramMethod method,
+ PrunedItems.Builder prunedItemsBuilder,
+ DexMethodSignatureSet nonPrivateVirtualMethods,
+ LocalEnumUnboxingUtilityClass localUtilityClass,
+ Map<DexMethod, DexEncodedMethod> localUtilityMethods) {
+ if (method.getDefinition().isClassInitializer()
+ && enumDataMap.representativeType(method.getHolderType()) != method.getHolderType()) {
+ assert method.getDefinition().getCode().isEmptyVoidMethod();
+ prunedItemsBuilder.addRemovedMethod(method.getReference());
+ } else if (method.getDefinition().isInstanceInitializer()) {
+ prunedItemsBuilder.addRemovedMethod(method.getReference());
+ } else if (method.getDefinition().isNonPrivateVirtualMethod()) {
+ nonPrivateVirtualMethods.add(method.getReference());
+ } else {
+ directMoveAndMap(localUtilityClass, localUtilityMethods, method);
+ }
+ }
+
private Collection<DexEncodedMethod> createLocalUtilityMethods(
DexProgramClass unboxedEnum,
Set<DexProgramClass> subEnums,
@@ -541,77 +561,133 @@
localUtilityClass
.getDefinition()
.forEachMethod(method -> localUtilityMethods.put(method.getReference(), method));
- // First generate all methods from the super enums, for any override, generate the dispatch
- // method and both the move and super contextual move.
- DexMethodSignatureSet methodsWithOverride = DexMethodSignatureSet.create();
- forEachWhilePruningInstanceInitializers(
- unboxedEnum,
- prunedItemsBuilder,
- method -> {
- DexEncodedMethod superEnumUtilityMethod =
- installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
- Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
- if (method.getDefinition().isNonPrivateVirtualMethod()) {
- for (DexProgramClass subEnum : subEnums) {
- ProgramMethod override = subEnum.lookupProgramMethod(method.getReference());
- if (override != null) {
- methodsWithOverride.add(method);
- DexEncodedMethod subEnumLocalUtilityMethod =
- installLocalUtilityMethod(localUtilityClass, localUtilityMethods, override);
- overrideToUtilityMethods.put(
- override.getReference(), subEnumLocalUtilityMethod.getReference());
- }
- }
- }
- if (overrideToUtilityMethods.isEmpty()) {
- lensBuilder.moveAndMap(
- method.getReference(),
- superEnumUtilityMethod.getReference(),
- method.getDefinition().isStatic());
- } else {
- DexMethod dispatch =
- installDispatchMethod(
- localUtilityClass,
- localUtilityMethods,
- method,
- superEnumUtilityMethod,
- overrideToUtilityMethods)
- .getReference();
- recordEmulatedDispatch(
- method.getReference(), superEnumUtilityMethod.getReference(), dispatch);
- for (DexMethod override : overrideToUtilityMethods.keySet()) {
- recordEmulatedDispatch(override, overrideToUtilityMethods.get(override), dispatch);
- }
- }
- });
- // TODO(b/271385332): Check dispatch methods without implementation in the superEnum.
+
+ // First generate all methods but the ones requiring emulated dispatch.
+ DexMethodSignatureSet nonPrivateVirtualMethods = DexMethodSignatureSet.create();
+ unboxedEnum.forEachProgramMethod(
+ method ->
+ processMethod(
+ method,
+ prunedItemsBuilder,
+ nonPrivateVirtualMethods,
+ localUtilityClass,
+ localUtilityMethods));
// Second for each subEnum generate the remaining methods if not already generated.
for (DexProgramClass subEnum : subEnums) {
- forEachWhilePruningInstanceInitializers(
- subEnum,
- prunedItemsBuilder,
- method -> {
- if (method.getDefinition().isClassInitializer()) {
- assert method.getDefinition().getCode().isEmptyVoidMethod();
- prunedItemsBuilder.addRemovedMethod(method.getReference());
- return;
- }
- if (!methodsWithOverride.contains(method.getReference())) {
- DexEncodedMethod newLocalUtilityMethod =
- installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
- // We cannot remap the subtype <clinit> to the same method than the superEnum one,
- // so we have to extend the design to support subtype <clinit> here.
- assert !method.getDefinition().isClassInitializer();
- lensBuilder.moveAndMap(
- method.getReference(),
- newLocalUtilityMethod.getReference(),
- method.getDefinition().isStatic());
- }
- });
+ subEnum.forEachProgramMethod(
+ method ->
+ processMethod(
+ method,
+ prunedItemsBuilder,
+ nonPrivateVirtualMethods,
+ localUtilityClass,
+ localUtilityMethods));
}
+
+ // Then analyze each method that may require emulated dispatch.
+ for (DexMethodSignature nonPrivateVirtualMethod : nonPrivateVirtualMethods) {
+ processVirtualMethod(
+ nonPrivateVirtualMethod, unboxedEnum, subEnums, localUtilityClass, localUtilityMethods);
+ }
+
return localUtilityMethods.values();
}
+ private void processVirtualMethod(
+ DexMethodSignature nonPrivateVirtualMethod,
+ DexProgramClass unboxedEnum,
+ Set<DexProgramClass> subEnums,
+ LocalEnumUnboxingUtilityClass localUtilityClass,
+ Map<DexMethod, DexEncodedMethod> localUtilityMethods) {
+ // Emulated dispatch is required if there is a "super method" in the superEnum or above,
+ // and at least one override.
+ DexMethod reference = nonPrivateVirtualMethod.withHolder(unboxedEnum.getType(), factory);
+ ProgramMethodSet subimplementations = ProgramMethodSet.create();
+ for (DexProgramClass subEnum : subEnums) {
+ ProgramMethod subMethod = subEnum.lookupProgramMethod(reference);
+ if (subMethod != null) {
+ subimplementations.add(subMethod);
+ }
+ }
+ DexClassAndMethod superMethod = unboxedEnum.lookupProgramMethod(reference);
+ if (superMethod == null) {
+ assert !subimplementations.isEmpty();
+ superMethod = appView.appInfo().lookupSuperTarget(reference, unboxedEnum, appView);
+ assert superMethod == null || superMethod.getReference() == factory.enumMembers.toString;
+ }
+ if (superMethod == null || subimplementations.isEmpty()) {
+ // No emulated dispatch is required, just move everything.
+ if (superMethod != null) {
+ assert superMethod.isProgramMethod();
+ directMoveAndMap(localUtilityClass, localUtilityMethods, superMethod.asProgramMethod());
+ }
+ for (ProgramMethod override : subimplementations) {
+ directMoveAndMap(localUtilityClass, localUtilityMethods, override);
+ }
+ return;
+ }
+ // These methods require emulated dispatch.
+ emulatedDispatchMoveAndMap(
+ localUtilityClass, localUtilityMethods, superMethod, subimplementations);
+ }
+
+ private void emulatedDispatchMoveAndMap(
+ LocalEnumUnboxingUtilityClass localUtilityClass,
+ Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+ DexClassAndMethod superMethod,
+ ProgramMethodSet subimplementations) {
+ assert !subimplementations.isEmpty();
+ DexMethod superUtilityMethod;
+ if (superMethod.isProgramMethod()) {
+ superUtilityMethod =
+ installLocalUtilityMethod(
+ localUtilityClass, localUtilityMethods, superMethod.asProgramMethod())
+ .getReference();
+ } else {
+ // All methods but toString() are final or non-virtual.
+ // We could support other cases by setting correctly the superUtilityMethod here.
+ assert superMethod.getReference() == factory.enumMembers.toString;
+ superUtilityMethod = localUtilityClass.computeToStringUtilityMethod(factory);
+ }
+ Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
+ for (ProgramMethod subMethod : subimplementations) {
+ DexEncodedMethod subEnumLocalUtilityMethod =
+ installLocalUtilityMethod(localUtilityClass, localUtilityMethods, subMethod);
+ overrideToUtilityMethods.put(
+ subMethod.getReference(), subEnumLocalUtilityMethod.getReference());
+ }
+ DexMethod dispatch =
+ installDispatchMethod(
+ localUtilityClass,
+ localUtilityMethods,
+ subimplementations.iterator().next(),
+ superUtilityMethod,
+ overrideToUtilityMethods)
+ .getReference();
+ if (superMethod.isProgramMethod()) {
+ recordEmulatedDispatch(superMethod.getReference(), superUtilityMethod, dispatch);
+ } else {
+ lensBuilder.mapToDispatch(
+ superMethod
+ .getReference()
+ .withHolder(localUtilityClass.getSynthesizingContext().getType(), factory),
+ dispatch);
+ }
+ for (DexMethod override : overrideToUtilityMethods.keySet()) {
+ recordEmulatedDispatch(override, overrideToUtilityMethods.get(override), dispatch);
+ }
+ }
+
+ private void directMoveAndMap(
+ LocalEnumUnboxingUtilityClass localUtilityClass,
+ Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+ ProgramMethod method) {
+ DexEncodedMethod utilityMethod =
+ installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
+ lensBuilder.moveAndMap(
+ method.getReference(), utilityMethod.getReference(), method.getDefinition().isStatic());
+ }
+
public void recordEmulatedDispatch(DexMethod from, DexMethod move, DexMethod dispatch) {
// Move is used for getRenamedSignature and to remap invoke-super.
// Map is used to remap all the other invokes.
@@ -622,14 +698,14 @@
private DexEncodedMethod installDispatchMethod(
LocalEnumUnboxingUtilityClass localUtilityClass,
Map<DexMethod, DexEncodedMethod> localUtilityMethods,
- ProgramMethod method,
- DexEncodedMethod superEnumUtilityMethod,
+ ProgramMethod representative,
+ DexMethod superUtilityMethod,
Map<DexMethod, DexMethod> map) {
assert !map.isEmpty();
DexMethod newLocalUtilityMethodReference =
factory.createFreshMethodNameWithoutHolder(
- "_dispatch_" + method.getName().toString(),
- fixupProto(factory.prependHolderToProto(method.getReference())),
+ "_dispatch_" + representative.getName().toString(),
+ fixupProto(factory.prependHolderToProto(representative.getReference())),
localUtilityClass.getType(),
newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
Int2ObjectMap<DexMethod> methodMap = new Int2ObjectArrayMap<>();
@@ -637,7 +713,7 @@
map.forEach(
(methodReference, newMethodReference) ->
typeToMethod.put(methodReference.getHolderType(), newMethodReference));
- DexProgramClass unboxedEnum = method.getHolder();
+ DexProgramClass unboxedEnum = localUtilityClass.getSynthesizingContext();
assert enumDataMap.get(unboxedEnum).valuesTypes != null;
enumDataMap
.get(unboxedEnum)
@@ -650,10 +726,7 @@
});
CfCodeWithLens codeWithLens =
new EnumUnboxingMethodDispatchCfCodeProvider(
- appView,
- localUtilityClass.getType(),
- superEnumUtilityMethod.getReference(),
- methodMap)
+ appView, localUtilityClass.getType(), superUtilityMethod, methodMap)
.generateCfCode();
DexEncodedMethod newLocalUtilityMethod =
DexEncodedMethod.builder()
@@ -661,8 +734,8 @@
.setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
.setCode(codeWithLens)
.setClassFileVersion(unboxedEnum.getInitialClassFileVersion())
- .setApiLevelForDefinition(superEnumUtilityMethod.getApiLevelForDefinition())
- .setApiLevelForCode(superEnumUtilityMethod.getApiLevelForCode())
+ .setApiLevelForDefinition(representative.getDefinition().getApiLevelForDefinition())
+ .setApiLevelForCode(representative.getDefinition().getApiLevelForCode())
.build();
dispatchMethods.put(
newLocalUtilityMethod.asProgramMethod(localUtilityClass.getDefinition()), codeWithLens);
@@ -685,20 +758,6 @@
return newLocalUtilityMethod;
}
- private void forEachWhilePruningInstanceInitializers(
- DexProgramClass clazz,
- PrunedItems.Builder prunedItemsBuilder,
- Consumer<? super ProgramMethod> consumer) {
- clazz.forEachProgramMethod(
- method -> {
- if (method.getDefinition().isInstanceInitializer()) {
- prunedItemsBuilder.addRemovedMethod(method.getReference());
- } else {
- consumer.accept(method);
- }
- });
- }
-
private DexEncodedMethod createLocalUtilityMethod(
ProgramMethod method,
LocalEnumUnboxingUtilityClass localUtilityClass,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
index 4e7e556..a7fdb84 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.graph.ClassAccessFlags;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
@@ -67,24 +68,35 @@
return method;
}
+ private DexString computeGetInstanceFieldMethodName(DexField field, DexItemFactory factory) {
+ String fieldName = field.getName().toString();
+ if (field.getHolderType() == getSynthesizingContext().getType()) {
+ return factory.createString(
+ "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
+ }
+ assert field == factory.enumMembers.nameField || field == factory.enumMembers.ordinalField;
+ return field.getName();
+ }
+
+ private DexProto computeGetInstanceFieldMethodProto(DexField field, DexItemFactory factory) {
+ return factory.createProto(field.getType(), factory.intType);
+ }
+
+ public DexMethod computeToStringUtilityMethod(DexItemFactory factory) {
+ DexField nameField = factory.enumMembers.nameField;
+ DexString name = computeGetInstanceFieldMethodName(nameField, factory);
+ DexProto proto = computeGetInstanceFieldMethodProto(nameField, factory);
+ return factory.createMethod(getDefinition().getType(), proto, name);
+ }
+
private ProgramMethod ensureGetInstanceFieldMethod(
AppView<AppInfoWithLiveness> appView, DexField field) {
DexItemFactory dexItemFactory = appView.dexItemFactory();
- String fieldName = field.getName().toString();
- DexString methodName;
- if (field.getHolderType() == getSynthesizingContext().getType()) {
- methodName =
- dexItemFactory.createString(
- "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
- } else {
- assert field == appView.dexItemFactory().enumMembers.nameField
- || field == appView.dexItemFactory().enumMembers.ordinalField;
- methodName = field.getName();
- }
+ DexString methodName = computeGetInstanceFieldMethodName(field, dexItemFactory);
return internalEnsureMethod(
appView,
methodName,
- dexItemFactory.createProto(field.getType(), dexItemFactory.intType),
+ computeGetInstanceFieldMethodProto(field, dexItemFactory),
method ->
new EnumUnboxingCfCodeProvider.EnumUnboxingInstanceFieldCfCodeProvider(
appView, getType(), data, field)
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java
new file mode 100644
index 0000000..a88db9a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2023, 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.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.StringUtils;
+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 SuperToStringEnumMergingTest extends EnumUnboxingTestBase {
+
+ private static final String EXPECTED_RESULT =
+ StringUtils.lines("> A", "B", "! C", "> - A", "- B", "! - C");
+
+ 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 SuperToStringEnumMergingTest(
+ TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+ this.parameters = parameters;
+ this.enumValueOptimization = enumValueOptimization;
+ this.enumKeepRules = enumKeepRules;
+ }
+
+ @Test
+ public void testEnumUnboxing() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(SuperToStringEnumMergingTest.class)
+ .addKeepMainRule(Main.class)
+ .addKeepRules(enumKeepRules.getKeepRules())
+ .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+ .addEnumUnboxingInspector(
+ inspector -> inspector.assertUnboxed(EnumToString.class, EnumToStringOverride.class))
+ .enableInliningAnnotations()
+ .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+ .setMinApi(parameters)
+ .allowDiagnosticInfoMessages()
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_RESULT);
+ }
+
+ enum EnumToString {
+ A {
+ @NeverInline
+ @Override
+ public String toString() {
+ return "> " + super.toString();
+ }
+ },
+ B,
+ C {
+ @NeverInline
+ @Override
+ public String toString() {
+ return "! " + super.toString();
+ }
+ };
+ }
+
+ enum EnumToStringOverride {
+ A {
+ @NeverInline
+ @Override
+ public String toString() {
+ return "> " + super.toString();
+ }
+ },
+ B,
+ C {
+ @NeverInline
+ @Override
+ public String toString() {
+ return "! " + super.toString();
+ }
+ };
+
+ @NeverInline
+ @Override
+ public String toString() {
+ return "- " + super.toString();
+ }
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ System.out.println(EnumToString.A.toString());
+ System.out.println(EnumToString.B.toString());
+ System.out.println(EnumToString.C.toString());
+ System.out.println(EnumToStringOverride.A.toString());
+ System.out.println(EnumToStringOverride.B.toString());
+ System.out.println(EnumToStringOverride.C.toString());
+ }
+ }
+}