| // 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; |
| |
| import static com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow.asBaseInFlowOrNull; |
| |
| import com.android.tools.r8.errors.Unreachable; |
| 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.DexMethod; |
| 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.value.AbstractValue; |
| import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState; |
| import com.android.tools.r8.ir.analysis.value.objectstate.ObjectStateAnalysis; |
| 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.InvokeCustom; |
| import com.android.tools.r8.ir.code.InvokeMethod; |
| import com.android.tools.r8.ir.code.InvokeMethodWithReceiver; |
| import com.android.tools.r8.ir.code.Value; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow; |
| 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.ConcreteMonomorphicMethodState; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodStateOrBottom; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodStateOrUnknown; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodState; |
| 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.FieldValueFactory; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow; |
| import com.android.tools.r8.optimize.argumentpropagation.codescanner.InstanceFieldReadAbstractFunction; |
| 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; |
| import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.MethodReprocessingCriteria; |
| 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; |
| import java.util.Collection; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Supplier; |
| |
| /** |
| * Analyzes each {@link IRCode} during the primary optimization to collect information about the |
| * arguments passed to method parameters. |
| * |
| * <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 = |
| AssumeAndCheckCastAliasedValueConfiguration.getInstance(); |
| |
| private final AppView<AppInfoWithLiveness> appView; |
| |
| private final ArgumentPropagatorCodeScannerModeling modeling; |
| |
| private final FieldValueFactory fieldValueFactory = new FieldValueFactory(); |
| |
| private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory(); |
| |
| private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet(); |
| |
| private final ArgumentPropagatorReprocessingCriteriaCollection reprocessingCriteriaCollection; |
| |
| /** |
| * Maps each non-private virtual method to the upper most method in the class hierarchy with the |
| * same method signature. Virtual methods that do not override other virtual methods are mapped to |
| * themselves. |
| */ |
| 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. |
| */ |
| private final MethodStateCollectionByReference methodStates = |
| MethodStateCollectionByReference.createConcurrent(); |
| |
| public ArgumentPropagatorCodeScanner(AppView<AppInfoWithLiveness> appView) { |
| this(appView, new ArgumentPropagatorReprocessingCriteriaCollection(appView)); |
| } |
| |
| ArgumentPropagatorCodeScanner( |
| AppView<AppInfoWithLiveness> appView, |
| ArgumentPropagatorReprocessingCriteriaCollection reprocessingCriteriaCollection) { |
| this.appView = appView; |
| this.modeling = new ArgumentPropagatorCodeScannerModeling(appView); |
| this.reprocessingCriteriaCollection = reprocessingCriteriaCollection; |
| } |
| |
| public synchronized void addMonomorphicVirtualMethods(Collection<DexMethod> extension) { |
| monomorphicVirtualMethods.addAll(extension); |
| } |
| |
| public synchronized void addVirtualRootMethods(Map<DexMethod, DexMethod> extension) { |
| virtualRootMethods.putAll(extension); |
| } |
| |
| public FieldStateCollection getFieldStates() { |
| return fieldStates; |
| } |
| |
| public MethodStateCollectionByReference getMethodStates() { |
| return methodStates; |
| } |
| |
| DexMethod getVirtualRootMethod(ProgramMethod method) { |
| return virtualRootMethods.get(method.getReference()); |
| } |
| |
| // TODO(b/296030319): Allow lookups in the FieldStateCollection using DexField keys to avoid the |
| // need for definitionFor here. |
| private boolean isFieldValueAlreadyUnknown(DexField field) { |
| return isFieldValueAlreadyUnknown(appView.definitionFor(field).asProgramField()); |
| } |
| |
| private boolean isFieldValueAlreadyUnknown(ProgramField field) { |
| return fieldStates.get(field).isUnknown(); |
| } |
| |
| protected boolean isMethodParameterAlreadyUnknown( |
| MethodParameter methodParameter, ProgramMethod method) { |
| assert methodParameter.getMethod().isIdenticalTo(method.getReference()); |
| MethodState methodState = |
| methodStates.get( |
| method.getDefinition().belongsToDirectPool() || isMonomorphicVirtualMethod(method) |
| ? method.getReference() |
| : getVirtualRootMethod(method)); |
| if (methodState.isPolymorphic()) { |
| methodState = methodState.asPolymorphic().getMethodStateForBounds(DynamicType.unknown()); |
| } |
| if (methodState.isMonomorphic()) { |
| ValueState parameterState = |
| methodState.asMonomorphic().getParameterState(methodParameter.getIndex()); |
| return parameterState.isUnknown(); |
| } |
| assert methodState.isBottom() || methodState.isUnknown(); |
| return methodState.isUnknown(); |
| } |
| |
| boolean isMonomorphicVirtualMethod(ProgramMethod method) { |
| boolean isMonomorphicVirtualMethod = isMonomorphicVirtualMethod(method.getReference()); |
| assert method.getDefinition().belongsToVirtualPool() || !isMonomorphicVirtualMethod; |
| return isMonomorphicVirtualMethod; |
| } |
| |
| boolean isMonomorphicVirtualMethod(DexMethod method) { |
| return monomorphicVirtualMethods.contains(method); |
| } |
| |
| public void scan( |
| ProgramMethod method, |
| IRCode code, |
| AbstractValueSupplier abstractValueSupplier, |
| Timing timing) { |
| timing.begin("Argument propagation scanner"); |
| 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) { |
| NonEmptyValueState inFlowState = computeInFlowState(field.getType(), fieldPut.value(), context); |
| if (inFlowState != null) { |
| return inFlowState; |
| } |
| |
| if (field.getType().isArrayType()) { |
| Nullability nullability = fieldPut.value().getType().nullability(); |
| return ConcreteArrayTypeValueState.create(nullability); |
| } |
| |
| AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value()); |
| if (abstractValue.isUnknown()) { |
| abstractValue = |
| getFallbackAbstractValueForField( |
| field, |
| () -> ObjectStateAnalysis.computeObjectState(fieldPut.value(), appView, context)); |
| } |
| 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); |
| } |
| } |
| |
| // If the value is an argument of the enclosing method or defined by a field-get, then clearly we |
| // have no information about its abstract value (yet). Instead of treating this as having an |
| // unknown runtime value, we instead record a flow constraint. |
| private InFlow computeInFlow(Value value, ProgramMethod context) { |
| Value valueRoot = value.getAliasedValue(aliasedValueConfiguration); |
| if (valueRoot.isArgument()) { |
| MethodParameter inParameter = |
| methodParameterFactory.create(context, valueRoot.getDefinition().asArgument().getIndex()); |
| return widenBaseInFlow(inParameter, context); |
| } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) { |
| FieldGet fieldGet = valueRoot.getDefinition().asFieldGet(); |
| ProgramField field = fieldGet.resolveField(appView, context).getProgramField(); |
| if (field == null) { |
| return null; |
| } |
| if (fieldGet.isInstanceGet()) { |
| Value receiverValue = fieldGet.asInstanceGet().object(); |
| BaseInFlow receiverInFlow = asBaseInFlowOrNull(computeInFlow(receiverValue, context)); |
| if (receiverInFlow != null |
| && receiverInFlow.equals(widenBaseInFlow(receiverInFlow, context))) { |
| return new InstanceFieldReadAbstractFunction(receiverInFlow, field.getReference()); |
| } |
| } |
| return widenBaseInFlow(fieldValueFactory.create(field), context); |
| } |
| return null; |
| } |
| |
| private InFlow widenBaseInFlow(BaseInFlow inFlow, ProgramMethod context) { |
| if (inFlow.isFieldValue()) { |
| if (isFieldValueAlreadyUnknown(inFlow.asFieldValue().getField())) { |
| return AbstractFunction.unknown(); |
| } |
| } else { |
| assert inFlow.isMethodParameter(); |
| if (isMethodParameterAlreadyUnknown(inFlow.asMethodParameter(), context)) { |
| return AbstractFunction.unknown(); |
| } |
| } |
| return inFlow; |
| } |
| |
| private NonEmptyValueState computeInFlowState( |
| DexType staticType, Value value, ProgramMethod context) { |
| InFlow inFlow = computeInFlow(value, context); |
| if (inFlow == null) { |
| return null; |
| } |
| if (inFlow.isUnknownAbstractFunction()) { |
| return ValueState.unknown(); |
| } |
| assert inFlow.isBaseInFlow() || inFlow.isInstanceFieldReadAbstractFunction(); |
| return ConcreteValueState.create(staticType, inFlow); |
| } |
| |
| // 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, ObjectState::empty); |
| 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, Supplier<ObjectState> objectStateSupplier) { |
| if (field.getAccessFlags().isFinal() && field.getAccessFlags().isStatic()) { |
| return appView |
| .abstractValueFactory() |
| .createSingleFieldValue(field.getReference(), objectStateSupplier.get()); |
| } |
| return AbstractValue.unknown(); |
| } |
| |
| private void scan( |
| InvokeMethod invoke, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| Timing timing) { |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| if (invokedMethod.getHolderType().isArrayType()) { |
| // Nothing to propagate; the targeted method is not a program method. |
| return; |
| } |
| |
| if (appView.options().testing.checkReceiverAlwaysNullInCallSiteOptimization |
| && invoke.isInvokeMethodWithReceiver() |
| && invoke.asInvokeMethodWithReceiver().getReceiver().isAlwaysNull(appView)) { |
| // Nothing to propagate; the invoke instruction always fails. |
| return; |
| } |
| |
| SingleResolutionResult<?> resolutionResult = |
| invoke.resolveMethod(appView, context).asSingleResolution(); |
| if (resolutionResult == null) { |
| // Nothing to propagate; the invoke instruction fails. |
| return; |
| } |
| |
| if (!resolutionResult.getResolvedHolder().isProgramClass()) { |
| // Nothing to propagate; this could dispatch to a program method, but we cannot optimize |
| // methods that override non-program methods. |
| return; |
| } |
| |
| ProgramMethod resolvedMethod = resolutionResult.getResolvedProgramMethod(); |
| if (resolvedMethod.getDefinition().isLibraryMethodOverride().isPossiblyTrue()) { |
| assert resolvedMethod.getDefinition().isLibraryMethodOverride().isTrue(); |
| // Nothing to propagate; we don't know anything about methods that can be called from outside |
| // the program. |
| return; |
| } |
| |
| if (invoke.arguments().size() != resolvedMethod.getDefinition().getNumberOfArguments() |
| || invoke.isInvokeStatic() != resolvedMethod.getAccessFlags().isStatic()) { |
| // Nothing to propagate; the invoke instruction fails. |
| return; |
| } |
| |
| if (invoke.isInvokeInterface()) { |
| if (!resolutionResult.getInitialResolutionHolder().isInterface()) { |
| // Nothing to propagate; the invoke instruction fails. |
| return; |
| } |
| } |
| |
| if (invoke.isInvokeSuper()) { |
| // Use the super target instead of the resolved method to ensure that we propagate the |
| // argument information to the targeted method. |
| DexClassAndMethod target = |
| resolutionResult.lookupInvokeSuperTarget(context.getHolder(), appView); |
| if (target == null) { |
| // Nothing to propagate; the invoke instruction fails. |
| return; |
| } |
| if (!target.isProgramMethod()) { |
| throw new Unreachable( |
| "Expected super target of a non-library override to be a program method (" |
| + "resolved program method: " |
| + resolvedMethod |
| + ", " |
| + "super non-program method: " |
| + target |
| + ")"); |
| } |
| resolvedMethod = target.asProgramMethod(); |
| } |
| |
| // Find the method where to store the information about the arguments from this invoke. |
| // If the invoke may dispatch to more than one method, we intentionally do not compute all |
| // possible dispatch targets and propagate the information to these methods (this is expensive). |
| // Instead we record the information in one place and then later propagate the information to |
| // all dispatch targets. |
| addTemporaryMethodState(invoke, resolvedMethod, abstractValueSupplier, context, timing); |
| } |
| |
| protected void addTemporaryMethodState( |
| InvokeMethod invoke, |
| ProgramMethod resolvedMethod, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| Timing timing) { |
| timing.begin("Add method state"); |
| methodStates.addTemporaryMethodState( |
| appView, |
| getRepresentative(invoke, resolvedMethod), |
| existingMethodState -> |
| computeMethodState( |
| invoke, |
| resolvedMethod, |
| abstractValueSupplier, |
| context, |
| existingMethodState, |
| timing), |
| timing); |
| timing.end(); |
| } |
| |
| private MethodState computeMethodState( |
| InvokeMethod invoke, |
| ProgramMethod resolvedMethod, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| MethodState existingMethodState, |
| Timing timing) { |
| assert !existingMethodState.isUnknown(); |
| |
| // If this invoke may target at most one method, then we compute a state that maps each |
| // parameter to the abstract value and dynamic type provided by this call site. Otherwise, we |
| // compute a polymorphic method state, which includes information about the receiver's dynamic |
| // type bounds. |
| timing.begin("Compute method state for invoke"); |
| MethodState result; |
| if (shouldUsePolymorphicMethodState(invoke, resolvedMethod)) { |
| assert existingMethodState.isBottom() || existingMethodState.isPolymorphic(); |
| result = |
| computePolymorphicMethodState( |
| invoke.asInvokeMethodWithReceiver(), |
| resolvedMethod, |
| abstractValueSupplier, |
| context, |
| existingMethodState.asPolymorphicOrBottom()); |
| } else { |
| assert existingMethodState.isBottom() || existingMethodState.isMonomorphic(); |
| result = |
| computeMonomorphicMethodState( |
| invoke, |
| resolvedMethod, |
| invoke.lookupSingleProgramTarget(appView, context), |
| abstractValueSupplier, |
| context, |
| existingMethodState.asMonomorphicOrBottom()); |
| } |
| timing.end(); |
| return result; |
| } |
| |
| // 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. |
| private MethodState computePolymorphicMethodState( |
| InvokeMethodWithReceiver invoke, |
| ProgramMethod resolvedMethod, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| ConcretePolymorphicMethodStateOrBottom existingMethodState) { |
| DynamicTypeWithUpperBound dynamicReceiverType = invoke.getReceiver().getDynamicType(appView); |
| // TODO(b/331587404): Investigate if we can replace the receiver by null before entering this |
| // pass, so that this special case is not needed. |
| if (dynamicReceiverType.isNullType()) { |
| assert appView.testing().allowNullDynamicTypeInCodeScanner : "b/250634405"; |
| // This can happen if we were unable to determine that the receiver is a phi value where null |
| // information has not been propagated down. Ideally this case would never happen as it should |
| // be possible to replace the receiver by the null constant in this case. |
| // |
| // Since the receiver is known to be null, no argument information should be propagated to the |
| // callees, so we return bottom here. |
| return MethodState.bottom(); |
| } |
| |
| ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context); |
| DynamicTypeWithUpperBound bounds = |
| computeBoundsForPolymorphicMethodState(resolvedMethod, singleTarget, dynamicReceiverType); |
| MethodState existingMethodStateForBounds = |
| existingMethodState.isPolymorphic() |
| ? existingMethodState.asPolymorphic().getMethodStateForBounds(bounds) |
| : MethodState.bottom(); |
| |
| if (existingMethodStateForBounds.isPolymorphic()) { |
| assert false; |
| return MethodState.unknown(); |
| } |
| |
| // If we already don't know anything about the parameters for the given type bounds, then don't |
| // compute a method state. |
| if (existingMethodStateForBounds.isUnknown()) { |
| return MethodState.bottom(); |
| } |
| |
| ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds = |
| computeMonomorphicMethodState( |
| invoke, |
| resolvedMethod, |
| singleTarget, |
| abstractValueSupplier, |
| context, |
| existingMethodStateForBounds.asMonomorphicOrBottom(), |
| dynamicReceiverType); |
| return ConcretePolymorphicMethodState.create(bounds, methodStateForBounds); |
| } |
| |
| private DynamicTypeWithUpperBound computeBoundsForPolymorphicMethodState( |
| ProgramMethod resolvedMethod, |
| ProgramMethod singleTarget, |
| DynamicTypeWithUpperBound dynamicReceiverType) { |
| DynamicTypeWithUpperBound bounds = |
| singleTarget != null |
| ? DynamicType.createExact( |
| singleTarget.getHolderType().toTypeElement(appView).asClassType()) |
| : dynamicReceiverType.withNullability(Nullability.maybeNull()); |
| |
| // We intentionally drop the nullability for the type bounds. This increases the number of |
| // collisions in the polymorphic method states, which does not change the precision (since the |
| // nullability does not have any impact on the possible dispatch targets) and is good for state |
| // pruning. |
| assert bounds.getDynamicUpperBoundType().nullability().isMaybeNull(); |
| |
| // If the bounds are trivial (i.e., the upper bound is equal to the holder of the virtual root |
| // method), then widen the type bounds to 'unknown'. |
| DexMethod virtualRootMethod = getVirtualRootMethod(resolvedMethod); |
| if (virtualRootMethod == null) { |
| assert false : "Unexpected virtual method without root: " + resolvedMethod; |
| return bounds; |
| } |
| |
| DynamicType trivialBounds = |
| DynamicType.create( |
| appView, virtualRootMethod.getHolderType().toTypeElement(appView).asClassType()); |
| if (bounds.equals(trivialBounds)) { |
| return DynamicType.unknown(); |
| } |
| return bounds; |
| } |
| |
| private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState( |
| InvokeMethod invoke, |
| ProgramMethod resolvedMethod, |
| ProgramMethod singleTarget, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| ConcreteMonomorphicMethodStateOrBottom existingMethodState) { |
| return computeMonomorphicMethodState( |
| invoke, |
| resolvedMethod, |
| singleTarget, |
| abstractValueSupplier, |
| context, |
| existingMethodState, |
| invoke.isInvokeMethodWithReceiver() |
| ? invoke.getFirstArgument().getDynamicType(appView) |
| : null); |
| } |
| |
| @SuppressWarnings("UnusedVariable") |
| private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState( |
| InvokeMethod invoke, |
| ProgramMethod resolvedMethod, |
| ProgramMethod singleTarget, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| ConcreteMonomorphicMethodStateOrBottom existingMethodState, |
| DynamicType dynamicReceiverType) { |
| List<ValueState> parameterStates = new ArrayList<>(invoke.arguments().size()); |
| |
| MethodReprocessingCriteria methodReprocessingCriteria = |
| singleTarget != null |
| ? reprocessingCriteriaCollection.getReprocessingCriteria(singleTarget) |
| : MethodReprocessingCriteria.alwaysReprocess(); |
| |
| int argumentIndex = 0; |
| if (invoke.isInvokeMethodWithReceiver()) { |
| assert dynamicReceiverType != null; |
| parameterStates.add( |
| computeParameterStateForReceiver( |
| resolvedMethod, |
| dynamicReceiverType, |
| existingMethodState, |
| methodReprocessingCriteria.getParameterReprocessingCriteria(0))); |
| argumentIndex++; |
| } |
| |
| for (; argumentIndex < invoke.arguments().size(); argumentIndex++) { |
| parameterStates.add( |
| computeParameterStateForNonReceiver( |
| invoke, |
| singleTarget, |
| argumentIndex, |
| invoke.getArgument(argumentIndex), |
| abstractValueSupplier, |
| context, |
| existingMethodState)); |
| } |
| |
| // We simulate that the return value is used for methods with void return type. This ensures |
| // that we will widen the method state to unknown if/when all parameter states become unknown. |
| boolean isReturnValueUsed = invoke.getReturnType().isVoidType() || invoke.hasUsedOutValue(); |
| return ConcreteMonomorphicMethodState.create(isReturnValueUsed, parameterStates); |
| } |
| |
| // For receivers there is not much point in trying to track an abstract value. Therefore we only |
| // track the dynamic type for receivers. |
| // TODO(b/190154391): Consider validating the above hypothesis by using |
| // computeParameterStateForNonReceiver() for receivers. |
| private ValueState computeParameterStateForReceiver( |
| ProgramMethod resolvedMethod, |
| DynamicType dynamicReceiverType, |
| ConcreteMonomorphicMethodStateOrBottom existingMethodState, |
| ParameterReprocessingCriteria parameterReprocessingCriteria) { |
| // Don't compute a state for this parameter if the stored state is already unknown. |
| if (existingMethodState.isMonomorphic() |
| && existingMethodState.asMonomorphic().getParameterState(0).isUnknown()) { |
| return ValueState.unknown(); |
| } |
| |
| // For receivers we only track the dynamic type. Therefore, if there is no need to track the |
| // dynamic type of the receiver of the targeted method, then just return unknown. |
| if (!parameterReprocessingCriteria.shouldReprocessDueToDynamicType()) { |
| return ValueState.unknown(); |
| } |
| |
| DynamicType widenedDynamicReceiverType = |
| WideningUtils.widenDynamicReceiverType(appView, resolvedMethod, dynamicReceiverType); |
| return widenedDynamicReceiverType.isUnknown() |
| ? ValueState.unknown() |
| : new ConcreteReceiverValueState(dynamicReceiverType); |
| } |
| |
| @SuppressWarnings("UnusedVariable") |
| private ValueState computeParameterStateForNonReceiver( |
| InvokeMethod invoke, |
| ProgramMethod singleTarget, |
| int argumentIndex, |
| Value argument, |
| AbstractValueSupplier abstractValueSupplier, |
| ProgramMethod context, |
| ConcreteMonomorphicMethodStateOrBottom existingMethodState) { |
| ValueState modeledState = |
| modeling.modelParameterStateForArgumentToFunction( |
| invoke, singleTarget, argumentIndex, argument, context); |
| if (modeledState != null) { |
| return modeledState; |
| } |
| |
| // Don't compute a state for this parameter if the stored state is already unknown. |
| if (existingMethodState.isMonomorphic() |
| && existingMethodState.asMonomorphic().getParameterState(argumentIndex).isUnknown()) { |
| return ValueState.unknown(); |
| } |
| |
| DexType parameterType = |
| invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic()); |
| |
| // 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 corresponding parameter of the methods |
| // potentially called from this invoke instruction. |
| NonEmptyValueState inFlowState = computeInFlowState(parameterType, argument, context); |
| if (inFlowState != null) { |
| return inFlowState; |
| } |
| |
| // Only track the nullability for array types. |
| if (parameterType.isArrayType()) { |
| Nullability nullability = argument.getType().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 (parameterType.isClassType()) { |
| DynamicType dynamicType = argument.getDynamicType(appView); |
| DynamicType widenedDynamicType = |
| WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType); |
| 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); |
| } |
| } |
| |
| @SuppressWarnings("ReferenceEquality") |
| private DexMethod getRepresentative(InvokeMethod invoke, ProgramMethod resolvedMethod) { |
| if (resolvedMethod.getDefinition().belongsToDirectPool()) { |
| return resolvedMethod.getReference(); |
| } |
| |
| if (isMonomorphicVirtualMethod(resolvedMethod)) { |
| return resolvedMethod.getReference(); |
| } |
| |
| if (invoke.isInvokeInterface()) { |
| assert !isMonomorphicVirtualMethod(resolvedMethod); |
| return getVirtualRootMethod(resolvedMethod); |
| } |
| |
| assert invoke.isInvokeSuper() || invoke.isInvokeVirtual(); |
| |
| DexMethod rootMethod = getVirtualRootMethod(resolvedMethod); |
| assert rootMethod != null; |
| assert !isMonomorphicVirtualMethod(resolvedMethod) |
| || rootMethod == resolvedMethod.getReference(); |
| return rootMethod; |
| } |
| |
| private boolean shouldUsePolymorphicMethodState( |
| InvokeMethod invoke, ProgramMethod resolvedMethod) { |
| return !resolvedMethod.getDefinition().belongsToDirectPool() |
| && !isMonomorphicVirtualMethod(getRepresentative(invoke, resolvedMethod)); |
| } |
| |
| private void scan(InvokeCustom invoke) { |
| // If the bootstrap method is program declared it will be called. The call is with runtime |
| // provided arguments so ensure that the argument information is unknown. |
| DexMethodHandle bootstrapMethod = invoke.getCallSite().bootstrapMethod; |
| SingleResolutionResult<?> resolution = |
| appView |
| .appInfo() |
| .resolveMethodLegacy(bootstrapMethod.asMethod(), bootstrapMethod.isInterface) |
| .asSingleResolution(); |
| if (resolution != null && resolution.getResolvedHolder().isProgramClass()) { |
| methodStates.set(resolution.getResolvedProgramMethod(), UnknownMethodState.get()); |
| } |
| } |
| } |