Interleave argument and field propagation
Bug: b/296030319
Change-Id: Ic0d3c149c5903bac51279ad4c0ca659b4430056f
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index e050ed4..fdb9d4b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -193,6 +193,14 @@
return null;
}
+ public boolean isSingleStatefulFieldValue() {
+ return false;
+ }
+
+ public boolean isSingleStatelessFieldValue() {
+ return false;
+ }
+
public SingleNullValue asSingleNullValue() {
return null;
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
index 7d06ddf..580d2d9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
@@ -40,6 +40,11 @@
}
@Override
+ public boolean isSingleStatefulFieldValue() {
+ return true;
+ }
+
+ @Override
public String toString() {
return "SingleStatefulFieldValue(" + field.toSourceString() + ")";
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
index 7b55767..be620a8 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
@@ -25,6 +25,11 @@
}
@Override
+ public boolean isSingleStatelessFieldValue() {
+ return true;
+ }
+
+ @Override
public String toString() {
return "SingleStatelessFieldValue(" + field.toSourceString() + ")";
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 9987a33..3c46cf1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -698,7 +698,14 @@
}
private void setAbstractReturnValue(AbstractValue value) {
- assert !abstractReturnValue.isSingleValue() || abstractReturnValue.equals(value)
+ assert !abstractReturnValue.isSingleValue()
+ || abstractReturnValue.equals(value)
+ || (abstractReturnValue.isSingleStatelessFieldValue()
+ && value.isSingleStatefulFieldValue()
+ && abstractReturnValue
+ .asSingleFieldValue()
+ .getField()
+ .isIdenticalTo(value.asSingleFieldValue().getField()))
: "return single value changed from " + abstractReturnValue + " to " + value;
abstractReturnValue = value;
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 135a011..058be03 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -15,6 +15,7 @@
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.conversion.PostMethodProcessor;
import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
@@ -89,8 +90,11 @@
// Disable argument propagation for methods that should not be optimized by setting their
// method state to unknown.
new ArgumentPropagatorUnoptimizableFieldsAndMethods(
- appView, immediateSubtypingInfo, codeScanner.getMethodStates())
- .initializeUnoptimizableMethodStates(classes);
+ appView,
+ immediateSubtypingInfo,
+ codeScanner.getFieldStates(),
+ codeScanner.getMethodStates())
+ .run(classes);
// Compute the mapping from virtual methods to their root virtual method and the set of
// monomorphic virtual methods.
@@ -222,8 +226,9 @@
throws ExecutionException {
// Unset the scanner since all code objects have been scanned at this point.
assert appView.isAllCodeProcessed();
- MethodStateCollectionByReference codeScannerResult = codeScanner.getMethodStates();
- appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(codeScannerResult);
+ FieldStateCollection fieldStates = codeScanner.getFieldStates();
+ MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+ appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(methodStates);
codeScanner = null;
postMethodProcessorBuilder.rewrittenWithLens(appView);
@@ -233,12 +238,14 @@
appView,
converter,
immediateSubtypingInfo,
- codeScannerResult,
+ fieldStates,
+ methodStates,
stronglyConnectedProgramComponents,
interfaceDispatchOutsideProgram)
.propagateOptimizationInfo(executorService, timing);
+ // TODO(b/296030319): Also publish the computed optimization information for fields.
new ArgumentPropagatorOptimizationInfoPopulator(
- appView, converter, codeScannerResult, postMethodProcessorBuilder)
+ appView, converter, methodStates, postMethodProcessorBuilder)
.populateOptimizationInfo(executorService, timing);
timing.end();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 0eae64d..7cfd03a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -11,18 +11,21 @@
import com.android.tools.r8.graph.DexMethodHandle;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.type.DynamicType;
import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
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.objectstate.ObjectState;
import com.android.tools.r8.ir.code.AbstractValueSupplier;
import com.android.tools.r8.ir.code.AliasedValueConfiguration;
import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
+import com.android.tools.r8.ir.code.FieldGet;
+import com.android.tools.r8.ir.code.FieldPut;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.Invoke;
import com.android.tools.r8.ir.code.InvokeCustom;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
@@ -36,10 +39,17 @@
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodStateOrBottom;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteReceiverValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValueFactory;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ArgumentPropagatorReprocessingCriteriaCollection;
@@ -47,6 +57,7 @@
import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ParameterReprocessingCriteria;
import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.Sets;
import java.util.ArrayList;
@@ -61,6 +72,7 @@
*
* <p>State pruning is applied on-the-fly to avoid storing redundant information.
*/
+// TODO(b/330130322): Consider extending the flow graph with method-return nodes.
public class ArgumentPropagatorCodeScanner {
private static AliasedValueConfiguration aliasedValueConfiguration =
@@ -70,6 +82,8 @@
private final ArgumentPropagatorCodeScannerModeling modeling;
+ private final FieldValueFactory fieldValueFactory = new FieldValueFactory();
+
private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
@@ -84,6 +98,12 @@
private final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
/**
+ * The abstract program state for this optimization. Intuitively maps each field to its abstract
+ * value and dynamic type.
+ */
+ private final FieldStateCollection fieldStates = FieldStateCollection.createConcurrent();
+
+ /**
* The abstract program state for this optimization. Intuitively maps each parameter to its
* abstract value and dynamic type.
*/
@@ -110,6 +130,10 @@
virtualRootMethods.putAll(extension);
}
+ public FieldStateCollection getFieldStates() {
+ return fieldStates;
+ }
+
public MethodStateCollectionByReference getMethodStates() {
return methodStates;
}
@@ -118,6 +142,10 @@
return virtualRootMethods.get(method.getReference());
}
+ private boolean isFieldValueAlreadyUnknown(ProgramField field) {
+ return fieldStates.get(field).isUnknown();
+ }
+
protected boolean isMethodParameterAlreadyUnknown(
MethodParameter methodParameter, ProgramMethod method) {
MethodState methodState =
@@ -153,17 +181,163 @@
AbstractValueSupplier abstractValueSupplier,
Timing timing) {
timing.begin("Argument propagation scanner");
- for (Invoke invoke : code.<Invoke>instructions(Instruction::isInvoke)) {
- if (invoke.isInvokeMethod()) {
- scan(invoke.asInvokeMethod(), abstractValueSupplier, method, timing);
- } else if (invoke.isInvokeCustom()) {
- scan(invoke.asInvokeCustom());
+ for (Instruction instruction : code.instructions()) {
+ if (instruction.isFieldPut()) {
+ scan(instruction.asFieldPut(), abstractValueSupplier, method, timing);
+ } else if (instruction.isInvokeMethod()) {
+ scan(instruction.asInvokeMethod(), abstractValueSupplier, method, timing);
+ } else if (instruction.isInvokeCustom()) {
+ scan(instruction.asInvokeCustom());
}
}
timing.end();
}
private void scan(
+ FieldPut fieldPut,
+ AbstractValueSupplier abstractValueSupplier,
+ ProgramMethod context,
+ Timing timing) {
+ ProgramField field = fieldPut.resolveField(appView, context).getProgramField();
+ if (field == null) {
+ // Nothing to propagate.
+ return;
+ }
+ addTemporaryFieldState(fieldPut, field, abstractValueSupplier, context, timing);
+ }
+
+ private void addTemporaryFieldState(
+ FieldPut fieldPut,
+ ProgramField field,
+ AbstractValueSupplier abstractValueSupplier,
+ ProgramMethod context,
+ Timing timing) {
+ timing.begin("Add field state");
+ fieldStates.addTemporaryFieldState(
+ field,
+ () -> computeFieldState(fieldPut, field, abstractValueSupplier, context, timing),
+ timing,
+ (existingFieldState, fieldStateToAdd) -> {
+ NonEmptyValueState newFieldState =
+ existingFieldState.mutableJoin(
+ appView,
+ fieldStateToAdd,
+ field.getType(),
+ StateCloner.getCloner(),
+ Action.empty());
+ return narrowFieldState(field, newFieldState);
+ });
+ timing.end();
+ }
+
+ private NonEmptyValueState computeFieldState(
+ FieldPut fieldPut,
+ ProgramField resolvedField,
+ AbstractValueSupplier abstractValueSupplier,
+ ProgramMethod context,
+ Timing timing) {
+ timing.begin("Compute field state for field-put");
+ NonEmptyValueState result =
+ computeFieldState(fieldPut, resolvedField, abstractValueSupplier, context);
+ timing.end();
+ return result;
+ }
+
+ private NonEmptyValueState computeFieldState(
+ FieldPut fieldPut,
+ ProgramField field,
+ AbstractValueSupplier abstractValueSupplier,
+ ProgramMethod context) {
+ Value valueRoot = fieldPut.value().getAliasedValue(aliasedValueConfiguration);
+ InFlow inFlow = null;
+ if (valueRoot.isArgument()) {
+ // If the value is an argument of the enclosing method, then clearly we have no information
+ // about its abstract value. Instead of treating this as having an unknown runtime value, we
+ // instead record a flow constraint that specifies that all values that flow into the
+ // parameter of this enclosing method also flows into the current field.
+ MethodParameter inParameter =
+ methodParameterFactory.create(context, valueRoot.getDefinition().asArgument().getIndex());
+ if (isMethodParameterAlreadyUnknown(inParameter, context)) {
+ return ValueState.unknown();
+ }
+ inFlow = inParameter;
+ } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+ FieldGet fieldGet = valueRoot.getDefinition().asFieldGet();
+ ProgramField otherField = fieldGet.resolveField(appView, context).getProgramField();
+ if (otherField != null) {
+ FieldValue fieldValue = fieldValueFactory.create(otherField);
+ if (isFieldValueAlreadyUnknown(otherField)) {
+ return ValueState.unknown();
+ }
+ inFlow = fieldValue;
+ }
+ }
+
+ if (inFlow != null) {
+ return ConcreteValueState.create(field.getType(), inFlow);
+ }
+
+ if (field.getType().isArrayType()) {
+ Nullability nullability = fieldPut.value().getType().nullability();
+ return ConcreteArrayTypeValueState.create(nullability);
+ }
+
+ AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value());
+ if (abstractValue.isUnknown()) {
+ // TODO(b/296030319): Add the current object state to the computed fallback abstract value.
+ abstractValue = getFallbackAbstractValueForField(field);
+ }
+ if (field.getType().isClassType()) {
+ DynamicType dynamicType =
+ WideningUtils.widenDynamicNonReceiverType(
+ appView, fieldPut.value().getDynamicType(appView), field.getType());
+ return ConcreteClassTypeValueState.create(abstractValue, dynamicType);
+ } else {
+ assert field.getType().isPrimitiveType();
+ return ConcretePrimitiveTypeValueState.create(abstractValue);
+ }
+ }
+
+ // Strengthens the abstract value of static final fields to a (self-)SingleFieldValue when the
+ // abstract value is unknown. The soundness of this is based on the fact that static final fields
+ // will never have their value changed after the <clinit> finishes, so value in a static final
+ // field can always be rematerialized by reading the field.
+ private NonEmptyValueState narrowFieldState(ProgramField field, NonEmptyValueState fieldState) {
+ AbstractValue fallbackAbstractValue = getFallbackAbstractValueForField(field);
+ if (!fallbackAbstractValue.isUnknown()) {
+ AbstractValue abstractValue = fieldState.getAbstractValue(appView);
+ if (!abstractValue.isUnknown()) {
+ return fieldState;
+ }
+ if (field.getType().isArrayType()) {
+ // We do not track an abstract value for array types.
+ return fieldState;
+ }
+ if (field.getType().isClassType()) {
+ DynamicType dynamicType =
+ fieldState.isReferenceState()
+ ? fieldState.asReferenceState().getDynamicType()
+ : DynamicType.unknown();
+ return new ConcreteClassTypeValueState(fallbackAbstractValue, dynamicType);
+ } else {
+ assert field.getType().isPrimitiveType();
+ return new ConcretePrimitiveTypeValueState(fallbackAbstractValue);
+ }
+ }
+ return fieldState;
+ }
+
+ // TODO(b/296030319): Also handle effectively final fields.
+ private AbstractValue getFallbackAbstractValueForField(ProgramField field) {
+ if (field.getAccessFlags().isFinal() && field.getAccessFlags().isStatic()) {
+ return appView
+ .abstractValueFactory()
+ .createSingleFieldValue(field.getReference(), ObjectState.empty());
+ }
+ return AbstractValue.unknown();
+ }
+
+ private void scan(
InvokeMethod invoke,
AbstractValueSupplier abstractValueSupplier,
ProgramMethod context,
@@ -182,10 +356,7 @@
}
SingleResolutionResult<?> resolutionResult =
- appView
- .appInfo()
- .unsafeResolveMethodDueToDexFormatLegacy(invokedMethod)
- .asSingleResolution();
+ invoke.resolveMethod(appView, context).asSingleResolution();
if (resolutionResult == null) {
// Nothing to propagate; the invoke instruction fails.
return;
@@ -309,7 +480,6 @@
return result;
}
- @SuppressWarnings("UnusedVariable")
// TODO(b/190154391): Add a strategy that widens the dynamic receiver type to allow easily
// experimenting with the performance/size trade-off between precise/imprecise handling of
// dynamic dispatch.
@@ -517,6 +687,7 @@
// instead record a flow constraint that specifies that all values that flow into the parameter
// of this enclosing method also flows into the corresponding parameter of the methods
// potentially called from this invoke instruction.
+ InFlow inFlow = null;
if (argumentRoot.isArgument()) {
MethodParameter forwardedParameter =
methodParameterFactory.create(
@@ -524,43 +695,47 @@
if (isMethodParameterAlreadyUnknown(forwardedParameter, context)) {
return ValueState.unknown();
}
- if (parameterTypeElement.isClassType()) {
- return new ConcreteClassTypeValueState(forwardedParameter);
- } else if (parameterTypeElement.isArrayType()) {
- return new ConcreteArrayTypeValueState(forwardedParameter);
- } else {
- assert parameterTypeElement.isPrimitiveType();
- return new ConcretePrimitiveTypeValueState(forwardedParameter);
+ inFlow = forwardedParameter;
+ } else if (argumentRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+ // If the value is defined by a program field read, then record a flow constraint that
+ // specifies that all values that flow into the field also flows into the current parameter of
+ // this invoke instruction.
+ FieldGet fieldGet = argumentRoot.getDefinition().asFieldGet();
+ ProgramField field = fieldGet.resolveField(appView, context).getProgramField();
+ if (field != null) {
+ if (isFieldValueAlreadyUnknown(field)) {
+ return ValueState.unknown();
+ }
+ inFlow = fieldValueFactory.create(field);
}
}
+ if (inFlow != null) {
+ return ConcreteValueState.create(parameterType, inFlow);
+ }
+
// Only track the nullability for array types.
- if (parameterTypeElement.isArrayType()) {
+ if (parameterType.isArrayType()) {
Nullability nullability = argument.getType().nullability();
- return nullability.isMaybeNull()
- ? ValueState.unknown()
- : new ConcreteArrayTypeValueState(nullability);
+ return ConcreteArrayTypeValueState.create(nullability);
}
AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(argument);
// For class types, we track both the abstract value and the dynamic type. If both are unknown,
// then use UnknownParameterState.
- if (parameterTypeElement.isClassType()) {
+ if (parameterType.isClassType()) {
DynamicType dynamicType = argument.getDynamicType(appView);
DynamicType widenedDynamicType =
WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
- return abstractValue.isUnknown() && widenedDynamicType.isUnknown()
- ? ValueState.unknown()
- : new ConcreteClassTypeValueState(abstractValue, widenedDynamicType);
+ return ConcreteClassTypeValueState.create(abstractValue, widenedDynamicType);
+ } else {
+ // For primitive types, we only track the abstract value, thus if the abstract value is
+ // unknown,
+ // we use UnknownParameterState.
+ assert parameterType.isPrimitiveType();
+ return ConcretePrimitiveTypeValueState.create(abstractValue);
}
-
- // For primitive types, we only track the abstract value, thus if the abstract value is unknown,
- // we use UnknownParameterState.
- assert parameterTypeElement.isPrimitiveType();
- return abstractValue.isUnknown()
- ? ValueState.unknown()
- : new ConcretePrimitiveTypeValueState(abstractValue);
}
@SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
index 26a2167..97903a9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
@@ -31,6 +32,7 @@
private final AppView<AppInfoWithLiveness> appView;
private final PrimaryR8IRConverter converter;
+ private final FieldStateCollection fieldStates;
private final MethodStateCollectionByReference methodStates;
private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
@@ -43,12 +45,14 @@
AppView<AppInfoWithLiveness> appView,
PrimaryR8IRConverter converter,
ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+ FieldStateCollection fieldStates,
MethodStateCollectionByReference methodStates,
List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram) {
this.appView = appView;
this.converter = converter;
this.immediateSubtypingInfo = immediateSubtypingInfo;
+ this.fieldStates = fieldStates;
this.methodStates = methodStates;
this.stronglyConnectedProgramComponents = stronglyConnectedProgramComponents;
this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
@@ -67,7 +71,7 @@
// Solve the parameter flow constraints.
timing.begin("Solve flow constraints");
- new InFlowPropagator(appView, converter, methodStates).run(executorService);
+ new InFlowPropagator(appView, converter, fieldStates, methodStates).run(executorService);
timing.end();
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
index eec2d32..315c9f7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
@@ -7,10 +7,14 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepFieldInfo;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -20,20 +24,40 @@
private final AppView<AppInfoWithLiveness> appView;
private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+ private final FieldStateCollection fieldStates;
private final MethodStateCollectionByReference methodStates;
public ArgumentPropagatorUnoptimizableFieldsAndMethods(
AppView<AppInfoWithLiveness> appView,
ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+ FieldStateCollection fieldStates,
MethodStateCollectionByReference methodStates) {
this.appView = appView;
this.immediateSubtypingInfo = immediateSubtypingInfo;
+ this.fieldStates = fieldStates;
this.methodStates = methodStates;
}
+ public void run(Collection<DexProgramClass> stronglyConnectedComponent) {
+ initializeUnoptimizableFieldStates(stronglyConnectedComponent);
+ initializeUnoptimizableMethodStates(stronglyConnectedComponent);
+ }
+
+ private void initializeUnoptimizableFieldStates(
+ Collection<DexProgramClass> stronglyConnectedComponent) {
+ for (DexProgramClass clazz : stronglyConnectedComponent) {
+ clazz.forEachProgramField(
+ field -> {
+ if (isUnoptimizableField(field)) {
+ disableValuePropagationForField(field);
+ }
+ });
+ }
+ }
+
// TODO(b/190154391): Consider if we should bail out for classes that inherit from a missing
// class.
- public void initializeUnoptimizableMethodStates(
+ private void initializeUnoptimizableMethodStates(
Collection<DexProgramClass> stronglyConnectedComponent) {
ProgramMethodSet unoptimizableVirtualMethods =
MethodOverridesCollector.findAllMethodsAndOverridesThatMatches(
@@ -47,26 +71,35 @@
&& !method.getAccessFlags().isFinal()) {
return true;
} else {
- disableArgumentPropagationForMethod(method);
+ disableValuePropagationForMethodParameters(method);
}
}
return false;
});
- unoptimizableVirtualMethods.forEach(this::disableArgumentPropagationForMethod);
+ unoptimizableVirtualMethods.forEach(this::disableValuePropagationForMethodParameters);
}
- private void disableArgumentPropagationForMethod(ProgramMethod method) {
+ private void disableValuePropagationForField(ProgramField field) {
+ fieldStates.set(field, ValueState.unknown());
+ }
+
+ private void disableValuePropagationForMethodParameters(ProgramMethod method) {
methodStates.set(method, UnknownMethodState.get());
}
+ private boolean isUnoptimizableField(ProgramField field) {
+ KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+ InternalOptions options = appView.options();
+ return !keepInfo.isFieldPropagationAllowed(options);
+ }
+
private boolean isUnoptimizableMethod(ProgramMethod method) {
assert !method.getDefinition().belongsToVirtualPool()
|| !method.getDefinition().isLibraryMethodOverride().isUnknown()
: "Unexpected virtual method without library method override information: "
+ method.toSourceString();
- AppInfoWithLiveness appInfo = appView.appInfo();
InternalOptions options = appView.options();
return method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
- || !appInfo.getKeepInfo().getMethodInfo(method).isArgumentPropagationAllowed(options);
+ || !appView.getKeepInfo(method).isArgumentPropagationAllowed(options);
}
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
index da529db..5d353f9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
@@ -5,7 +5,41 @@
public interface AbstractFunction extends InFlow {
+ static IdentityAbstractFunction identity() {
+ return IdentityAbstractFunction.get();
+ }
+
static UnknownAbstractFunction unknown() {
return UnknownAbstractFunction.get();
}
+
+ /**
+ * Applies the current abstract function to the given {@param state}.
+ *
+ * <p>It is guaranteed by the caller that the given {@param state} is the abstract state for the
+ * field or parameter this function depends on, i.e., the node returned by {@link
+ * #getBaseInFlow()}.
+ */
+ NonEmptyValueState apply(ConcreteValueState state);
+
+ /**
+ * Returns the (single) program field or parameter graph node that this function depends on. Upon
+ * any change to the abstract state of this graph node this abstract function must be
+ * re-evaluated.
+ */
+ InFlow getBaseInFlow();
+
+ @Override
+ default boolean isAbstractFunction() {
+ return true;
+ }
+
+ @Override
+ default AbstractFunction asAbstractFunction() {
+ return this;
+ }
+
+ default boolean isIdentity() {
+ return false;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
index f05f9e6..5bc8659 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
@@ -11,7 +11,6 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
-import java.util.function.Function;
public abstract class ConcreteValueState extends NonEmptyValueState {
@@ -28,6 +27,17 @@
this.inFlow = inFlow;
}
+ public static ConcreteValueState create(DexType staticType, InFlow inFlow) {
+ if (staticType.isArrayType()) {
+ return new ConcreteArrayTypeValueState(inFlow);
+ } else if (staticType.isClassType()) {
+ return new ConcreteClassTypeValueState(inFlow);
+ } else {
+ assert staticType.isPrimitiveType();
+ return new ConcretePrimitiveTypeValueState(inFlow);
+ }
+ }
+
public abstract ValueState clearInFlow();
void internalClearInFlow() {
@@ -68,16 +78,6 @@
}
@Override
- public NonEmptyValueState mutableJoin(
- AppView<AppInfoWithLiveness> appView,
- Function<ValueState, NonEmptyValueState> stateSupplier,
- DexType staticType,
- StateCloner cloner,
- Action onChangedAction) {
- return mutableJoin(appView, stateSupplier.apply(this), staticType, cloner, onChangedAction);
- }
-
- @Override
public final NonEmptyValueState mutableJoin(
AppView<AppInfoWithLiveness> appView,
ValueState state,
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java
new file mode 100644
index 0000000..d323112
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramFieldMap;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+public class FieldStateCollection {
+
+ private final ProgramFieldMap<NonEmptyValueState> fieldStates;
+
+ private FieldStateCollection(ProgramFieldMap<NonEmptyValueState> fieldStates) {
+ this.fieldStates = fieldStates;
+ }
+
+ public static FieldStateCollection createConcurrent() {
+ return new FieldStateCollection(ProgramFieldMap.createConcurrent());
+ }
+
+ public NonEmptyValueState addTemporaryFieldState(
+ AppView<AppInfoWithLiveness> appView,
+ ProgramField field,
+ Supplier<NonEmptyValueState> fieldStateSupplier,
+ Timing timing) {
+ return addTemporaryFieldState(
+ field,
+ fieldStateSupplier,
+ timing,
+ (existingFieldState, fieldStateToAdd) ->
+ existingFieldState.mutableJoin(
+ appView,
+ fieldStateToAdd,
+ field.getType(),
+ StateCloner.getCloner(),
+ Action.empty()));
+ }
+
+ /**
+ * This intentionally takes a {@link Supplier<NonEmptyValueState>} to avoid computing the field
+ * state for a given field put when nothing is known about the value of the field.
+ */
+ public NonEmptyValueState addTemporaryFieldState(
+ ProgramField field,
+ Supplier<NonEmptyValueState> fieldStateSupplier,
+ Timing timing,
+ BiFunction<ConcreteValueState, ConcreteValueState, NonEmptyValueState> joiner) {
+ ValueState joinState =
+ fieldStates.compute(
+ field,
+ (f, existingFieldState) -> {
+ if (existingFieldState == null) {
+ return fieldStateSupplier.get();
+ }
+ assert !existingFieldState.isBottom();
+ if (existingFieldState.isUnknown()) {
+ return existingFieldState;
+ }
+ NonEmptyValueState fieldStateToAdd = fieldStateSupplier.get();
+ if (fieldStateToAdd.isUnknown()) {
+ return fieldStateToAdd;
+ }
+ timing.begin("Join temporary field state");
+ ConcreteValueState existingConcreteFieldState = existingFieldState.asConcrete();
+ ConcreteValueState concreteFieldStateToAdd = fieldStateToAdd.asConcrete();
+ NonEmptyValueState joinResult =
+ joiner.apply(existingConcreteFieldState, concreteFieldStateToAdd);
+ timing.end();
+ return joinResult;
+ });
+ assert joinState.isNonEmpty();
+ return joinState.asNonEmpty();
+ }
+
+ public void forEach(BiConsumer<ProgramField, ValueState> consumer) {
+ fieldStates.forEach(consumer);
+ }
+
+ public ValueState get(ProgramField field) {
+ NonEmptyValueState fieldState = fieldStates.get(field);
+ return fieldState != null ? fieldState : ValueState.bottom(field);
+ }
+
+ public void set(ProgramField field, NonEmptyValueState state) {
+ fieldStates.put(field, state);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
new file mode 100644
index 0000000..24ad150
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.DexField;
+
+// TODO(b/296030319): Change DexField to implement InFlow and use DexField in all places instead of
+// FieldValue to avoid wrappers? This would also remove the need for the FieldValueFactory.
+public class FieldValue implements InFlow {
+
+ private final DexField field;
+
+ public FieldValue(DexField field) {
+ this.field = field;
+ }
+
+ public DexField getField() {
+ return field;
+ }
+
+ @Override
+ public boolean isFieldValue() {
+ return true;
+ }
+
+ @Override
+ public FieldValue asFieldValue() {
+ return this;
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ FieldValue fieldValue = (FieldValue) obj;
+ return field.isIdenticalTo(fieldValue.field);
+ }
+
+ @Override
+ public int hashCode() {
+ return field.hashCode();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java
new file mode 100644
index 0000000..2ae1d1b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2024, 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.
+// Copyright (c) 2021, 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.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.ProgramField;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class FieldValueFactory {
+
+ private final Map<DexField, FieldValue> fieldValues = new ConcurrentHashMap<>();
+
+ public FieldValue create(ProgramField field) {
+ return fieldValues.computeIfAbsent(field.getReference(), FieldValue::new);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
new file mode 100644
index 0000000..de16de1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.errors.Unreachable;
+
+public class IdentityAbstractFunction implements AbstractFunction {
+
+ private static final IdentityAbstractFunction INSTANCE = new IdentityAbstractFunction();
+
+ private IdentityAbstractFunction() {}
+
+ static IdentityAbstractFunction get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public NonEmptyValueState apply(ConcreteValueState state) {
+ return state;
+ }
+
+ @Override
+ public InFlow getBaseInFlow() {
+ throw new Unreachable();
+ }
+
+ @Override
+ public boolean isIdentity() {
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
index b651045..d858bef 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
@@ -3,8 +3,26 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+import com.android.tools.r8.optimize.compose.UpdateChangedFlagsAbstractFunction;
+
public interface InFlow {
+ default boolean isAbstractFunction() {
+ return false;
+ }
+
+ default AbstractFunction asAbstractFunction() {
+ return null;
+ }
+
+ default boolean isFieldValue() {
+ return false;
+ }
+
+ default FieldValue asFieldValue() {
+ return null;
+ }
+
default boolean isMethodParameter() {
return false;
}
@@ -12,4 +30,12 @@
default MethodParameter asMethodParameter() {
return null;
}
+
+ default boolean isUpdateChangedFlagsAbstractFunction() {
+ return false;
+ }
+
+ default UpdateChangedFlagsAbstractFunction asUpdateChangedFlagsAbstractFunction() {
+ return null;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
index a715205..97d0a2f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
@@ -7,7 +7,6 @@
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexMethodSignature;
import com.android.tools.r8.graph.ProgramMethod;
-import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -17,10 +16,6 @@
super(methodStates);
}
- public static MethodStateCollectionByReference create() {
- return new MethodStateCollectionByReference(new IdentityHashMap<>());
- }
-
public static MethodStateCollectionByReference createConcurrent() {
return new MethodStateCollectionByReference(new ConcurrentHashMap<>());
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
index a244616..029a696 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
@@ -8,7 +8,6 @@
import com.android.tools.r8.graph.ProgramMethod;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
public class MethodStateCollectionBySignature extends MethodStateCollection<DexMethodSignature> {
@@ -20,10 +19,6 @@
return new MethodStateCollectionBySignature(new HashMap<>());
}
- public static MethodStateCollectionBySignature createConcurrent() {
- return new MethodStateCollectionBySignature(new ConcurrentHashMap<>());
- }
-
@Override
DexMethodSignature getKey(ProgramMethod method) {
return method.getMethodSignature();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
index 19bc95a..fd4186b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
@@ -4,12 +4,6 @@
package com.android.tools.r8.optimize.argumentpropagation.codescanner;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.Action;
-import java.util.function.Function;
-
public abstract class NonEmptyValueState extends ValueState {
@Override
@@ -21,19 +15,4 @@
public NonEmptyValueState asNonEmpty() {
return this;
}
-
- public final NonEmptyValueState mutableJoin(
- AppView<AppInfoWithLiveness> appView,
- Function<ValueState, NonEmptyValueState> stateSupplier,
- DexType staticType,
- StateCloner cloner) {
- return mutableJoin(appView, stateSupplier, staticType, cloner, Action.empty());
- }
-
- public abstract NonEmptyValueState mutableJoin(
- AppView<AppInfoWithLiveness> appView,
- Function<ValueState, NonEmptyValueState> stateSupplier,
- DexType staticType,
- StateCloner cloner,
- Action onChangedAction);
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
new file mode 100644
index 0000000..e450a76
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.codescanner;
+
+import java.util.Objects;
+
+/**
+ * Encodes the `x | const` abstract function. This is currently used as part of the modeling of
+ * updateChangedFlags, since the updateChangedFlags function is invoked with `changedFlags | 1` as
+ * an argument.
+ */
+public class OrAbstractFunction implements AbstractFunction {
+
+ public final InFlow inFlow;
+ public final long constant;
+
+ public OrAbstractFunction(InFlow inFlow, long constant) {
+ this.inFlow = inFlow;
+ this.constant = constant;
+ }
+
+ @Override
+ public NonEmptyValueState apply(ConcreteValueState state) {
+ // TODO(b/302483644): Implement this abstract function to allow correct value propagation of
+ // updateChangedFlags(x | 1).
+ return state;
+ }
+
+ @Override
+ public InFlow getBaseInFlow() {
+ if (inFlow.isAbstractFunction()) {
+ return inFlow.asAbstractFunction().getBaseInFlow();
+ }
+ assert inFlow.isFieldValue() || inFlow.isMethodParameter();
+ return inFlow;
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ OrAbstractFunction fn = (OrAbstractFunction) obj;
+ return inFlow.equals(fn.inFlow) && constant == fn.constant;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getClass(), inFlow, constant);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
index 07a87b5..8ac1736 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+import com.android.tools.r8.errors.Unreachable;
+
public class UnknownAbstractFunction implements AbstractFunction {
private static final UnknownAbstractFunction INSTANCE = new UnknownAbstractFunction();
@@ -12,4 +14,14 @@
static UnknownAbstractFunction get() {
return INSTANCE;
}
+
+ @Override
+ public NonEmptyValueState apply(ConcreteValueState state) {
+ return ValueState.unknown();
+ }
+
+ @Override
+ public InFlow getBaseInFlow() {
+ throw new Unreachable();
+ }
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
index f9e7a1d..6e3601b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
@@ -7,9 +7,9 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.UnknownValue;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.Action;
-import java.util.function.Function;
public class UnknownValueState extends NonEmptyValueState {
@@ -22,7 +22,7 @@
}
@Override
- public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
+ public UnknownValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
return AbstractValue.unknown();
}
@@ -32,24 +32,14 @@
}
@Override
- public ValueState mutableCopy() {
+ public UnknownValueState mutableCopy() {
return this;
}
@Override
- public ValueState mutableJoin(
+ public UnknownValueState mutableJoin(
AppView<AppInfoWithLiveness> appView,
- ValueState parameterState,
- DexType parameterType,
- StateCloner cloner,
- Action onChangedAction) {
- return this;
- }
-
- @Override
- public NonEmptyValueState mutableJoin(
- AppView<AppInfoWithLiveness> appView,
- Function<ValueState, NonEmptyValueState> stateSupplier,
+ ValueState state,
DexType staticType,
StateCloner cloner,
Action onChangedAction) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
new file mode 100644
index 0000000..542b17e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -0,0 +1,221 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FlowGraph;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.Node;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.collections.ProgramFieldSet;
+import com.google.common.collect.Lists;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+public class DefaultFieldValueJoiner {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final List<FlowGraph> flowGraphs;
+
+ public DefaultFieldValueJoiner(AppView<AppInfoWithLiveness> appView, List<FlowGraph> flowGraphs) {
+ this.appView = appView;
+ this.flowGraphs = flowGraphs;
+ }
+
+ public Collection<Deque<Node>> joinDefaultFieldValuesForFieldsWithReadBeforeWrite(
+ ExecutorService executorService) throws ExecutionException {
+ // Find all the fields where we need to determine if each field read is guaranteed to be
+ // dominated by a write.
+ Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = getFieldsOfInterest();
+
+ // If constructor inlining is disabled, then we focus on whether each instance initializer
+ // definitely assigns the given field before it is read. We do the same for final and static
+ // fields.
+ Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields =
+ removeFieldsNotSubjectToInitializerAnalysis(fieldsOfInterest);
+ ProgramFieldSet fieldsWithLiveDefaultValue = ProgramFieldSet.createConcurrent();
+ analyzeInstanceInitializers(fieldsOfInterest, fieldsWithLiveDefaultValue::add, executorService);
+
+ // For non-final fields where writes in instance initializers may have been subject to
+ // constructor inlining, we find all new-instance instructions (including subtype allocations)
+ // and check if the field is written on each allocation before it is possibly read.
+ analyzeNewInstanceInstructions(nonFinalInstanceFields, fieldsWithLiveDefaultValue::add);
+
+ return updateFlowGraphs(fieldsWithLiveDefaultValue, executorService);
+ }
+
+ private Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
+ Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = new IdentityHashMap<>();
+ for (FlowGraph flowGraph : flowGraphs) {
+ // TODO(b/296030319): We only need to include the fields where including the default value
+ // would make a difference. Then we can assert below in updateFlowGraphs() that adding the
+ // default value changes the field state.
+ flowGraph.forEachFieldNode(
+ node -> {
+ ProgramField field = node.getField();
+ fieldsOfInterest
+ .computeIfAbsent(field.getHolder(), ignoreKey(ArrayList::new))
+ .add(field);
+ });
+ }
+ return fieldsOfInterest;
+ }
+
+ private Map<DexProgramClass, List<ProgramField>> removeFieldsNotSubjectToInitializerAnalysis(
+ Map<DexProgramClass, List<ProgramField>> fieldsOfInterest) {
+ // When constructor inlining is disabled, we only analyze the initializers of each field holder.
+ if (!appView.options().canInitNewInstanceUsingSuperclassConstructor()) {
+ return Collections.emptyMap();
+ }
+
+ // When constructor inlining is enabled, we can still limit the analysis to the instance
+ // initializers for final fields. We can do the same for static fields as <clinit> is not
+ // subject to inlining.
+ Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields = new IdentityHashMap<>();
+ MapUtils.removeIf(
+ fieldsOfInterest,
+ (holder, fields) -> {
+ fields.removeIf(
+ field -> {
+ if (!field.getAccessFlags().isFinal() && !field.getAccessFlags().isStatic()) {
+ nonFinalInstanceFields
+ .computeIfAbsent(holder, ignoreKey(ArrayList::new))
+ .add(field);
+ }
+ return false;
+ });
+ return fields.isEmpty();
+ });
+ return nonFinalInstanceFields;
+ }
+
+ private void analyzeInstanceInitializers(
+ Map<DexProgramClass, List<ProgramField>> fieldsOfInterest,
+ Consumer<ProgramField> concurrentLiveDefaultValueConsumer,
+ ExecutorService executorService)
+ throws ExecutionException {
+ ThreadUtils.processMap(
+ fieldsOfInterest,
+ (clazz, fields) -> {
+ ProgramFieldSet instanceFieldsWithLiveDefaultValue = ProgramFieldSet.create();
+ ProgramFieldSet staticFieldsWithLiveDefaultValue = ProgramFieldSet.create();
+ partitionFields(
+ fields, instanceFieldsWithLiveDefaultValue, staticFieldsWithLiveDefaultValue);
+ analyzeClassInitializerAssignments(
+ clazz, staticFieldsWithLiveDefaultValue, concurrentLiveDefaultValueConsumer);
+ analyzeInstanceInitializerAssignments(
+ clazz, instanceFieldsWithLiveDefaultValue, concurrentLiveDefaultValueConsumer);
+ },
+ appView.options().getThreadingModule(),
+ executorService);
+ }
+
+ private void partitionFields(
+ Collection<ProgramField> fields,
+ ProgramFieldSet instanceFieldsWithLiveDefaultValue,
+ ProgramFieldSet staticFieldsWithLiveDefaultValue) {
+ for (ProgramField field : fields) {
+ if (field.getAccessFlags().isStatic()) {
+ staticFieldsWithLiveDefaultValue.add(field);
+ } else {
+ instanceFieldsWithLiveDefaultValue.add(field);
+ }
+ }
+ }
+
+ private void analyzeClassInitializerAssignments(
+ DexProgramClass clazz,
+ ProgramFieldSet staticFieldsWithLiveDefaultValue,
+ Consumer<ProgramField> liveDefaultValueConsumer) {
+ if (staticFieldsWithLiveDefaultValue.isEmpty()) {
+ return;
+ }
+ if (clazz.hasClassInitializer()) {
+ IRCode code =
+ clazz
+ .getProgramClassInitializer()
+ .buildIR(appView, MethodConversionOptions.nonConverting());
+ FieldReadBeforeWriteAnalysis analysis = new FieldReadBeforeWriteAnalysis(appView, code);
+ staticFieldsWithLiveDefaultValue.removeIf(analysis::isStaticFieldNeverReadBeforeWrite);
+ }
+ staticFieldsWithLiveDefaultValue.forEach(liveDefaultValueConsumer);
+ }
+
+ private void analyzeInstanceInitializerAssignments(
+ DexProgramClass clazz,
+ ProgramFieldSet instanceFieldsWithLiveDefaultValue,
+ Consumer<ProgramField> liveDefaultValueConsumer) {
+ if (instanceFieldsWithLiveDefaultValue.isEmpty()) {
+ return;
+ }
+ List<ProgramMethod> instanceInitializers =
+ Lists.newArrayList(clazz.programInstanceInitializers());
+ // TODO(b/296030319): Handle multiple instance initializers.
+ if (instanceInitializers.size() == 1) {
+ ProgramMethod instanceInitializer = ListUtils.first(instanceInitializers);
+ IRCode code = instanceInitializer.buildIR(appView, MethodConversionOptions.nonConverting());
+ FieldReadBeforeWriteAnalysis analysis = new FieldReadBeforeWriteAnalysis(appView, code);
+ instanceFieldsWithLiveDefaultValue.removeIf(analysis::isInstanceFieldNeverReadBeforeWrite);
+ }
+ instanceFieldsWithLiveDefaultValue.forEach(liveDefaultValueConsumer);
+ }
+
+ private void analyzeNewInstanceInstructions(
+ Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields,
+ Consumer<ProgramField> liveDefaultValueConsumer) {
+ // Conservatively treat all fields as maybe read before written.
+ // TODO(b/296030319): Implement analysis by building IR for all methods that instantiate the
+ // relevant classes and analyzing the puts to the newly created instances.
+ for (ProgramField field : IterableUtils.flatten(nonFinalInstanceFields.values())) {
+ liveDefaultValueConsumer.accept(field);
+ }
+ }
+
+ private Collection<Deque<Node>> updateFlowGraphs(
+ ProgramFieldSet fieldsWithLiveDefaultValue, ExecutorService executorService)
+ throws ExecutionException {
+ return ThreadUtils.processItemsWithResultsThatMatches(
+ flowGraphs,
+ flowGraph -> {
+ Deque<Node> worklist = new ArrayDeque<>();
+ flowGraph.forEachFieldNode(
+ node -> {
+ ProgramField field = node.getField();
+ if (fieldsWithLiveDefaultValue.contains(field)) {
+ node.addDefaultValue(
+ appView,
+ () -> {
+ if (node.isUnknown()) {
+ node.clearPredecessors();
+ }
+ node.addToWorkList(worklist);
+ });
+ }
+ });
+ return worklist;
+ },
+ worklist -> !worklist.isEmpty(),
+ appView.options().getThreadingModule(),
+ executorService);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java
new file mode 100644
index 0000000..f11d945
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java
@@ -0,0 +1,237 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.ConcreteMutableFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.KnownFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.DequeUtils;
+import com.android.tools.r8.utils.LazyBox;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+class FieldReadBeforeWriteAnalysis {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final IRCode code;
+ private final ProgramMethod context;
+ private final LazyBox<DominatorTree> lazyDominatorTree;
+ private final List<BasicBlock> returnBlocks;
+
+ private Map<BasicBlock, AbstractFieldSet> fieldsMaybeReadBeforeBlockInclusiveCache;
+
+ public FieldReadBeforeWriteAnalysis(AppView<AppInfoWithLiveness> appView, IRCode code) {
+ this.appView = appView;
+ this.code = code;
+ this.context = code.context();
+ this.lazyDominatorTree = new LazyBox<>(() -> new DominatorTree(code));
+ this.returnBlocks = code.computeNormalExitBlocks();
+ }
+
+ public boolean isInstanceFieldNeverReadBeforeWrite(ProgramField field) {
+ assert field.getHolder() == context.getHolder();
+ InstancePut instancePut = null;
+ for (InstancePut candidate :
+ code.getThis().<InstancePut>uniqueUsers(Instruction::isInstancePut)) {
+ if (candidate.getField().isIdenticalTo(field.getReference())) {
+ if (instancePut != null) {
+ // In the somewhat unusual case (?) that the same constructor assigns the same field
+ // multiple times, we simply bail out and conservatively report that the field is maybe
+ // read before it is written.
+ return false;
+ }
+ instancePut = candidate;
+ }
+ }
+ // TODO(b/296030319): Improve precision using escape analysis for receiver.
+ return instancePut != null
+ && !isFieldMaybeReadBeforeInstructionInInitializer(field, instancePut)
+ && lazyDominatorTree.computeIfAbsent().dominatesAllOf(instancePut.getBlock(), returnBlocks);
+ }
+
+ public boolean isStaticFieldNeverReadBeforeWrite(ProgramField field) {
+ assert field.getHolder() == context.getHolder();
+ StaticPut staticPut = null;
+ for (StaticPut candidate : code.<StaticPut>instructions(Instruction::isStaticPut)) {
+ if (candidate.getField().isIdenticalTo(field.getReference())) {
+ if (staticPut != null) {
+ // In the somewhat unusual case (?) that the same constructor assigns the same field
+ // multiple times, we simply bail out and conservatively report that the field is maybe
+ // read before it is written.
+ return false;
+ }
+ staticPut = candidate;
+ }
+ }
+ return staticPut != null
+ && !isFieldMaybeReadBeforeInstructionInInitializer(field, staticPut)
+ && lazyDominatorTree.computeIfAbsent().dominatesAllOf(staticPut.getBlock(), returnBlocks);
+ }
+
+ private boolean isFieldMaybeReadBeforeInstructionInInitializer(
+ DexClassAndField field, Instruction instruction) {
+ BasicBlock block = instruction.getBlock();
+
+ // First check if the field may be read in any of the (transitive) predecessor blocks.
+ if (fieldMaybeReadBeforeBlock(field, block)) {
+ return true;
+ }
+
+ // Then check if any of the instructions that precede the given instruction in the current block
+ // may read the field.
+ InstructionIterator instructionIterator = block.iterator();
+ while (instructionIterator.hasNext()) {
+ Instruction current = instructionIterator.next();
+ if (current == instruction) {
+ break;
+ }
+ if (current.readSet(appView, context).contains(field)) {
+ return true;
+ }
+ }
+
+ // Otherwise, the field is not read prior to the given instruction.
+ return false;
+ }
+
+ private boolean fieldMaybeReadBeforeBlock(DexClassAndField field, BasicBlock block) {
+ for (BasicBlock predecessor : block.getPredecessors()) {
+ if (fieldMaybeReadBeforeBlockInclusive(field, predecessor)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean fieldMaybeReadBeforeBlockInclusive(DexClassAndField field, BasicBlock block) {
+ return getOrCreateFieldsMaybeReadBeforeBlockInclusive().get(block).contains(field);
+ }
+
+ private Map<BasicBlock, AbstractFieldSet> getOrCreateFieldsMaybeReadBeforeBlockInclusive() {
+ if (fieldsMaybeReadBeforeBlockInclusiveCache == null) {
+ fieldsMaybeReadBeforeBlockInclusiveCache = createFieldsMaybeReadBeforeBlockInclusive();
+ }
+ return fieldsMaybeReadBeforeBlockInclusiveCache;
+ }
+
+ /**
+ * Eagerly creates a mapping from each block to the set of fields that may be read in that block
+ * and its transitive predecessors.
+ */
+ private Map<BasicBlock, AbstractFieldSet> createFieldsMaybeReadBeforeBlockInclusive() {
+ Map<BasicBlock, AbstractFieldSet> result = new IdentityHashMap<>();
+ Deque<BasicBlock> worklist = DequeUtils.newArrayDeque(code.entryBlock());
+ while (!worklist.isEmpty()) {
+ BasicBlock block = worklist.removeFirst();
+ boolean seenBefore = result.containsKey(block);
+ AbstractFieldSet readSet =
+ result.computeIfAbsent(block, ignore -> EmptyFieldSet.getInstance());
+ if (readSet.isTop()) {
+ // We already have unknown information for this block.
+ continue;
+ }
+
+ assert readSet.isKnownFieldSet();
+ KnownFieldSet knownReadSet = readSet.asKnownFieldSet();
+ int oldSize = seenBefore ? knownReadSet.size() : -1;
+
+ // Everything that is read in the predecessor blocks should also be included in the read set
+ // for the current block, so here we join the information from the predecessor blocks into the
+ // current read set.
+ boolean blockOrPredecessorMaybeReadAnyField = false;
+ for (BasicBlock predecessor : block.getPredecessors()) {
+ AbstractFieldSet predecessorReadSet =
+ result.getOrDefault(predecessor, EmptyFieldSet.getInstance());
+ if (predecessorReadSet.isBottom()) {
+ continue;
+ }
+ if (predecessorReadSet.isTop()) {
+ blockOrPredecessorMaybeReadAnyField = true;
+ break;
+ }
+ assert predecessorReadSet.isConcreteFieldSet();
+ if (!knownReadSet.isConcreteFieldSet()) {
+ knownReadSet = new ConcreteMutableFieldSet();
+ }
+ knownReadSet.asConcreteFieldSet().addAll(predecessorReadSet.asConcreteFieldSet());
+ }
+
+ if (!blockOrPredecessorMaybeReadAnyField) {
+ // Finally, we update the read set with the fields that are read by the instructions in the
+ // current block. This can be skipped if the block has already been processed.
+ if (seenBefore) {
+ assert verifyFieldSetContainsAllFieldReadsInBlock(knownReadSet, block, context);
+ } else {
+ for (Instruction instruction : block.getInstructions()) {
+ AbstractFieldSet instructionReadSet = instruction.readSet(appView, context);
+ if (instructionReadSet.isBottom()) {
+ continue;
+ }
+ if (instructionReadSet.isTop()) {
+ blockOrPredecessorMaybeReadAnyField = true;
+ break;
+ }
+ if (!knownReadSet.isConcreteFieldSet()) {
+ knownReadSet = new ConcreteMutableFieldSet();
+ }
+ knownReadSet.asConcreteFieldSet().addAll(instructionReadSet.asConcreteFieldSet());
+ }
+ }
+ }
+
+ boolean changed = false;
+ if (blockOrPredecessorMaybeReadAnyField) {
+ // Record that this block reads all fields.
+ result.put(block, UnknownFieldSet.getInstance());
+ changed = true;
+ } else {
+ if (knownReadSet != readSet) {
+ result.put(block, knownReadSet.asConcreteFieldSet());
+ }
+ if (knownReadSet.size() != oldSize) {
+ assert knownReadSet.size() > oldSize;
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ // Rerun the analysis for all successors because the state of the current block changed.
+ worklist.addAll(block.getSuccessors());
+ }
+ }
+ return result;
+ }
+
+ private boolean verifyFieldSetContainsAllFieldReadsInBlock(
+ KnownFieldSet readSet, BasicBlock block, ProgramMethod context) {
+ for (Instruction instruction : block.getInstructions()) {
+ AbstractFieldSet instructionReadSet = instruction.readSet(appView, context);
+ assert !instructionReadSet.isTop();
+ if (instructionReadSet.isBottom()) {
+ continue;
+ }
+ for (DexEncodedField field : instructionReadSet.asConcreteFieldSet().getFields()) {
+ assert readSet.contains(field);
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
new file mode 100644
index 0000000..7b438c4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FieldNode;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FlowGraph;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.Node;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.ParameterNode;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.PrintStream;
+
+// A simple (unused) writer to aid debugging of field and parameter flow propagation graphs.
+@VisibleForTesting
+public class FlowGraphWriter {
+
+ private final FlowGraph flowGraph;
+
+ public FlowGraphWriter(FlowGraph flowGraph) {
+ this.flowGraph = flowGraph;
+ }
+
+ public void write(PrintStream out) {
+ out.println("digraph {");
+ out.println(" stylesheet = \"/frameworks/g3doc/includes/graphviz-style.css\"");
+ flowGraph.forEachNode(node -> writeNode(out, node));
+ out.println("}");
+ }
+
+ private void writeEdge(PrintStream out) {
+ out.println(" -> ");
+ }
+
+ private void writeNode(PrintStream out, Node node) {
+ if (!node.hasSuccessors()) {
+ writeNodeLabel(out, node);
+ return;
+ }
+ node.forEachSuccessor(
+ (successor, transferFunctions) -> {
+ writeNodeLabel(out, node);
+ writeEdge(out);
+ writeNodeLabel(out, successor);
+ });
+ }
+
+ private void writeNodeLabel(PrintStream out, Node node) {
+ if (node.isFieldNode()) {
+ writeFieldNodeLabel(out, node.asFieldNode());
+ } else {
+ assert node.isParameterNode();
+ writeParameterNodeLabel(out, node.asParameterNode());
+ }
+ }
+
+ private void writeFieldNodeLabel(PrintStream out, FieldNode node) {
+ out.print("\"");
+ ProgramField field = node.getField();
+ out.print(field.getHolderType().getSimpleName());
+ out.print(".");
+ out.print(field.getName().toSourceString());
+ out.print("\"");
+ }
+
+ private void writeParameterNodeLabel(PrintStream out, ParameterNode node) {
+ out.print("\"");
+ ProgramMethod method = node.getMethod();
+ out.print(method.getHolderType().getSimpleName());
+ out.print(".");
+ out.print(method.getName().toSourceString());
+ out.print("(");
+ out.print(node.getParameterIndex());
+ out.print(")\"");
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
index add2a26..27fcb12 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
@@ -1,10 +1,10 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, 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.optimize.argumentpropagation.propagation;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.graph.ProgramField.asProgramFieldOrNull;
import static com.android.tools.r8.utils.MapUtils.ignoreKey;
import com.android.tools.r8.errors.Unreachable;
@@ -12,11 +12,23 @@
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.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+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.conversion.IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
@@ -27,49 +39,63 @@
import com.android.tools.r8.optimize.argumentpropagation.utils.BidirectedGraph;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.ProgramFieldMap;
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
import java.util.ArrayDeque;
-import java.util.ArrayList;
+import java.util.Collection;
import java.util.Deque;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
import java.util.function.Consumer;
public class InFlowPropagator {
final AppView<AppInfoWithLiveness> appView;
final IRConverter converter;
+ final FieldStateCollection fieldStates;
final MethodStateCollectionByReference methodStates;
public InFlowPropagator(
AppView<AppInfoWithLiveness> appView,
IRConverter converter,
+ FieldStateCollection fieldStates,
MethodStateCollectionByReference methodStates) {
this.appView = appView;
this.converter = converter;
+ this.fieldStates = fieldStates;
this.methodStates = methodStates;
}
public void run(ExecutorService executorService) throws ExecutionException {
- // Build a graph with an edge from parameter p -> parameter p' if all argument information for p
- // must be included in the argument information for p'.
- FlowGraph flowGraph = new FlowGraph(appView.appInfo().classes());
+ // Compute strongly connected components so that we can compute the fixpoint of multiple flow
+ // graphs in parallel.
+ List<FlowGraph> flowGraphs = computeStronglyConnectedFlowGraphs();
+ processFlowGraphs(flowGraphs, executorService);
- List<Set<ParameterNode>> stronglyConnectedComponents =
- flowGraph.computeStronglyConnectedComponents();
- ThreadUtils.processItems(
- stronglyConnectedComponents,
- this::process,
- appView.options().getThreadingModule(),
- executorService);
+ // Account for the fact that fields that are read before they are written also needs to include
+ // the default value in the field state. We only need to analyze if a given field is read before
+ // it is written if the field has a non-trivial state in the flow graph. Therefore, we only
+ // perform this analysis after having computed the initial fixpoint(s). The hypothesis is that
+ // many fields will have reached the unknown state after the initial fixpoint, meaning there is
+ // fewer fields to analyze.
+ Collection<Deque<Node>> worklists =
+ includeDefaultValuesInFieldStates(flowGraphs, executorService);
+
+ // Since the inclusion of default values changes the flow graphs, we need to repeat the
+ // fixpoint.
+ processWorklists(worklists, executorService);
// The algorithm only changes the parameter states of each monomorphic method state. In case any
// of these method states have effectively become unknown, we replace them by the canonicalized
@@ -77,10 +103,40 @@
postProcessMethodStates(executorService);
}
- private void process(Set<ParameterNode> stronglyConnectedComponent) {
- // Build a worklist containing all the parameter nodes.
- Deque<ParameterNode> worklist = new ArrayDeque<>(stronglyConnectedComponent);
+ private List<FlowGraph> computeStronglyConnectedFlowGraphs() {
+ // Build a graph with an edge from parameter p -> parameter p' if all argument information for p
+ // must be included in the argument information for p'.
+ FlowGraph flowGraph = new FlowGraph(appView.appInfo().classes());
+ List<Set<Node>> stronglyConnectedComponents = flowGraph.computeStronglyConnectedComponents();
+ return ListUtils.map(stronglyConnectedComponents, FlowGraph::new);
+ }
+ private Collection<Deque<Node>> includeDefaultValuesInFieldStates(
+ List<FlowGraph> flowGraphs, ExecutorService executorService) throws ExecutionException {
+ DefaultFieldValueJoiner joiner = new DefaultFieldValueJoiner(appView, flowGraphs);
+ return joiner.joinDefaultFieldValuesForFieldsWithReadBeforeWrite(executorService);
+ }
+
+ private void processFlowGraphs(List<FlowGraph> flowGraphs, ExecutorService executorService)
+ throws ExecutionException {
+ ThreadUtils.processItems(
+ flowGraphs, this::process, appView.options().getThreadingModule(), executorService);
+ }
+
+ private void processWorklists(Collection<Deque<Node>> worklists, ExecutorService executorService)
+ throws ExecutionException {
+ ThreadUtils.processItems(
+ worklists, this::process, appView.options().getThreadingModule(), executorService);
+ }
+
+ private void process(FlowGraph flowGraph) {
+ // Build a worklist containing all the nodes.
+ Deque<Node> worklist = new ArrayDeque<>();
+ flowGraph.forEachNode(worklist::add);
+ process(worklist);
+ }
+
+ private void process(Deque<Node> worklist) {
// Repeatedly propagate argument information through edges in the flow graph until there are no
// more changes.
// TODO(b/190154391): Consider a path p1 -> p2 -> p3 in the graph. If we process p2 first, then
@@ -88,41 +144,56 @@
// need to reprocess p2 and then p3. If we always process leaves in the graph first, we would
// process p1, then p2, then p3, and then be done.
while (!worklist.isEmpty()) {
- ParameterNode parameterNode = worklist.removeLast();
- parameterNode.unsetPending();
- propagate(
- parameterNode,
- affectedNode -> {
- // No need to enqueue the affected node if it is already in the worklist or if it does
- // not have any successors (i.e., the successor is a leaf).
- if (!affectedNode.isPending() && affectedNode.hasSuccessors()) {
- worklist.add(affectedNode);
- affectedNode.setPending();
- }
- });
+ Node node = worklist.removeLast();
+ node.unsetInWorklist();
+ propagate(node, worklist);
}
}
- private void propagate(
- ParameterNode parameterNode, Consumer<ParameterNode> affectedNodeConsumer) {
- ValueState parameterState = parameterNode.getState();
- if (parameterState.isBottom()) {
+ private void propagate(Node node, Deque<Node> worklist) {
+ if (node.isBottom()) {
return;
}
- List<ParameterNode> newlyUnknownParameterNodes = new ArrayList<>();
- for (ParameterNode successorNode : parameterNode.getSuccessors()) {
- ValueState newParameterState =
- successorNode.addState(
- appView,
- parameterState.asNonEmpty(),
- () -> affectedNodeConsumer.accept(successorNode));
- if (newParameterState.isUnknown()) {
- newlyUnknownParameterNodes.add(successorNode);
+ if (node.isUnknown()) {
+ assert !node.hasPredecessors();
+ for (Node successorNode : node.getSuccessors()) {
+ assert !successorNode.isUnknown();
+ successorNode.clearPredecessors(node);
+ successorNode.setStateToUnknown();
+ successorNode.addToWorkList(worklist);
}
+ node.clearDanglingSuccessors();
+ } else {
+ propagateNode(node, worklist);
}
- for (ParameterNode newlyUnknownParameterNode : newlyUnknownParameterNodes) {
- newlyUnknownParameterNode.clearPredecessors();
- }
+ }
+
+ private void propagateNode(Node node, Deque<Node> worklist) {
+ ConcreteValueState state = node.getState().asConcrete();
+ node.removeSuccessorIf(
+ (successorNode, transferFunctions) -> {
+ assert !successorNode.isUnknown();
+ for (AbstractFunction transferFunction : transferFunctions) {
+ NonEmptyValueState transferState = transferFunction.apply(state);
+ if (transferState.isUnknown()) {
+ successorNode.setStateToUnknown();
+ successorNode.addToWorkList(worklist);
+ } else {
+ ConcreteValueState concreteTransferState = transferState.asConcrete();
+ successorNode.addState(
+ appView, concreteTransferState, () -> successorNode.addToWorkList(worklist));
+ }
+ // If this successor has become unknown, there is no point in continuing to propagate
+ // flow to it from any of its predecessors. We therefore clear the predecessors to
+ // improve performance of the fixpoint computation.
+ if (successorNode.isUnknown()) {
+ successorNode.clearPredecessors(node);
+ return true;
+ }
+ assert !successorNode.isEffectivelyUnknown();
+ }
+ return false;
+ });
}
private void postProcessMethodStates(ExecutorService executorService) throws ExecutionException {
@@ -151,30 +222,85 @@
}
}
- public class FlowGraph extends BidirectedGraph<ParameterNode> {
+ public class FlowGraph extends BidirectedGraph<Node> {
- private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> nodes = new IdentityHashMap<>();
+ private final ProgramFieldMap<FieldNode> fieldNodes = ProgramFieldMap.create();
+ private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> parameterNodes =
+ new IdentityHashMap<>();
public FlowGraph(Iterable<DexProgramClass> classes) {
classes.forEach(this::add);
}
+ public FlowGraph(Collection<Node> nodes) {
+ for (Node node : nodes) {
+ if (node.isFieldNode()) {
+ FieldNode fieldNode = node.asFieldNode();
+ fieldNodes.put(fieldNode.getField(), fieldNode);
+ } else {
+ ParameterNode parameterNode = node.asParameterNode();
+ parameterNodes
+ .computeIfAbsent(
+ parameterNode.getMethod().getReference(),
+ ignoreKey(Int2ReferenceOpenHashMap::new))
+ .put(parameterNode.getParameterIndex(), parameterNode);
+ }
+ }
+ }
+
+ public void forEachFieldNode(Consumer<? super FieldNode> consumer) {
+ fieldNodes.values().forEach(consumer);
+ }
+
@Override
- public void forEachNeighbor(ParameterNode node, Consumer<? super ParameterNode> consumer) {
+ public void forEachNeighbor(Node node, Consumer<? super Node> consumer) {
node.getPredecessors().forEach(consumer);
node.getSuccessors().forEach(consumer);
}
@Override
- public void forEachNode(Consumer<? super ParameterNode> consumer) {
- nodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
+ public void forEachNode(Consumer<? super Node> consumer) {
+ forEachFieldNode(consumer);
+ parameterNodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
}
private void add(DexProgramClass clazz) {
- clazz.forEachProgramMethod(this::add);
+ clazz.forEachProgramField(this::addField);
+ clazz.forEachProgramMethod(this::addMethodParameters);
}
- private void add(ProgramMethod method) {
+ private void addField(ProgramField field) {
+ ValueState fieldState = fieldStates.get(field);
+
+ // No need to create nodes for fields with no in-flow or no useful information.
+ if (fieldState.isBottom() || fieldState.isUnknown()) {
+ return;
+ }
+
+ ConcreteValueState concreteFieldState = fieldState.asConcrete();
+
+ // No need to create a node for a field that doesn't depend on any other nodes, unless some
+ // other node depends on this field, in which case that other node will lead to creation of a
+ // node for the current field.
+ if (!concreteFieldState.hasInFlow()) {
+ return;
+ }
+
+ FieldNode node = getOrCreateFieldNode(field, concreteFieldState);
+ for (InFlow inFlow : concreteFieldState.getInFlow()) {
+ if (addInFlow(inFlow, node).shouldBreak()) {
+ assert node.isUnknown();
+ break;
+ }
+ }
+
+ if (!node.getState().isUnknown()) {
+ assert node.getState() == concreteFieldState;
+ node.setState(concreteFieldState.clearInFlow());
+ }
+ }
+
+ private void addMethodParameters(ProgramMethod method) {
MethodState methodState = methodStates.get(method);
// No need to create nodes for parameters with no in-flow or no useful information.
@@ -187,11 +313,11 @@
List<ValueState> parameterStates = monomorphicMethodState.getParameterStates();
for (int parameterIndex = 0; parameterIndex < parameterStates.size(); parameterIndex++) {
ValueState parameterState = parameterStates.get(parameterIndex);
- add(method, parameterIndex, monomorphicMethodState, parameterState);
+ addMethodParameter(method, parameterIndex, monomorphicMethodState, parameterState);
}
}
- private void add(
+ private void addMethodParameter(
ProgramMethod method,
int parameterIndex,
ConcreteMonomorphicMethodState methodState,
@@ -204,20 +330,18 @@
ConcreteValueState concreteParameterState = parameterState.asConcrete();
- // No need to create a node for a parameter that doesn't depend on any other parameters
- // (unless some other parameter depends on this parameter).
+ // No need to create a node for a parameter that doesn't depend on any other parameters,
+ // unless some other node depends on this parameter, in which case that other node will lead
+ // to the creation of a node for the current parameter.
if (!concreteParameterState.hasInFlow()) {
return;
}
ParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
for (InFlow inFlow : concreteParameterState.getInFlow()) {
- if (inFlow.isMethodParameter()) {
- if (addInFlow(inFlow.asMethodParameter(), node).shouldBreak()) {
- break;
- }
- } else {
- throw new Unreachable();
+ if (addInFlow(inFlow, node).shouldBreak()) {
+ assert node.isUnknown();
+ break;
}
}
@@ -227,7 +351,60 @@
}
}
- private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, ParameterNode node) {
+ // Returns BREAK if the current node has been set to unknown.
+ private TraversalContinuation<?, ?> addInFlow(InFlow inFlow, Node node) {
+ if (inFlow.isAbstractFunction()) {
+ return addInFlow(inFlow.asAbstractFunction(), node);
+ } else if (inFlow.isFieldValue()) {
+ return addInFlow(inFlow.asFieldValue(), node);
+ } else if (inFlow.isMethodParameter()) {
+ return addInFlow(inFlow.asMethodParameter(), node);
+ } else {
+ throw new Unreachable(inFlow.getClass().getTypeName());
+ }
+ }
+
+ private TraversalContinuation<?, ?> addInFlow(AbstractFunction inFlow, Node node) {
+ InFlow baseInFlow = inFlow.getBaseInFlow();
+ if (baseInFlow.isFieldValue()) {
+ return addInFlow(baseInFlow.asFieldValue(), node, inFlow);
+ } else {
+ assert baseInFlow.isMethodParameter();
+ return addInFlow(baseInFlow.asMethodParameter(), node, inFlow);
+ }
+ }
+
+ private TraversalContinuation<?, ?> addInFlow(FieldValue inFlow, Node node) {
+ return addInFlow(inFlow, node, AbstractFunction.identity());
+ }
+
+ private TraversalContinuation<?, ?> addInFlow(
+ FieldValue inFlow, Node node, AbstractFunction transferFunction) {
+ ProgramField field = asProgramFieldOrNull(appView.definitionFor(inFlow.getField()));
+ if (field == null) {
+ assert false;
+ return TraversalContinuation.doContinue();
+ }
+
+ ValueState fieldState = getFieldState(field);
+ if (fieldState.isUnknown()) {
+ // The current node depends on a field for which we don't know anything.
+ node.clearPredecessors();
+ node.setStateToUnknown();
+ return TraversalContinuation.doBreak();
+ }
+
+ FieldNode fieldNode = getOrCreateFieldNode(field, fieldState);
+ node.addPredecessor(fieldNode, transferFunction);
+ return TraversalContinuation.doContinue();
+ }
+
+ private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, Node node) {
+ return addInFlow(inFlow, node, AbstractFunction.identity());
+ }
+
+ private TraversalContinuation<?, ?> addInFlow(
+ MethodParameter inFlow, Node node, AbstractFunction transferFunction) {
ProgramMethod enclosingMethod = getEnclosingMethod(inFlow);
if (enclosingMethod == null) {
// This is a parameter of a single caller inlined method. Since this method has been
@@ -239,15 +416,15 @@
MethodState enclosingMethodState = getMethodState(enclosingMethod);
if (enclosingMethodState.isBottom()) {
- // The current method is called from a dead method; no need to propagate any information
- // from the dead call site.
+ // The current node takes a value from a dead method; no need to propagate any information
+ // from the dead assignment.
return TraversalContinuation.doContinue();
}
if (enclosingMethodState.isUnknown()) {
- // The parameter depends on another parameter for which we don't know anything.
+ // The current node depends on a parameter for which we don't know anything.
node.clearPredecessors();
- node.setState(ValueState.unknown());
+ node.setStateToUnknown();
return TraversalContinuation.doBreak();
}
@@ -259,21 +436,26 @@
enclosingMethod,
inFlow.getIndex(),
enclosingMethodState.asConcrete().asMonomorphic());
- node.addPredecessor(predecessor);
+ node.addPredecessor(predecessor, transferFunction);
return TraversalContinuation.doContinue();
}
+ private FieldNode getOrCreateFieldNode(ProgramField field, ValueState fieldState) {
+ return fieldNodes.computeIfAbsent(field, f -> new FieldNode(f, fieldState));
+ }
+
private ParameterNode getOrCreateParameterNode(
ProgramMethod method, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
Int2ReferenceMap<ParameterNode> parameterNodesForMethod =
- nodes.computeIfAbsent(method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
+ parameterNodes.computeIfAbsent(
+ method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
return parameterNodesForMethod.compute(
parameterIndex,
(ignore, parameterNode) ->
parameterNode != null
? parameterNode
: new ParameterNode(
- methodState, parameterIndex, method.getArgumentType(parameterIndex)));
+ method, methodState, parameterIndex, method.getArgumentType(parameterIndex)));
}
private ProgramMethod getEnclosingMethod(MethodParameter methodParameter) {
@@ -282,6 +464,15 @@
asProgramClassOrNull(appView.definitionFor(methodParameter.getMethod().getHolderType())));
}
+ private ValueState getFieldState(ProgramField field) {
+ if (field == null) {
+ // Conservatively return unknown if for some reason we can't find the field.
+ assert false;
+ return ValueState.unknown();
+ }
+ return fieldStates.get(field);
+ }
+
private MethodState getMethodState(ProgramMethod method) {
if (method == null) {
// Conservatively return unknown if for some reason we can't find the method.
@@ -292,87 +483,254 @@
}
}
- static class ParameterNode {
+ public abstract static class Node {
- private final ConcreteMonomorphicMethodState methodState;
- private final int parameterIndex;
- private final DexType parameterType;
+ private final Set<Node> predecessors = Sets.newIdentityHashSet();
+ private final Map<Node, Set<AbstractFunction>> successors = new IdentityHashMap<>();
- private final Set<ParameterNode> predecessors = Sets.newIdentityHashSet();
- private final Set<ParameterNode> successors = Sets.newIdentityHashSet();
+ private boolean inWorklist = true;
- private boolean pending = true;
-
- ParameterNode(
- ConcreteMonomorphicMethodState methodState, int parameterIndex, DexType parameterType) {
- this.methodState = methodState;
- this.parameterIndex = parameterIndex;
- this.parameterType = parameterType;
+ void addState(
+ AppView<AppInfoWithLiveness> appView,
+ ConcreteValueState stateToAdd,
+ Action onChangedAction) {
+ ValueState oldState = getState();
+ ValueState newState =
+ oldState.mutableJoin(
+ appView, stateToAdd, getStaticType(), StateCloner.getCloner(), onChangedAction);
+ if (newState != oldState) {
+ setState(newState);
+ onChangedAction.execute();
+ }
}
- void addPredecessor(ParameterNode predecessor) {
- predecessor.successors.add(this);
+ abstract ValueState getState();
+
+ abstract DexType getStaticType();
+
+ abstract void setState(ValueState valueState);
+
+ void setStateToUnknown() {
+ setState(ValueState.unknown());
+ }
+
+ void addPredecessor(Node predecessor, AbstractFunction abstractFunction) {
+ predecessor.successors.computeIfAbsent(this, ignoreKey(HashSet::new)).add(abstractFunction);
predecessors.add(predecessor);
}
void clearPredecessors() {
- for (ParameterNode predecessor : predecessors) {
+ for (Node predecessor : predecessors) {
predecessor.successors.remove(this);
}
predecessors.clear();
}
- Set<ParameterNode> getPredecessors() {
+ void clearPredecessors(Node cause) {
+ for (Node predecessor : predecessors) {
+ if (predecessor != cause) {
+ predecessor.successors.remove(this);
+ }
+ }
+ predecessors.clear();
+ }
+
+ Set<Node> getPredecessors() {
return predecessors;
}
- ValueState getState() {
- return methodState.getParameterState(parameterIndex);
+ boolean hasPredecessors() {
+ return !predecessors.isEmpty();
}
- Set<ParameterNode> getSuccessors() {
- return successors;
+ void clearDanglingSuccessors() {
+ successors.clear();
+ }
+
+ Set<Node> getSuccessors() {
+ return successors.keySet();
+ }
+
+ public void forEachSuccessor(BiConsumer<Node, Set<AbstractFunction>> consumer) {
+ successors.forEach(consumer);
+ }
+
+ public void removeSuccessorIf(BiPredicate<Node, Set<AbstractFunction>> predicate) {
+ successors.entrySet().removeIf(entry -> predicate.test(entry.getKey(), entry.getValue()));
}
boolean hasSuccessors() {
return !successors.isEmpty();
}
- boolean isPending() {
- return pending;
+ boolean isBottom() {
+ return getState().isBottom();
}
- ValueState addState(
- AppView<AppInfoWithLiveness> appView,
- NonEmptyValueState parameterStateToAdd,
- Action onChangedAction) {
- ValueState oldParameterState = getState();
- ValueState newParameterState =
- oldParameterState.mutableJoin(
- appView,
- parameterStateToAdd,
- parameterType,
- StateCloner.getCloner(),
- onChangedAction);
- if (newParameterState != oldParameterState) {
- setState(newParameterState);
+ boolean isFieldNode() {
+ return false;
+ }
+
+ FieldNode asFieldNode() {
+ return null;
+ }
+
+ boolean isParameterNode() {
+ return false;
+ }
+
+ ParameterNode asParameterNode() {
+ return null;
+ }
+
+ boolean isEffectivelyUnknown() {
+ return getState().isConcrete() && getState().asConcrete().isEffectivelyUnknown();
+ }
+
+ boolean isUnknown() {
+ return getState().isUnknown();
+ }
+
+ // No need to enqueue the affected node if it is already in the worklist or if it does not have
+ // any successors (i.e., the successor is a leaf).
+ void addToWorkList(Deque<Node> worklist) {
+ if (!inWorklist && hasSuccessors()) {
+ worklist.add(this);
+ inWorklist = true;
+ }
+ }
+
+ void unsetInWorklist() {
+ assert inWorklist;
+ inWorklist = false;
+ }
+ }
+
+ public static class FieldNode extends Node {
+
+ private final ProgramField field;
+ private ValueState fieldState;
+
+ FieldNode(ProgramField field, ValueState fieldState) {
+ this.field = field;
+ this.fieldState = fieldState;
+ }
+
+ public ProgramField getField() {
+ return field;
+ }
+
+ @Override
+ DexType getStaticType() {
+ return field.getType();
+ }
+
+ void addDefaultValue(AppView<AppInfoWithLiveness> appView, Action onChangedAction) {
+ AbstractValueFactory abstractValueFactory = appView.abstractValueFactory();
+ AbstractValue defaultValue;
+ if (field.getAccessFlags().isStatic() && field.getDefinition().hasExplicitStaticValue()) {
+ defaultValue = field.getDefinition().getStaticValue().toAbstractValue(abstractValueFactory);
+ } else if (field.getType().isPrimitiveType()) {
+ defaultValue = abstractValueFactory.createZeroValue();
+ } else {
+ defaultValue = abstractValueFactory.createUncheckedNullValue();
+ }
+ NonEmptyValueState fieldStateToAdd;
+ if (field.getType().isArrayType()) {
+ Nullability defaultNullability = Nullability.definitelyNull();
+ fieldStateToAdd = ConcreteArrayTypeValueState.create(defaultNullability);
+ } else if (field.getType().isClassType()) {
+ assert defaultValue.isNull() || defaultValue.isSingleStringValue();
+ DynamicType dynamicType =
+ defaultValue.isNull()
+ ? DynamicType.definitelyNull()
+ : DynamicType.createExact(
+ TypeElement.stringClassType(appView, Nullability.definitelyNotNull()));
+ fieldStateToAdd = ConcreteClassTypeValueState.create(defaultValue, dynamicType);
+ } else {
+ assert field.getType().isPrimitiveType();
+ fieldStateToAdd = ConcretePrimitiveTypeValueState.create(defaultValue);
+ }
+ if (fieldStateToAdd.isConcrete()) {
+ addState(appView, fieldStateToAdd.asConcrete(), onChangedAction);
+ } else {
+ // We should always be able to map static field values to an unknown abstract value.
+ assert false;
+ setStateToUnknown();
onChangedAction.execute();
}
- return newParameterState;
}
- void setPending() {
- assert !isPending();
- pending = true;
+ @Override
+ ValueState getState() {
+ return fieldState;
}
+ @Override
+ void setState(ValueState fieldState) {
+ this.fieldState = fieldState;
+ }
+
+ @Override
+ boolean isFieldNode() {
+ return true;
+ }
+
+ @Override
+ FieldNode asFieldNode() {
+ return this;
+ }
+ }
+
+ static class ParameterNode extends Node {
+
+ private final ProgramMethod method;
+ private final ConcreteMonomorphicMethodState methodState;
+ private final int parameterIndex;
+ private final DexType parameterType;
+
+ ParameterNode(
+ ProgramMethod method,
+ ConcreteMonomorphicMethodState methodState,
+ int parameterIndex,
+ DexType parameterType) {
+ this.method = method;
+ this.methodState = methodState;
+ this.parameterIndex = parameterIndex;
+ this.parameterType = parameterType;
+ }
+
+ ProgramMethod getMethod() {
+ return method;
+ }
+
+ int getParameterIndex() {
+ return parameterIndex;
+ }
+
+ @Override
+ DexType getStaticType() {
+ return parameterType;
+ }
+
+ @Override
+ ValueState getState() {
+ return methodState.getParameterState(parameterIndex);
+ }
+
+ @Override
void setState(ValueState parameterState) {
methodState.setParameterState(parameterIndex, parameterState);
}
- void unsetPending() {
- assert pending;
- pending = false;
+ @Override
+ boolean isParameterNode() {
+ return true;
+ }
+
+ @Override
+ ParameterNode asParameterNode() {
+ return this;
}
}
}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
index 2a54b56..ebba6d5 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
@@ -4,10 +4,13 @@
package com.android.tools.r8.optimize.compose;
import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+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.DexString;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.ir.code.InstanceGet;
@@ -17,9 +20,10 @@
import com.android.tools.r8.ir.code.Or;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.OrAbstractFunction;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BitUtils;
import com.android.tools.r8.utils.BooleanUtils;
import com.google.common.collect.Iterables;
@@ -147,19 +151,43 @@
assert argument.getType().isInt();
- DexString expectedFieldName;
- ValueState state = ValueState.bottomPrimitiveTypeParameter();
- if (!hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2) {
+ DexString expectedFieldName =
+ !hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2
+ ? rewrittenComposeReferences.changedFieldName
+ : rewrittenComposeReferences.defaultFieldName;
+ DexField expectedField =
+ appView
+ .dexItemFactory()
+ .createField(
+ context.getHolderType(), appView.dexItemFactory().intType, expectedFieldName);
+
+ UpdateChangedFlagsAbstractFunction inFlow = null;
+ if (expectedFieldName.isIdenticalTo(rewrittenComposeReferences.changedFieldName)) {
// We are looking at an argument to the $$changed parameter of the @Composable function.
// We generally expect this argument to be defined by a call to updateChangedFlags().
if (argument.isDefinedByInstructionSatisfying(Instruction::isInvokeStatic)) {
InvokeStatic invokeStatic = argument.getDefinition().asInvokeStatic();
- DexMethod maybeUpdateChangedFlagsMethod = invokeStatic.getInvokedMethod();
- if (!maybeUpdateChangedFlagsMethod.isIdenticalTo(
- rewrittenComposeReferences.updatedChangedFlagsMethod)) {
+ SingleResolutionResult<?> resolutionResult =
+ invokeStatic.resolveMethod(appView, context).asSingleResolution();
+ if (resolutionResult == null) {
return null;
}
- // Assume the call does not impact the $$changed capture and strip the call.
+ DexClassAndMethod invokeSingleTarget =
+ resolutionResult
+ .lookupDispatchTarget(appView, invokeStatic, context)
+ .getSingleDispatchTarget();
+ if (invokeSingleTarget == null) {
+ return null;
+ }
+ inFlow =
+ invokeSingleTarget
+ .getOptimizationInfo()
+ .getAbstractFunction()
+ .asUpdateChangedFlagsAbstractFunction();
+ if (inFlow == null) {
+ return null;
+ }
+ // By accounting for the abstract function we can safely strip the call.
argument = invokeStatic.getFirstArgument();
}
// Allow the argument to be defined by `this.$$changed | 1`.
@@ -175,17 +203,12 @@
argument = otherOperand;
// Update the model from bottom to a special value that effectively throws away any known
// information about the lowermost bit of $$changed.
- state =
- new ConcretePrimitiveTypeValueState(
- appView
- .abstractValueFactory()
- .createDefiniteBitsNumberValue(
- BitUtils.ALL_BITS_SET_MASK, BitUtils.ALL_BITS_SET_MASK << 1));
+ inFlow =
+ new UpdateChangedFlagsAbstractFunction(
+ new OrAbstractFunction(new FieldValue(expectedField), 1));
+ } else {
+ inFlow = new UpdateChangedFlagsAbstractFunction(new FieldValue(expectedField));
}
- expectedFieldName = rewrittenComposeReferences.changedFieldName;
- } else {
- // We are looking at an argument to the $$default parameter of the @Composable function.
- expectedFieldName = rewrittenComposeReferences.defaultFieldName;
}
// At this point we expect that the restart lambda is reading either this.$$changed or
@@ -196,12 +219,14 @@
// Check that the instance-get is reading the capture field that we expect it to.
InstanceGet instanceGet = argument.getDefinition().asInstanceGet();
- if (!instanceGet.getField().getName().isIdenticalTo(expectedFieldName)) {
+ if (!instanceGet.getField().isIdenticalTo(expectedField)) {
return null;
}
// Return the argument model. Note that, for the $$default field, this is always bottom, which
// is equivalent to modeling that this call does not contribute any new argument information.
- return state;
+ return inFlow != null
+ ? new ConcretePrimitiveTypeValueState(inFlow)
+ : ValueState.bottomPrimitiveTypeParameter();
}
}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
index 258b69b..1dc5061 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
@@ -3,9 +3,13 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.optimize.compose;
+import static com.android.tools.r8.graph.ProgramField.asProgramFieldOrNull;
+
import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -21,9 +25,15 @@
import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorOptimizationInfoPopulator;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.LazyBox;
import com.android.tools.r8.utils.Timing;
@@ -71,10 +81,16 @@
return optimizeComposableFunctionsCalledFromWave(wave, executorService);
}
- @SuppressWarnings("UnusedVariable")
private Set<ComposableCallGraphNode> optimizeComposableFunctionsCalledFromWave(
Set<ComposableCallGraphNode> wave, ExecutorService executorService)
throws ExecutionException {
+ prepareForInFlowPropagator();
+
+ InFlowPropagator inFlowPropagator =
+ new InFlowPropagator(
+ appView, converter, codeScanner.getFieldStates(), codeScanner.getMethodStates());
+ inFlowPropagator.run(executorService);
+
ArgumentPropagatorOptimizationInfoPopulator optimizationInfoPopulator =
new ArgumentPropagatorOptimizationInfoPopulator(appView, null, null, null);
Set<ComposableCallGraphNode> optimizedComposableFunctions = Sets.newIdentityHashSet();
@@ -92,36 +108,79 @@
return optimizedComposableFunctions;
}
- private MethodState getMethodState(ComposableCallGraphNode node) {
- assert processed.containsAll(node.getCallers());
- MethodState methodState = codeScanner.getMethodStates().get(node.getMethod());
- return widenMethodState(methodState);
+ private void prepareForInFlowPropagator() {
+ FieldStateCollection fieldStates = codeScanner.getFieldStates();
+
+ // Set all field states to unknown since we are not guaranteed to have processes all field
+ // writes.
+ fieldStates.forEach(
+ (field, fieldState) ->
+ fieldStates.addTemporaryFieldState(
+ appView, field, ValueState::unknown, Timing.empty()));
+
+ // Widen all parameter states that have in-flow to unknown, except when the in-flow is an
+ // update-changed-flags abstract function.
+ MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+ methodStates.forEach(
+ (method, methodState) -> {
+ if (!methodState.isMonomorphic()) {
+ assert methodState.isUnknown();
+ return;
+ }
+ ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+ for (int parameterIndex = 0;
+ parameterIndex < monomorphicMethodState.size();
+ parameterIndex++) {
+ ValueState parameterState = monomorphicMethodState.getParameterState(parameterIndex);
+ if (parameterState.isConcrete()) {
+ ConcreteValueState concreteParameterState = parameterState.asConcrete();
+ prepareParameterStateForInFlowPropagator(
+ method, monomorphicMethodState, parameterIndex, concreteParameterState);
+ }
+ }
+ });
}
- /**
- * If a parameter state of the current method state encodes that it is greater than (lattice wise)
- * than another parameter in the program, then widen the parameter state to unknown. This is
- * needed since we are not guaranteed to have seen all possible call sites of the callers of this
- * method.
- */
- private MethodState widenMethodState(MethodState methodState) {
- assert !methodState.isBottom();
- assert !methodState.isPolymorphic();
- if (methodState.isMonomorphic()) {
- ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
- for (int i = 0; i < monomorphicMethodState.size(); i++) {
- if (monomorphicMethodState.getParameterState(i).isConcrete()) {
- ConcreteValueState concreteParameterState =
- monomorphicMethodState.getParameterState(i).asConcrete();
- if (concreteParameterState.hasInFlow()) {
- monomorphicMethodState.setParameterState(i, ValueState.unknown());
- }
- }
- }
- } else {
- assert methodState.isUnknown();
+ private void prepareParameterStateForInFlowPropagator(
+ DexMethod method,
+ ConcreteMonomorphicMethodState methodState,
+ int parameterIndex,
+ ConcreteValueState parameterState) {
+ if (!parameterState.hasInFlow()) {
+ return;
}
- return methodState;
+
+ UpdateChangedFlagsAbstractFunction transferFunction = null;
+ if (parameterState.getInFlow().size() == 1) {
+ transferFunction =
+ Iterables.getOnlyElement(parameterState.getInFlow())
+ .asUpdateChangedFlagsAbstractFunction();
+ }
+ if (transferFunction == null) {
+ methodState.setParameterState(parameterIndex, ValueState.unknown());
+ return;
+ }
+
+ // This is a call to a composable function from a restart function.
+ InFlow baseInFlow = transferFunction.getBaseInFlow();
+ assert baseInFlow.isFieldValue();
+
+ ProgramField field =
+ asProgramFieldOrNull(appView.definitionFor(baseInFlow.asFieldValue().getField()));
+ assert field != null;
+
+ codeScanner
+ .getFieldStates()
+ .addTemporaryFieldState(
+ appView,
+ field,
+ () -> new ConcretePrimitiveTypeValueState(new MethodParameter(method, parameterIndex)),
+ Timing.empty());
+ }
+
+ private MethodState getMethodState(ComposableCallGraphNode node) {
+ assert processed.containsAll(node.getCallers());
+ return codeScanner.getMethodStates().get(node.getMethod());
}
public void scan(ProgramMethod method, IRCode code, Timing timing) {
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
index 5ae767a..c3dbf97 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
@@ -4,14 +4,60 @@
package com.android.tools.r8.optimize.compose;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import java.util.Objects;
public class UpdateChangedFlagsAbstractFunction implements AbstractFunction {
- @SuppressWarnings("UnusedVariable")
private final InFlow inFlow;
public UpdateChangedFlagsAbstractFunction(InFlow inFlow) {
this.inFlow = inFlow;
}
+
+ @Override
+ public NonEmptyValueState apply(ConcreteValueState state) {
+ // TODO(b/302483644): Implement this abstract function to allow correct value propagation of
+ // updateChangedFlags(x | 1).
+ return state;
+ }
+
+ @Override
+ public InFlow getBaseInFlow() {
+ if (inFlow.isAbstractFunction()) {
+ return inFlow.asAbstractFunction().getBaseInFlow();
+ }
+ assert inFlow.isFieldValue() || inFlow.isMethodParameter();
+ return inFlow;
+ }
+
+ @Override
+ public boolean isUpdateChangedFlagsAbstractFunction() {
+ return true;
+ }
+
+ @Override
+ public UpdateChangedFlagsAbstractFunction asUpdateChangedFlagsAbstractFunction() {
+ return this;
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ UpdateChangedFlagsAbstractFunction fn = (UpdateChangedFlagsAbstractFunction) obj;
+ return inFlow.equals(fn.inFlow);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getClass(), inFlow);
+ }
}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index db53845..5981161 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -40,6 +40,10 @@
return new Builder(this);
}
+ public boolean isFieldPropagationAllowed(GlobalKeepInfoConfiguration configuration) {
+ return isOptimizationAllowed(configuration) && isShrinkingAllowed(configuration);
+ }
+
public boolean isFieldTypeStrengtheningAllowed(GlobalKeepInfoConfiguration configuration) {
return internalIsFieldTypeStrengtheningAllowed();
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java
new file mode 100644
index 0000000..e1074d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2024, 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.callsites;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+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.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RestartLambdaPropagationTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters)
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject mainClassSubject = inspector.clazz(Main.class);
+ assertThat(mainClassSubject, isPresent());
+
+ MethodSubject restartableMethodSubject =
+ mainClassSubject.uniqueMethodWithOriginalName("restartableMethod");
+ assertThat(restartableMethodSubject, isPresent());
+ assertEquals(
+ parameters.isDexRuntime() ? 1 : 2,
+ restartableMethodSubject.getParameters().size());
+ assertEquals(
+ parameters.isDexRuntime(),
+ restartableMethodSubject
+ .streamInstructions()
+ .anyMatch(instruction -> instruction.isConstNumber(42)));
+ })
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Postponing!", "Restarting!", "42", "Stopping!");
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ Runnable restarter = restartableMethod(true, 42);
+ restarter.run();
+ }
+
+ @NeverInline
+ static Runnable restartableMethod(boolean doRestart, int flags) {
+ if (doRestart) {
+ System.out.println("Postponing!");
+ return () -> {
+ System.out.println("Restarting!");
+ Runnable restarter = restartableMethod(false, flags);
+ if (restarter == null) {
+ System.out.println("Stopping!");
+ } else {
+ throw new RuntimeException();
+ }
+ };
+ }
+ System.out.println(flags);
+ return null;
+ }
+ }
+}