acc8975cab
FunctionType now has a List<NamedType> which must be sorted by name. Previously, named parameters were stored in a Map<String, DartType>. FunctionNode still has a List<VariableDeclaration>, but this list must now be sorted by name. BUG= R=ahe@google.com Review URL: https://codereview.chromium.org/2502343002 .
780 lines
26 KiB
Dart
780 lines
26 KiB
Dart
// Copyright (c) 2016, 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.
|
|
|
|
library kernel.tree_shaker;
|
|
|
|
import '../ast.dart';
|
|
import '../class_hierarchy.dart';
|
|
import '../core_types.dart';
|
|
|
|
Program transformProgram(Program program) {
|
|
new TreeShaker(program).transform(program);
|
|
return program;
|
|
}
|
|
|
|
/// Tree shaking based on class hierarchy analysis.
|
|
///
|
|
/// Any dynamic dispatch not on `this` is conservatively assumed to target
|
|
/// any instantiated class that implements a member matching the selector.
|
|
///
|
|
/// Member bodies are analyzed relative to a given "host class" which is the
|
|
/// concrete type of `this` (or null if in static context), so dispatches on
|
|
/// `this` can be resolved more precisely.
|
|
///
|
|
/// The tree shaker computes the following in a fixed-point iteration:
|
|
/// - a set of instantiated classes
|
|
/// - for each member, a set of potential host classes
|
|
/// - a set of names used in dynamic dispatch not on `this`
|
|
///
|
|
/// The `dart:mirrors` library is not supported.
|
|
//
|
|
// TODO(asgerf): Shake off parts of the core libraries based on the Target.
|
|
// TODO(asgerf): Tree shake unused instance fields.
|
|
class TreeShaker {
|
|
final Program program;
|
|
final ClassHierarchy hierarchy;
|
|
final CoreTypes coreTypes;
|
|
|
|
/// Names used in a dynamic dispatch invocation that could not be resolved
|
|
/// to a concrete target (i.e. not on `this`).
|
|
final Set<Name> _dispatchedNames = new Set<Name>();
|
|
|
|
/// Instance members that are potential targets for dynamic dispatch, but
|
|
/// whose name has not yet been seen in a dynamic dispatch invocation.
|
|
///
|
|
/// The map is indexed by the name of the member, and value is a list of
|
|
/// interleaved (host class, member) pairs.
|
|
final Map<Name, List<TreeNode>> _dispatchTargetCandidates =
|
|
<Name, List<TreeNode>>{};
|
|
|
|
/// Map from classes to the set of members that are reachable with that
|
|
/// class as host.
|
|
///
|
|
/// The map is implemented as a list, indexed according to
|
|
/// [ClassHierarchy.getClassIndex].
|
|
final List<Set<Member>> _usedMembersWithHost;
|
|
|
|
/// Map from used members (regardless of host) to a summary object describing
|
|
/// how the member invokes other members on `this`.
|
|
///
|
|
/// The summary object is a heterogenous list containing the [Member]s that
|
|
/// are invoked using `super` and the [Name]s that are dispatched on `this`.
|
|
///
|
|
/// Names that are dispatched as a setter are preceded by the
|
|
/// [_setterSentinel] object, to distinguish them from getter/call names.
|
|
final Map<Member, List<Node>> _usedMembers = <Member, List<Node>>{};
|
|
|
|
/// The level to which a class must be retained after tree shaking.
|
|
///
|
|
/// See [ClassRetention].
|
|
final List<ClassRetention> _classRetention;
|
|
|
|
/// Interleaved (host class, member) pairs that are reachable but have not yet
|
|
/// been analyzed for more uses.
|
|
final List<TreeNode> _worklist = new List<TreeNode>();
|
|
|
|
/// Classes whose interface can be used by external code to invoke user code.
|
|
final Set<Class> _escapedClasses = new Set<Class>();
|
|
|
|
/// AST visitor for finding static uses and dynamic dispatches in code.
|
|
_TreeShakerVisitor _visitor;
|
|
|
|
/// AST visitor for analyzing type annotations on external members.
|
|
_ExternalTypeVisitor _covariantVisitor;
|
|
_ExternalTypeVisitor _contravariantVisitor;
|
|
_ExternalTypeVisitor _bivariantVisitor;
|
|
|
|
TreeShaker(Program program, {ClassHierarchy hierarchy, CoreTypes coreTypes})
|
|
: this._internal(program, hierarchy ?? new ClassHierarchy(program),
|
|
coreTypes ?? new CoreTypes(program));
|
|
|
|
bool isMemberUsed(Member member) {
|
|
return _usedMembers.containsKey(member);
|
|
}
|
|
|
|
bool isInstantiated(Class classNode) {
|
|
return getClassRetention(classNode).index >= ClassRetention.Instance.index;
|
|
}
|
|
|
|
bool isHierarchyUsed(Class classNode) {
|
|
return getClassRetention(classNode).index >= ClassRetention.Hierarchy.index;
|
|
}
|
|
|
|
ClassRetention getClassRetention(Class classNode) {
|
|
int index = hierarchy.getClassIndex(classNode);
|
|
return _classRetention[index];
|
|
}
|
|
|
|
/// Applies the tree shaking results to the program.
|
|
///
|
|
/// This removes unused classes, members, and hierarchy data.
|
|
void transform(Program program) {
|
|
new _TreeShakingTransformer(this).transform(program);
|
|
}
|
|
|
|
TreeShaker._internal(this.program, ClassHierarchy hierarchy, this.coreTypes)
|
|
: this.hierarchy = hierarchy,
|
|
this._usedMembersWithHost =
|
|
new List<Set<Member>>(hierarchy.classes.length),
|
|
this._classRetention = new List<ClassRetention>.filled(
|
|
hierarchy.classes.length, ClassRetention.None) {
|
|
_visitor = new _TreeShakerVisitor(this);
|
|
_covariantVisitor = new _ExternalTypeVisitor(this, isCovariant: true);
|
|
_contravariantVisitor =
|
|
new _ExternalTypeVisitor(this, isContravariant: true);
|
|
_bivariantVisitor = new _ExternalTypeVisitor(this,
|
|
isCovariant: true, isContravariant: true);
|
|
_build();
|
|
}
|
|
|
|
void _build() {
|
|
if (program.mainMethod == null) {
|
|
throw 'Cannot perform tree shaking on a program without a main method';
|
|
}
|
|
if (program.mainMethod.function.positionalParameters.length > 0) {
|
|
// The main method takes a List<String> as argument.
|
|
_addInstantiatedExternalSubclass(coreTypes.listClass);
|
|
_addInstantiatedExternalSubclass(coreTypes.stringClass);
|
|
}
|
|
_addDispatchedName(new Name('noSuchMethod'));
|
|
_addPervasiveUses();
|
|
_addUsedMember(null, program.mainMethod);
|
|
_iterateWorklist();
|
|
}
|
|
|
|
/// Registers some extremely commonly used core classes as instantiated, so
|
|
/// we don't have to register them for every use we find.
|
|
void _addPervasiveUses() {
|
|
_addInstantiatedExternalSubclass(coreTypes.stringClass);
|
|
_addInstantiatedExternalSubclass(coreTypes.intClass);
|
|
_addInstantiatedExternalSubclass(coreTypes.boolClass);
|
|
_addInstantiatedExternalSubclass(coreTypes.nullClass);
|
|
}
|
|
|
|
/// Registers the given name as seen in a dynamic dispatch, and discovers used
|
|
/// instance members accordingly.
|
|
void _addDispatchedName(Name name) {
|
|
// TODO(asgerf): make use of selector arity and getter/setter kind
|
|
if (_dispatchedNames.add(name)) {
|
|
List<TreeNode> targets = _dispatchTargetCandidates[name];
|
|
if (targets != null) {
|
|
for (int i = 0; i < targets.length; i += 2) {
|
|
_addUsedMember(targets[i], targets[i + 1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Registers the given method as a potential target of dynamic dispatch on
|
|
/// the given class.
|
|
void _addDispatchTarget(Class host, Member member) {
|
|
if (_dispatchedNames.contains(member.name)) {
|
|
_addUsedMember(host, member);
|
|
} else {
|
|
_dispatchTargetCandidates.putIfAbsent(member.name, _makeTreeNodeList)
|
|
..add(host)
|
|
..add(member);
|
|
}
|
|
}
|
|
|
|
static List<TreeNode> _makeTreeNodeList() => <TreeNode>[];
|
|
|
|
/// Registers the given class as instantiated and discovers new dispatch
|
|
/// target candidates accordingly.
|
|
void _addInstantiatedClass(Class classNode) {
|
|
int index = hierarchy.getClassIndex(classNode);
|
|
ClassRetention retention = _classRetention[index];
|
|
if (retention.index < ClassRetention.Instance.index) {
|
|
_classRetention[index] = ClassRetention.Instance;
|
|
_propagateClassInstanceLevel(classNode, retention);
|
|
}
|
|
}
|
|
|
|
/// Register that an external subclass of the given class may be instantiated.
|
|
void _addInstantiatedExternalSubclass(Class classNode) {
|
|
int index = hierarchy.getClassIndex(classNode);
|
|
ClassRetention retention = _classRetention[index];
|
|
if (retention.index < ClassRetention.ExternalInstance.index) {
|
|
_classRetention[index] = ClassRetention.ExternalInstance;
|
|
_propagateClassExternalInstanceLevel(classNode, retention);
|
|
}
|
|
}
|
|
|
|
void _propagateClassExternalInstanceLevel(
|
|
Class classNode, ClassRetention oldRetention) {
|
|
if (oldRetention.index >= ClassRetention.ExternalInstance.index) {
|
|
return;
|
|
}
|
|
_propagateClassInstanceLevel(classNode, oldRetention);
|
|
for (Member member in hierarchy.getInterfaceMembers(classNode)) {
|
|
if (member is Field) {
|
|
_covariantVisitor.visit(member.type);
|
|
} else {
|
|
_addCallToExternalProcedure(member);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Called when the retention level for [classNode] has been raised from
|
|
/// [oldRetention] to instance level.
|
|
///
|
|
/// Ensures that the relevant members are put in the worklist, and super types
|
|
/// and raised to hierarchy level.
|
|
void _propagateClassInstanceLevel(
|
|
Class classNode, ClassRetention oldRetention) {
|
|
if (oldRetention.index >= ClassRetention.Instance.index) {
|
|
return;
|
|
}
|
|
_propagateClassHierarchyLevel(classNode, oldRetention);
|
|
for (Member member in hierarchy.getDispatchTargets(classNode)) {
|
|
_addDispatchTarget(classNode, member);
|
|
}
|
|
for (Member member
|
|
in hierarchy.getDispatchTargets(classNode, setters: true)) {
|
|
_addDispatchTarget(classNode, member);
|
|
}
|
|
// TODO(asgerf): Shake off unused instance fields.
|
|
// For now, just register them all inherited fields as used to ensure the
|
|
// effects of their initializers are taken into account. To shake a field,
|
|
// we still need to preserve the side effects of the initializer.
|
|
for (Class node = classNode; node != null; node = node.superclass) {
|
|
for (Field field in node.mixin.fields) {
|
|
if (!field.isStatic) {
|
|
_addUsedMember(classNode, field);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Called when the retention level for [classNode] has been raised from
|
|
/// [oldRetention] to hierarchy level or higher.
|
|
///
|
|
/// Ensure that all super types and type parameter bounds are also raised
|
|
/// to hierarchy level.
|
|
void _propagateClassHierarchyLevel(
|
|
Class classNode, ClassRetention oldRetention) {
|
|
if (oldRetention.index >= ClassRetention.Hierarchy.index) {
|
|
return;
|
|
}
|
|
_propagateClassNamespaceLevel(classNode, oldRetention);
|
|
var visitor = _visitor;
|
|
classNode.supertype?.accept(visitor);
|
|
classNode.mixedInType?.accept(visitor);
|
|
visitList(classNode.implementedTypes, visitor);
|
|
visitList(classNode.typeParameters, visitor);
|
|
}
|
|
|
|
/// Called when the retention level for [classNode] has been raised from
|
|
/// [oldRetention] to namespace level or higher.
|
|
///
|
|
/// Ensures that all annotations on the class are analyzed.
|
|
void _propagateClassNamespaceLevel(
|
|
Class classNode, ClassRetention oldRetention) {
|
|
if (oldRetention.index >= ClassRetention.Namespace.index) {
|
|
return;
|
|
}
|
|
visitList(classNode.annotations, _visitor);
|
|
}
|
|
|
|
/// Registers the given class as being used in a type annotation.
|
|
void _addClassUsedInType(Class classNode) {
|
|
int index = hierarchy.getClassIndex(classNode);
|
|
ClassRetention retention = _classRetention[index];
|
|
if (retention.index < ClassRetention.Hierarchy.index) {
|
|
_classRetention[index] = ClassRetention.Hierarchy;
|
|
_propagateClassHierarchyLevel(classNode, retention);
|
|
}
|
|
}
|
|
|
|
/// Registers the given class or library as containing static members.
|
|
void _addStaticNamespace(TreeNode container) {
|
|
assert(container is Class || container is Library);
|
|
if (container is Class) {
|
|
int index = hierarchy.getClassIndex(container);
|
|
var oldRetention = _classRetention[index];
|
|
if (oldRetention == ClassRetention.None) {
|
|
_classRetention[index] = ClassRetention.Namespace;
|
|
_propagateClassNamespaceLevel(container, oldRetention);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Registers the given member as being used, in the following sense:
|
|
/// - Fields are used if they can be read or written or their initializer is
|
|
/// evaluated.
|
|
/// - Constructors are used if they can be invoked, either directly or through
|
|
/// the initializer list of another constructor.
|
|
/// - Procedures are used if they can be invoked or torn off.
|
|
void _addUsedMember(Class host, Member member) {
|
|
if (host != null) {
|
|
// Check if the member has been seen with this host before.
|
|
int index = hierarchy.getClassIndex(host);
|
|
Set<Member> members = _usedMembersWithHost[index] ??= new Set<Member>();
|
|
if (!members.add(member)) return;
|
|
_usedMembers.putIfAbsent(member, _makeIncompleteSummary);
|
|
} else {
|
|
// Check if the member has been seen before.
|
|
if (_usedMembers.containsKey(member)) return;
|
|
_usedMembers[member] = _makeIncompleteSummary();
|
|
if (member is! Constructor) {
|
|
_addStaticNamespace(member.parent);
|
|
}
|
|
}
|
|
_worklist..add(host)..add(member);
|
|
if (member is Procedure && member.isExternal) {
|
|
_addCallToExternalProcedure(member);
|
|
}
|
|
}
|
|
|
|
/// Models the impact of a call from user code to an external implementation
|
|
/// of [member] based on its type annotations.
|
|
///
|
|
/// Types in covariant position are assumed to be instantiated externally,
|
|
/// and types in contravariant position are assumed to have their methods
|
|
/// invoked by the external code.
|
|
void _addCallToExternalProcedure(Procedure member) {
|
|
FunctionNode function = member.function;
|
|
_covariantVisitor.visit(function.returnType);
|
|
for (int i = 0; i < function.positionalParameters.length; ++i) {
|
|
_contravariantVisitor.visit(function.positionalParameters[i].type);
|
|
}
|
|
for (int i = 0; i < function.namedParameters.length; ++i) {
|
|
_contravariantVisitor.visit(function.namedParameters[i].type);
|
|
}
|
|
}
|
|
|
|
/// Called when external code may invoke the interface of the given class.
|
|
void _addEscapedClass(Class node) {
|
|
if (!_escapedClasses.add(node)) return;
|
|
for (Member member in hierarchy.getInterfaceMembers(node)) {
|
|
if (member is Procedure) {
|
|
_addDispatchedName(member.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates a incomplete summary object, indicating that a member has not
|
|
/// yet been analyzed.
|
|
static List<Node> _makeIncompleteSummary() => <Node>[null];
|
|
|
|
bool isIncompleteSummary(List<Node> summary) {
|
|
return summary.isNotEmpty && summary[0] == null;
|
|
}
|
|
|
|
void _iterateWorklist() {
|
|
while (_worklist.isNotEmpty) {
|
|
// Get the host and member.
|
|
Member member = _worklist.removeLast();
|
|
Class host = _worklist.removeLast();
|
|
|
|
// Analyze the method body if we have not done so before.
|
|
List<Node> summary = _usedMembers[member];
|
|
if (isIncompleteSummary(summary)) {
|
|
summary.clear();
|
|
_visitor.analyzeAndBuildSummary(member, summary);
|
|
}
|
|
|
|
// Apply the summary in the context of this host.
|
|
for (int i = 0; i < summary.length; ++i) {
|
|
Node summaryNode = summary[i];
|
|
if (summaryNode is Member) {
|
|
_addUsedMember(host, summaryNode);
|
|
} else if (summaryNode is Name) {
|
|
Member target = hierarchy.getDispatchTarget(host, summaryNode);
|
|
if (target != null) {
|
|
_addUsedMember(host, target);
|
|
}
|
|
} else if (identical(summaryNode, _setterSentinel)) {
|
|
Name name = summary[++i];
|
|
Member target = hierarchy.getDispatchTarget(host, name, setter: true);
|
|
if (target != null) {
|
|
_addUsedMember(host, target);
|
|
}
|
|
} else {
|
|
throw 'Unexpected summary node: $summaryNode';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String getDiagnosticString() {
|
|
return """
|
|
dispatchNames: ${_dispatchedNames.length}
|
|
dispatchTargetCandidates.keys: ${_dispatchTargetCandidates.length}
|
|
usedMembersWithHost: ${_usedMembersWithHost.length}
|
|
usedMembers: ${_usedMembers.length}
|
|
classRetention: ${_classRetention.length}
|
|
escapedClasses: ${_escapedClasses.length}
|
|
""";
|
|
}
|
|
}
|
|
|
|
/// Sentinel that occurs in method summaries in front of each name that should
|
|
/// be interpreted as a setter.
|
|
final Node _setterSentinel = const InvalidType();
|
|
|
|
/// Searches the AST for static references and dynamically dispatched names.
|
|
class _TreeShakerVisitor extends RecursiveVisitor {
|
|
final TreeShaker shaker;
|
|
final CoreTypes coreTypes;
|
|
List<Node> summary;
|
|
|
|
_TreeShakerVisitor(TreeShaker shaker)
|
|
: this.shaker = shaker,
|
|
this.coreTypes = shaker.coreTypes;
|
|
|
|
void analyzeAndBuildSummary(Node node, List<Node> summary) {
|
|
this.summary = summary;
|
|
node.accept(this);
|
|
}
|
|
|
|
@override
|
|
visitFunctionNode(FunctionNode node) {
|
|
switch (node.asyncMarker) {
|
|
case AsyncMarker.Sync:
|
|
break;
|
|
case AsyncMarker.SyncStar:
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.iterableClass);
|
|
break;
|
|
case AsyncMarker.Async:
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.futureClass);
|
|
break;
|
|
case AsyncMarker.AsyncStar:
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.streamClass);
|
|
break;
|
|
case AsyncMarker.SyncYielding:
|
|
break;
|
|
}
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
void addUseFrom(Member target, Class from) {
|
|
shaker._addUsedMember(from, target);
|
|
}
|
|
|
|
void addUseFromCurrentHost(Member target) {
|
|
summary.add(target);
|
|
}
|
|
|
|
void addStaticUse(Member target) {
|
|
shaker._addUsedMember(null, target);
|
|
}
|
|
|
|
void addSelfDispatch(Name name, {bool setter: false}) {
|
|
if (setter) {
|
|
summary..add(_setterSentinel)..add(name);
|
|
} else {
|
|
summary.add(name);
|
|
}
|
|
}
|
|
|
|
@override
|
|
visitSuperInitializer(SuperInitializer node) {
|
|
addUseFromCurrentHost(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitRedirectingInitializer(RedirectingInitializer node) {
|
|
addUseFromCurrentHost(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitConstructorInvocation(ConstructorInvocation node) {
|
|
shaker._addInstantiatedClass(node.target.enclosingClass);
|
|
addUseFrom(node.target, node.target.enclosingClass);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitStaticInvocation(StaticInvocation node) {
|
|
addStaticUse(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitDirectMethodInvocation(DirectMethodInvocation node) {
|
|
if (node.receiver is! ThisExpression) {
|
|
// TODO(asgerf): Support arbitrary direct calls.
|
|
throw 'Direct calls are only supported on "this"';
|
|
}
|
|
addUseFromCurrentHost(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitMethodInvocation(MethodInvocation node) {
|
|
if (node.receiver is ThisExpression) {
|
|
addSelfDispatch(node.name);
|
|
} else {
|
|
shaker._addDispatchedName(node.name);
|
|
}
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitStaticGet(StaticGet node) {
|
|
addStaticUse(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitStaticSet(StaticSet node) {
|
|
addStaticUse(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitDirectPropertyGet(DirectPropertyGet node) {
|
|
if (node.receiver is! ThisExpression) {
|
|
// TODO(asgerf): Support arbitrary direct calls.
|
|
throw 'Direct calls are only supported on "this"';
|
|
}
|
|
addUseFromCurrentHost(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitDirectPropertySet(DirectPropertySet node) {
|
|
if (node.receiver is! ThisExpression) {
|
|
// TODO(asgerf): Support arbitrary direct calls.
|
|
throw 'Direct calls are only supported on "this"';
|
|
}
|
|
addUseFromCurrentHost(node.target);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitPropertyGet(PropertyGet node) {
|
|
if (node.receiver is ThisExpression) {
|
|
addSelfDispatch(node.name);
|
|
} else {
|
|
shaker._addDispatchedName(node.name);
|
|
}
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitPropertySet(PropertySet node) {
|
|
if (node.receiver is ThisExpression) {
|
|
addSelfDispatch(node.name, setter: true);
|
|
} else {
|
|
shaker._addDispatchedName(node.name);
|
|
}
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitListLiteral(ListLiteral node) {
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.listClass);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitMapLiteral(MapLiteral node) {
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.mapClass);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
static final Name _toStringName = new Name('toString');
|
|
|
|
@override
|
|
visitStringConcatenation(StringConcatenation node) {
|
|
shaker._addDispatchedName(_toStringName);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitInterfaceType(InterfaceType node) {
|
|
shaker._addClassUsedInType(node.classNode);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
visitDoubleLiteral(DoubleLiteral node) {
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.doubleClass);
|
|
}
|
|
|
|
@override
|
|
visitSymbolLiteral(SymbolLiteral node) {
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.symbolClass);
|
|
// Note: we do not support 'dart:mirrors' right now, so nothing else needs
|
|
// to be done for symbols.
|
|
}
|
|
|
|
@override
|
|
visitTypeLiteral(TypeLiteral node) {
|
|
shaker._addInstantiatedExternalSubclass(coreTypes.typeClass);
|
|
node.visitChildren(this);
|
|
}
|
|
}
|
|
|
|
/// The degree to which a class is needed in a program.
|
|
///
|
|
/// Each level implies those before it.
|
|
enum ClassRetention {
|
|
/// The class can be removed.
|
|
None,
|
|
|
|
/// The class contains used static members but is otherwise unused.
|
|
Namespace,
|
|
|
|
/// The class is used in a type or has an instantiated subtype, or for some
|
|
/// other reason must have its hierarchy information preserved.
|
|
Hierarchy,
|
|
|
|
/// The class is instantiated.
|
|
Instance,
|
|
|
|
/// The class has an instantiated external subclass.
|
|
ExternalInstance,
|
|
}
|
|
|
|
/// Removes classes and members that are not needed.
|
|
///
|
|
/// There must not be any dangling references in the program afterwards.
|
|
class _TreeShakingTransformer extends Transformer {
|
|
final TreeShaker shaker;
|
|
|
|
_TreeShakingTransformer(this.shaker);
|
|
|
|
void transform(Program program) {
|
|
for (var library in program.libraries) {
|
|
if (library.importUri.scheme == 'dart') {
|
|
// As long as patching happens in the backend, we cannot shake off
|
|
// anything in the core libraries.
|
|
continue;
|
|
}
|
|
library.transformChildren(this);
|
|
// Note: we can't shake off empty libraries yet since we don't check if
|
|
// there are private names that use the library.
|
|
}
|
|
}
|
|
|
|
Class visitClass(Class node) {
|
|
switch (shaker.getClassRetention(node)) {
|
|
case ClassRetention.None:
|
|
return null; // Remove the class.
|
|
|
|
case ClassRetention.Namespace:
|
|
// The class is only a namespace for static members. Remove its
|
|
// hierarchy information. This is mandatory, since these references
|
|
// might otherwise become dangling.
|
|
node.supertype = shaker.coreTypes.objectClass.asRawSupertype;
|
|
node.implementedTypes.clear();
|
|
node.typeParameters.clear();
|
|
// Mixin applications cannot have static members.
|
|
assert(node.mixedInType == null);
|
|
// Unused members will be removed below.
|
|
break;
|
|
|
|
case ClassRetention.Hierarchy:
|
|
case ClassRetention.Instance:
|
|
case ClassRetention.ExternalInstance:
|
|
break;
|
|
}
|
|
node.transformChildren(this);
|
|
if (node.constructors.isEmpty && node.procedures.isEmpty) {
|
|
// The VM does not like classes without any members, so ensure there is
|
|
// always a constructor left.
|
|
node.addMember(new Constructor(new FunctionNode(new EmptyStatement())));
|
|
}
|
|
return node;
|
|
}
|
|
|
|
Member defaultMember(Member node) {
|
|
if (!shaker.isMemberUsed(node)) {
|
|
return null; // Remove unused member.
|
|
}
|
|
return node;
|
|
}
|
|
|
|
TreeNode defaultTreeNode(TreeNode node) {
|
|
return node; // Do not traverse into other nodes.
|
|
}
|
|
}
|
|
|
|
class _ExternalTypeVisitor extends DartTypeVisitor {
|
|
final TreeShaker shaker;
|
|
final bool isCovariant;
|
|
final bool isContravariant;
|
|
ClassHierarchy get hierarchy => shaker.hierarchy;
|
|
|
|
_ExternalTypeVisitor(this.shaker,
|
|
{this.isCovariant: false, this.isContravariant: false});
|
|
|
|
void visit(DartType type) => type?.accept(this);
|
|
|
|
/// Analyze [type] with the opposite variance.
|
|
void visitContravariant(DartType type) {
|
|
if (isCovariant && isContravariant) {
|
|
type?.accept(this);
|
|
} else if (isContravariant) {
|
|
type?.accept(shaker._covariantVisitor);
|
|
} else {
|
|
type?.accept(shaker._contravariantVisitor);
|
|
}
|
|
}
|
|
|
|
visitCovariant(DartType type) => type?.accept(this);
|
|
|
|
visitBivariant(DartType type) => shaker._bivariantVisitor.visit(type);
|
|
|
|
visitInvalidType(InvalidType node) {}
|
|
|
|
visitDynamicType(DynamicType node) {
|
|
// TODO(asgerf): Find a suitable model for untyped externals, e.g. track
|
|
// them to the first type boundary.
|
|
}
|
|
|
|
visitVoidType(VoidType node) {}
|
|
|
|
visitInterfaceType(InterfaceType node) {
|
|
if (isCovariant) {
|
|
shaker._addInstantiatedExternalSubclass(node.classNode);
|
|
}
|
|
if (isContravariant) {
|
|
shaker._addEscapedClass(node.classNode);
|
|
}
|
|
for (int i = 0; i < node.typeArguments.length; ++i) {
|
|
DartType typeArgument = node.typeArguments[i];
|
|
// In practice we don't get much out of analyzing variance here, so
|
|
// just use a whitelist of classes that can be seen as covariant
|
|
// for external purposes.
|
|
// TODO(asgerf): Variance analysis might pay off for other external APIs.
|
|
if (isWhitelistedCovariant(node.classNode)) {
|
|
visitCovariant(typeArgument);
|
|
} else {
|
|
visitBivariant(typeArgument);
|
|
}
|
|
}
|
|
}
|
|
|
|
visitFunctionType(FunctionType node) {
|
|
visit(node.returnType);
|
|
for (int i = 0; i < node.positionalParameters.length; ++i) {
|
|
visitContravariant(node.positionalParameters[i]);
|
|
}
|
|
for (int i = 0; i < node.namedParameters.length; ++i) {
|
|
visitContravariant(node.namedParameters[i].type);
|
|
}
|
|
}
|
|
|
|
visitTypeParameterType(TypeParameterType node) {}
|
|
|
|
/// Just treat a couple of whitelisted classes as having covariant type
|
|
/// parameters.
|
|
bool isWhitelistedCovariant(Class classNode) {
|
|
if (classNode.typeParameters.isEmpty) return false;
|
|
CoreTypes coreTypes = shaker.coreTypes;
|
|
return classNode == coreTypes.iteratorClass ||
|
|
classNode == coreTypes.iterableClass ||
|
|
classNode == coreTypes.futureClass ||
|
|
classNode == coreTypes.streamClass ||
|
|
classNode == coreTypes.listClass ||
|
|
classNode == coreTypes.mapClass;
|
|
}
|
|
}
|