blob: 71225f74b1173773ce271a280a61a163101c400b [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'dart:collection';
import 'package:compiler/src/common/names.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/elements/indexed.dart';
import 'package:compiler/src/elements/names.dart';
import 'package:compiler/src/inferrer/abstract_value_domain.dart';
import 'package:compiler/src/js_model/js_world.dart';
import 'package:compiler/src/universe/call_structure.dart';
import 'package:compiler/src/universe/function_set.dart';
import 'package:compiler/src/universe/selector.dart';
class _QueuedMember {
/// The member to be processed.
final MemberEntity member;
/// The class we want to look for ancestors of [member] from. This might be
/// a supertype of member's enclosing class or a mixin application.
final ClassEntity cls;
/// Whether or not [cls] is the declarer of [member]. In most cases this is
/// true when [cls] equals member's [MemberEntity.enclosingClass]. However
/// for mixins this can be true when [cls] is the mixin application class.
final bool isDeclaration;
_QueuedMember(this.member, this.cls, {required this.isDeclaration});
}
class DynamicCallTarget {
final MemberEntity member;
final bool isVirtual;
factory DynamicCallTarget.virtual(MemberEntity member) =>
DynamicCallTarget(member, isVirtual: true);
factory DynamicCallTarget.concrete(MemberEntity member) =>
DynamicCallTarget(member, isVirtual: false);
DynamicCallTarget(this.member, {required this.isVirtual});
@override
int get hashCode => Object.hash(member, isVirtual);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is DynamicCallTarget &&
member == other.member &&
isVirtual == other.isVirtual);
}
@override
String toString() => 'TargetResult($member, virtual: $isVirtual)';
}
class MemberHierarchyBuilder {
final JClosedWorld closedWorld;
final Map<SelectorMask, Iterable<DynamicCallTarget>> _callCache = {};
final Map<Selector, Set<MemberEntity>> _selectorRoots = {};
final Map<MemberEntity, Set<MemberEntity>> _overrides = {};
MemberHierarchyBuilder(this.closedWorld);
/// Uses [visited] to detect and avoid cycles in the override graph.
void _forEachOverride(MemberEntity member,
void Function(MemberEntity override) f, Set<MemberEntity> visited) {
final overrides = _overrides[member];
if (overrides == null) return;
for (final override in overrides) {
if (!visited.add(override)) continue;
f(override);
_forEachOverride(override, f, visited);
}
}
/// Recursively applies [f] to each override of [entity].
void forEachOverride(
MemberEntity entity, void Function(MemberEntity override) f) {
_forEachOverride(entity, f, {});
}
/// Finds the first non-strict superclass of [cls] that contains a member
/// matching [selector] and returns that member.
///
/// Returns the first non-abstract match found while ascending the class
/// hierarchy. If no non-abstract matches are found then the first abstract
/// match is used.
///
/// If [virtualResult] is true, the resulting [DynamicCallTarget] will be
/// virtual when the match is non-abstract and has overrides.
/// Otherwise the resulting [DynamicCallTarget] is concrete.
DynamicCallTarget? findSuperclassTarget(ClassEntity cls, Selector selector,
{required bool virtualResult}) {
MemberEntity? firstAbstractMatch;
ClassEntity? current = cls;
final elementEnv = closedWorld.elementEnvironment;
while (current != null) {
final match =
elementEnv.lookupLocalClassMember(current, selector.memberName);
if (match != null && !skipMember(match, selector)) {
if (match.isAbstract) {
firstAbstractMatch ??= match;
} else {
return DynamicCallTarget(match,
isVirtual: virtualResult && _hasOverride(match));
}
}
current = elementEnv.getSuperClass(current);
}
return firstAbstractMatch != null
? DynamicCallTarget.virtual(firstAbstractMatch)
: null;
}
/// For each subclass/subtype try to find a member matching selector.
///
/// If we find a match then it covers the entire subtree below that match so
/// we do not need to check subclasses/subtypes below it.
Iterable<DynamicCallTarget> findMatchingAncestors(
ClassEntity baseCls, Selector selector,
{required bool isSubtype}) {
final Set<DynamicCallTarget> results = {};
final Queue<ClassEntity> toVisit = Queue();
final Set<ClassEntity> visited = {};
void addSubclasses(ClassEntity cls) {
for (final subclass in closedWorld.classHierarchy
.getClassHierarchyNode(cls)
.directSubclasses) {
final subclassCls = subclass.cls;
if (visited.add(subclassCls)) toVisit.add(subclassCls);
}
}
void addSubtypes(ClassEntity cls) {
for (final subtype
in closedWorld.classHierarchy.getClassSet(baseCls).subtypeNodes) {
final subtypeCls = subtype.cls;
if (visited.add(subtypeCls)) toVisit.add(subtypeCls);
}
}
addSubclasses(baseCls);
if (isSubtype) {
addSubtypes(baseCls);
// We only need to add this in the subtype case because
// `findMatchInSuperclass` will already check superclasses of `baseCls`.
// If this is a subtype query then we need to make sure we add supertypes
// of `baseCls`.
toVisit.add(baseCls);
}
while (toVisit.isNotEmpty) {
final current = toVisit.removeFirst();
final match =
findSuperclassTarget(current, selector, virtualResult: true);
if (match != null) {
results.add(match);
} else {
addSubclasses(current);
if (isSubtype) {
addSubtypes(current);
}
}
}
return results;
}
/// Returns the set of root member declarations for [selector]. [cls] acts as
/// a subclass filter for these roots.
///
/// The set of roots is the smallest set of members that are an ancestor of
/// every implementation of [selector]. This is with the caveat that we
/// consider each mixin application to declare a "copy" of each member of that
/// mixin. They share the same [MemberEntity] but each copy is considered a
/// separate potential root.
Iterable<DynamicCallTarget> rootsForSelector(
ClassEntity cls, Selector selector) {
final roots = _selectorRoots[_normalizeSelector(selector)];
if (roots == null) return const [];
final classHierarchy = closedWorld.classHierarchy;
return roots
.where((r) =>
selector.appliesStructural(r) &&
classHierarchy.isSubclassOf(r.enclosingClass!, cls))
.map((r) => DynamicCallTarget(r, isVirtual: _hasOverride(r)))
.toSet();
}
/// Returns a set of common ancestors (not necessarily the smallest) for all
/// possible targets when calling [selector] on an object with type
/// [receiverType].
///
/// Caches results for each receiver/selector pair. If [receiverType] is
/// `null` the receiver is considered dynamic and any member that matches
/// [selector] is a possible target. Also adds relevant [noSuchMethod] and
/// `null` targets if needed for the call.
Iterable<DynamicCallTarget> rootsForCall(
AbstractValue? receiverType, Selector selector) {
final domain = closedWorld.abstractValueDomain;
receiverType ??= domain.dynamicType;
final selectorMask = SelectorMask(selector, receiverType);
final cachedResult = _callCache[selectorMask];
if (cachedResult != null) return cachedResult;
Iterable<DynamicCallTarget> targetsForReceiver =
domain.findRootsOfTargets(receiverType, selector, this);
// TODO(natebiggs): Can we calculate this as part of the above call to
// findRootsOfTargets?
final needsNoSuchMethod = selector != Selectors.noSuchMethod_ &&
domain
.needsNoSuchMethodHandling(receiverType, selector)
.isPotentiallyTrue;
final isNull = domain.isNull(receiverType);
if (isNull.isPotentiallyTrue) {
// Add relevant member if null is a potential target.
final nullMatch = closedWorld.elementEnvironment.lookupLocalClassMember(
closedWorld.commonElements.jsNullClass, selector.memberName);
if (nullMatch != null) {
targetsForReceiver = {
...targetsForReceiver,
DynamicCallTarget.concrete(nullMatch)
};
}
}
final result = needsNoSuchMethod
? targetsForReceiver
.followedBy(rootsForCall(receiverType, Selectors.noSuchMethod_))
.toSet()
: targetsForReceiver;
return _callCache[selectorMask] = result;
}
/// Map call selectors to a getter with the same name. This allows us to
/// ignore their call structure since overridden methods can have different
/// call structures. This also handles method tear-offs.
static Selector _normalizeSelector(Selector selector) =>
selector.isCall ? Selector.getter(selector.memberName) : selector;
static bool _skipMemberInternal(MemberEntity member) {
return member.isStatic;
}
static bool skipMember(MemberEntity member, Selector selector) {
return _skipMemberInternal(member) || !selector.appliesStructural(member);
}
bool _hasOverride(MemberEntity member) {
return _overrides.containsKey(member);
}
static List<Selector> _selectorsForMember(MemberEntity member) {
final List<Selector> result = [];
if (member is FieldEntity) {
// A field implicitly defines both a getter and setter. Each of these can
// have different overrides so we consider them separately.
result.add(Selector.getter(member.memberName));
result.add(Selector.setter(member.memberName));
} else {
final selector = Selector.fromElement(member);
result.add(_normalizeSelector(selector));
}
return result;
}
void _processNext(
Queue<_QueuedMember> queue,
Selector selector,
Map<ClassEntity, MemberEntity?> roots,
void Function(MemberEntity parent, MemberEntity override) join) {
final state = queue.removeFirst();
final cls = state.cls;
final member = state.member;
if (state.isDeclaration && !roots.containsKey(cls)) {
// Only add the potential root if we're processing a declaration of
// the member and have not already found an override for the class'
// declaration.
roots[cls] = member;
}
final elementEnv = closedWorld.elementEnvironment;
final elementMap = closedWorld.elementMap;
final name = selector.memberName;
void addParent(
MemberEntity parent, ClassEntity parentCls, bool fromSuperclass) {
if ((_overrides[parent] ??= {}).add(member)) {
join(parent, member);
}
// Both the class we are considering and the enclosing class of [member]
// are not roots since we found an override for them.
roots[cls] = null;
roots[member.enclosingClass!] = null;
// Enqueue parent to continue processing from there. parentCls can be a
// mixin application since we consider these to have duplicates of the
// mixin's members.
queue.add(_QueuedMember(parent, parentCls, isDeclaration: true));
}
void processInterfaces(IndexedClass toProcess) {
final interfaces = elementMap.getInterfaces(toProcess);
for (final interface in interfaces) {
final interfaceCls = interface.element;
// For unnamed mixin applications, members can be owned by the
// underlying mixin which is also an interface for that same
// application. This would cause a self-override unless we explicitly
// ignore that interface relationship.
if (interfaceCls == member.enclosingClass) continue;
final match = elementEnv.lookupLocalClassMember(interfaceCls, name);
if (match != null &&
!MemberHierarchyBuilder._skipMemberInternal(match)) {
addParent(match, interfaceCls, false);
} else {
// The interface does not contain a match so enqueue the current
// member to continue checking the superclasses and supertypes of the
// interface.
queue.add(_QueuedMember(member, interfaceCls, isDeclaration: false));
}
}
}
void addInterfaces(IndexedClass toProcess) {
processInterfaces(toProcess);
// Supertypes of the mixin for this class are also supertypes of this
// class.
final mixinClass = elementEnv.getEffectiveMixinClass(toProcess);
if (mixinClass != null) {
processInterfaces(mixinClass as IndexedClass);
}
}
// Check and add any interfaces for the current class.
addInterfaces(cls as IndexedClass);
// Check each superclass in ascending order until we find an ancestor.
ClassEntity? current = elementEnv.getSuperClass(cls);
while (current != null) {
final match = elementEnv.lookupLocalClassMember(current, name);
if (match != null && !MemberHierarchyBuilder._skipMemberInternal(match)) {
addParent(match, current, true);
break;
}
// If this superclass does not declare a relevant member then check its
// interfaces too since a supertype can contain an ancestor.
addInterfaces(current as IndexedClass);
current = elementEnv.getSuperClass(current);
}
}
void _processMember(
MemberEntity member,
Map<Selector, Map<ClassEntity, MemberEntity?>> roots,
void Function(MemberEntity parent, MemberEntity override) join,
Queue<_QueuedMember> queue) {
if (_skipMemberInternal(member)) return;
final cls = member.enclosingClass!;
final mixinUses = closedWorld.mixinUsesOf(cls);
// Process each selector matching member separately.
for (final selector in _selectorsForMember(member)) {
for (final mixinUse in mixinUses) {
queue.add(_QueuedMember(member, mixinUse, isDeclaration: true));
}
queue.add(_QueuedMember(member, cls, isDeclaration: true));
final selectorRoots = roots[selector] ??= {};
while (queue.isNotEmpty) {
_processNext(queue, selector, selectorRoots, join);
}
}
}
void init(void Function(MemberEntity parent, MemberEntity override) join) {
final liveMembers = closedWorld.liveInstanceMembers
.followedBy(closedWorld.liveAbstractInstanceMembers);
// Track root declarations for each selector encountered. Each inner map
// tracks declarations of the selector. A null member indicates we found
// an override for that declaration.
final Map<Selector, Map<ClassEntity, MemberEntity?>> roots = {};
final queue = Queue<_QueuedMember>();
for (final member in liveMembers) {
assert(queue.isEmpty);
_processMember(member, roots, join, queue);
}
// Finalize roots for each selector. Deduplicate remaining root declarations
// and store them for later.
roots.forEach((selector, selectorRoots) {
final validRoots = selectorRoots.values.whereType<MemberEntity>().toSet();
if (validRoots.isNotEmpty) _selectorRoots[selector] = validRoots;
});
}
// TODO(natebiggs): Clean up debug code below.
void debugCall(String selectorName, String className,
{bool nullReceiver = false,
bool nullValue = false,
bool subClass = false,
bool subType = false,
bool setter = false,
CallStructure? call}) {
final allMembers = closedWorld.liveInstanceMembers
.followedBy(closedWorld.liveAbstractInstanceMembers);
final cls = allMembers
.firstWhere((e) => e.enclosingClass!.name == className)
.enclosingClass!;
final domain = closedWorld.abstractValueDomain;
final receiver = nullValue
? domain.nullType
: (nullReceiver
? null
: (subClass
? domain.createNonNullSubclass(cls)
: (subType
? domain.createNonNullSubtype(cls)
: domain.createNullableExact(cls))));
final name = Name(selectorName, null);
final selector = call != null
? Selector.call(name, call)
: (setter ? Selector.setter(name) : Selector.getter(name));
print('Receiver: $receiver, Selector: $selector');
print('Locate members: ${closedWorld.locateMembers(selector, receiver)}');
print('Targets for call: ${rootsForCall(receiver, selector).toList()}');
}
void dumpOverrides([String? memberName]) {
print(_overrides.entries
.where((e) => memberName == null || e.key.name == memberName)
.map((e) => '${e.key}\n ${e.value.join('\n ')}')
.join('\n'));
}
void dumpRoots([String? selectorName]) {
(_selectorRoots.entries
.where((e) => selectorName == null || e.key.name == selectorName)
.map((e) {
final members = e.value.map((m) => m).toList();
members.sort((a, b) => a.toString().compareTo(b.toString()));
return MapEntry(e.key, members.join(', '));
}).toList()
..sort((a, b) {
final keyComp = a.key.toString().compareTo(b.key.toString());
return keyComp != 0 ? keyComp : a.value.compareTo(b.value);
}))
.forEach((e) => print('${e.key}: ${e.value}'));
}
void dumpCache({String? selectorName, String? receiverString}) {
_callCache.entries
.where((e) =>
(selectorName == null || e.key.name == selectorName) &&
(receiverString == null ||
e.key.receiver.toString().contains(receiverString)))
.forEach((e) => print('${e.key}: ${e.value.join(', ')}'));
}
}