blob: 03455fbe328978cb8db70b9d5a0b59553e2130bd [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.analysis.fieldvalueanalysis;
import static com.android.tools.r8.ir.code.Opcodes.ARRAY_PUT;
import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT;
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.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.graph.DexValue.DexValueNull;
import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.analysis.type.Nullability;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState;
import com.android.tools.r8.ir.analysis.value.NullOrAbstractValue;
import com.android.tools.r8.ir.analysis.value.ObjectState;
import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
import com.android.tools.r8.ir.code.ArrayPut;
import com.android.tools.r8.ir.code.FieldInstruction;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.NewArrayEmpty;
import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.ClassInitializerDefaultsOptimization.ClassInitializerDefaultsResult;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.ir.optimize.info.field.InstanceFieldArgumentInitializationInfo;
import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.Timing;
public class StaticFieldValueAnalysis extends FieldValueAnalysis {
private final StaticFieldValues.Builder builder;
private StaticFieldValueAnalysis(
AppView<AppInfoWithLiveness> appView, IRCode code, OptimizationFeedback feedback) {
super(appView, code, feedback);
builder = StaticFieldValues.builder(code.context().getHolder());
}
public static StaticFieldValues run(
AppView<?> appView,
IRCode code,
ClassInitializerDefaultsResult classInitializerDefaultsResult,
OptimizationFeedback feedback,
Timing timing) {
assert appView.appInfo().hasLiveness();
assert appView.enableWholeProgramOptimizations();
assert code.context().getDefinition().isClassInitializer();
timing.begin("Analyze class initializer");
StaticFieldValues result =
new StaticFieldValueAnalysis(appView.withLiveness(), code, feedback)
.analyze(classInitializerDefaultsResult, code.context().getHolderType());
timing.end();
return result;
}
@Override
boolean isStaticFieldValueAnalysis() {
return true;
}
@Override
StaticFieldValueAnalysis asStaticFieldValueAnalysis() {
return this;
}
StaticFieldValues analyze(
ClassInitializerDefaultsResult classInitializerDefaultsResult, DexType holderType) {
computeFieldOptimizationInfo(classInitializerDefaultsResult);
return builder.build();
}
@Override
void computeFieldOptimizationInfo(ClassInitializerDefaultsResult classInitializerDefaultsResult) {
super.computeFieldOptimizationInfo(classInitializerDefaultsResult);
classInitializerDefaultsResult.forEachOptimizedField(
(field, value) -> {
if (putsPerField.containsKey(field)
|| !appView.appInfo().isFieldOnlyWrittenInMethod(field, context.getDefinition())) {
return;
}
AbstractValueFactory factory = appView.abstractValueFactory();
if (value.isDexValueNumber()) {
feedback.recordFieldHasAbstractValue(
field,
appView,
factory.createSingleNumberValue(value.asDexValueNumber().getRawValue()));
} else if (value.isDexValueString()) {
feedback.recordFieldHasAbstractValue(
field,
appView,
factory.createSingleStringValue(value.asDexValueString().getValue()));
} else if (value.isDexItemBasedValueString()) {
// TODO(b/150835624): Extend to dex item based const strings.
} else {
assert false : value.getClass().getName();
}
});
}
@Override
boolean isSubjectToOptimization(DexEncodedField field) {
return field.isStatic()
&& field.getHolderType() == context.getHolderType()
&& appView.appInfo().isFieldOnlyWrittenInMethod(field, context.getDefinition());
}
@Override
boolean isSubjectToOptimizationIgnoringPinning(DexEncodedField field) {
return field.isStatic()
&& field.getHolderType() == context.getHolderType()
&& appView
.appInfo()
.isFieldOnlyWrittenInMethodIgnoringPinning(field, context.getDefinition());
}
@Override
void updateFieldOptimizationInfo(DexEncodedField field, FieldInstruction fieldPut, Value value) {
AbstractValue abstractValue = getOrComputeAbstractValue(value, field);
updateFieldOptimizationInfo(field, value, abstractValue, false);
}
void updateFieldOptimizationInfo(
DexEncodedField field, Value value, AbstractValue abstractValue, boolean maybeNull) {
builder.recordStaticField(field, abstractValue, appView.dexItemFactory());
// We cannot modify FieldOptimizationInfo of pinned fields.
if (appView.appInfo().isPinned(field)) {
return;
}
// Abstract value.
feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
// Dynamic upper bound type.
TypeElement fieldType =
TypeElement.fromDexType(field.field.type, Nullability.maybeNull(), appView);
TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
if (dynamicUpperBoundType.strictlyLessThan(fieldType, appView)) {
if (maybeNull && dynamicUpperBoundType.isDefinitelyNotNull()) {
assert dynamicUpperBoundType.isReferenceType();
dynamicUpperBoundType = dynamicUpperBoundType.asReferenceType().asMaybeNull();
}
feedback.markFieldHasDynamicUpperBoundType(field, dynamicUpperBoundType);
}
// Dynamic lower bound type.
ClassTypeElement dynamicLowerBoundType = value.getDynamicLowerBoundType(appView);
if (dynamicLowerBoundType != null) {
assert dynamicLowerBoundType.lessThanOrEqual(dynamicUpperBoundType, appView);
if (maybeNull && dynamicLowerBoundType.isDefinitelyNotNull()) {
dynamicLowerBoundType = dynamicLowerBoundType.asMaybeNull().asClassType();
}
feedback.markFieldHasDynamicLowerBoundType(field, dynamicLowerBoundType);
}
}
public void updateFieldOptimizationInfoWith2Values(
DexEncodedField field, Value valuePut, DexValue valueBeforePut) {
// We are interested in the AbstractValue only if it's null or a value, so we can use the value
// if the code is protected by a null check.
if (valueBeforePut != DexValueNull.NULL) {
return;
}
AbstractValue abstractValue =
NullOrAbstractValue.create(getOrComputeAbstractValue(valuePut, field));
updateFieldOptimizationInfo(field, valuePut, abstractValue, true);
}
private AbstractValue getOrComputeAbstractValue(Value value, DexEncodedField field) {
Value root = value.getAliasedValue();
AbstractValue abstractValue = root.getAbstractValue(appView, context);
if (abstractValue.isUnknown()) {
return computeSingleFieldValue(field, root);
}
return abstractValue;
}
private SingleFieldValue computeSingleFieldValue(DexEncodedField field, Value value) {
assert !value.hasAliasedValue();
SingleFieldValue result = computeSingleEnumFieldValue(value);
if (result != null) {
return result;
}
return appView
.abstractValueFactory()
.createSingleFieldValue(field.field, computeObjectState(value));
}
/**
* If {@param value} is defined by a new-instance instruction that instantiates the enclosing enum
* class, and the value is assigned into exactly one static enum field on the enclosing enum
* class, then returns a {@link SingleFieldValue} instance. Otherwise, returns {@code null}.
*
* <p>Note that enum constructors also store the newly instantiated enums in the {@code $VALUES}
* array field on the enum. Therefore, this code also allows {@param value} to be stored into an
* array as long as the array is identified as being the {@code $VALUES} array.
*/
private SingleFieldValue computeSingleEnumFieldValue(Value value) {
if (!context.getHolder().isEnum()) {
return null;
}
assert !value.hasAliasedValue();
if (isEnumValuesArray(value)) {
return computeSingleEnumFieldValueForValuesArray(value);
}
return computeSingleEnumFieldValueForInstance(value);
}
private SingleFieldValue computeSingleEnumFieldValueForValuesArray(Value value) {
if (!value.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)) {
return null;
}
NewArrayEmpty newArrayEmpty = value.definition.asNewArrayEmpty();
if (newArrayEmpty.type.toBaseType(appView.dexItemFactory()) != context.getHolder().type) {
return null;
}
if (value.hasDebugUsers() || value.hasPhiUsers()) {
return null;
}
if (!newArrayEmpty.size().isConstNumber()) {
return null;
}
int valuesSize = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue();
if (valuesSize == 0) {
// No need to compute the state of an empty array.
return null;
}
ObjectState[] valuesState = new ObjectState[valuesSize];
DexEncodedField valuesField = null;
for (Instruction user : value.uniqueUsers()) {
switch (user.opcode()) {
case ARRAY_PUT:
ArrayPut arrayPut = user.asArrayPut();
if (arrayPut.array() != value) {
return null;
}
if (!arrayPut.index().isConstNumber()) {
return null;
}
int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
if (index < 0 || index >= valuesSize) {
return null;
}
ObjectState objectState = computeEnumInstanceObjectState(arrayPut.value());
if (objectState == null || objectState.isEmpty()) {
// We need the state of all fields for the analysis to be valuable.
return null;
}
assert verifyValuesArrayIndexMatchesOrdinal(index, objectState);
if (valuesState[index] != null) {
return null;
}
valuesState[index] = objectState;
break;
case STATIC_PUT:
DexEncodedField field =
context.getHolder().lookupStaticField(user.asStaticPut().getField());
if (field == null) {
return null;
}
if (valuesField != null) {
return null;
}
valuesField = field;
break;
default:
return null;
}
}
if (valuesField == null) {
return null;
}
for (ObjectState objectState : valuesState) {
if (objectState == null) {
return null;
}
}
return appView
.abstractValueFactory()
.createSingleFieldValue(valuesField.field, new EnumValuesObjectState(valuesState));
}
private ObjectState computeEnumInstanceObjectState(Value value) {
Value root = value.getAliasedValue();
if (root.isPhi()) {
return ObjectState.empty();
}
Instruction definition = root.getDefinition();
if (definition.isNewInstance()) {
return computeObjectState(definition.outValue());
}
if (definition.isStaticGet()) {
// TODO(b/166532388) : Enums with many instance rely on staticGets to set the $VALUES data
// instead of directly keeping the values in registers. We could consider analysing these
// and answer the analysed object state here.
return ObjectState.empty();
}
return ObjectState.empty();
}
private boolean verifyValuesArrayIndexMatchesOrdinal(int ordinal, ObjectState objectState) {
DexEncodedField ordinalField =
appView
.appInfo()
.resolveField(appView.dexItemFactory().enumMembers.ordinalField, context)
.getResolvedField();
assert ordinalField != null;
AbstractValue ordinalState = objectState.getAbstractFieldValue(ordinalField);
assert ordinalState != null;
assert ordinalState.isSingleNumberValue();
assert ordinalState.asSingleNumberValue().getIntValue() == ordinal;
return true;
}
private SingleFieldValue computeSingleEnumFieldValueForInstance(Value value) {
if (!value.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
return null;
}
NewInstance newInstance = value.definition.asNewInstance();
// Some enums have direct subclasses, and the subclass is instantiated here.
if (newInstance.clazz != context.getHolderType()) {
DexClass dexClass = appView.definitionFor(newInstance.clazz);
if (dexClass == null || dexClass.superType != context.getHolderType()) {
return null;
}
}
if (value.hasDebugUsers() || value.hasPhiUsers()) {
return null;
}
DexEncodedField enumField = null;
for (Instruction user : value.uniqueUsers()) {
switch (user.opcode()) {
case ARRAY_PUT:
// Check that this is assigning the enum into the enum values array.
ArrayPut arrayPut = user.asArrayPut();
if (arrayPut.value().getAliasedValue() != value || !isEnumValuesArray(arrayPut.array())) {
return null;
}
break;
case INVOKE_DIRECT:
// Check that this is the corresponding constructor call.
InvokeDirect invoke = user.asInvokeDirect();
if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())
|| invoke.getReceiver() != value) {
return null;
}
break;
case STATIC_PUT:
DexEncodedField field =
context.getHolder().lookupStaticField(user.asStaticPut().getField());
if (field != null && field.accessFlags.isEnum()) {
if (enumField != null) {
return null;
}
enumField = field;
}
break;
default:
return null;
}
}
if (enumField == null) {
return null;
}
return appView
.abstractValueFactory()
.createSingleFieldValue(enumField.field, computeObjectState(value));
}
private ObjectState computeObjectState(Value value) {
assert !value.hasAliasedValue();
if (!value.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
return ObjectState.empty();
}
NewInstance newInstance = value.definition.asNewInstance();
InvokeDirect uniqueConstructorInvoke =
newInstance.getUniqueConstructorInvoke(appView.dexItemFactory());
if (uniqueConstructorInvoke == null) {
return ObjectState.empty();
}
DexClassAndMethod singleTarget = uniqueConstructorInvoke.lookupSingleTarget(appView, context);
if (singleTarget == null) {
return ObjectState.empty();
}
InstanceFieldInitializationInfoCollection initializationInfos =
singleTarget
.getDefinition()
.getOptimizationInfo()
.getInstanceInitializerInfo(uniqueConstructorInvoke)
.fieldInitializationInfos();
if (initializationInfos.isEmpty()) {
return ObjectState.empty();
}
ObjectState.Builder builder = ObjectState.builder();
initializationInfos.forEach(
appView,
(field, initializationInfo) -> {
// If the instance field is not written only in the instance initializer, then we can't
// conclude that this field will have a constant value.
//
// We have special handling for library fields that satisfy the property that they are
// only written in their corresponding instance initializers. This is needed since we
// don't analyze these instance initializers in the Enqueuer, as they are in the library.
if (!appView.appInfo().isInstanceFieldWrittenOnlyInInstanceInitializers(field)
&& !appView.dexItemFactory().enumMembers.isNameOrOrdinalField(field.getReference())) {
return;
}
if (initializationInfo.isArgumentInitializationInfo()) {
InstanceFieldArgumentInitializationInfo argumentInitializationInfo =
initializationInfo.asArgumentInitializationInfo();
Value argument =
uniqueConstructorInvoke.getArgument(argumentInitializationInfo.getArgumentIndex());
builder.recordFieldHasValue(field, argument.getAbstractValue(appView, context));
} else if (initializationInfo.isSingleValue()) {
builder.recordFieldHasValue(field, initializationInfo.asSingleValue());
}
});
return builder.build();
}
private boolean isEnumValuesArray(Value value) {
assert context.getHolder().isEnum();
DexItemFactory dexItemFactory = appView.dexItemFactory();
DexField valuesField =
dexItemFactory.createField(
context.getHolderType(),
context.getHolderType().toArrayType(1, dexItemFactory),
dexItemFactory.enumValuesFieldName);
Value root = value.getAliasedValue();
if (root.isPhi()) {
return false;
}
Instruction definition = root.definition;
if (definition.isNewArrayEmpty()) {
for (Instruction user : root.aliasedUsers()) {
if (user.isStaticPut() && user.asStaticPut().getField() == valuesField) {
return true;
}
}
} else if (definition.isStaticGet()) {
return definition.asStaticGet().getField() == valuesField;
}
return false;
}
}