Allow enums with subtypes without enum flag to be unboxed
Bug: b/315186101
Change-Id: I98d002594af56c2f63e953d1236652f3d3c882f8
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index dba763b..888f1e8 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -184,6 +184,10 @@
set(Constants.ACC_ENUM);
}
+ public void unsetEnum() {
+ unset(Constants.ACC_ENUM);
+ }
+
public boolean isRecord() {
return isSet(Constants.ACC_RECORD);
}
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 0eb2d7e..6050c9d 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -490,7 +490,6 @@
(clazz, allocationSitesForClass) -> {
DexType type = lens.lookupType(clazz.type);
if (type.isPrimitiveType()) {
- assert clazz.isEnum();
return;
}
DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 5132141..373c579 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -1417,12 +1417,12 @@
DexClassAndMethod mostAccurateTarget = singleTarget == null ? resolvedMethod : singleTarget;
if (mostAccurateTarget.isProgramMethod()) {
- if (mostAccurateTarget.getHolder().isEnum()
- && resolvedMethod.getDefinition().isInstanceInitializer()) {
+ DexProgramClass candidateOrNull =
+ getEnumUnboxingCandidateOrNull(mostAccurateTarget.getHolderType());
+ if (candidateOrNull != null && resolvedMethod.getDefinition().isInstanceInitializer()) {
// The enum instance initializer is only allowed to be called from an initializer of the
// enum itself.
- if (getEnumUnboxingCandidateOrNull(code.context().getHolderType())
- != getEnumUnboxingCandidateOrNull(mostAccurateTarget.getHolderType())
+ if (getEnumUnboxingCandidateOrNull(code.context().getHolderType()) != candidateOrNull
|| !context.getDefinition().isInitializer()) {
return Reason.INVALID_INIT;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 86709f7..c0a7655 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -4,7 +4,6 @@
package com.android.tools.r8.ir.optimize.enums;
-import static com.android.tools.r8.utils.MapUtils.ignoreKey;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
@@ -14,16 +13,14 @@
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.ir.optimize.enums.eligibility.Reason;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.KeepInfoCollection;
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Set;
+import java.util.List;
class EnumUnboxingCandidateAnalysis {
@@ -35,12 +32,9 @@
private final AppView<AppInfoWithLiveness> appView;
private final EnumUnboxerImpl enumUnboxer;
private final DexItemFactory factory;
- private EnumUnboxingCandidateInfoCollection enumToUnboxCandidates =
+ private final EnumUnboxingCandidateInfoCollection enumToUnboxCandidates =
new EnumUnboxingCandidateInfoCollection();
- private final Map<DexType, Set<DexProgramClass>> enumSubclasses = new IdentityHashMap<>();
- private final Set<DexType> ineligibleCandidates = Sets.newIdentityHashSet();
-
EnumUnboxingCandidateAnalysis(AppView<AppInfoWithLiveness> appView, EnumUnboxerImpl enumUnboxer) {
this.appView = appView;
this.enumUnboxer = enumUnboxer;
@@ -54,13 +48,13 @@
// disables the enum unboxer.
return enumToUnboxCandidates;
}
+ ImmediateProgramSubtypingInfo subtypingInfo = ImmediateProgramSubtypingInfo.create(appView);
+ // In recent Kotlin enum subclasses are not flagged as enum, so we cannot rely on the flag.
for (DexProgramClass clazz : appView.appInfo().classes()) {
- if (clazz.isEnum()) {
- analyzeEnum(graphLensForPrimaryOptimizationPass, clazz);
+ if (clazz.isEnum() && clazz.superType.isIdenticalTo(factory.enumType)) {
+ analyzeEnum(graphLensForPrimaryOptimizationPass, clazz, subtypingInfo);
}
}
- removeIneligibleCandidates();
- setEnumSubclassesOnCandidates();
removeEnumsInAnnotations();
removePinnedCandidates();
if (appView.options().protoShrinking().isProtoShrinkingEnabled()) {
@@ -70,42 +64,28 @@
return enumToUnboxCandidates;
}
- private void setEnumSubclassesOnCandidates() {
- enumToUnboxCandidates.forEachCandidateInfo(
- info -> {
- DexType type = info.getEnumClass().getType();
- enumToUnboxCandidates.setEnumSubclasses(
- type, enumSubclasses.getOrDefault(type, ImmutableSet.of()));
- });
- }
-
- private void removeIneligibleCandidates() {
- for (DexType ineligibleCandidate : ineligibleCandidates) {
- enumToUnboxCandidates.removeCandidate(ineligibleCandidate);
+ private void analyzeEnum(
+ GraphLens graphLensForPrimaryOptimizationPass,
+ DexProgramClass clazz,
+ ImmediateProgramSubtypingInfo subtypingInfo) {
+ if (!isSuperEnumUnboxingCandidate(clazz)) {
+ return;
}
- }
- @SuppressWarnings("ReferenceEquality")
- private void analyzeEnum(GraphLens graphLensForPrimaryOptimizationPass, DexProgramClass clazz) {
- if (clazz.superType == factory.enumType) {
- if (isSuperEnumUnboxingCandidate(clazz)) {
- enumToUnboxCandidates.addCandidate(appView, clazz, graphLensForPrimaryOptimizationPass);
+ List<DexProgramClass> subtypes = subtypingInfo.getSubclasses(clazz);
+ ImmutableSet.Builder<DexProgramClass> subEnumClassesBuilder = ImmutableSet.builder();
+ for (DexProgramClass subEnum : subtypes) {
+ if (!isSubEnumUnboxingCandidate(subEnum)) {
+ return;
}
- } else {
- if (isSubEnumUnboxingCandidate(clazz)
- && appView.options().testing.enableEnumWithSubtypesUnboxing) {
- enumSubclasses
- .computeIfAbsent(clazz.superType, ignoreKey(Sets::newIdentityHashSet))
- .add(clazz);
- } else {
- ineligibleCandidates.add(clazz.superType);
- }
+ subEnumClassesBuilder.add(subEnum);
}
+ enumToUnboxCandidates.addCandidate(
+ appView, clazz, subEnumClassesBuilder.build(), graphLensForPrimaryOptimizationPass);
}
@SuppressWarnings("ReferenceEquality")
private boolean isSubEnumUnboxingCandidate(DexProgramClass clazz) {
- assert clazz.isEnum();
boolean result = true;
// Javac does not seem to generate enums with more than a single subtype level.
// TODO(b/273910479): Stop using isEffectivelyFinal.
@@ -132,29 +112,14 @@
return result;
}
- @SuppressWarnings("ReferenceEquality")
private boolean isSuperEnumUnboxingCandidate(DexProgramClass clazz) {
assert clazz.isEnum();
-
- // This is used in debug mode, where we don't do quick returns to log all the reasons an enum
- // is not unboxed.
- boolean result = true;
-
- // TODO(b/271385332): Change this into an assert when legacy is removed.
- if (clazz.superType != factory.enumType) {
- if (!enumUnboxer.reportFailure(clazz, Reason.INVALID_LIBRARY_SUPERTYPE)) {
- return false;
- }
- result = false;
- }
-
+ assert clazz.superType.isIdenticalTo(factory.enumType);
if (clazz.instanceFields().size() > MAX_INSTANCE_FIELDS_FOR_UNBOXING) {
- if (!enumUnboxer.reportFailure(clazz, Reason.MANY_INSTANCE_FIELDS)) {
- return false;
- }
- result = false;
+ enumUnboxer.reportFailure(clazz, Reason.MANY_INSTANCE_FIELDS);
+ return false;
}
- return result;
+ return true;
}
private void removeEnumsInAnnotations() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
index aaa428d..c2444a8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
@@ -34,11 +34,16 @@
public void addCandidate(
AppView<AppInfoWithLiveness> appView,
DexProgramClass enumClass,
+ Set<DexProgramClass> subclasses,
GraphLens graphLensForPrimaryOptimizationPass) {
assert !enumTypeToInfo.containsKey(enumClass.type);
enumTypeToInfo.put(
enumClass.type,
- new EnumUnboxingCandidateInfo(appView, enumClass, graphLensForPrimaryOptimizationPass));
+ new EnumUnboxingCandidateInfo(
+ appView, enumClass, subclasses, graphLensForPrimaryOptimizationPass));
+ for (DexProgramClass subclass : subclasses) {
+ subEnumToSuperEnumMap.put(subclass.getType(), enumClass.getType());
+ }
}
public boolean hasSubtypes(DexType enumType) {
@@ -50,13 +55,6 @@
enumTypeToInfo.get(enumType).getSubclasses(), DexClass::getType);
}
- public void setEnumSubclasses(DexType superEnum, Set<DexProgramClass> subclasses) {
- enumTypeToInfo.get(superEnum).setSubclasses(subclasses);
- for (DexProgramClass subclass : subclasses) {
- subEnumToSuperEnumMap.put(subclass.getType(), superEnum);
- }
- }
-
public void addPrunedMethod(ProgramMethod method) {
prunedMethods.add(method.getReference());
}
@@ -174,10 +172,12 @@
public EnumUnboxingCandidateInfo(
AppView<AppInfoWithLiveness> appView,
DexProgramClass enumClass,
+ Set<DexProgramClass> subclasses,
GraphLens graphLensForPrimaryOptimizationPass) {
assert enumClass != null;
assert appView.graphLens() == graphLensForPrimaryOptimizationPass;
this.enumClass = enumClass;
+ this.subclasses = subclasses;
this.methodDependencies =
LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(
graphLensForPrimaryOptimizationPass);
@@ -188,10 +188,6 @@
return subclasses;
}
- public void setSubclasses(Set<DexProgramClass> subclasses) {
- this.subclasses = subclasses;
- }
-
public DexProgramClass getEnumClass() {
return enumClass;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
index 56b05f6..3cf50f1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
@@ -11,18 +11,13 @@
public abstract class Reason {
public static final Reason ELIGIBLE = new StringReason("ELIGIBLE");
- public static final Reason ACCESSIBILITY = new StringReason("ACCESSIBILITY");
public static final Reason ANNOTATION = new StringReason("ANNOTATION");
public static final Reason PINNED = new StringReason("PINNED");
public static final Reason DOWN_CAST = new StringReason("DOWN_CAST");
- public static final Reason INVALID_LIBRARY_SUPERTYPE =
- new StringReason("INVALID_LIBRARY_SUPERTYPE");
- public static final Reason SUBTYPES = new StringReason("SUBTYPES");
public static final Reason SUBENUM_SUBTYPES = new StringReason("SUBENUM_SUBTYPES");
public static final Reason SUBENUM_INVALID_HIERARCHY =
new StringReason("SUBENUM_INVALID_HIERARCHY");
public static final Reason SUBENUM_INSTANCE_FIELDS = new StringReason("SUBENUM_INSTANCE_FIELDS");
- public static final Reason SUBENUM_STATIC_MEMBER = new StringReason("SUBENUM_STATIC_MEMBER");
public static final Reason MANY_INSTANCE_FIELDS = new StringReason("MANY_INSTANCE_FIELDS");
public static final Reason DEFAULT_METHOD_INVOKE = new StringReason("DEFAULT_METHOD_INVOKE");
public static final Reason UNRESOLVABLE_FIELD = new StringReason("UNRESOLVABLE_FIELD");
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index b71ad68..97aadb3 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2411,7 +2411,6 @@
public boolean enableSwitchToIfRewriting = true;
public boolean enableEnumUnboxingDebugLogs =
System.getProperty("com.android.tools.r8.enableEnumUnboxingDebugLogs") != null;
- public boolean enableEnumWithSubtypesUnboxing = true;
public boolean enableVerticalClassMergerLensAssertion = false;
public boolean forceRedundantConstNumberRemoval = false;
public boolean enableExperimentalDesugaredLibraryKeepRuleGenerator = false;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
index 4f1d44e..c2446e9 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingKeepSubtypeTest.java
@@ -42,7 +42,6 @@
.addKeepMainRule(Main.class)
.addKeepRules("-keep class " + SUBTYPE_NAME + " { public void method(); }")
.addKeepRules(enumKeepRules.getKeepRules())
- .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
.addEnumUnboxingInspector(
inspector -> inspector.assertNotUnboxed(EnumWithVirtualOverride.class))
.addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.java
new file mode 100644
index 0000000..61df4af
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/NoEnumFlagEnumMergingTest.java
@@ -0,0 +1,127 @@
+// 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.ToolHelper;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+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 NoEnumFlagEnumMergingTest 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 NoEnumFlagEnumMergingTest(
+ TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+ this.parameters = parameters;
+ this.enumValueOptimization = enumValueOptimization;
+ this.enumKeepRules = enumKeepRules;
+ }
+
+ @Test
+ public void testEnumUnboxing() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramFiles(getEnumSubtypesOrOtherInputs(false))
+ .addProgramClassFileData(getSubEnumProgramData(getEnumSubtypesOrOtherInputs(true)))
+ .addKeepMainRule(Main.class)
+ .addKeepRules(enumKeepRules.getKeepRules())
+ .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum2Cases.class))
+ .enableInliningAnnotations()
+ .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("336", "74", "96", "44");
+ }
+
+ private List<Path> getEnumSubtypesOrOtherInputs(boolean enumSubtypes) throws IOException {
+ return ToolHelper.getClassFilesForInnerClasses(getClass()).stream()
+ .filter(path -> isMyEnum2CasesSubtype(path) == enumSubtypes)
+ .collect(Collectors.toList());
+ }
+
+ private boolean isMyEnum2CasesSubtype(Path c) {
+ return c.toString().contains("MyEnum2Cases") && !c.toString().endsWith("MyEnum2Cases.class");
+ }
+
+ private List<byte[]> getSubEnumProgramData(List<Path> input) {
+ // Some Kotlin enum subclasses don't have the enum flag set. See b/315186101.
+ return input.stream()
+ .map(
+ path -> {
+ try {
+ String subtype = path.getFileName().toString();
+ String subtypeNoClass =
+ subtype.substring(0, (subtype.length() - ".class".length()));
+ String descr =
+ DescriptorUtils.replaceSimpleClassNameInDescriptor(
+ DescriptorUtils.javaTypeToDescriptor(MyEnum2Cases.class.getTypeName()),
+ subtypeNoClass);
+ return transformer(path, Reference.classFromDescriptor(descr))
+ .setAccessFlags(ClassAccessFlags::unsetEnum)
+ .transform();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toList());
+ }
+
+ enum MyEnum2Cases {
+ A(8) {
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num * another;
+ }
+ },
+ B(32) {
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num + another;
+ }
+ };
+ final long num;
+
+ MyEnum2Cases(long num) {
+ this.num = num;
+ }
+
+ public abstract long operate(long another);
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ System.out.println(MyEnum2Cases.A.operate(42));
+ System.out.println(MyEnum2Cases.B.operate(42));
+ System.out.println(indirect(MyEnum2Cases.A));
+ System.out.println(indirect(MyEnum2Cases.B));
+ }
+
+ @NeverInline
+ public static long indirect(MyEnum2Cases e) {
+ return e.operate(12);
+ }
+ }
+}