blob: 541cd9211fc4f66e6f5ecc1dc91519e002d34227 [file] [log] [blame]
// 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.ir.optimize.enums;
import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.CfCode;
import com.android.tools.r8.graph.ClassAccessFlags;
import com.android.tools.r8.graph.DexAnnotationSet;
import com.android.tools.r8.graph.DexApplication.Builder;
import com.android.tools.r8.graph.DexEncodedField;
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.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.EnumValueInfoMapCollection;
import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfo;
import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
import com.android.tools.r8.graph.FieldAccessFlags;
import com.android.tools.r8.graph.MethodAccessFlags;
import com.android.tools.r8.graph.ParameterAnnotationsList;
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;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.MemberType;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.IRConverter;
import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider;
import com.android.tools.r8.origin.SynthesizedOrigin;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.ImmutableList;
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;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
public class EnumUnboxingRewriter {
public static final String ENUM_UNBOXING_UTILITY_CLASS_NAME = "$r8$EnumUnboxingUtility";
public static final String ENUM_UNBOXING_UTILITY_METHOD_PREFIX = "$enumboxing$";
private static final int REQUIRED_CLASS_FILE_VERSION = 52;
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory factory;
private final EnumValueInfoMapCollection enumsToUnbox;
private final Map<DexMethod, DexEncodedMethod> utilityMethods = new ConcurrentHashMap<>();
private final Map<DexField, DexEncodedField> extraUtilityFields = new ConcurrentHashMap<>();
private final DexMethod ordinalUtilityMethod;
private final DexMethod equalsUtilityMethod;
private final DexMethod compareToUtilityMethod;
private final DexMethod valuesUtilityMethod;
EnumUnboxingRewriter(AppView<AppInfoWithLiveness> appView, Set<DexType> enumsToUnbox) {
this.appView = appView;
this.factory = appView.dexItemFactory();
EnumValueInfoMapCollection.Builder builder = EnumValueInfoMapCollection.builder();
for (DexType toUnbox : enumsToUnbox) {
builder.put(toUnbox, appView.appInfo().withLiveness().getEnumValueInfoMap(toUnbox));
}
this.enumsToUnbox = builder.build();
// Custom methods for java.lang.Enum methods ordinal, equals and compareTo.
this.ordinalUtilityMethod =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.intType, factory.intType),
ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "ordinal");
this.equalsUtilityMethod =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.booleanType, factory.intType, factory.intType),
ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "equals");
this.compareToUtilityMethod =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.intType, factory.intType, factory.intType),
ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "compareTo");
// Custom methods for generated field $VALUES initialization.
this.valuesUtilityMethod =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.intArrayType, factory.intType),
ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "values");
}
public EnumValueInfoMapCollection getEnumsToUnbox() {
return enumsToUnbox;
}
Set<Phi> rewriteCode(IRCode code) {
// We should not process the enum methods, they will be removed and they may contain invalid
// rewriting rules.
if (enumsToUnbox.isEmpty()) {
return Sets.newIdentityHashSet();
}
assert code.isConsistentSSABeforeTypesAreCorrect();
Map<Instruction, DexType> convertedEnums = new IdentityHashMap<>();
Set<Phi> affectedPhis = Sets.newIdentityHashSet();
InstructionListIterator iterator = code.instructionListIterator();
while (iterator.hasNext()) {
Instruction instruction = iterator.next();
// Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
// counterpart.
if (instruction.isInvokeMethodWithReceiver()) {
InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
DexMethod invokedMethod = invokeMethod.getInvokedMethod();
DexType enumType = getEnumTypeOrNull(invokeMethod.getReceiver(), convertedEnums);
if (enumType != null) {
if (invokedMethod == factory.enumMethods.ordinal) {
replaceEnumInvoke(
iterator, invokeMethod, ordinalUtilityMethod, m -> synthesizeOrdinalMethod());
continue;
} else if (invokedMethod == factory.enumMethods.equals) {
replaceEnumInvoke(
iterator, invokeMethod, equalsUtilityMethod, m -> synthesizeEqualsMethod());
continue;
} else if (invokedMethod == factory.enumMethods.compareTo) {
replaceEnumInvoke(
iterator, invokeMethod, compareToUtilityMethod, m -> synthesizeCompareToMethod());
continue;
} else if (invokedMethod == factory.enumMethods.name
|| invokedMethod == factory.enumMethods.toString) {
DexMethod toStringMethod = computeDefaultToStringUtilityMethod(enumType);
iterator.replaceCurrentInstruction(
new InvokeStatic(
toStringMethod, invokeMethod.outValue(), invokeMethod.arguments()));
continue;
}
}
// TODO(b/147860220): rewrite also other enum methods.
} else if (instruction.isInvokeStatic()) {
InvokeStatic invokeStatic = instruction.asInvokeStatic();
DexMethod invokedMethod = invokeStatic.getInvokedMethod();
if (invokedMethod == factory.enumMethods.valueOf
&& invokeStatic.inValues().get(0).isConstClass()) {
DexType enumType =
invokeStatic.inValues().get(0).getConstInstruction().asConstClass().getValue();
if (enumsToUnbox.containsEnum(enumType)) {
DexMethod valueOfMethod = computeValueOfUtilityMethod(enumType);
Value outValue = invokeStatic.outValue();
Value rewrittenOutValue = null;
if (outValue != null) {
rewrittenOutValue = code.createValue(TypeElement.getInt());
affectedPhis.addAll(outValue.uniquePhiUsers());
}
InvokeStatic invoke =
new InvokeStatic(
valueOfMethod,
rewrittenOutValue,
Collections.singletonList(invokeStatic.inValues().get(1)));
iterator.replaceCurrentInstruction(invoke);
convertedEnums.put(invoke, enumType);
continue;
}
}
}
// Rewrites direct access to enum values into the corresponding int, $VALUES is not
// supported.
if (instruction.isStaticGet()) {
StaticGet staticGet = instruction.asStaticGet();
DexType holder = staticGet.getField().holder;
if (enumsToUnbox.containsEnum(holder)) {
if (staticGet.outValue() == null) {
iterator.removeInstructionIgnoreOutValue();
continue;
}
EnumValueInfoMap enumValueInfoMap = enumsToUnbox.getEnumValueInfoMap(holder);
assert enumValueInfoMap != null;
affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
EnumValueInfo enumValueInfo = enumValueInfoMap.getEnumValueInfo(staticGet.getField());
if (enumValueInfo == null && staticGet.getField().name == factory.enumValuesFieldName) {
utilityMethods.computeIfAbsent(
valuesUtilityMethod, m -> synthesizeValuesUtilityMethod());
DexField fieldValues = createValuesField(holder);
extraUtilityFields.computeIfAbsent(fieldValues, this::computeValuesEncodedField);
DexMethod methodValues = createValuesMethod(holder);
utilityMethods.computeIfAbsent(
methodValues,
m -> computeValuesEncodedMethod(m, fieldValues, enumValueInfoMap.size()));
Value rewrittenOutValue =
code.createValue(
ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
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";
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();
DexType enumType = getEnumTypeOrNull(arrayAccess);
if (enumType != null) {
instruction = arrayAccess.withMemberType(MemberType.INT);
iterator.replaceCurrentInstruction(instruction);
convertedEnums.put(instruction, enumType);
}
assert validateArrayAccess(arrayAccess);
}
}
assert code.isConsistentSSABeforeTypesAreCorrect();
return affectedPhis;
}
private void replaceEnumInvoke(
InstructionListIterator iterator,
InvokeMethodWithReceiver invokeMethod,
DexMethod method,
Function<DexMethod, DexEncodedMethod> synthesizor) {
utilityMethods.computeIfAbsent(method, synthesizor);
Instruction instruction =
new InvokeStatic(method, invokeMethod.outValue(), invokeMethod.arguments());
iterator.replaceCurrentInstruction(instruction);
}
private boolean validateArrayAccess(ArrayAccess arrayAccess) {
ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
if (arrayType == null) {
assert arrayAccess.array().getType().isNullType();
return true;
}
assert arrayAccess.getMemberType() != MemberType.OBJECT
|| arrayType.getNesting() > 1
|| arrayType.getBaseType().isReferenceType();
return true;
}
private DexType getEnumTypeOrNull(Value receiver, Map<Instruction, DexType> convertedEnums) {
TypeElement type = receiver.getType();
if (type.isInt()) {
return convertedEnums.get(receiver.definition);
}
if (!type.isClassType()) {
return null;
}
DexType enumType = type.asClassType().getClassType();
return enumsToUnbox.containsEnum(enumType) ? enumType : null;
}
public String compatibleName(DexType type) {
return type.toSourceString().replace('.', '$');
}
private DexField createValuesField(DexType type) {
return factory.createField(
factory.enumUnboxingUtilityType,
factory.intArrayType,
factory.enumValuesFieldName + "$field$" + compatibleName(type));
}
private DexEncodedField computeValuesEncodedField(DexField field) {
return new DexEncodedField(
field,
FieldAccessFlags.fromSharedAccessFlags(
Constants.ACC_SYNTHETIC | Constants.ACC_STATIC | Constants.ACC_PUBLIC),
DexAnnotationSet.empty(),
null);
}
private DexMethod createValuesMethod(DexType type) {
return factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.intArrayType),
factory.enumValuesFieldName + "$method$" + compatibleName(type));
}
private DexEncodedMethod computeValuesEncodedMethod(
DexMethod method, DexField fieldValues, int numEnumInstances) {
CfCode cfCode =
new EnumUnboxingCfCodeProvider.EnumUnboxingValuesCfCodeProvider(
appView,
factory.enumUnboxingUtilityType,
fieldValues,
numEnumInstances,
valuesUtilityMethod)
.generateCfCode();
return synthesizeUtilityMethod(cfCode, method, true);
}
private DexMethod computeValueOfUtilityMethod(DexType type) {
assert enumsToUnbox.containsEnum(type);
DexMethod valueOf =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.intType, factory.stringType),
"valueOf" + compatibleName(type));
utilityMethods.computeIfAbsent(valueOf, m -> synthesizeValueOfUtilityMethod(m, type));
return valueOf;
}
private DexMethod computeDefaultToStringUtilityMethod(DexType type) {
assert enumsToUnbox.containsEnum(type);
DexMethod toString =
factory.createMethod(
factory.enumUnboxingUtilityType,
factory.createProto(factory.stringType, factory.intType),
"toString" + compatibleName(type));
utilityMethods.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 null;
}
if (arrayType.getNesting() != 1) {
return null;
}
TypeElement baseType = arrayType.getBaseType();
if (!baseType.isClassType()) {
return null;
}
DexType classType = baseType.asClassType().getClassType();
return enumsToUnbox.containsEnum(classType) ? classType : null;
}
void synthesizeEnumUnboxingUtilityMethods(
Builder<?> builder, IRConverter converter, ExecutorService executorService)
throws ExecutionException {
// Synthesize a class which holds various utility methods that may be called from the IR
// rewriting. If any of these methods are not used, they will be removed by the Enqueuer.
List<DexEncodedMethod> requiredMethods = new ArrayList<>(utilityMethods.values());
// Sort for deterministic order.
requiredMethods.sort((m1, m2) -> m1.method.name.slowCompareTo(m2.method.name));
if (requiredMethods.isEmpty()) {
return;
}
List<DexEncodedField> fields = new ArrayList<>(extraUtilityFields.values());
fields.sort((f1, f2) -> f1.field.name.slowCompareTo(f2.field.name));
DexProgramClass utilityClass =
appView.definitionForProgramType(factory.enumUnboxingUtilityType);
assert utilityClass != null : "Should have been synthesized upfront.";
utilityClass.appendStaticFields(fields);
utilityClass.addDirectMethods(requiredMethods);
assert requiredMethods.stream().allMatch(DexEncodedMethod::isPublic);
if (utilityClassInMainDexList()) {
builder.addToMainDexList(Collections.singletonList(utilityClass.type));
}
// TODO(b/147860220): Use processMethodsConcurrently on requiredMethods instead.
converter.optimizeSynthesizedClass(utilityClass, executorService);
}
public static DexProgramClass synthesizeEmptyEnumUnboxingUtilityClass(AppView<?> appView) {
DexItemFactory factory = appView.dexItemFactory();
return new DexProgramClass(
factory.enumUnboxingUtilityType,
null,
new SynthesizedOrigin("EnumUnboxing ", EnumUnboxingRewriter.class),
ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC),
factory.objectType,
DexTypeList.empty(),
factory.createString("enumunboxing"),
null,
Collections.emptyList(),
null,
Collections.emptyList(),
DexAnnotationSet.empty(),
DexEncodedField.EMPTY_ARRAY,
DexEncodedField.EMPTY_ARRAY,
DexEncodedMethod.EMPTY_ARRAY,
DexEncodedMethod.EMPTY_ARRAY,
factory.getSkipNameValidationForTesting(),
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(
appView,
factory.enumUnboxingUtilityType,
enumType,
enumsToUnbox.getEnumValueInfoMap(enumType))
.generateCfCode();
return synthesizeUtilityMethod(cfCode, method, false);
}
// TODO(b/150178516): Add a test for this case.
private boolean utilityClassInMainDexList() {
for (DexType toUnbox : enumsToUnbox.enumSet()) {
if (appView.appInfo().isInMainDexList(toUnbox)) {
return true;
}
}
return false;
}
private DexEncodedMethod synthesizeOrdinalMethod() {
CfCode cfCode =
EnumUnboxingCfMethods.EnumUnboxingMethods_ordinal(appView.options(), ordinalUtilityMethod);
return synthesizeUtilityMethod(cfCode, ordinalUtilityMethod, false);
}
private DexEncodedMethod synthesizeEqualsMethod() {
CfCode cfCode =
EnumUnboxingCfMethods.EnumUnboxingMethods_equals(appView.options(), equalsUtilityMethod);
return synthesizeUtilityMethod(cfCode, equalsUtilityMethod, false);
}
private DexEncodedMethod synthesizeCompareToMethod() {
CfCode cfCode =
EnumUnboxingCfMethods.EnumUnboxingMethods_compareTo(
appView.options(), compareToUtilityMethod);
return synthesizeUtilityMethod(cfCode, compareToUtilityMethod, false);
}
private DexEncodedMethod synthesizeValuesUtilityMethod() {
CfCode cfCode =
EnumUnboxingCfMethods.EnumUnboxingMethods_values(appView.options(), valuesUtilityMethod);
return synthesizeUtilityMethod(cfCode, valuesUtilityMethod, false);
}
private DexEncodedMethod synthesizeUtilityMethod(CfCode cfCode, DexMethod method, boolean sync) {
return new DexEncodedMethod(
method,
synthesizedMethodAccessFlags(sync),
DexAnnotationSet.empty(),
ParameterAnnotationsList.empty(),
cfCode,
REQUIRED_CLASS_FILE_VERSION,
true);
}
private MethodAccessFlags synthesizedMethodAccessFlags(boolean sync) {
int access = Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC | Constants.ACC_STATIC;
if (sync) {
access = access | Constants.ACC_SYNCHRONIZED;
}
return MethodAccessFlags.fromSharedAccessFlags(access, false);
}
}