blob: bd33595d2e8885218cb405893a1f59983890d801 [file] [log] [blame]
// 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;
}
}