| // 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. |
| library kernel.transformations.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 '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)) { |
| _dependentInvocations ??= new Set<_Invocation>(); |
| _dependentInvocations.add(invocation); |
| } |
| } |
| |
| void invalidateDependentInvocations(_WorkList workList) { |
| 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; |
| |
| Type process(TypeFlowAnalysis typeFlowAnalysis); |
| |
| _Invocation(this.selector, this.args); |
| |
| // 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) => |
| (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, |
| new Type.cone( |
| typeFlowAnalysis.environment.coreTypes.invocationClass.rawType) |
| ]); |
| |
| 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. |
| if (selector.member.function != null) { |
| typeChecksNeeded = selector.member.function.typeParameters |
| .any((t) => t.isGenericCovariantImpl); |
| } |
| } |
| |
| @override |
| Type process(TypeFlowAnalysis typeFlowAnalysis) { |
| assertx(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: |
| assertx(args.values.length == firstParamIndex); |
| assertx(args.names.isEmpty); |
| return fieldValue.getValue( |
| typeFlowAnalysis, field.isStatic ? null : args.values[0]); |
| |
| case CallKind.PropertySet: |
| assertx(args.values.length == firstParamIndex + 1); |
| assertx(args.names.isEmpty); |
| 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. |
| 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: |
| assertx(args.values.length == firstParamIndex); |
| assertx(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); |
| } |
| fieldValue.setValue(initializerResult, typeFlowAnalysis, |
| field.isStatic ? null : args.receiver); |
| return const EmptyType(); |
| } |
| |
| // Make dartanalyzer happy. |
| throw 'Unexpected call kind ${selector.callKind}'; |
| } |
| |
| Type _processFunction(TypeFlowAnalysis typeFlowAnalysis) { |
| final Member member = selector.member; |
| if (selector.memberAgreesToCallKind(member)) { |
| if (_argumentsValid()) { |
| return typeFlowAnalysis |
| .getSummary(member) |
| .apply(args, typeFlowAnalysis.hierarchyCache, typeFlowAnalysis); |
| } else { |
| assertx(selector.callKind == CallKind.Method); |
| return _processNoSuchMethod(args.receiver, typeFlowAnalysis); |
| } |
| } else { |
| if (selector.callKind == CallKind.PropertyGet) { |
| // Tear-off. |
| // TODO(alexmarkov): capture receiver type |
| assertx((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 |
| assertx((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 function = selector.member.function; |
| assertx(function != null); |
| |
| final int positionalArguments = args.positionalCount; |
| |
| final int firstParamIndex = numTypeParams(selector.member) + |
| (hasReceiverArg(selector.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; |
| |
| @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, null); |
| |
| _DispatchableInvocation(Selector selector, Args<Type> args) |
| : super(selector, args) { |
| assertx(selector is! DirectSelector); |
| } |
| |
| @override |
| Type process(TypeFlowAnalysis typeFlowAnalysis) { |
| assertx(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 = new Type.empty(); |
| |
| 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. |
| assertx(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); |
| |
| 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._calledViaDynamicSelector.add(target); |
| } else if (selector is VirtualSelector) { |
| typeFlowAnalysis._calledViaThis.add(target); |
| } else { |
| typeFlowAnalysis._calledViaInterfaceSelector.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.name == "call")) { |
| tracePrint("Possible closure call, result is dynamic"); |
| result = new Type.nullableAny(); |
| } |
| |
| return result; |
| } |
| |
| void _collectTargetsForReceiverType( |
| Type receiver, |
| Map<Member, _ReceiverTypeBuilder> targets, |
| TypeFlowAnalysis typeFlowAnalysis) { |
| assertx(receiver != const EmptyType()); // should be filtered earlier |
| |
| final bool isNullableReceiver = receiver is NullableType; |
| if (isNullableReceiver) { |
| receiver = (receiver as NullableType).baseType; |
| assertx(receiver is! NullableType); |
| } |
| |
| if (selector is InterfaceSelector) { |
| final staticReceiverType = |
| new Type.fromStatic(selector.member.enclosingClass.rawType); |
| receiver = receiver.intersection( |
| staticReceiverType, typeFlowAnalysis.hierarchyCache); |
| assertx(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 as ConeType).dartType); |
| } |
| |
| assertx(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 { |
| assertx(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.nullClass; |
| |
| 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) { |
| Class class_ = receiver.classNode; |
| |
| Member target = typeFlowAnalysis.hierarchyCache.hierarchy |
| .getDispatchTarget(class_, selector.name, setter: selector.isSetter); |
| |
| if (target != null) { |
| if (kPrintTrace) { |
| tracePrint("Found $target for concrete receiver $receiver"); |
| } |
| _getReceiverTypeBuilder(targets, target).addConcreteType(receiver); |
| } else { |
| if (typeFlowAnalysis.hierarchyCache.hasNonTrivialNoSuchMethod(class_)) { |
| 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. |
| // assertx(selector.member.enclosingClass == |
| // _typeFlowAnalysis.environment.coreTypes.objectClass, details: selector); |
| selector = new DynamicSelector(selector.callKind, selector.name); |
| } |
| |
| final receiver = args.receiver; |
| final _DynamicTargetSet dynamicTargetSet = typeFlowAnalysis.hierarchyCache |
| .getDynamicTargetSet(selector as DynamicSelector); |
| |
| dynamicTargetSet.addDependentInvocation(this); |
| |
| assertx(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) { |
| assertx(!_isPolymorphic); |
| assertx((_monomorphicTarget == null) || (_monomorphicTarget == target)); |
| _monomorphicTarget = target; |
| |
| _notifyCallSites(); |
| } |
| |
| void addCallSite(Call callSite) { |
| if (selector is DirectSelector) { |
| return; |
| } |
| |
| _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) { |
| assert(selector is! DirectSelector); |
| |
| if (_isPolymorphic) { |
| callSite.setPolymorphic(); |
| } else { |
| 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() { |
| if (_callSites != null) { |
| _callSites.forEach(_notifyCallSite); |
| } |
| } |
| } |
| |
| /// 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) { |
| if (_list == null) { |
| if (_type == null) { |
| _type = type; |
| return; |
| } |
| |
| assertx(_type is ConcreteType); |
| assertx(_type != type); |
| |
| _list = new List<ConcreteType>(); |
| _list.add(_type); |
| |
| _type = null; |
| } |
| |
| assertx(_list.last.classId.compareTo(type.classId) < 0); |
| _list.add(type); |
| } |
| |
| /// Appends an arbitrary Type. May be called only once. |
| /// Should not be used in conjunction with [addConcreteType]. |
| void addType(Type type) { |
| assertx(_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) { |
| if (_list == null) { |
| t = const EmptyType(); |
| } else { |
| t = new SetType(_list); |
| } |
| } else { |
| assertx(_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) { |
| if (sa.approximation == null) { |
| final rawArgs = |
| _typeFlowAnalysis.summaryCollector.rawArguments(selector); |
| sa.approximation = new _DispatchableInvocation(selector, rawArgs); |
| Statistics.approximateInvocationsCreated++; |
| } |
| Statistics.approximateInvocationsUsed++; |
| return sa.approximation; |
| } |
| |
| ++sa.count; |
| Statistics.maxInvocationsCachedPerSelector = |
| max(Statistics.maxInvocationsCachedPerSelector, sa.count); |
| } |
| |
| bool added = _invocations.add(invocation); |
| assertx(added); |
| ++Statistics.invocationsAddedToCache; |
| return invocation; |
| } |
| } |
| |
| class _FieldValue extends _DependencyTracker { |
| final Field field; |
| final Type staticType; |
| final Summary typeGuardSummary; |
| Type value; |
| |
| _FieldValue(this.field, this.typeGuardSummary) |
| : staticType = new Type.fromStatic(field.type) { |
| if (field.initializer == null && _isDefaultValueOfFieldObservable()) { |
| value = new Type.nullable(const EmptyType()); |
| } else { |
| value = const EmptyType(); |
| } |
| } |
| |
| bool _isDefaultValueOfFieldObservable() { |
| if (field.isStatic) { |
| return true; |
| } |
| |
| final enclosingClass = field.enclosingClass; |
| assertx(enclosingClass != null); |
| |
| // 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) { |
| assertx(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); |
| return 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 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); |
| assertx(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 _ClassData extends _DependencyTracker implements ClassId<_ClassData> { |
| final int _id; |
| final Class class_; |
| final Set<_ClassData> supertypes; // List of super-types including this. |
| final Set<_ClassData> _allocatedSubtypes = new Set<_ClassData>(); |
| |
| /// Flag indicating if this class has a noSuchMethod() method not inherited |
| /// from Object. |
| /// Lazy initialized by ClassHierarchyCache.hasNonTrivialNoSuchMethod(). |
| bool hasNonTrivialNoSuchMethod; |
| |
| _ClassData(this._id, this.class_, this.supertypes) { |
| supertypes.add(this); |
| } |
| |
| ConcreteType _concreteType; |
| ConcreteType get concreteType => |
| _concreteType ??= new ConcreteType(this, class_, null); |
| |
| Type _specializedConeType; |
| Type get specializedConeType => |
| _specializedConeType ??= _calculateConeTypeSpecialization(); |
| |
| Type _calculateConeTypeSpecialization() { |
| final int numSubTypes = _allocatedSubtypes.length; |
| if (numSubTypes == 0) { |
| return new Type.empty(); |
| } else if (numSubTypes == 1) { |
| return _allocatedSubtypes.single.concreteType; |
| } else { |
| List<ConcreteType> types = new List<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(_ClassData subType) { |
| _allocatedSubtypes.add(subType); |
| _specializedConeType = null; // Reset cached specialization. |
| } |
| |
| @override |
| int get hashCode => _id; |
| |
| @override |
| bool operator ==(other) => (other is _ClassData) && (this._id == other._id); |
| |
| @override |
| int compareTo(_ClassData other) => this._id.compareTo(other._id); |
| |
| @override |
| String toString() => "_C $class_"; |
| |
| 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>>{}; |
| |
| RuntimeTypeTranslator closedTypeTranslator; |
| |
| GenericInterfacesInfoImpl(this.hierarchy) { |
| closedTypeTranslator = RuntimeTypeTranslator.forClosedTypes(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))); |
| |
| 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 = new List<Type>(flattenedTypeArgs.length); |
| for (int i = 0; i < flattenedTypeArgs.length; ++i) { |
| final translated = closedTypeTranslator.translate(flattenedTypeArgs[i]); |
| assertx(translated is RuntimeType || translated is AnyType); |
| result[i] = translated; |
| } |
| cachedFlattenedTypeArgsForNonGeneric[klass] = result; |
| return result; |
| } |
| } |
| |
| class _ClassHierarchyCache implements TypeHierarchy { |
| final TypeFlowAnalysis _typeFlowAnalysis; |
| final ClosedWorldClassHierarchy hierarchy; |
| final TypeEnvironment environment; |
| final Set<Class> allocatedClasses = new Set<Class>(); |
| final Map<Class, _ClassData> classes = <Class, _ClassData>{}; |
| 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) |
| : objectNoSuchMethod = hierarchy.getDispatchTarget( |
| environment.coreTypes.objectClass, noSuchMethodName) { |
| assertx(objectNoSuchMethod != null); |
| } |
| |
| _ClassData getClassData(Class c) { |
| return classes[c] ??= _createClassData(c); |
| } |
| |
| _ClassData _createClassData(Class c) { |
| final supertypes = new Set<_ClassData>(); |
| for (var sup in c.supers) { |
| supertypes.addAll(getClassData(sup.classNode).supertypes); |
| } |
| return new _ClassData(++_classIdCounter, c, supertypes); |
| } |
| |
| ConcreteType addAllocatedClass(Class cl) { |
| assertx(!cl.isAbstract); |
| assertx(!_sealed); |
| |
| final _ClassData classData = getClassData(cl); |
| |
| if (allocatedClasses.add(cl)) { |
| classData.addAllocatedSubtype(classData); |
| classData.invalidateDependentInvocations(_typeFlowAnalysis.workList); |
| |
| for (var supertype in classData.supertypes) { |
| supertype.addAllocatedSubtype(classData); |
| supertype.invalidateDependentInvocations(_typeFlowAnalysis.workList); |
| } |
| |
| for (var targetSet in _dynamicTargets.values) { |
| _addDynamicTarget(cl, targetSet); |
| } |
| } |
| |
| return classData.concreteType; |
| } |
| |
| void seal() { |
| _sealed = true; |
| } |
| |
| @override |
| bool isSubtype(DartType subType, DartType superType) { |
| if (kPrintTrace) { |
| tracePrint("isSubtype for sub = $subType (${subType.runtimeType})," |
| " sup = $superType (${superType.runtimeType})"); |
| } |
| if (subType == superType) { |
| return true; |
| } |
| |
| if (superType is DynamicType || |
| superType is VoidType || |
| subType is BottomType) { |
| return true; |
| } |
| if (subType is DynamicType || |
| subType is VoidType || |
| superType is BottomType) { |
| return false; |
| } |
| |
| // TODO(alexmarkov): handle function types properly |
| if (subType is FunctionType) { |
| subType = _typeFlowAnalysis.environment.rawFunctionType; |
| } |
| if (superType is FunctionType) { |
| superType = _typeFlowAnalysis.environment.rawFunctionType; |
| } |
| // TODO(alexmarkov): handle generic types properly. |
| assertx(subType is! TypeParameterType); |
| assertx(superType is! TypeParameterType); |
| |
| assertx(subType is InterfaceType, details: subType); // TODO(alexmarkov) |
| assertx(superType is InterfaceType, details: superType); // TODO(alexmarkov) |
| |
| // InterfaceTypes should be raw, since we don't handle type arguments |
| // (although frankly we can't distinguish between raw C and C<dynamic). |
| assertx((subType as InterfaceType) |
| .typeArguments |
| .every((t) => t == const DynamicType())); |
| assertx((superType as InterfaceType) |
| .typeArguments |
| .every((t) => t == const DynamicType())); |
| |
| Class subClass = (subType as InterfaceType).classNode; |
| Class superClass = (superType as InterfaceType).classNode; |
| if (subClass == superClass) { |
| return true; |
| } |
| |
| // TODO(alexmarkov): handle FutureOr more precisely (requires generics). |
| if (superClass == _typeFlowAnalysis.environment.futureOrClass) { |
| return true; |
| } |
| |
| _ClassData subClassData = getClassData(subClass); |
| _ClassData superClassData = getClassData(superClass); |
| |
| return subClassData.supertypes.contains(superClassData); |
| } |
| |
| @override |
| Type specializeTypeCone(DartType base) { |
| if (kPrintTrace) { |
| tracePrint("specializeTypeCone for $base"); |
| } |
| Statistics.typeConeSpecializations++; |
| |
| // TODO(alexmarkov): handle function types properly |
| if (base is FunctionType) { |
| base = _typeFlowAnalysis.environment.rawFunctionType; |
| } |
| |
| if (base is TypeParameterType) { |
| base = (base as TypeParameterType).bound; |
| } |
| |
| assertx(base is InterfaceType); // TODO(alexmarkov) |
| final baseClass = (base as InterfaceType).classNode; |
| |
| // TODO(alexmarkov): take type arguments into account. |
| |
| // TODO(alexmarkov): consider approximating type if number of allocated |
| // subtypes is too large |
| |
| if (base == const DynamicType() || |
| base == _typeFlowAnalysis.environment.objectType || |
| // TODO(alexmarkov): handle FutureOr more precisely (requires generics). |
| baseClass == _typeFlowAnalysis.environment.futureOrClass) { |
| return const AnyType(); |
| } |
| |
| _ClassData classData = getClassData(baseClass); |
| |
| if (!_sealed) { |
| classData.addDependentInvocation(_typeFlowAnalysis.currentInvocation); |
| } |
| |
| return classData.specializedConeType; |
| } |
| |
| bool hasNonTrivialNoSuchMethod(Class c) { |
| final classData = getClassData(c); |
| classData.hasNonTrivialNoSuchMethod ??= |
| (hierarchy.getDispatchTarget(c, noSuchMethodName) != |
| objectNoSuchMethod); |
| return classData.hasNonTrivialNoSuchMethod; |
| } |
| |
| _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) { |
| assertx(!_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 get futureOrClass => environment.coreTypes.futureOrClass; |
| Class get futureClass => environment.coreTypes.futureClass; |
| Class get functionClass => environment.coreTypes.functionClass; |
| } |
| |
| 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) { |
| assertx(invocation.result == null); |
| if (_isPending(invocation)) { |
| // Re-add the invocation to the tail of the pending queue. |
| pending.remove(invocation); |
| assertx(!_isPending(invocation)); |
| } |
| pending.add(invocation); |
| } |
| |
| void invalidateInvocation(_Invocation invocation) { |
| Statistics.invocationsInvalidated++; |
| if (invocation.result != null) { |
| invocation.invalidatedResult = invocation.result; |
| invocation.result = null; |
| } |
| enqueueInvocation(invocation); |
| } |
| |
| void process() { |
| while (pending.isNotEmpty) { |
| assertx(callStack.isEmpty && processing.isEmpty); |
| Statistics.iterationsOverInvocationsWorkList++; |
| processInvocation(pending.first); |
| } |
| } |
| |
| Type processInvocation(_Invocation invocation) { |
| if (invocation.result != null) { |
| // Already processed. |
| Statistics.usedCachedResultsOfInvocations++; |
| return invocation.result; |
| } |
| |
| // Test if tracing is enabled to avoid expensive message formatting. |
| if (kPrintTrace) { |
| tracePrint('PROCESSING $invocation'); |
| } |
| |
| if (processing.add(invocation)) { |
| callStack.add(invocation); |
| pending.remove(invocation); |
| |
| Type result = invocation.process(_typeFlowAnalysis); |
| |
| assertx(result != null); |
| invocation.result = result; |
| |
| if (invocation.invalidatedResult != null) { |
| if (invocation.invalidatedResult != result) { |
| invocation.invalidateDependentInvocations(this); |
| |
| invocation.invalidationCounter++; |
| Statistics.maxInvalidationsPerInvocation = max( |
| Statistics.maxInvalidationsPerInvocation, |
| invocation.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 (invocation.invalidationCounter > _Invocation.invalidationLimit) { |
| result = result.union( |
| invocation.invalidatedResult, _typeFlowAnalysis.hierarchyCache); |
| invocation.result = result; |
| } |
| } |
| invocation.invalidatedResult = null; |
| } |
| |
| // Invocation is still pending - it was invalidated while being processed. |
| // Move result to invalidatedResult. |
| if (_isPending(invocation)) { |
| Statistics.invocationsInvalidatedDuringProcessing++; |
| invocation.invalidatedResult = result; |
| invocation.result = null; |
| } |
| |
| final last = callStack.removeLast(); |
| assertx(identical(last, invocation)); |
| |
| processing.remove(invocation); |
| |
| Statistics.invocationsProcessed++; |
| if (kPrintTrace) { |
| tracePrint('END PROCESSING $invocation, RESULT $result'); |
| } |
| return result; |
| } else { |
| // Recursive invocation, approximate with static type. |
| Statistics.recursiveInvocationsApproximated++; |
| final staticType = |
| new Type.fromStatic(invocation.selector.staticReturnType); |
| if (kPrintTrace) { |
| tracePrint( |
| "Approximated recursive invocation with static type $staticType"); |
| tracePrint('END PROCESSING $invocation, RESULT $staticType'); |
| } |
| return staticType; |
| } |
| } |
| } |
| |
| class TypeFlowAnalysis implements EntryPointsListener, CallHandler { |
| final Target target; |
| final TypeEnvironment environment; |
| final LibraryIndex libraryIndex; |
| final PragmaAnnotationParser annotationMatcher; |
| NativeCodeOracle nativeCodeOracle; |
| _ClassHierarchyCache hierarchyCache; |
| SummaryCollector summaryCollector; |
| _InvocationsCache _invocationsCache; |
| _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> _calledViaDynamicSelector = 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, |
| {PragmaAnnotationParser matcher}) |
| : annotationMatcher = |
| matcher ?? new ConstantPragmaAnnotationParser(coreTypes) { |
| nativeCodeOracle = new NativeCodeOracle(libraryIndex, annotationMatcher); |
| hierarchyCache = new _ClassHierarchyCache( |
| this, hierarchy, _genericInterfacesInfo, environment); |
| summaryCollector = new SummaryCollector( |
| target, environment, this, nativeCodeOracle, hierarchyCache); |
| _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) { |
| Summary setterSummary = null; |
| if (field.isGenericCovariantImpl) { |
| setterSummary = summaryCollector.createSummary(field, |
| fieldSummaryType: FieldSummaryType.kFieldGuard); |
| } |
| return _fieldValues[field] ??= new _FieldValue(field, setterSummary); |
| } |
| |
| void process() { |
| workList.process(); |
| hierarchyCache.seal(); |
| } |
| |
| bool isMemberUsed(Member member) { |
| if (member is Field) { |
| return _fieldValues.containsKey(member); |
| } else { |
| return _summaries.containsKey(member); |
| } |
| } |
| |
| bool isClassAllocated(Class c) => hierarchyCache.allocatedClasses.contains(c); |
| |
| Call callSite(TreeNode node) => summaryCollector.callSites[node]; |
| |
| TypeCheck explicitCast(AsExpression cast) => |
| summaryCollector.explicitCasts[cast]; |
| |
| Type fieldType(Field field) => _fieldValues[field]?.value; |
| |
| Args<Type> argumentTypes(Member member) => _summaries[member]?.argumentTypes; |
| |
| List<VariableDeclaration> uncheckedParameters(Member member) => |
| _summaries[member]?.uncheckedParameters; |
| |
| bool isTearOffTaken(Member member) => _tearOffTaken.contains(member); |
| |
| /// Returns true if this member is called dynamically. |
| /// Getters are not tracked. For fields, only setter is tracked. |
| bool isCalledDynamically(Member member) => |
| _calledViaDynamicSelector.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) => |
| _calledViaDynamicSelector.contains(member) || |
| _calledViaInterfaceSelector.contains(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 { |
| assertx(!isResultUsed); |
| |
| if (invocation.result == null) { |
| workList.enqueueInvocation(invocation); |
| } |
| |
| return null; |
| } |
| } |
| |
| @override |
| void typeCheckTriggered() { |
| currentInvocation.typeChecksNeeded = true; |
| } |
| |
| /// ---- Implementation of [EntryPointsListener] interface. ---- |
| |
| @override |
| void addDirectFieldAccess(Field field, Type value) { |
| final fieldValue = getFieldValue(field); |
| if (field.isStatic) { |
| fieldValue.setValue(value, this, /*receiver_type=*/ null); |
| } else { |
| final receiver = new Type.cone(new InterfaceType(field.parent)); |
| fieldValue.setValue(value, this, receiver); |
| } |
| } |
| |
| @override |
| void addRawCall(Selector selector) { |
| if (kPrintDebug) { |
| debugPrint("ADD RAW CALL: $selector"); |
| } |
| assertx(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); |
| } |
| } |