Index: pkg/compiler/lib/src/js_backend/lookup_map_analysis.dart |
diff --git a/pkg/compiler/lib/src/js_backend/lookup_map_analysis.dart b/pkg/compiler/lib/src/js_backend/lookup_map_analysis.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9d44af11ec373cf2d4c3d1b93404f22f2c478193 |
--- /dev/null |
+++ b/pkg/compiler/lib/src/js_backend/lookup_map_analysis.dart |
@@ -0,0 +1,335 @@ |
+// Copyright (c) 2015, 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. |
+ |
+/// Analysis to determine how to generate code for `LookupMap`s. |
+library compiler.src.js_backend.lookup_map_analysis; |
+ |
+import '../common/registry.dart' show Registry; |
+import '../compiler.dart' show Compiler; |
+import '../constants/values.dart' show |
+ ConstantValue, |
+ ConstructedConstantValue, |
+ ListConstantValue, |
+ NullConstantValue, |
+ TypeConstantValue; |
+import '../dart_types.dart' show DartType; |
+import '../elements/elements.dart' show Elements, Element, ClassElement, |
+ FieldElement, FunctionElement, FunctionSignature; |
+import '../enqueue.dart' show Enqueuer; |
+import 'js_backend.dart' show JavaScriptBackend; |
+import '../dart_types.dart' show DynamicType, InterfaceType; |
+ |
+/// An analysis and optimization to remove unused entries from a `LookupMap`. |
+/// |
+/// `LookupMaps` are defined in `package:lookup_map/lookup_map.dart`. They are |
+/// simple maps that contain constant expressions as keys, and that only support |
+/// the lookup operation. |
+/// |
+/// This analysis and optimization will tree-shake the contents of the maps by |
+/// looking at the program and finding which keys are clearly unused. Not all |
+/// constants can be approximated statically, so this optimization is limited to |
+/// the following keys: |
+/// |
+/// * Const expressions that can only be created via const constructors. This |
+/// excludes primitives, strings, and any const type that overrides the == |
+/// operator. |
+/// |
+/// * Type literals. |
+/// |
+/// Type literals are more complex than const expressions because they can be |
+/// created in multiple ways. We can approximate the possible set of keys if we |
+/// follow these rules: |
+/// |
+/// * Include all type-literals used explicitly in the code (excluding |
+/// obviously the uses that can be removed from LookupMaps) |
+/// |
+/// * Include every reflectable type-literal if a mirror API is used to create |
+/// types (e.g. ClassMirror.reflectedType). |
+/// |
+/// * Include all allocated types if the program contains `e.runtimeType` |
+/// expressions. |
+/// |
+/// * Include all generic-type arguments, if the program uses type |
+/// variables in expressions such as `class A<T> { Type get extract => T }`. |
+/// |
+// TODO(sigmund): add support for const expressions, currently this |
+// implementation only supports Type literals. To support const expressions we |
+// need to change some of the invariants below (e.g. we can no longer use the |
+// ClassElement of a type to refer to keys we need to discover). |
+// TODO(sigmund): detect uses of mirrors |
+class LookupMapAnalysis { |
+ /// Reference to [JavaScriptBackend] to be able to enqueue work when we |
+ /// discover that a key in a map is potentially used. |
+ final JavaScriptBackend backend; |
+ |
+ /// The resolved [ClassElement] associated with `LookupMap`. |
+ ClassElement typeLookupMapClass; |
+ |
+ /// The resolved [FieldElement] for `LookupMap._entries`. |
+ FieldElement entriesField; |
+ |
+ /// The resolved [FieldElement] for `LookupMap._key`. |
+ FieldElement keyField; |
+ |
+ /// The resolved [FieldElement] for `LookupMap._value`. |
+ FieldElement valueField; |
+ |
+ /// Constant instances of `LookupMap` and information about them tracked by |
+ /// this analysis. |
+ final Map<ConstantValue, _LookupMapInfo> _lookupMaps = {}; |
+ |
+ /// Types that we have discovered to be in use in the program. |
+ final _inUse = new Set<ClassElement>(); |
+ |
+ /// Pending work to do if we discover that a new type is in use. For each type |
+ /// that we haven't seen, we record the list of lookup-maps that use such type |
+ /// as a key. |
+ final _pending = <ClassElement, List<_LookupMapInfo>>{}; |
+ |
+ /// Whether the backend is currently processing the codegen queue. |
+ // TODO(sigmund): is there a better way to do this. Do we need to plumb the |
+ // enqueuer on each callback? |
+ bool get _inCodegen => backend.compiler.phase == Compiler.PHASE_COMPILING; |
+ |
+ LookupMapAnalysis(this.backend); |
+ |
+ /// Whether this analysis and optimization is enabled. |
+ bool get _isEnabled { |
+ // `lookupMap==off` kept here to make it easy to test disabling this feature |
+ if (const String.fromEnvironment('lookupMap') == 'off') return false; |
+ return typeLookupMapClass != null; |
+ } |
+ |
+ /// Initializes this analysis by providing the resolver information of |
+ /// `LookupMap`. |
+ void initRuntimeClass(ClassElement cls) { |
+ cls.computeType(backend.compiler); |
+ entriesField = cls.lookupMember('_entries'); |
+ keyField = cls.lookupMember('_key'); |
+ valueField = cls.lookupMember('_value'); |
+ // TODO(sigmund): Maybe inline nested maps make the output code smaller? |
+ typeLookupMapClass = cls; |
+ } |
+ |
+ /// Whether [constant] is an instance of a `LookupMap`. |
+ bool isLookupMap(ConstantValue constant) => |
+ _isEnabled && |
+ constant is ConstructedConstantValue && |
+ constant.type.asRaw().element.isSubclassOf(typeLookupMapClass); |
+ |
+ /// Registers an instance of a lookup-map with the analysis. |
+ void registerLookupMapReference(ConstantValue lookupMap) { |
+ if (!_isEnabled || !_inCodegen) return; |
+ assert(isLookupMap(lookupMap)); |
+ _lookupMaps.putIfAbsent(lookupMap, |
+ () => new _LookupMapInfo(lookupMap, this).._updateUsed()); |
+ } |
+ |
+ /// Records that [type] is used in the program, and updates every map that |
+ /// has it as a key. |
+ void _addUse(ClassElement type) { |
+ if (_inUse.add(type)) { |
+ _pending[type]?.forEach((info) => info._markUsed(type)); |
+ _pending.remove(type); |
+ } |
+ } |
+ |
+ /// Callback from the enqueuer, invoked when [element] is instantiated. |
+ void registerInstantiatedClass(ClassElement element) { |
+ if (!_isEnabled || !_inCodegen) return; |
+ // TODO(sigmund): only add if .runtimeType is ever used |
+ _addUse(element); |
+ } |
+ |
+ /// Callback from the enqueuer, invoked when [type] is instantiated. |
+ void registerInstantiatedType(InterfaceType type, Registry registry) { |
+ if (!_isEnabled || !_inCodegen) return; |
+ // TODO(sigmund): only add if .runtimeType is ever used |
+ _addUse(type.element); |
+ // TODO(sigmund): only do this when type-argument expressions are used? |
+ _addGenerics(type, registry); |
+ } |
+ |
+ /// Records generic type arguments in [type], in case they are retrieved and |
+ /// returned using a type-argument expression. |
+ void _addGenerics(InterfaceType type, Registry registry) { |
+ if (!type.isGeneric) return; |
+ for (var arg in type.typeArguments) { |
+ if (arg is InterfaceType) { |
+ _addUse(arg.element); |
+ // Note: this call was needed to generate correct code for |
+ // type_lookup_map/generic_type_test |
+ // TODO(sigmund): can we get rid of this? |
+ backend.registerInstantiatedConstantType( |
+ backend.typeImplementation.rawType, registry); |
+ _addGenerics(arg, registry); |
+ } |
+ } |
+ } |
+ |
+ /// Callback from the codegen enqueuer, invoked when a type constant |
+ /// corresponding to the [element] is used in the program. |
+ void registerTypeConstant(Element element) { |
+ if (!_isEnabled || !_inCodegen) return; |
+ assert(element.isClass); |
+ _addUse(element); |
+ } |
+ |
+ /// Callback from the backend, invoked when reaching the end of the enqueuing |
+ /// process, but before emitting the code. At this moment we have discovered |
+ /// all types used in the program and we can tree-shake anything that is |
+ /// unused. |
+ void onQueueClosed() { |
+ if (!_isEnabled || !_inCodegen) return; |
+ |
+ _lookupMaps.values.forEach((info) { |
+ assert (!info.emitted); |
+ info.emitted = true; |
+ info._prepareForEmission(); |
+ }); |
+ |
+ // When --verbose is passed, we show the total number and set of keys that |
+ // were tree-shaken from lookup maps. |
+ Compiler compiler = backend.compiler; |
+ if (compiler.verbose) { |
+ var sb = new StringBuffer(); |
+ int count = 0; |
+ for (var info in _lookupMaps.values) { |
+ for (var key in info.unusedEntries.keys) { |
+ if (count != 0) sb.write(','); |
+ sb.write(key.unparse()); |
+ count++; |
+ } |
+ } |
+ compiler.log(count == 0 |
+ ? 'lookup-map: nothing was tree-shaken' |
+ : 'lookup-map: found $count unused keys ($sb)'); |
+ } |
+ } |
+} |
+ |
+/// Internal information about the entries on a lookup-map. |
+class _LookupMapInfo { |
+ /// The original reference to the constant value. |
+ /// |
+ /// This reference will be mutated in place to remove it's entries when the |
+ /// map is first seen during codegen, and to restore them (or a subset of |
+ /// them) when we have finished discovering which entries are used. This has |
+ /// the side-effect that `orignal.getDependencies()` will be empty during |
+ /// most of codegen until we are ready to emit the constants. However, |
+ /// restoring the entries before emitting code lets us keep the emitter logic |
+ /// agnostic of this optimization. |
+ final ConstructedConstantValue original; |
+ |
+ /// Reference to the lookup map analysis to be able to refer to data shared |
+ /// accross infos. |
+ final LookupMapAnalysis analysis; |
+ |
+ /// Whether we have already emitted this constant. |
+ bool emitted = false; |
+ |
+ /// Whether the `LookupMap` constant was built using the `LookupMap.pair` |
+ /// constructor. |
+ bool singlePair; |
+ |
+ /// Entries in the lookup map whose keys have not been seen in the rest of the |
+ /// program. |
+ Map<ClassElement, ConstantValue> unusedEntries = |
+ <ClassElement, ConstantValue>{}; |
+ |
+ /// Entries that have been used, and thus will be part of the generated code. |
+ Map<ClassElement, ConstantValue> usedEntries = |
+ <ClassElement, ConstantValue>{}; |
+ |
+ /// Internal helper to memoize the mapping between map class elements and |
+ /// their corresponding type constants. |
+ Map<ClassElement, TypeConstantValue> _typeConstants = |
+ <ClassElement, TypeConstantValue>{}; |
+ |
+ /// Creates and initializes the information containing all keys of the |
+ /// original map marked as unused. |
+ _LookupMapInfo(this.original, this.analysis) { |
+ ConstantValue key = original.fields[analysis.keyField]; |
+ singlePair = !key.isNull; |
+ |
+ if (singlePair) { |
+ TypeConstantValue typeKey = key; |
+ ClassElement cls = typeKey.representedType.element; |
+ _typeConstants[cls] = typeKey; |
+ unusedEntries[cls] = original.fields[analysis.valueField]; |
+ |
+ // Note: we modify the constant in-place, see comment in [original]. |
+ original.fields[analysis.keyField] = new NullConstantValue(); |
+ original.fields[analysis.valueField] = new NullConstantValue(); |
+ } else { |
+ ListConstantValue list = original.fields[analysis.entriesField]; |
+ List<ConstantValue> keyValuePairs = list.entries; |
+ for (int i = 0; i < keyValuePairs.length; i += 2) { |
+ TypeConstantValue type = keyValuePairs[i]; |
+ ClassElement cls = type.representedType.element; |
+ if (cls == null || !cls.isClass) { |
+ // TODO(sigmund): report an error |
+ continue; |
+ } |
+ _typeConstants[cls] = type; |
+ unusedEntries[cls] = keyValuePairs[i + 1]; |
+ } |
+ |
+ // Note: we modify the constant in-place, see comment in [original]. |
+ original.fields[analysis.entriesField] = |
+ new ListConstantValue(list.type, []); |
+ } |
+ } |
+ |
+ /// Check every key in unusedEntries and mark it as used if the analysis has |
+ /// already discovered them. This is meant to be called once to finalize |
+ /// initialization after constructing an instance of this class. Afterwards, |
+ /// we call [_markUsed] on each individual key as it gets discovered. |
+ void _updateUsed() { |
+ // Note: we call toList because `_markUsed` modifies the map. |
+ for (ClassElement type in unusedEntries.keys.toList()) { |
+ if (analysis._inUse.contains(type)) { |
+ _markUsed(type); |
+ } else { |
+ analysis._pending.putIfAbsent(type, () => []).add(this); |
+ } |
+ } |
+ } |
+ |
+ /// Marks that [type] is a key that has been seen, and thus, the corresponding |
+ /// entry in this map should be considered reachable. |
+ void _markUsed(ClassElement type) { |
+ assert(!emitted); |
+ assert(unusedEntries.containsKey(type)); |
+ assert(!usedEntries.containsKey(type)); |
+ ConstantValue constant = unusedEntries.remove(type); |
+ usedEntries[type] = constant; |
+ analysis.backend.registerCompileTimeConstant(constant, |
+ analysis.backend.compiler.globalDependencies, |
+ addForEmission: false); |
+ } |
+ |
+ /// Restores [original] to contain all of the entries marked as possibly used. |
+ void _prepareForEmission() { |
+ ListConstantValue originalEntries = original.fields[analysis.entriesField]; |
+ DartType listType = originalEntries.type; |
+ List<ConstantValue> keyValuePairs = <ConstantValue>[]; |
+ usedEntries.forEach((key, value) { |
+ keyValuePairs.add(_typeConstants[key]); |
+ keyValuePairs.add(value); |
+ }); |
+ |
+ // Note: we are restoring the entries here, see comment in [original]. |
+ if (singlePair) { |
+ assert (keyValuePairs.length == 0 || keyValuePairs.length == 2); |
+ if (keyValuePairs.length == 2) { |
+ original.fields[analysis.keyField] = keyValuePairs[0]; |
+ original.fields[analysis.valueField] = keyValuePairs[1]; |
+ } |
+ } else { |
+ original.fields[analysis.entriesField] = |
+ new ListConstantValue(listType, keyValuePairs); |
+ } |
+ } |
+} |