blob: 75eff47e9192aad0cf952923eefb1c32f3376a3e [file] [log] [blame]
// Copyright (c) 2017, the Dart 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.
/// Global type flow analysis.
import 'dart:collection';
import 'dart:core' hide Type;
import 'dart:math' show max;
import 'package:kernel/target/targets.dart' show Target;
import 'package:kernel/ast.dart' hide Statement, StatementVisitor;
import 'package:kernel/class_hierarchy.dart' show ClosedWorldClassHierarchy;
import 'package:kernel/library_index.dart' show LibraryIndex;
import 'package:kernel/core_types.dart' show CoreTypes;
import 'package:kernel/type_environment.dart';
import 'calls.dart';
import 'native_code.dart';
import 'protobuf_handler.dart' show ProtobufHandler;
import 'summary.dart';
import 'summary_collector.dart';
import 'types.dart';
import 'utils.dart';
import '../pragma.dart';
// TODO(alexmarkov)
// Unordered list of various improvements in type flow analysis,
// organized in several categories:
//
// === Correctness ===
// * Verify incremental re-calculation by fresh analysis starting with known
// allocated classes.
//
// === Precision ===
// * Handle '==' with null.
// * Special type inference rules for binary int operators.
// * Support function types, better handle closures.
// * Support generic types: substitution, passing type arguments. Figure out
// when generic type should be approximated.
//
// === Efficiency of the analysis ===
// * Add benchmark to measure analysis time continuously.
// * Figure out better strategy of processing an invocation if its result is
// not used. Consider creating summaries eagerly (to discover allocated
// classes early) but analyzing them lazily.
//
/// Maintains set of dependent invocations.
class _DependencyTracker {
Set<_Invocation>? _dependentInvocations;
void addDependentInvocation(_Invocation invocation) {
if (!identical(invocation, this)) {
var dependentInvocations = _dependentInvocations;
if (dependentInvocations == null) {
_dependentInvocations = dependentInvocations = Set<_Invocation>();
}
dependentInvocations.add(invocation);
}
}
void invalidateDependentInvocations(_WorkList workList) {
final dependentInvocations = _dependentInvocations;
if (dependentInvocations != null) {
if (kPrintTrace) {
tracePrint(' - CHANGED: $this');
for (var di in dependentInvocations) {
tracePrint(' - invalidating $di');
}
}
dependentInvocations.forEach(workList.invalidateInvocation);
}
}
}
/// _Invocation class represents the in-flight invocation detached from a
/// particular call site, e.g. it is a selector and arguments.
/// This is the basic unit of processing in type flow analysis.
/// Call sites calling the same method with the same argument types
/// may reuse results of the analysis through the same _Invocation instance.
abstract class _Invocation extends _DependencyTracker
with LinkedListEntry<_Invocation> {
final Selector selector;
final Args<Type> args;
Type? result;
/// Result of the invocation calculated before invocation was invalidated.
/// Used to check if the re-analysis of the invocation yields the same
/// result or not (to avoid invalidation of callers if result hasn't changed).
Type? invalidatedResult;
/// Number of times result of this invocation was invalidated.
int invalidationCounter = 0;
/// Whether a call-site directed to this invocation can call through the
/// unchecked entry-point.
bool typeChecksNeeded = false;
/// If an invocation is invalidated more than [invalidationLimit] times,
/// its result is saturated in order to guarantee convergence.
static const int invalidationLimit = 1000;
_Invocation(this.selector, this.args);
Type process(TypeFlowAnalysis typeFlowAnalysis);
/// Returns result of this invocation if its available without
/// further analysis, or `null` if it's not available.
/// Used for recursive calls while this invocation is being processed.
Type? get resultForRecursiveInvocation => result;
/// Use [type] as a current computed result of this invocation.
/// If this invocation was invalidated, and the invalidated result is
/// different, then invalidate all dependent invocations as well.
/// Result type may be saturated if this invocation was invalidated
/// too many times.
void setResult(TypeFlowAnalysis typeFlowAnalysis, Type type) {
result = type;
if (invalidatedResult != null) {
if (invalidatedResult != result) {
invalidateDependentInvocations(typeFlowAnalysis.workList);
invalidationCounter++;
Statistics.maxInvalidationsPerInvocation =
max(Statistics.maxInvalidationsPerInvocation, invalidationCounter);
// In rare cases, loops in dependencies and approximation of
// recursive invocations may cause infinite bouncing of result
// types. To prevent infinite looping and guarantee convergence of
// the analysis, result is saturated after invocation is invalidated
// at least [_Invocation.invalidationLimit] times.
if (invalidationCounter > _Invocation.invalidationLimit) {
result = result!
.union(invalidatedResult!, typeFlowAnalysis.hierarchyCache);
}
}
invalidatedResult = null;
}
}
// Only take selector and args into account as _Invocation objects
// are cached in _InvocationsCache using selector and args as a key.
@override
bool operator ==(other) =>
identical(this, other) ||
(other is _Invocation) &&
(this.selector == other.selector) &&
(this.args == other.args);
@override
int get hashCode => (selector.hashCode ^ args.hashCode + 31) & kHashMask;
@override
String toString() => "_Invocation $selector $args";
/// Processes noSuchMethod() invocation and returns its result.
/// Used if target is not found or number of arguments is incorrect.
Type _processNoSuchMethod(Type receiver, TypeFlowAnalysis typeFlowAnalysis) {
tracePrint("Processing noSuchMethod for receiver $receiver");
final nsmSelector = new InterfaceSelector(
typeFlowAnalysis.hierarchyCache.objectNoSuchMethod,
callKind: CallKind.Method);
final nsmArgs = new Args<Type>([
receiver,
typeFlowAnalysis.hierarchyCache.fromStaticType(
typeFlowAnalysis.environment.coreTypes.invocationLegacyRawType, false)
]);
final nsmInvocation =
typeFlowAnalysis._invocationsCache.getInvocation(nsmSelector, nsmArgs);
final Type type =
typeFlowAnalysis.workList.processInvocation(nsmInvocation);
// Result of this invocation depends on the result of noSuchMethod
// invocation.
nsmInvocation.addDependentInvocation(this);
return type;
}
}
class _DirectInvocation extends _Invocation {
_DirectInvocation(DirectSelector selector, Args<Type> args)
: super(selector, args) {
// We don't emit [TypeCheck] statements for bounds checks of type
// parameters, so if there are any type parameters, we must assume
// they could fail bounds checks.
//
// TODO(sjindel): Use [TypeCheck] to avoid bounds checks.
final function = selector.member.function;
if (function != null) {
typeChecksNeeded =
function.typeParameters.any((t) => t.isGenericCovariantImpl);
} else {
Field field = selector.member as Field;
if (selector.callKind == CallKind.PropertySet) {
// TODO(dartbug.com/40615): Use TFA results to improve this criterion.
typeChecksNeeded = field.isGenericCovariantImpl;
}
}
}
@override
Type process(TypeFlowAnalysis typeFlowAnalysis) {
assert(typeFlowAnalysis.currentInvocation == this);
if (selector.member is Field) {
return _processField(typeFlowAnalysis);
} else {
return _processFunction(typeFlowAnalysis);
}
}
Type _processField(TypeFlowAnalysis typeFlowAnalysis) {
final Field field = selector.member as Field;
final int firstParamIndex = field.isStatic ? 0 : 1;
final _FieldValue fieldValue = typeFlowAnalysis.getFieldValue(field);
switch (selector.callKind) {
case CallKind.PropertyGet:
assert(args.values.length == firstParamIndex);
assert(args.names.isEmpty);
fieldValue.isGetterUsed = true;
return fieldValue.getValue(
typeFlowAnalysis, field.isStatic ? null : args.values[0]);
case CallKind.PropertySet:
case CallKind.SetFieldInConstructor:
assert(args.values.length == firstParamIndex + 1);
assert(args.names.isEmpty);
if (selector.callKind == CallKind.PropertySet) {
fieldValue.isSetterUsed = true;
}
final Type setterArg = args.values[firstParamIndex];
fieldValue.setValue(
setterArg, typeFlowAnalysis, field.isStatic ? null : args.receiver);
return const EmptyType();
case CallKind.Method:
// Call via field.
// TODO(alexmarkov): support function types and use inferred type
// to get more precise return type.
fieldValue.isGetterUsed = true;
final receiver = fieldValue.getValue(
typeFlowAnalysis, field.isStatic ? null : args.values[0]);
if (receiver != const EmptyType()) {
typeFlowAnalysis.applyCall(/* callSite = */ null,
DynamicSelector.kCall, new Args.withReceiver(args, receiver),
isResultUsed: false, processImmediately: false);
}
return new Type.nullableAny();
case CallKind.FieldInitializer:
assert(args.values.length == firstParamIndex);
assert(args.names.isEmpty);
Type initializerResult = typeFlowAnalysis
.getSummary(field)
.apply(args, typeFlowAnalysis.hierarchyCache, typeFlowAnalysis);
if (field.isStatic &&
!field.isConst &&
initializerResult is! NullableType) {
// If initializer of a static field throws an exception,
// then field is initialized with null value.
// TODO(alexmarkov): Try to prove that static field initializer
// does not throw exception.
initializerResult = new Type.nullable(initializerResult);
}
if (kPrintTrace) {
tracePrint("Result of ${field} initializer: $initializerResult");
}
fieldValue.setValue(initializerResult, typeFlowAnalysis,
field.isStatic ? null : args.receiver);
fieldValue.isInitialized = true;
return const EmptyType();
}
}
Type _processFunction(TypeFlowAnalysis typeFlowAnalysis) {
final Member member = selector.member!;
if (selector.memberAgreesToCallKind(member)) {
if (_argumentsValid()) {
final summary = typeFlowAnalysis.getSummary(member);
// If result type is known upfront (doesn't depend on the flow),
// set it eagerly so recursive invocations are able to use it.
final summaryResult = summary.result;
if (summaryResult is Type &&
!typeFlowAnalysis.workList._isPending(this)) {
assert(result == null || result == summaryResult);
setResult(typeFlowAnalysis, summaryResult);
}
return summary.apply(
args, typeFlowAnalysis.hierarchyCache, typeFlowAnalysis);
} else {
assert(selector.callKind == CallKind.Method);
return _processNoSuchMethod(args.receiver, typeFlowAnalysis);
}
} else {
if (selector.callKind == CallKind.PropertyGet) {
// Tear-off.
// TODO(alexmarkov): capture receiver type
assert((member is Procedure) && !member.isGetter && !member.isSetter);
typeFlowAnalysis.addRawCall(new DirectSelector(member));
typeFlowAnalysis._tearOffTaken.add(member);
return new Type.nullableAny();
} else {
// Call via getter.
// TODO(alexmarkov): capture receiver type
assert((selector.callKind == CallKind.Method) &&
(member is Procedure) &&
member.isGetter);
typeFlowAnalysis.addRawCall(
new DirectSelector(member, callKind: CallKind.PropertyGet));
typeFlowAnalysis.applyCall(/* callSite = */ null, DynamicSelector.kCall,
new Args.withReceiver(args, new Type.nullableAny()),
isResultUsed: false, processImmediately: false);
return new Type.nullableAny();
}
}
}
bool _argumentsValid() {
final member = selector.member!;
final function = member.function!;
final int positionalArguments = args.positionalCount;
final int firstParamIndex =
numTypeParams(member) + (hasReceiverArg(member) ? 1 : 0);
final int requiredParameters =
firstParamIndex + function.requiredParameterCount;
if (positionalArguments < requiredParameters) {
return false;
}
final int positionalParameters =
firstParamIndex + function.positionalParameters.length;
if (positionalArguments > positionalParameters) {
return false;
}
if (args.names.isNotEmpty) {
// TODO(dartbug.com/32292): make sure parameters are sorted in kernel AST
// and iterate parameters in parallel, without lookup.
for (var name in args.names) {
if (findNamedParameter(function, name) == null) {
return false;
}
}
}
return true;
}
}
class _DispatchableInvocation extends _Invocation {
bool _isPolymorphic = false;
Set<Call>? _callSites; // Populated only if not polymorphic.
Member? _monomorphicTarget;
_DirectInvocation? _monomorphicDirectInvocation;
@override
set typeChecksNeeded(bool value) {
if (typeChecksNeeded) return;
if (value) {
super.typeChecksNeeded = true;
_notifyCallSites();
}
}
/// Marker for noSuchMethod() invocation in the map of invocation targets.
static final Member kNoSuchMethodMarker = new Procedure(
new Name('noSuchMethod&&'), ProcedureKind.Method, new FunctionNode(null),
fileUri: dummyUri);
_DispatchableInvocation(Selector selector, Args<Type> args)
: super(selector, args) {
assert(selector is! DirectSelector);
}
@override
Type process(TypeFlowAnalysis typeFlowAnalysis) {
assert(typeFlowAnalysis.currentInvocation == this);
// Collect all possible targets for this invocation,
// along with more accurate receiver types for each target.
final targets = <Member, _ReceiverTypeBuilder>{};
_collectTargetsForReceiverType(args.receiver, targets, typeFlowAnalysis);
// Calculate result as a union of results of direct invocations
// corresponding to each target.
Type result = const EmptyType();
if (targets.isEmpty) {
tracePrint("No targets...");
} else {
if (targets.length == 1) {
final target = targets.keys.single;
if (target != kNoSuchMethodMarker) {
_setMonomorphicTarget(target);
} else {
_setPolymorphic();
}
} else {
_setPolymorphic();
}
targets
.forEach((Member target, _ReceiverTypeBuilder receiverTypeBuilder) {
Type receiver = receiverTypeBuilder.toType();
Type type;
if (target == kNoSuchMethodMarker) {
// Non-dynamic call-sites must hit NSM-forwarders in Dart 2.
assert(selector is DynamicSelector);
type = _processNoSuchMethod(receiver, typeFlowAnalysis);
} else {
final directSelector =
new DirectSelector(target, callKind: selector.callKind);
Args<Type> directArgs = args;
if (args.receiver != receiver) {
directArgs = new Args<Type>.withReceiver(args, receiver);
}
final directInvocation = typeFlowAnalysis._invocationsCache
.getInvocation(directSelector, directArgs);
if (!_isPolymorphic) {
assert(target == _monomorphicTarget);
_monomorphicDirectInvocation =
directInvocation as _DirectInvocation;
}
type = typeFlowAnalysis.workList.processInvocation(directInvocation);
if (kPrintTrace) {
tracePrint('Dispatch: $directInvocation, result: $type');
}
// Result of this invocation depends on the results of direct
// invocations corresponding to each target.
directInvocation.addDependentInvocation(this);
if (selector.callKind != CallKind.PropertyGet) {
if (selector is DynamicSelector) {
typeFlowAnalysis._methodsAndSettersCalledDynamically.add(target);
} else if (selector is VirtualSelector) {
typeFlowAnalysis._calledViaThis.add(target);
} else {
typeFlowAnalysis._calledViaInterfaceSelector.add(target);
}
} else {
if (selector is DynamicSelector) {
typeFlowAnalysis._gettersCalledDynamically.add(target);
}
}
if (directInvocation.typeChecksNeeded) {
typeChecksNeeded = true;
}
}
result = result.union(type, typeFlowAnalysis.hierarchyCache);
});
}
// TODO(alexmarkov): handle closures more precisely
if ((selector is DynamicSelector) && (selector.name.text == "call")) {
tracePrint("Possible closure call, result is dynamic");
result = new Type.nullableAny();
}
return result;
}
void _collectTargetsForReceiverType(
Type receiver,
Map<Member, _ReceiverTypeBuilder> targets,
TypeFlowAnalysis typeFlowAnalysis) {
assert(receiver != const EmptyType()); // should be filtered earlier
final bool isNullableReceiver = receiver is NullableType;
if (isNullableReceiver) {
receiver = receiver.baseType;
assert(receiver is! NullableType);
}
if (selector is InterfaceSelector) {
final staticReceiverType = new ConeType(typeFlowAnalysis.hierarchyCache
.getTFClass(selector.member!.enclosingClass!));
receiver = receiver.intersection(
staticReceiverType, typeFlowAnalysis.hierarchyCache);
assert(receiver is! NullableType);
if (kPrintTrace) {
tracePrint("Narrowed down receiver type: $receiver");
}
}
if (receiver is ConeType) {
// Specialization of type cone will add dependency of the current
// invocation to the receiver class. A new allocated class discovered
// in the receiver cone will invalidate this invocation.
receiver = typeFlowAnalysis.hierarchyCache
.specializeTypeCone(receiver.cls, allowWideCone: false);
}
assert(targets.isEmpty);
if (receiver is ConcreteType) {
_collectTargetsForConcreteType(receiver, targets, typeFlowAnalysis);
} else if (receiver is SetType) {
for (var type in receiver.types) {
_collectTargetsForConcreteType(type, targets, typeFlowAnalysis);
}
} else if (receiver is AnyType) {
_collectTargetsForSelector(targets, typeFlowAnalysis);
} else {
assert(receiver is EmptyType);
}
if (isNullableReceiver) {
_collectTargetsForNull(targets, typeFlowAnalysis);
}
}
// TODO(alexmarkov): Consider caching targets for Null type.
void _collectTargetsForNull(Map<Member, _ReceiverTypeBuilder> targets,
TypeFlowAnalysis typeFlowAnalysis) {
Class nullClass =
typeFlowAnalysis.environment.coreTypes.deprecatedNullClass;
Member? target = typeFlowAnalysis.hierarchyCache.hierarchy
.getDispatchTarget(nullClass, selector.name, setter: selector.isSetter);
if (target != null) {
if (kPrintTrace) {
tracePrint("Found $target for null receiver");
}
_getReceiverTypeBuilder(targets, target).addNull();
}
}
void _collectTargetsForConcreteType(
ConcreteType receiver,
Map<Member, _ReceiverTypeBuilder> targets,
TypeFlowAnalysis typeFlowAnalysis) {
final TFClass cls = receiver.cls;
Member? target =
(cls as _TFClassImpl).getDispatchTarget(selector, typeFlowAnalysis);
if (target != null) {
if (kPrintTrace) {
tracePrint("Found $target for concrete receiver $receiver");
}
_getReceiverTypeBuilder(targets, target).addConcreteType(receiver);
} else {
if (typeFlowAnalysis.hierarchyCache.hasNonTrivialNoSuchMethod(cls)) {
if (kPrintTrace) {
tracePrint("Found non-trivial noSuchMethod for receiver $receiver");
}
_getReceiverTypeBuilder(targets, kNoSuchMethodMarker)
.addConcreteType(receiver);
} else if (selector is DynamicSelector) {
if (kPrintTrace) {
tracePrint(
"Dynamic selector - adding noSuchMethod for receiver $receiver");
}
_getReceiverTypeBuilder(targets, kNoSuchMethodMarker)
.addConcreteType(receiver);
} else {
if (kPrintTrace) {
tracePrint("Target is not found for receiver $receiver");
}
}
}
}
void _collectTargetsForSelector(Map<Member, _ReceiverTypeBuilder> targets,
TypeFlowAnalysis typeFlowAnalysis) {
Selector selector = this.selector;
if (selector is InterfaceSelector) {
// TODO(alexmarkov): support generic types and make sure inferred types
// are always same or better than static types.
// assert(selector.member.enclosingClass ==
// _typeFlowAnalysis.environment.coreTypes.objectClass);
selector = new DynamicSelector(selector.callKind, selector.name);
}
final receiver = args.receiver;
final _DynamicTargetSet dynamicTargetSet = typeFlowAnalysis.hierarchyCache
.getDynamicTargetSet(selector as DynamicSelector);
dynamicTargetSet.addDependentInvocation(this);
assert(targets.isEmpty);
for (Member target in dynamicTargetSet.targets) {
_getReceiverTypeBuilder(targets, target).addType(receiver);
}
// Conservatively include noSuchMethod if selector is not from Object,
// as class might miss the implementation.
if (!dynamicTargetSet.isObjectMember) {
_getReceiverTypeBuilder(targets, kNoSuchMethodMarker).addType(receiver);
}
}
_ReceiverTypeBuilder _getReceiverTypeBuilder(
Map<Member, _ReceiverTypeBuilder> targets, Member member) =>
targets[member] ??= new _ReceiverTypeBuilder();
void _setPolymorphic() {
if (!_isPolymorphic) {
_isPolymorphic = true;
_monomorphicTarget = null;
typeChecksNeeded = true;
_notifyCallSites();
_callSites = null;
}
}
void _setMonomorphicTarget(Member target) {
assert(!_isPolymorphic);
assert((_monomorphicTarget == null) || (_monomorphicTarget == target));
_monomorphicTarget = target;
_notifyCallSites();
}
void addCallSite(Call callSite) {
_notifyCallSite(callSite);
if (!callSite.isPolymorphic) {
(_callSites ??= new Set<Call>()).add(callSite);
}
}
/// Notify call site about changes in polymorphism or checkedness of this
/// invocation.
void _notifyCallSite(Call callSite) {
if (_isPolymorphic) {
callSite.setPolymorphic();
} else {
final monomorphicTarget = _monomorphicTarget;
if (monomorphicTarget != null) {
callSite.addTarget(monomorphicTarget);
}
}
if (typeChecksNeeded) {
callSite.setUseCheckedEntry();
}
}
/// Notify call sites monitoring this invocation about changes in
/// polymorphism of this invocation.
void _notifyCallSites() {
final callSites = _callSites;
if (callSites != null) {
callSites.forEach(_notifyCallSite);
}
}
@override
Type? get resultForRecursiveInvocation {
if (result != null) {
return result;
}
final monomorphicDirectInvocation = _monomorphicDirectInvocation;
if (monomorphicDirectInvocation != null) {
return monomorphicDirectInvocation.resultForRecursiveInvocation;
}
return null;
}
}
/// Efficient builder of receiver type.
///
/// Supports the following operations:
/// 1) Add 1..N concrete types ordered by classId OR add 1 arbitrary type.
/// 2) Make type nullable.
class _ReceiverTypeBuilder {
Type? _type;
List<ConcreteType>? _list;
bool _nullable = false;
/// Appends a ConcreteType. May be called multiple times.
/// Should not be used in conjunction with [addType].
void addConcreteType(ConcreteType type) {
final list = _list;
if (list == null) {
final Type? t = _type;
if (t == null) {
_type = type;
return;
}
final ct = t as ConcreteType;
assert(ct != type);
assert(ct.cls.id < type.cls.id);
_list = <ConcreteType>[ct, type];
_type = null;
} else {
assert(list.last.cls.id < type.cls.id);
list.add(type);
}
}
/// Appends an arbitrary Type. May be called only once.
/// Should not be used in conjunction with [addConcreteType].
void addType(Type type) {
assert(_type == null && _list == null);
_type = type;
}
/// Makes the resulting type nullable.
void addNull() {
_nullable = true;
}
/// Returns union of added types.
Type toType() {
Type? t = _type;
if (t == null) {
final list = _list;
if (list == null) {
t = const EmptyType();
} else {
t = SetType(list);
}
} else {
assert(_list == null);
}
if (_nullable) {
if (t is! NullableType) {
t = new NullableType(t);
}
}
return t;
}
}
/// Keeps track of number of cached [_Invocation] objects with
/// a particular selector and provides approximation if needed.
class _SelectorApproximation {
/// Approximation [_Invocation] with raw arguments is created and used
/// after number of [_Invocation] objects with same selector but
/// different arguments reaches this limit.
static const int maxInvocationsPerSelector = 5000;
int count = 0;
_Invocation? approximation;
}
/// Maintains ([Selector], [Args]) => [_Invocation] cache.
/// Invocations are cached in order to reuse previously calculated result.
class _InvocationsCache {
final TypeFlowAnalysis _typeFlowAnalysis;
final Set<_Invocation> _invocations = new Set<_Invocation>();
final Map<Selector, _SelectorApproximation> _approximations =
<Selector, _SelectorApproximation>{};
_InvocationsCache(this._typeFlowAnalysis);
_Invocation getInvocation(Selector selector, Args<Type> args) {
++Statistics.invocationsQueriedInCache;
_Invocation invocation = (selector is DirectSelector)
? new _DirectInvocation(selector, args)
: new _DispatchableInvocation(selector, args);
_Invocation? result = _invocations.lookup(invocation);
if (result != null) {
return result;
}
if (selector is InterfaceSelector) {
// Detect if there are too many invocations per selector. In such case,
// approximate extra invocations with a single invocation with raw
// arguments.
final sa = (_approximations[selector] ??= new _SelectorApproximation());
if (sa.count >= _SelectorApproximation.maxInvocationsPerSelector) {
_Invocation? approximation = sa.approximation;
if (approximation == null) {
final rawArgs =
_typeFlowAnalysis.summaryCollector.rawArguments(selector);
sa.approximation =
approximation = _DispatchableInvocation(selector, rawArgs);
Statistics.approximateInvocationsCreated++;
}
Statistics.approximateInvocationsUsed++;
return approximation;
}
++sa.count;
Statistics.maxInvocationsCachedPerSelector =
max(Statistics.maxInvocationsCachedPerSelector, sa.count);
}
bool added = _invocations.add(invocation);
assert(added);
++Statistics.invocationsAddedToCache;
return invocation;
}
}
class _FieldValue extends _DependencyTracker {
final Field field;
final Type staticType;
final Summary? typeGuardSummary;
Type value = const EmptyType();
/// Flag indicating if field initializer was executed.
bool isInitialized = false;
/// Flag indicating if field getter was executed.
bool isGetterUsed = false;
/// Flag indicating if field setter was executed.
bool isSetterUsed = false;
_FieldValue(this.field, this.typeGuardSummary, TypesBuilder typesBuilder)
: staticType = typesBuilder.fromStaticType(field.type, true) {
if (field.initializer == null && _isDefaultValueOfFieldObservable()) {
value = new Type.nullable(const EmptyType());
}
}
bool _isDefaultValueOfFieldObservable() {
if (field.isLate) {
return false;
}
if (field.isStatic) {
return true;
}
final enclosingClass = field.enclosingClass!;
// Default value is not observable if every generative constructor
// is redirecting or initializes the field.
return !enclosingClass.constructors.every((Constructor constr) {
for (var initializer in constr.initializers) {
if ((initializer is RedirectingInitializer) ||
((initializer is FieldInitializer) &&
(initializer.field == field))) {
return true;
}
}
return false;
});
}
void ensureInitialized(
TypeFlowAnalysis typeFlowAnalysis, Type? receiverType) {
if (field.initializer != null) {
assert(field.isStatic == (receiverType == null));
final args = !field.isStatic ? <Type>[receiverType!] : const <Type>[];
final initializerInvocation = typeFlowAnalysis._invocationsCache
.getInvocation(
new DirectSelector(field, callKind: CallKind.FieldInitializer),
new Args<Type>(args));
// It may update the field value.
typeFlowAnalysis.workList.processInvocation(initializerInvocation);
}
}
Type getValue(TypeFlowAnalysis typeFlowAnalysis, Type? receiverType) {
ensureInitialized(typeFlowAnalysis, receiverType);
addDependentInvocation(typeFlowAnalysis.currentInvocation);
final typeGuardSummary = this.typeGuardSummary;
return (typeGuardSummary != null)
? typeGuardSummary.apply(Args([receiverType!, value]),
typeFlowAnalysis.hierarchyCache, typeFlowAnalysis)
: value;
}
void setValue(
Type newValue, TypeFlowAnalysis typeFlowAnalysis, Type? receiverType) {
// Make sure type cones are specialized before putting them into field
// value, in order to ensure that dependency is established between
// cone's base type and corresponding field setter.
//
// This ensures correct invalidation in the following scenario:
//
// 1) setValue(Cone(X)).
// It sets field value to Cone(X).
//
// 2) setValue(Y).
// It calculates Cone(X) U Y, specializing Cone(X).
// This establishes class X --> setter(Y) dependency.
// If X does not have allocated subclasses, then Cone(X) is specialized
// to Empty and the new field value is Y.
//
// 3) A new allocated subtype is added to X.
// This invalidates setter(Y). However, recalculation of setter(Y)
// does not yield correct field value, as value calculated on step 1 is
// already lost, and repeating setValue(Y) will not change field value.
//
// The eager specialization of field value ensures that specialization
// will happen on step 1 and dependency class X --> setter(Cone(X))
// is established.
//
final hierarchy = typeFlowAnalysis.hierarchyCache;
// TODO(sjindel/tfa): Perform narrowing inside 'TypeCheck'.
final typeGuardSummary = this.typeGuardSummary;
final narrowedNewValue = typeGuardSummary != null
? typeGuardSummary
.apply(new Args([receiverType!, newValue]), hierarchy,
typeFlowAnalysis)
.intersection(staticType, hierarchy)
: newValue.specialize(hierarchy).intersection(staticType, hierarchy);
Type newType =
value.union(narrowedNewValue, hierarchy).specialize(hierarchy);
assert(newType.isSpecialized);
if (newType != value) {
if (kPrintTrace) {
tracePrint("Set field $field value $newType");
}
invalidateDependentInvocations(typeFlowAnalysis.workList);
value = newType;
}
}
@override
String toString() => "_FieldValue $field => $value";
}
class _DynamicTargetSet extends _DependencyTracker {
final DynamicSelector selector;
final Set<Member> targets = new Set<Member>();
final bool isObjectMember;
_DynamicTargetSet(this.selector, this.isObjectMember);
}
class _TFClassImpl extends TFClass {
/// Maximum number of concrete types to use when calculating
/// subtype cone specialization. If number of allocated types
/// exceeds this constant, then WideConeType approximation is used.
static const int maxAllocatedTypesInSetSpecializations = 128;
final Set<_TFClassImpl> supertypes; // List of super-types including this.
final Set<_TFClassImpl> _allocatedSubtypes = new Set<_TFClassImpl>();
final Map<Selector, Member> _dispatchTargets = <Selector, Member>{};
final _DependencyTracker dependencyTracker = new _DependencyTracker();
/// Flag indicating if this class has a noSuchMethod() method not inherited
/// from Object.
/// Lazy initialized by ClassHierarchyCache.hasNonTrivialNoSuchMethod().
bool? hasNonTrivialNoSuchMethod;
_TFClassImpl(int id, Class classNode, this.supertypes)
: super(id, classNode) {
supertypes.add(this);
}
late final ConcreteType concreteType = ConcreteType(this, null);
Type? _specializedConeType;
Type get specializedConeType =>
_specializedConeType ??= _calculateConeTypeSpecialization();
bool get hasWideCone =>
_allocatedSubtypes.length > maxAllocatedTypesInSetSpecializations;
late final WideConeType _wideConeType = WideConeType(this);
WideConeType get wideConeType {
assert(hasWideCone);
return _wideConeType;
}
Type _calculateConeTypeSpecialization() {
final int numSubTypes = _allocatedSubtypes.length;
if (numSubTypes == 0) {
return const EmptyType();
} else if (numSubTypes == 1) {
return _allocatedSubtypes.single.concreteType;
} else {
List<ConcreteType> types = <ConcreteType>[];
for (var sub in _allocatedSubtypes) {
types.add(sub.concreteType);
}
// SetType constructor expects a list of ConcreteTypes sorted by classId
// (for faster intersections and unions).
types.sort();
return new SetType(types);
}
}
void addAllocatedSubtype(_TFClassImpl subType) {
_allocatedSubtypes.add(subType);
_specializedConeType = null; // Reset cached specialization.
}
Member? getDispatchTarget(
Selector selector, TypeFlowAnalysis typeFlowAnalysis) {
Member? target = _dispatchTargets[selector];
if (target == null) {
target = typeFlowAnalysis.hierarchyCache.hierarchy.getDispatchTarget(
classNode, selector.name,
setter: selector.isSetter);
if (target != null) {
_dispatchTargets[selector] = target;
}
}
return target;
}
String dump() => "$this {supers: $supertypes}";
}
class GenericInterfacesInfoImpl implements GenericInterfacesInfo {
final ClosedWorldClassHierarchy hierarchy;
final supertypeOffsetsCache = <SubtypePair, int>{};
final cachedFlattenedTypeArgs = <Class, List<DartType>>{};
final cachedFlattenedTypeArgsForNonGeneric = <Class, List<Type>>{};
late final RuntimeTypeTranslatorImpl closedTypeTranslator;
GenericInterfacesInfoImpl(CoreTypes coreTypes, this.hierarchy) {
closedTypeTranslator =
RuntimeTypeTranslatorImpl.forClosedTypes(coreTypes, this);
}
List<DartType> flattenedTypeArgumentsFor(Class klass, {bool useCache: true}) {
final cached = useCache ? cachedFlattenedTypeArgs[klass] : null;
if (cached != null) return cached;
final flattenedTypeArguments = List<DartType>.from(klass.typeParameters.map(
(t) => new TypeParameterType(
t, TypeParameterType.computeNullabilityFromBound(t))));
for (final Supertype intf in hierarchy.genericSupertypesOf(klass)) {
int offset = findOverlap(flattenedTypeArguments, intf.typeArguments);
flattenedTypeArguments.addAll(
intf.typeArguments.skip(flattenedTypeArguments.length - offset));
supertypeOffsetsCache[SubtypePair(klass, intf.classNode)] = offset;
}
return flattenedTypeArguments;
}
int genericInterfaceOffsetFor(Class klass, Class iface) {
if (klass == iface) return 0;
final pair = new SubtypePair(klass, iface);
int? offset = supertypeOffsetsCache[pair];
if (offset != null) return offset;
flattenedTypeArgumentsFor(klass);
offset = supertypeOffsetsCache[pair];
if (offset == null) {
throw "Invalid call to genericInterfaceOffsetFor.";
}
return offset;
}
List<Type> flattenedTypeArgumentsForNonGeneric(Class klass) {
List<Type>? result = cachedFlattenedTypeArgsForNonGeneric[klass];
if (result != null) return result;
List<DartType> flattenedTypeArgs =
flattenedTypeArgumentsFor(klass, useCache: false);
result = <Type>[];
for (DartType arg in flattenedTypeArgs) {
final translated = closedTypeTranslator.translate(arg);
assert(translated is RuntimeType || translated is UnknownType);
result.add(translated as Type);
}
cachedFlattenedTypeArgsForNonGeneric[klass] = result;
return result;
}
}
// TODO(alexmarkov): Rename to _TypeHierarchyImpl.
class _ClassHierarchyCache extends TypeHierarchy {
final TypeFlowAnalysis _typeFlowAnalysis;
final ClosedWorldClassHierarchy hierarchy;
final TypeEnvironment environment;
final Set<Class> allocatedClasses = new Set<Class>();
final Map<Class, _TFClassImpl> classes = <Class, _TFClassImpl>{};
final GenericInterfacesInfo genericInterfacesInfo;
/// Object.noSuchMethod().
final Member objectNoSuchMethod;
static final Name noSuchMethodName = new Name("noSuchMethod");
/// Class hierarchy is sealed after analysis is finished.
/// Once it is sealed, no new allocated classes may be added and no new
/// targets of invocations may appear.
/// It also means that there is no need to add dependencies on classes.
bool _sealed = false;
int _classIdCounter = 0;
final Map<DynamicSelector, _DynamicTargetSet> _dynamicTargets =
<DynamicSelector, _DynamicTargetSet>{};
_ClassHierarchyCache(this._typeFlowAnalysis, this.hierarchy,
this.genericInterfacesInfo, this.environment, bool nullSafety)
: objectNoSuchMethod = hierarchy.getDispatchTarget(
environment.coreTypes.objectClass, noSuchMethodName)!,
super(environment.coreTypes, nullSafety);
@override
_TFClassImpl getTFClass(Class c) {
return classes[c] ??= _createTFClass(c);
}
_TFClassImpl _createTFClass(Class c) {
final supertypes = new Set<_TFClassImpl>();
for (var sup in c.supers) {
supertypes.addAll(getTFClass(sup.classNode).supertypes);
}
return new _TFClassImpl(++_classIdCounter, c, supertypes);
}
ConcreteType addAllocatedClass(Class cl) {
assert(!cl.isAbstract);
assert(!_sealed);
final _TFClassImpl classImpl = getTFClass(cl);
if (allocatedClasses.add(cl)) {
classImpl.addAllocatedSubtype(classImpl);
classImpl.dependencyTracker
.invalidateDependentInvocations(_typeFlowAnalysis.workList);
for (var supertype in classImpl.supertypes) {
supertype.addAllocatedSubtype(classImpl);
supertype.dependencyTracker
.invalidateDependentInvocations(_typeFlowAnalysis.workList);
}
for (var targetSet in _dynamicTargets.values) {
_addDynamicTarget(cl, targetSet);
}
}
return classImpl.concreteType;
}
void seal() {
_sealed = true;
}
@override
bool isSubtype(Class sub, Class sup) {
if (kPrintTrace) {
tracePrint("isSubtype for sub = $sub, sup = $sup");
}
if (identical(sub, sup)) {
return true;
}
_TFClassImpl subClassData = getTFClass(sub);
_TFClassImpl superClassData = getTFClass(sup);
return subClassData.supertypes.contains(superClassData);
}
@override
Type specializeTypeCone(TFClass baseClass, {bool allowWideCone = false}) {
if (kPrintTrace) {
tracePrint("specializeTypeCone for $baseClass");
}
Statistics.typeConeSpecializations++;
if (baseClass.classNode == coreTypes.objectClass) {
return const AnyType();
}
final _TFClassImpl cls = baseClass as _TFClassImpl;
if (allowWideCone && cls.hasWideCone) {
Statistics.typeSpecializationsUsedWideCone++;
return cls.wideConeType;
}
if (!_sealed) {
cls.dependencyTracker
.addDependentInvocation(_typeFlowAnalysis.currentInvocation);
}
return cls.specializedConeType;
}
bool hasNonTrivialNoSuchMethod(TFClass c) {
final classImpl = c as _TFClassImpl;
bool? value = classImpl.hasNonTrivialNoSuchMethod;
if (value == null) {
classImpl.hasNonTrivialNoSuchMethod = value =
(hierarchy.getDispatchTarget(c.classNode, noSuchMethodName) !=
objectNoSuchMethod);
}
return value;
}
_DynamicTargetSet getDynamicTargetSet(DynamicSelector selector) {
return (_dynamicTargets[selector] ??= _createDynamicTargetSet(selector));
}
_DynamicTargetSet _createDynamicTargetSet(DynamicSelector selector) {
// TODO(alexmarkov): consider caching the set of Object selectors.
final isObjectMethod = (hierarchy.getDispatchTarget(
_typeFlowAnalysis.environment.coreTypes.objectClass, selector.name,
setter: selector.isSetter) !=
null);
final targetSet = new _DynamicTargetSet(selector, isObjectMethod);
for (Class c in allocatedClasses) {
_addDynamicTarget(c, targetSet);
}
return targetSet;
}
void _addDynamicTarget(Class c, _DynamicTargetSet targetSet) {
assert(!_sealed);
final selector = targetSet.selector;
final member = hierarchy.getDispatchTarget(c, selector.name,
setter: selector.isSetter);
if (member != null) {
if (targetSet.targets.add(member)) {
targetSet.invalidateDependentInvocations(_typeFlowAnalysis.workList);
}
}
}
@override
List<DartType> flattenedTypeArgumentsFor(Class klass) =>
genericInterfacesInfo.flattenedTypeArgumentsFor(klass);
@override
int genericInterfaceOffsetFor(Class klass, Class iface) =>
genericInterfacesInfo.genericInterfaceOffsetFor(klass, iface);
@override
List<Type> flattenedTypeArgumentsForNonGeneric(Class klass) =>
genericInterfacesInfo.flattenedTypeArgumentsForNonGeneric(klass);
@override
String toString() {
StringBuffer buf = new StringBuffer();
buf.write("ClassHierarchyCache {\n");
buf.write(" allocated classes:\n");
allocatedClasses.forEach((c) {
buf.write(" $c\n");
});
buf.write(" classes:\n");
classes.values.forEach((c) {
buf.write(" ${c.dump()}\n");
});
buf.write("}\n");
return buf.toString();
}
}
class _WorkList {
final TypeFlowAnalysis _typeFlowAnalysis;
final LinkedList<_Invocation> pending = new LinkedList<_Invocation>();
final Set<_Invocation> processing = new Set<_Invocation>();
final List<_Invocation> callStack = <_Invocation>[];
_WorkList(this._typeFlowAnalysis);
bool _isPending(_Invocation invocation) => invocation.list != null;
void enqueueInvocation(_Invocation invocation) {
assert(invocation.result == null);
if (_isPending(invocation)) {
// Re-add the invocation to the tail of the pending queue.
pending.remove(invocation);
assert(!_isPending(invocation));
}
pending.add(invocation);
}
void invalidateInvocation(_Invocation invocation) {
Statistics.invocationsInvalidated++;
if (invocation.result != null) {
assert(invocation.invalidatedResult == null);
invocation.invalidatedResult = invocation.result;
invocation.result = null;
}
enqueueInvocation(invocation);
}
bool invalidateProtobufFields() {
final protobufHandler = _typeFlowAnalysis.protobufHandler;
if (protobufHandler == null) {
return false;
}
final fields = protobufHandler.getInvalidatedFields();
if (fields.isEmpty) {
return false;
}
// Protobuf handler replaced contents of static field initializers.
for (var field in fields) {
assert(field.isStatic);
// Reset summary in order to rebuild it.
_typeFlowAnalysis._summaries.remove(field);
// Invalidate (and enqueue) field initializer invocation.
final initializerInvocation = _typeFlowAnalysis._invocationsCache
.getInvocation(
DirectSelector(field, callKind: CallKind.FieldInitializer),
Args<Type>(const <Type>[]));
invalidateInvocation(initializerInvocation);
}
return true;
}
void process() {
for (;;) {
if (pending.isEmpty && !invalidateProtobufFields()) {
break;
}
assert(callStack.isEmpty && processing.isEmpty);
Statistics.iterationsOverInvocationsWorkList++;
processInvocation(pending.first);
}
}
Type processInvocation(_Invocation invocation) {
Type? result = invocation.result;
if (result != null) {
// Already processed.
Statistics.usedCachedResultsOfInvocations++;
return result;
}
// Test if tracing is enabled to avoid expensive message formatting.
if (kPrintTrace) {
tracePrint(
'PROCESSING $invocation, invalidatedResult ${invocation.invalidatedResult}',
1);
}
if (processing.add(invocation)) {
// Do not process too many calls in the call stack as
// it may cause stack overflow in the analysis.
const int kMaxCallsInCallStack = 500;
if (callStack.length > kMaxCallsInCallStack) {
Statistics.deepInvocationsDeferred++;
// If there is invalidatedResult, then use it.
// When actual result is inferred it will be compared against
// invalidatedResult and all dependent invocations will be invalidated
// accordingly.
//
// Otherwise, if invocation is not invalidated yet, use empty type
// as a result but immediately invalidate it in order to recompute.
// Static type would be too inaccurate.
if (invocation.invalidatedResult == null) {
invocation.result = const EmptyType();
}
// Conservatively assume that this invocation may trigger
// parameter type checks. This is needed because caller may not be
// invalidated and recomputed if this invocation yields the
// same result.
invocation.typeChecksNeeded = true;
invalidateInvocation(invocation);
assert(invocation.result == null);
assert(invocation.invalidatedResult != null);
assert(_isPending(invocation));
if (kPrintTrace) {
tracePrint("Processing deferred due to deep call stack.");
tracePrint(
'END PROCESSING $invocation, RESULT ${invocation.invalidatedResult}',
-1);
}
processing.remove(invocation);
return invocation.invalidatedResult!;
}
callStack.add(invocation);
pending.remove(invocation);
result = invocation.process(_typeFlowAnalysis);
invocation.setResult(_typeFlowAnalysis, result);
// setResult may saturate result to ensure convergence.
result = invocation.result!;
// Invocation is still pending - it was invalidated while being processed.
// Move result to invalidatedResult.
if (_isPending(invocation)) {
Statistics.invocationsInvalidatedDuringProcessing++;
invocation.invalidatedResult = invocation.result;
invocation.result = null;
}
final last = callStack.removeLast();
assert(identical(last, invocation));
processing.remove(invocation);
Statistics.invocationsProcessed++;
if (kPrintTrace) {
tracePrint('END PROCESSING $invocation, RESULT $result', -1);
}
return result;
} else {
// Recursive invocation.
final result = invocation.resultForRecursiveInvocation;
if (result != null) {
if (kPrintTrace) {
tracePrint("Already known type for recursive invocation: $result");
tracePrint('END PROCESSING $invocation, RESULT $result', -1);
}
return result;
}
// Fall back to static type.
Statistics.recursiveInvocationsApproximated++;
final staticType = _typeFlowAnalysis.hierarchyCache
.fromStaticType(invocation.selector.staticReturnType, true);
if (kPrintTrace) {
tracePrint(
"Approximated recursive invocation with static type $staticType");
tracePrint('END PROCESSING $invocation, RESULT $staticType', -1);
}
return staticType;
}
}
}
class TypeFlowAnalysis implements EntryPointsListener, CallHandler {
final Target target;
final TypeEnvironment environment;
final LibraryIndex libraryIndex;
final PragmaAnnotationParser annotationMatcher;
final ProtobufHandler? protobufHandler;
late NativeCodeOracle nativeCodeOracle;
late _ClassHierarchyCache hierarchyCache;
late SummaryCollector summaryCollector;
late _InvocationsCache _invocationsCache;
late _WorkList workList;
GenericInterfacesInfo _genericInterfacesInfo;
final Map<Member, Summary> _summaries = <Member, Summary>{};
final Map<Field, _FieldValue> _fieldValues = <Field, _FieldValue>{};
final Set<Member> _tearOffTaken = new Set<Member>();
final Set<Member> _methodsAndSettersCalledDynamically = new Set<Member>();
final Set<Member> _gettersCalledDynamically = new Set<Member>();
final Set<Member> _calledViaInterfaceSelector = new Set<Member>();
final Set<Member> _calledViaThis = new Set<Member>();
TypeFlowAnalysis(
this.target,
Component component,
CoreTypes coreTypes,
ClosedWorldClassHierarchy hierarchy,
this._genericInterfacesInfo,
this.environment,
this.libraryIndex,
this.protobufHandler,
PragmaAnnotationParser? matcher)
: annotationMatcher =
matcher ?? new ConstantPragmaAnnotationParser(coreTypes) {
nativeCodeOracle = new NativeCodeOracle(libraryIndex, annotationMatcher);
hierarchyCache = new _ClassHierarchyCache(this, hierarchy,
_genericInterfacesInfo, environment, target.flags.enableNullSafety);
summaryCollector = new SummaryCollector(
target,
environment,
hierarchy,
this,
hierarchyCache,
nativeCodeOracle,
hierarchyCache,
protobufHandler);
_invocationsCache = new _InvocationsCache(this);
workList = new _WorkList(this);
component.accept(new PragmaEntryPointsVisitor(
this, nativeCodeOracle, annotationMatcher));
}
_Invocation get currentInvocation => workList.callStack.last;
Summary getSummary(Member member) {
return _summaries[member] ??= summaryCollector.createSummary(member);
}
_FieldValue getFieldValue(Field field) {
_FieldValue? fieldValue = _fieldValues[field];
if (fieldValue == null) {
Summary? typeGuardSummary = null;
if (field.isGenericCovariantImpl) {
typeGuardSummary = summaryCollector.createSummary(field,
fieldSummaryType: FieldSummaryType.kFieldGuard);
}
fieldValue = _FieldValue(field, typeGuardSummary, hierarchyCache);
_fieldValues[field] = fieldValue;
}
return fieldValue;
}
void process() {
workList.process();
hierarchyCache.seal();
}
/// Returns true if analysis found that given member
/// could be executed / field could be accessed.
bool isMemberUsed(Member member) {
if (member is Field) {
return _fieldValues.containsKey(member);
} else {
return _summaries.containsKey(member);
}
}
/// Returns true if analysis found that initializer of the given [field]
/// could be executed.
bool isFieldInitializerUsed(Field field) {
final fieldValue = _fieldValues[field];
if (fieldValue != null) {
return fieldValue.isInitialized;
}
return false;
}
/// Returns true if analysis found that getter corresponding to the given
/// [field] could be executed.
bool isFieldGetterUsed(Field field) {
final fieldValue = _fieldValues[field];
if (fieldValue != null) {
return fieldValue.isGetterUsed;
}
return false;
}
/// Returns true if analysis found that setter corresponding to the given
/// [field] could be executed.
bool isFieldSetterUsed(Field field) {
final fieldValue = _fieldValues[field];
if (fieldValue != null) {
return fieldValue.isSetterUsed;
}
return false;
}
bool isClassAllocated(Class c) => hierarchyCache.allocatedClasses.contains(c);
Call? callSite(TreeNode node) => summaryCollector.callSites[node];
TypeCheck? explicitCast(AsExpression cast) =>
summaryCollector.explicitCasts[cast];
TypeCheck? isTest(IsExpression node) => summaryCollector.isTests[node];
NarrowNotNull? nullTest(TreeNode node) => summaryCollector.nullTests[node];
Type? fieldType(Field field) => _fieldValues[field]?.value;
Args<Type>? argumentTypes(Member member) => _summaries[member]?.argumentTypes;
Type? argumentType(Member member, VariableDeclaration memberParam) {
return _summaries[member]?.argumentType(member, memberParam);
}
List<VariableDeclaration>? uncheckedParameters(Member member) =>
_summaries[member]?.uncheckedParameters;
bool isTearOffTaken(Member member) => _tearOffTaken.contains(member);
/// Returns true if this member is called on a receiver with static type
/// dynamic. Getters are not tracked. For fields, only setter is tracked.
bool isCalledDynamically(Member member) =>
_methodsAndSettersCalledDynamically.contains(member);
/// Returns true if this getter (or implicit getter for field) is called
/// on a receiver with static type dynamic.
bool isGetterCalledDynamically(Member member) =>
_gettersCalledDynamically.contains(member);
/// Returns true if this member is called via this call.
/// Getters are not tracked. For fields, only setter is tracked.
bool isCalledViaThis(Member member) => _calledViaThis.contains(member);
/// Returns true if this member is called via non-this call.
/// Getters are not tracked. For fields, only setter is tracked.
bool isCalledNotViaThis(Member member) =>
_methodsAndSettersCalledDynamically.contains(member) ||
_calledViaInterfaceSelector.contains(member);
/// Update the summary parameters to reflect a signature change with moved
/// and/or removed parameters.
void adjustFunctionParameters(Member member) {
_summaries[member]?.adjustFunctionParameters(member);
}
/// ---- Implementation of [CallHandler] interface. ----
@override
Type applyCall(Call? callSite, Selector selector, Args<Type> args,
{bool isResultUsed: true, bool processImmediately: true}) {
_Invocation invocation = _invocationsCache.getInvocation(selector, args);
// Test if tracing is enabled to avoid expensive message formatting.
if (kPrintTrace) {
tracePrint("APPLY $invocation");
}
if ((callSite != null) && (invocation is _DispatchableInvocation)) {
invocation.addCallSite(callSite);
}
// TODO(alexmarkov): Figure out better strategy of processing
// an invocation if its result is not used.
// Enqueueing such invocations regresses the analysis time considerably.
if (processImmediately) {
if (isResultUsed) {
invocation.addDependentInvocation(currentInvocation);
}
return workList.processInvocation(invocation);
} else {
assert(!isResultUsed);
if (invocation.result == null) {
workList.enqueueInvocation(invocation);
}
return const EmptyType();
}
}
@override
void typeCheckTriggered() {
currentInvocation.typeChecksNeeded = true;
}
/// ---- Implementation of [EntryPointsListener] interface. ----
@override
void addFieldUsedInConstant(Field field, Type instance, Type value) {
assert(!field.isStatic);
final fieldValue = getFieldValue(field);
fieldValue.setValue(value, this, instance);
// Make sure the field is retained as removing fields used in constants
// may affect identity of the constants.
fieldValue.isGetterUsed = true;
}
@override
void addRawCall(Selector selector) {
if (kPrintDebug) {
debugPrint("ADD RAW CALL: $selector");
}
assert(selector is! DynamicSelector); // TODO(alexmarkov)
applyCall(null, selector, summaryCollector.rawArguments(selector),
isResultUsed: false, processImmediately: false);
}
@override
ConcreteType addAllocatedClass(Class c) {
if (kPrintDebug) {
debugPrint("ADD ALLOCATED CLASS: $c");
}
return hierarchyCache.addAllocatedClass(c);
}
@override
void recordMemberCalledViaInterfaceSelector(Member target) {
_calledViaInterfaceSelector.add(target);
}
@override
void recordMemberCalledViaThis(Member target) {
_calledViaThis.add(target);
}
@override
void recordTearOff(Member target) {
_tearOffTaken.add(target);
}
}