blob: b3a25d031c5299957d6c9557f196183a4ec88dd3 [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 com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
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.ir.analysis.type.ArrayTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.FieldInstruction;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.If;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.AppInfoWithLiveness.EnumValueInfo;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class EnumUnboxer {
private final AppView<AppInfoWithLiveness> appView;
private final Set<DexType> enumsToUnbox;
private final boolean debugLogEnabled;
private final Map<DexType, Reason> debugLogs;
private final DexItemFactory factory;
public EnumUnboxer(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
this.factory = appView.dexItemFactory();
if (appView.options().testing.enableEnumUnboxingDebugLogs) {
debugLogEnabled = true;
debugLogs = new ConcurrentHashMap<>();
} else {
debugLogEnabled = false;
debugLogs = null;
}
enumsToUnbox = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
}
public void unboxEnums(IRCode code) {
// TODO(b/147860220): To implement.
// Do not forget static get, which is implicitly valid (no inValue).
}
public void analyzeEnums(IRCode code) {
// Enum <clinit> and <init> are analyzed in between the two processing phases using optimization
// feedback.
DexClass dexClass = appView.definitionFor(code.method.method.holder);
if (dexClass.isEnum() && code.method.isInitializer()) {
return;
}
analyzeEnumsInMethod(code);
}
private void markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
assert enumClass.isEnum();
reportFailure(enumClass.type, reason);
enumsToUnbox.remove(enumClass.type);
}
private DexProgramClass getEnumUnboxingCandidateOrNull(TypeLatticeElement lattice) {
if (lattice.isClassType()) {
DexType classType = lattice.asClassTypeLatticeElement().getClassType();
return getEnumUnboxingCandidateOrNull(classType);
}
if (lattice.isArrayType()) {
ArrayTypeLatticeElement arrayLattice = lattice.asArrayTypeLatticeElement();
if (arrayLattice.getArrayBaseTypeLattice().isClassType()) {
DexType classType =
arrayLattice.getArrayBaseTypeLattice().asClassTypeLatticeElement().getClassType();
return getEnumUnboxingCandidateOrNull(classType);
}
}
return null;
}
private DexProgramClass getEnumUnboxingCandidateOrNull(DexType anyType) {
if (!enumsToUnbox.contains(anyType)) {
return null;
}
return appView.definitionForProgramType(anyType);
}
private void analyzeEnumsInMethod(IRCode code) {
for (BasicBlock block : code.blocks) {
for (Instruction instruction : block.getInstructions()) {
Value outValue = instruction.outValue();
DexProgramClass enumClass =
outValue == null ? null : getEnumUnboxingCandidateOrNull(outValue.getTypeLattice());
if (enumClass != null) {
validateEnumUsages(code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
}
}
for (Phi phi : block.getPhis()) {
DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getTypeLattice());
if (enumClass != null) {
validateEnumUsages(code, phi.uniqueUsers(), phi.uniquePhiUsers(), enumClass);
}
}
}
}
private Reason validateEnumUsages(
IRCode code, Set<Instruction> uses, Set<Phi> phiUses, DexProgramClass enumClass) {
for (Instruction user : uses) {
Reason reason = instructionAllowEnumUnboxing(user, code, enumClass);
if (reason != Reason.ELIGIBLE) {
markEnumAsUnboxable(reason, enumClass);
return reason;
}
}
for (Phi phi : phiUses) {
for (Value operand : phi.getOperands()) {
if (getEnumUnboxingCandidateOrNull(operand.getTypeLattice()) != enumClass) {
markEnumAsUnboxable(Reason.INVALID_PHI, enumClass);
return Reason.INVALID_PHI;
}
}
}
return Reason.ELIGIBLE;
}
public void finishEnumAnalysis() {
for (DexType toUnbox : enumsToUnbox) {
DexProgramClass enumClass = appView.definitionForProgramType(toUnbox);
assert enumClass != null;
DexEncodedMethod initializer = enumClass.lookupDirectMethod(factory.enumMethods.constructor);
if (initializer == null) {
// This case typically happens when a programmer uses EnumSet/EnumMap without using the
// enum keep rules. The code is incorrect in this case (EnumSet/EnumMap won't work).
// We bail out.
markEnumAsUnboxable(Reason.NO_INIT, enumClass);
continue;
}
if (initializer.getOptimizationInfo().mayHaveSideEffects()) {
markEnumAsUnboxable(Reason.INVALID_INIT, enumClass);
continue;
}
if (enumClass.classInitializationMayHaveSideEffects(appView)) {
markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
continue;
}
Map<DexField, EnumValueInfo> enumValueInfoMapFor =
appView.appInfo().withLiveness().getEnumValueInfoMapFor(enumClass.type);
if (enumValueInfoMapFor == null) {
markEnumAsUnboxable(Reason.MISSING_INFO_MAP, enumClass);
continue;
}
if (enumValueInfoMapFor.size() != enumClass.staticFields().size() - 1) {
markEnumAsUnboxable(Reason.UNEXPECTED_STATIC_FIELD, enumClass);
}
}
if (debugLogEnabled) {
reportEnumsAnalysis();
}
}
private Reason instructionAllowEnumUnboxing(
Instruction instruction, IRCode code, DexProgramClass enumClass) {
// All invokes in the library are invalid, besides a few cherry picked cases such as ordinal().
if (instruction.isInvokeMethod()) {
InvokeMethod invokeMethod = instruction.asInvokeMethod();
if (invokeMethod.getInvokedMethod().holder.isArrayType()) {
// The only valid methods is clone for values() to be correct.
if (invokeMethod.getInvokedMethod().name == factory.cloneMethodName) {
return Reason.ELIGIBLE;
}
return Reason.INVALID_INVOKE_ON_ARRAY;
}
DexEncodedMethod invokedEncodedMethod =
invokeMethod.lookupSingleTarget(appView, code.method.method.holder);
if (invokedEncodedMethod == null) {
return Reason.INVALID_INVOKE;
}
DexMethod invokedMethod = invokedEncodedMethod.method;
DexClass dexClass = appView.definitionFor(invokedMethod.holder);
if (dexClass == null) {
return Reason.INVALID_INVOKE;
}
if (dexClass.isProgramClass()) {
// All invokes in the program are generally valid, but specific care is required
// for values() and valueOf().
if (dexClass.isEnum() && factory.enumMethods.isValuesMethod(invokedMethod, dexClass)) {
return Reason.VALUES_INVOKE;
}
if (dexClass.isEnum() && factory.enumMethods.isValueOfMethod(invokedMethod, dexClass)) {
return Reason.VALUE_OF_INVOKE;
}
return Reason.ELIGIBLE;
}
if (dexClass.isClasspathClass()) {
return Reason.INVALID_INVOKE;
}
assert dexClass.isLibraryClass();
if (dexClass.type != factory.enumType) {
return Reason.UNSUPPORTED_LIBRARY_CALL;
}
// TODO(b/147860220): Methods toString(), name(), compareTo(), EnumSet and EnumMap may be
// interesting to model. A the moment rewrite only Enum#ordinal().
if (debugLogEnabled) {
if (invokedMethod == factory.enumMethods.compareTo) {
return Reason.COMPARE_TO_INVOKE;
}
if (invokedMethod == factory.enumMethods.name) {
return Reason.NAME_INVOKE;
}
if (invokedMethod == factory.enumMethods.toString) {
return Reason.TO_STRING_INVOKE;
}
}
if (invokedMethod != factory.enumMethods.ordinal) {
return Reason.UNSUPPORTED_LIBRARY_CALL;
}
return Reason.ELIGIBLE;
}
// A field put is valid only if the field is not on an enum, and the field type and the valuePut
// have identical enum type.
if (instruction.isFieldPut()) {
FieldInstruction fieldInstruction = instruction.asFieldInstruction();
DexEncodedField field = appView.appInfo().resolveField(fieldInstruction.getField());
if (field == null) {
return Reason.INVALID_FIELD_PUT;
}
DexProgramClass dexClass = appView.definitionForProgramType(field.field.holder);
if (dexClass == null) {
return Reason.INVALID_FIELD_PUT;
}
if (dexClass.isEnum()) {
return Reason.FIELD_PUT_ON_ENUM;
}
// The put value has to be of the field type.
if (field.field.type != enumClass.type) {
return Reason.TYPE_MISSMATCH_FIELD_PUT;
}
return Reason.ELIGIBLE;
}
// An If using enum as inValue is valid if it matches e == null
// or e == X with X of same enum type as e. Ex: if (e == MyEnum.A).
if (instruction.isIf()) {
If anIf = instruction.asIf();
assert (anIf.getType() == If.Type.EQ || anIf.getType() == If.Type.NE)
: "Comparing a reference with " + anIf.getType().toString();
// e == null.
if (anIf.isZeroTest()) {
return Reason.ELIGIBLE;
}
// e == MyEnum.X
TypeLatticeElement leftType = anIf.lhs().getTypeLattice();
TypeLatticeElement rightType = anIf.rhs().getTypeLattice();
if (leftType.equalUpToNullability(rightType)) {
assert leftType.isClassType();
assert leftType.asClassTypeLatticeElement().getClassType() == enumClass.type;
return Reason.ELIGIBLE;
}
return Reason.INVALID_IF_TYPES;
}
if (instruction.isAssume()) {
Value outValue = instruction.outValue();
return validateEnumUsages(code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
}
// Return is used for valueOf methods.
if (instruction.isReturn()) {
DexType returnType = code.method.method.proto.returnType;
if (returnType != enumClass.type && returnType.toBaseType(factory) != enumClass.type) {
return Reason.IMPLICIT_UP_CAST_IN_RETURN;
}
return Reason.ELIGIBLE;
}
return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
}
private void reportEnumsAnalysis() {
assert debugLogEnabled;
Reporter reporter = appView.options().reporter;
reporter.info(
new StringDiagnostic(
"Unboxed enums (Unboxing succeeded "
+ enumsToUnbox.size()
+ "): "
+ Arrays.toString(enumsToUnbox.toArray())));
StringBuilder sb = new StringBuilder();
sb.append("Boxed enums (Unboxing failed ").append(debugLogs.size()).append("):\n");
for (DexType enumType : debugLogs.keySet()) {
sb.append("- ")
.append(enumType)
.append(": ")
.append(debugLogs.get(enumType).toString())
.append('\n');
}
reporter.info(new StringDiagnostic(sb.toString()));
}
void reportFailure(DexType enumType, Reason reason) {
if (debugLogEnabled) {
debugLogs.put(enumType, reason);
}
}
public enum Reason {
ELIGIBLE,
SUBTYPES,
INTERFACE,
INSTANCE_FIELD,
UNEXPECTED_STATIC_FIELD,
VIRTUAL_METHOD,
UNEXPECTED_DIRECT_METHOD,
INVALID_PHI,
NO_INIT,
INVALID_INIT,
INVALID_CLINIT,
INVALID_INVOKE,
INVALID_INVOKE_ON_ARRAY,
IMPLICIT_UP_CAST_IN_RETURN,
VALUE_OF_INVOKE,
VALUES_INVOKE,
COMPARE_TO_INVOKE,
TO_STRING_INVOKE,
NAME_INVOKE,
UNSUPPORTED_LIBRARY_CALL,
MISSING_INFO_MAP,
INVALID_FIELD_PUT,
FIELD_PUT_ON_ENUM,
TYPE_MISSMATCH_FIELD_PUT,
INVALID_IF_TYPES,
OTHER_UNSUPPORTED_INSTRUCTION;
}
}