Remove MyEnum.values().length
- Add AssumeNonNull for array type.
- javac generated values() method for enum is now side-effect free.
Bug: 147860220
Change-Id: I206631a44145b4706ece770f939beac11fbaac9a
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index c2a890e..96eb447 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -513,7 +513,7 @@
assert appView.dexItemFactory().verifyNoCachedTypeLatticeElements();
// Collect switch maps and ordinals maps.
- if (options.enableEnumValueOptimization) {
+ if (options.enableEnumSwitchMapRemoval) {
appViewWithLiveness.setAppInfo(new SwitchMapCollector(appViewWithLiveness).run());
}
if (options.enableEnumValueOptimization || options.enableEnumUnboxing) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 00a5cc3..0f317ec 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -737,14 +737,17 @@
public class ObjectMethods {
+ public final DexMethod clone;
public final DexMethod getClass;
public final DexMethod constructor;
public final DexMethod finalize;
public final DexMethod toString;
private ObjectMethods() {
- getClass = createMethod(objectDescriptor, getClassMethodName, classDescriptor,
- DexString.EMPTY_ARRAY);
+ // The clone method is installed on each array, so one has to use method.match(clone).
+ clone = createMethod(objectType, createProto(objectType), cloneMethodName);
+ getClass = createMethod(objectDescriptor,
+ getClassMethodName, classDescriptor, DexString.EMPTY_ARRAY);
constructor = createMethod(objectDescriptor,
constructorMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
finalize = createMethod(objectDescriptor,
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index abc0440..c28bc4a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -4,6 +4,9 @@
package com.android.tools.r8.ir.code;
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isClassTypeVisibleFromContext;
+
import com.android.tools.r8.cf.LoadStoreHelper;
import com.android.tools.r8.cf.TypeVerificationHelper;
import com.android.tools.r8.cf.code.CfCheckCast;
@@ -11,12 +14,15 @@
import com.android.tools.r8.code.MoveObjectFrom16;
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
import com.android.tools.r8.ir.conversion.CfBuilder;
import com.android.tools.r8.ir.conversion.DexBuilder;
import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
public class CheckCast extends Instruction {
@@ -89,6 +95,44 @@
}
@Override
+ public boolean instructionMayHaveSideEffects(AppView<?> appView, DexType context) {
+ return instructionInstanceCanThrow(appView, context).isThrowing();
+ }
+
+ @Override
+ public AbstractError instructionInstanceCanThrow(AppView<?> appView, DexType context) {
+ if (appView.options().debug || !appView.appInfo().hasLiveness()) {
+ return AbstractError.top();
+ }
+ if (type.isPrimitiveType()) {
+ return AbstractError.top();
+ }
+ DexType baseType = type.toBaseType(appView.dexItemFactory());
+ if (baseType.isClassType()) {
+ DexClass dexClass = appView.definitionFor(baseType);
+ // * NoClassDefFoundError (resolution failure).
+ if (dexClass == null || !dexClass.isResolvable(appView)) {
+ return AbstractError.specific(appView.dexItemFactory().noClassDefFoundErrorType);
+ }
+ // * IllegalAccessError (not visible from the access context).
+ if (!isClassTypeVisibleFromContext(appView, context, dexClass)) {
+ return AbstractError.specific(appView.dexItemFactory().illegalAccessErrorType);
+ }
+ }
+ AppView<? extends AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+ TypeLatticeElement castType =
+ TypeLatticeElement.fromDexType(type, definitelyNotNull(), appView);
+ if (object()
+ .getDynamicUpperBoundType(appViewWithLiveness)
+ .lessThanOrEqualUpToNullability(castType, appView)) {
+ // This is a check-cast that has to be there for bytecode correctness, but R8 has proven
+ // that this cast will never throw.
+ return AbstractError.bottom();
+ }
+ return AbstractError.top();
+ }
+
+ @Override
public boolean instructionTypeCanThrow() {
return true;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index ff88eed..7f8d5d3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -50,6 +50,11 @@
}
@Override
+ public void removeInstructionIgnoreOutValue() {
+ instructionIterator.removeInstructionIgnoreOutValue();
+ }
+
+ @Override
public void replaceCurrentInstructionWithThrowNull(
AppView<? extends AppInfoWithSubtyping> appView,
IRCode code,
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 8db09e9..5c488c4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -65,6 +65,10 @@
return get(Opcodes.AND);
}
+ public boolean mayHaveArrayLength() {
+ return get(Opcodes.ARRAY_LENGTH);
+ }
+
public boolean mayHaveCheckCast() {
return get(Opcodes.CHECK_CAST);
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index 05bb7c5..e0043b9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -156,6 +156,11 @@
return true;
}
+ if (getInvokedMethod().holder.isArrayType()
+ && getInvokedMethod().match(appView.dexItemFactory().objectMethods.clone)) {
+ return false;
+ }
+
// Check if it is a call to one of library methods that are known to be side-effect free.
Predicate<InvokeMethod> noSideEffectsPredicate =
appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
index 06c5dfe..e48fb6f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
@@ -4,9 +4,12 @@
package com.android.tools.r8.ir.optimize;
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
@@ -62,27 +65,35 @@
ClassTypeLatticeElement dynamicLowerBoundType;
if (current.isInvokeMethod()) {
InvokeMethod invoke = current.asInvokeMethod();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
- DexType staticReturnTypeRaw = invoke.getInvokedMethod().proto.returnType;
+ DexType staticReturnTypeRaw = invokedMethod.proto.returnType;
if (!staticReturnTypeRaw.isReferenceType()) {
continue;
}
- DexEncodedMethod singleTarget =
- invoke.lookupSingleTarget(appView, code.method.method.holder);
- if (singleTarget == null) {
- continue;
- }
+ if (invokedMethod.holder.isArrayType()
+ && invokedMethod.match(appView.dexItemFactory().objectMethods.clone)) {
+ dynamicUpperBoundType =
+ TypeLatticeElement.fromDexType(invokedMethod.holder, definitelyNotNull(), appView);
+ dynamicLowerBoundType = null;
+ } else {
+ DexEncodedMethod singleTarget =
+ invoke.lookupSingleTarget(appView, code.method.method.holder);
+ if (singleTarget == null) {
+ continue;
+ }
- MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
- if (optimizationInfo.returnsArgument()) {
- // Don't insert an assume-instruction since we will replace all usages of the out-value by
- // the corresponding argument.
- continue;
- }
+ MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
+ if (optimizationInfo.returnsArgument()) {
+ // Don't insert an assume-instruction since we will replace all usages of the out-value
+ // by the corresponding argument.
+ continue;
+ }
- dynamicUpperBoundType = optimizationInfo.getDynamicUpperBoundType();
- dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
+ dynamicUpperBoundType = optimizationInfo.getDynamicUpperBoundType();
+ dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
+ }
} else if (current.isStaticGet()) {
StaticGet staticGet = current.asStaticGet();
DexEncodedField encodedField = appView.appInfo().resolveField(staticGet.getField());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
index 8c07d3d..4fa0312 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -127,7 +127,7 @@
// Case (5), field-get instructions that are guaranteed to read a non-null value.
FieldInstruction fieldInstruction = current.asFieldInstruction();
DexField field = fieldInstruction.getField();
- if (field.type.isClassType() && isNullableReferenceTypeWithUsers(outValue)) {
+ if (field.type.isReferenceType() && isNullableReferenceTypeWithUsers(outValue)) {
DexEncodedField encodedField = appView.appInfo().resolveField(field);
if (encodedField != null) {
FieldOptimizationInfo optimizationInfo = encodedField.getOptimizationInfo();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index ddf697d..06f4eae 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -19,6 +19,7 @@
import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.ConstString;
import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.IntSwitch;
@@ -46,7 +47,9 @@
@SuppressWarnings("ConstantConditions")
public void rewriteConstantEnumMethodCalls(IRCode code) {
- if (!code.metadata().mayHaveInvokeMethodWithReceiver()) {
+ IRMetadata metadata = code.metadata();
+ if (!metadata.mayHaveInvokeMethodWithReceiver()
+ && !(metadata.mayHaveInvokeStatic() && metadata.mayHaveArrayLength())) {
return;
}
@@ -54,65 +57,77 @@
while (iterator.hasNext()) {
Instruction current = iterator.next();
- if (!current.isInvokeMethodWithReceiver()) {
- continue;
- }
- InvokeMethodWithReceiver methodWithReceiver = current.asInvokeMethodWithReceiver();
- DexMethod invokedMethod = methodWithReceiver.getInvokedMethod();
- boolean isOrdinalInvoke = invokedMethod == factory.enumMethods.ordinal;
- boolean isNameInvoke = invokedMethod == factory.enumMethods.name;
- boolean isToStringInvoke = invokedMethod == factory.enumMethods.toString;
- if (!isOrdinalInvoke && !isNameInvoke && !isToStringInvoke) {
- continue;
- }
-
- Value receiver = methodWithReceiver.getReceiver().getAliasedValue();
- if (receiver.isPhi()) {
- continue;
- }
- Instruction definition = receiver.getDefinition();
- if (!definition.isStaticGet()) {
- continue;
- }
- DexField enumField = definition.asStaticGet().getField();
-
- EnumValueInfoMap valueInfoMap =
- appView.appInfo().withLiveness().getEnumValueInfoMap(enumField.type);
- if (valueInfoMap == null) {
- continue;
- }
-
- // The receiver value is identified as being from a constant enum field lookup by the fact
- // that it is a static-get to a field whose type is the same as the enclosing class (which
- // is known to be an enum type). An enum may still define a static field using the enum type
- // so ensure the field is present in the ordinal map for final validation.
- EnumValueInfo valueInfo = valueInfoMap.getEnumValueInfo(enumField);
- if (valueInfo == null) {
- continue;
- }
-
- Value outValue = methodWithReceiver.outValue();
- if (isOrdinalInvoke) {
- iterator.replaceCurrentInstruction(new ConstNumber(outValue, valueInfo.ordinal));
- } else if (isNameInvoke) {
- iterator.replaceCurrentInstruction(
- new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
- } else {
- assert isToStringInvoke;
- DexClass enumClazz = appView.appInfo().definitionFor(enumField.type);
- if (!enumClazz.accessFlags.isFinal()) {
+ if (current.isInvokeMethodWithReceiver()) {
+ InvokeMethodWithReceiver methodWithReceiver = current.asInvokeMethodWithReceiver();
+ DexMethod invokedMethod = methodWithReceiver.getInvokedMethod();
+ boolean isOrdinalInvoke = invokedMethod == factory.enumMethods.ordinal;
+ boolean isNameInvoke = invokedMethod == factory.enumMethods.name;
+ boolean isToStringInvoke = invokedMethod == factory.enumMethods.toString;
+ if (!isOrdinalInvoke && !isNameInvoke && !isToStringInvoke) {
continue;
}
- DexEncodedMethod singleTarget =
- appView
- .appInfo()
- .resolveMethodOnClass(valueInfo.type, factory.objectMethods.toString)
- .getSingleTarget();
- if (singleTarget != null && singleTarget.method != factory.enumMethods.toString) {
+
+ Value receiver = methodWithReceiver.getReceiver().getAliasedValue();
+ if (receiver.isPhi()) {
continue;
}
- iterator.replaceCurrentInstruction(
- new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+ Instruction definition = receiver.getDefinition();
+ if (!definition.isStaticGet()) {
+ continue;
+ }
+ DexField enumField = definition.asStaticGet().getField();
+ EnumValueInfoMap valueInfoMap =
+ appView.appInfo().withLiveness().getEnumValueInfoMap(enumField.type);
+ if (valueInfoMap == null) {
+ continue;
+ }
+
+ // The receiver value is identified as being from a constant enum field lookup by the fact
+ // that it is a static-get to a field whose type is the same as the enclosing class (which
+ // is known to be an enum type). An enum may still define a static field using the enum type
+ // so ensure the field is present in the ordinal map for final validation.
+ EnumValueInfo valueInfo = valueInfoMap.getEnumValueInfo(enumField);
+ if (valueInfo == null) {
+ continue;
+ }
+
+ Value outValue = methodWithReceiver.outValue();
+ if (isOrdinalInvoke) {
+ iterator.replaceCurrentInstruction(new ConstNumber(outValue, valueInfo.ordinal));
+ } else if (isNameInvoke) {
+ iterator.replaceCurrentInstruction(
+ new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+ } else {
+ assert isToStringInvoke;
+ DexClass enumClazz = appView.appInfo().definitionFor(enumField.type);
+ if (!enumClazz.accessFlags.isFinal()) {
+ continue;
+ }
+ DexEncodedMethod singleTarget =
+ appView
+ .appInfo()
+ .resolveMethodOnClass(valueInfo.type, factory.objectMethods.toString)
+ .getSingleTarget();
+ if (singleTarget != null && singleTarget.method != factory.enumMethods.toString) {
+ continue;
+ }
+ iterator.replaceCurrentInstruction(
+ new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+ }
+ } else if (current.isArrayLength()) {
+ // Rewrites MyEnum.values().length to a constant int.
+ Instruction arrayDefinition = current.asArrayLength().array().definition;
+ if (arrayDefinition != null && arrayDefinition.isInvokeStatic()) {
+ DexMethod invokedMethod = arrayDefinition.asInvokeStatic().getInvokedMethod();
+ if (factory.enumMethods.isValuesMethod(
+ invokedMethod, appView.definitionForProgramType(invokedMethod.holder))) {
+ EnumValueInfoMap enumValueInfoMap =
+ appView.appInfo().withLiveness().getEnumValueInfoMap(invokedMethod.holder);
+ if (enumValueInfoMap != null) {
+ iterator.replaceCurrentInstructionWithConstInt(code, enumValueInfoMap.size());
+ }
+ }
+ }
}
}
assert code.isConsistentSSA();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index b5215f6..d015712 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -96,7 +96,6 @@
import com.android.tools.r8.utils.Pair;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
@@ -961,10 +960,14 @@
// {v0}, T`.
mayHaveSideEffects = true;
} else {
+ mayHaveSideEffects = false;
// Otherwise, check if there is an instruction that has side effects.
- mayHaveSideEffects =
- Streams.stream(code.instructions())
- .anyMatch(instruction -> instruction.instructionMayHaveSideEffects(appView, context));
+ for (Instruction instruction : code.instructions()) {
+ if (instruction.instructionMayHaveSideEffects(appView, context)) {
+ mayHaveSideEffects = true;
+ break;
+ }
+ }
}
if (!mayHaveSideEffects) {
feedback.methodMayNotHaveSideEffects(method);
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 8d04c47..173d169 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -256,6 +256,7 @@
public boolean enableStringSwitchConversion =
System.getProperty("com.android.tools.r8.stringSwitchConversion") != null;
public boolean enableEnumValueOptimization = true;
+ public boolean enableEnumSwitchMapRemoval = true;
public final OutlineOptions outline = new OutlineOptions();
public boolean enableInitializedClassesInInstanceMethodsAnalysis = true;
public boolean enableRedundantFieldLoadElimination = true;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
index 399b6b0..1b56b1b 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -68,6 +68,7 @@
void enableEnumOptions(InternalOptions options, boolean enumValueOptimization) {
options.enableEnumUnboxing = true;
options.enableEnumValueOptimization = enumValueOptimization;
+ options.enableEnumSwitchMapRemoval = enumValueOptimization;
options.testing.enableEnumUnboxingDebugLogs = true;
}
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index 374f91d..d7eeae4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -42,7 +42,10 @@
.addProgramFiles(Paths.get(ToolHelper.EXAMPLES_KOTLIN_BUILD_DIR, "enumswitch.jar"))
.addKeepMainRule("enumswitch.EnumSwitchKt")
.addOptionsModification(
- options -> options.enableEnumValueOptimization = enableSwitchMapRemoval)
+ options -> {
+ options.enableEnumValueOptimization = enableSwitchMapRemoval;
+ options.enableEnumSwitchMapRemoval = enableSwitchMapRemoval;
+ })
.setMinApi(parameters.getRuntime())
.noMinification()
.compile()
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
index 2f9a1ec..78a36f2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
@@ -48,6 +48,7 @@
private void configure(InternalOptions options) {
options.enableEnumValueOptimization = enableOptimization;
+ options.enableEnumSwitchMapRemoval = enableOptimization;
}
@Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
new file mode 100644
index 0000000..7a5300d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
@@ -0,0 +1,157 @@
+// 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.rewrite.enums;
+
+import static junit.framework.TestCase.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+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 EnumValuesLengthTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public EnumValuesLengthTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testValuesLengthRemoved() throws Exception {
+ testForR8(parameters.getBackend())
+ .addKeepMainRule(Main.class)
+ .addInnerClasses(EnumValuesLengthTest.class)
+ .noMinification()
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ opt -> {
+ opt.enableEnumValueOptimization = true;
+ // We need to keep the switch map to ensure kept switch maps have their
+ // values array length rewritten.
+ opt.enableEnumSwitchMapRemoval = false;
+ })
+ .compile()
+ .inspect(this::assertValuesLengthRemoved)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
+ }
+
+ @Test
+ public void testValuesLengthSwitchMapRemoved() throws Exception {
+ // Make sure SwitchMap can still be removed with valuesLength optimization.
+ testForR8(parameters.getBackend())
+ .addKeepMainRule(Main.class)
+ .addInnerClasses(EnumValuesLengthTest.class)
+ .noMinification()
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ opt -> {
+ opt.enableEnumValueOptimization = true;
+ })
+ .compile()
+ .inspect(this::assertSwitchMapRemoved)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
+ }
+
+ private void assertSwitchMapRemoved(CodeInspector inspector) {
+ assertTrue(
+ inspector.allClasses().stream()
+ .noneMatch(c -> !c.getDexClass().isEnum() && !c.getFinalName().endsWith("Main")));
+ }
+
+ private void assertValuesLengthRemoved(CodeInspector inspector) {
+ for (FoundClassSubject clazz : inspector.allClasses()) {
+ clazz.forAllMethods(this::assertValuesLengthRemoved);
+ }
+ }
+
+ private void assertValuesLengthRemoved(FoundMethodSubject method) {
+ assertTrue(method.streamInstructions().noneMatch(InstructionSubject::isArrayLength));
+ assertTrue(
+ method
+ .streamInstructions()
+ .noneMatch(
+ instr ->
+ instr.isInvokeStatic() && instr.getMethod().name.toString().equals("values")));
+ }
+
+ public static class Main {
+
+ @NeverClassInline
+ enum E0 {}
+
+ @NeverClassInline
+ enum E2 {
+ A,
+ B
+ }
+
+ @NeverClassInline
+ enum E5 {
+ A,
+ B,
+ C,
+ D,
+ E
+ }
+
+ @NeverClassInline
+ enum EUnusedValues {
+ A,
+ B,
+ C
+ }
+
+ @NeverClassInline
+ enum ESwitch {
+ A,
+ B,
+ C,
+ D
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static void main(String[] args) {
+ EUnusedValues.values();
+ System.out.println(E0.values().length);
+ System.out.println(E2.values().length);
+ System.out.println(E5.values().length);
+ System.out.println(switchOn(ESwitch.A));
+ System.out.println(switchOn(ESwitch.B));
+ System.out.println(switchOn(ESwitch.C));
+ System.out.println(switchOn(ESwitch.D));
+ }
+
+ // SwitchMaps feature an array length on values, and some of them are not removed.
+ @NeverInline
+ static char switchOn(ESwitch e) {
+ switch (e) {
+ case A:
+ return 'a';
+ case C:
+ return 'c';
+ default:
+ return 'D';
+ }
+ }
+ }
+}