dart2js: add initial support for lookup-maps

R=herhut@google.com, sra@google.com

Review URL: https://codereview.chromium.org//1320503003 .
This commit is contained in:
Sigmund Cherem 2015-09-02 17:51:59 -07:00
parent e30f7638e2
commit 2c660aa2f0
28 changed files with 995 additions and 9 deletions

View file

@ -339,6 +339,10 @@ abstract class Backend {
return true;
}
/// Called after the queue is closed. [onQueueEmpty] may be called multiple
/// times, but [onQueueClosed] is only called once.
void onQueueClosed() {}
/// Called after [element] has been resolved.
// TODO(johnniwinther): Change [TreeElements] to [Registry] or a dependency
// node. [elements] is currently unused by the implementation.

View file

@ -112,7 +112,6 @@ class CodegenRegistry extends Registry {
void registerCompileTimeConstant(ConstantValue constant) {
backend.registerCompileTimeConstant(constant, this);
backend.constants.addCompileTimeConstantForEmission(constant);
}
void registerTypeVariableBoundsSubtypeCheck(DartType subtype,
@ -146,6 +145,7 @@ class CodegenRegistry extends Registry {
void registerTypeConstant(ClassElement element) {
backend.customElementsAnalysis.registerTypeConstant(element, world);
backend.lookupMapAnalysis.registerTypeConstant(element);
}
void registerStaticInvocation(Element element) {

View file

@ -1240,6 +1240,7 @@ abstract class Compiler implements DiagnosticListener {
}
emptyQueue(world);
world.queueIsClosed = true;
backend.onQueueClosed();
assert(compilationFailed || world.checkNoEnqueuedInvokedInstanceMethods());
}

View file

@ -233,6 +233,8 @@ class JavaScriptBackend extends Backend {
new Uri(scheme: 'dart', path: '_js_embedded_names');
static final Uri DART_ISOLATE_HELPER =
new Uri(scheme: 'dart', path: '_isolate_helper');
static final Uri PACKAGE_LOOKUP_MAP =
new Uri(scheme: 'package', path: 'lookup_map/lookup_map.dart');
static const String INVOKE_ON = '_getCachedInvocation';
static const String START_ROOT_ISOLATE = 'startRootIsolate';
@ -608,6 +610,9 @@ class JavaScriptBackend extends Backend {
/// constructors for custom elements.
CustomElementsAnalysis customElementsAnalysis;
/// Codegen support for tree-shaking entries of `LookupMap`.
LookupMapAnalysis lookupMapAnalysis;
/// Support for classifying `noSuchMethod` implementations.
NoSuchMethodRegistry noSuchMethodRegistry;
@ -641,6 +646,7 @@ class JavaScriptBackend extends Backend {
compiler, namer, generateSourceMap, useStartupEmitter);
typeVariableHandler = new TypeVariableHandler(compiler);
customElementsAnalysis = new CustomElementsAnalysis(this);
lookupMapAnalysis = new LookupMapAnalysis(this);
noSuchMethodRegistry = new NoSuchMethodRegistry(this);
constantCompilerTask = new JavaScriptConstantTask(compiler);
resolutionCallbacks = new JavaScriptResolutionCallbacks(this);
@ -949,11 +955,23 @@ class JavaScriptBackend extends Backend {
}
}
void registerCompileTimeConstant(ConstantValue constant, Registry registry) {
void registerCompileTimeConstant(ConstantValue constant, Registry registry,
{bool addForEmission: true}) {
registerCompileTimeConstantInternal(constant, registry);
for (ConstantValue dependency in constant.getDependencies()) {
registerCompileTimeConstant(dependency, registry);
if (!registry.isForResolution &&
lookupMapAnalysis.isLookupMap(constant)) {
// Note: internally, this registration will temporarily remove the
// constant dependencies and add them later on-demand.
lookupMapAnalysis.registerLookupMapReference(constant);
}
for (ConstantValue dependency in constant.getDependencies()) {
registerCompileTimeConstant(dependency, registry,
addForEmission: false);
}
if (addForEmission) constants.addCompileTimeConstantForEmission(constant);
}
void registerCompileTimeConstantInternal(ConstantValue constant,
@ -971,6 +989,11 @@ class JavaScriptBackend extends Backend {
} else if (constant.isType) {
enqueueInResolution(getCreateRuntimeType(), registry);
registry.registerInstantiation(typeImplementation.rawType);
TypeConstantValue typeConstant = constant;
DartType representedType = typeConstant.representedType;
if (representedType != const DynamicType()) {
lookupMapAnalysis.registerTypeConstant(representedType.element);
}
}
}
@ -996,7 +1019,7 @@ class JavaScriptBackend extends Backend {
Registry registry) {
assert(registry.isForResolution);
ConstantValue constant = constants.getConstantValueForMetadata(metadata);
registerCompileTimeConstant(constant, registry);
registerCompileTimeConstant(constant, registry, addForEmission: false);
metadataConstants.add(new Dependency(constant, annotatedElement));
}
@ -1129,6 +1152,13 @@ class JavaScriptBackend extends Backend {
}
customElementsAnalysis.registerInstantiatedClass(cls, enqueuer);
if (!enqueuer.isResolutionQueue) {
lookupMapAnalysis.registerInstantiatedClass(cls);
}
}
void registerInstantiatedType(InterfaceType type, Registry registry) {
lookupMapAnalysis.registerInstantiatedType(type, registry);
}
void registerUseInterceptor(Enqueuer enqueuer) {
@ -1438,7 +1468,6 @@ class JavaScriptBackend extends Backend {
constants.getConstantValueForVariable(element);
if (initialValue != null) {
registerCompileTimeConstant(initialValue, work.registry);
constants.addCompileTimeConstantForEmission(initialValue);
// We don't need to generate code for static or top-level
// variables. For instance variables, we may need to generate
// the checked setter.
@ -2133,6 +2162,8 @@ class JavaScriptBackend extends Backend {
jsBuiltinEnum = find(library, 'JsBuiltin');
} else if (uri == Uris.dart_html) {
htmlLibraryIsLoaded = true;
} else if (uri == PACKAGE_LOOKUP_MAP) {
lookupMapAnalysis.initRuntimeClass(find(library, 'LookupMap'));
}
annotations.onLibraryScanned(library);
});
@ -2569,7 +2600,8 @@ class JavaScriptBackend extends Backend {
registerCompileTimeConstant(
dependency.constant,
new CodegenRegistry(compiler,
dependency.annotatedElement.analyzableElement.treeElements));
dependency.annotatedElement.analyzableElement.treeElements),
addForEmission: false);
}
metadataConstants.clear();
}
@ -2577,6 +2609,8 @@ class JavaScriptBackend extends Backend {
return true;
}
void onQueueClosed() => lookupMapAnalysis.onQueueClosed();
void onElementResolved(Element element, TreeElements elements) {
if ((element.isFunction || element.isGenerativeConstructor) &&
annotations.noInline(element)) {

View file

@ -169,7 +169,6 @@ class CustomElementsAnalysisJoin {
ConstantValue constant = makeTypeConstant(classElement);
backend.registerCompileTimeConstant(
constant, compiler.globalDependencies);
backend.constants.addCompileTimeConstantForEmission(constant);
}
}
activeClasses.addAll(newActiveClasses);

View file

@ -88,6 +88,7 @@ import '../world.dart' show
import 'codegen/task.dart';
import 'constant_system_javascript.dart';
import 'patch_resolver.dart';
import 'lookup_map_analysis.dart' show LookupMapAnalysis;
part 'backend.dart';
part 'checked_mode_helpers.dart';

View file

@ -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);
}
}
}

View file

@ -1924,7 +1924,6 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor {
generateConstant(node.constant, node.sourceInformation);
registry.registerCompileTimeConstant(node.constant);
backend.constants.addCompileTimeConstantForEmission(node.constant);
}
visitNot(HNot node) {

View file

@ -11,6 +11,8 @@ dependencies:
sdk_library_metadata:
path: ../../sdk/lib/_internal/sdk_library_metadata
dart2js_info: ^0.0.2
lookup_map:
path: ../lookup_map
# Uncomment if running gclient, so you can depend directly on the downloaded

6
pkg/lookup_map/AUTHORS Normal file
View file

@ -0,0 +1,6 @@
# Below is a list of people and organizations that have contributed
# to the project. Names should be added to the list like so:
#
# Name/Organization <email address>
Google Inc.

View file

@ -0,0 +1,4 @@
# Changelog
## 0.0.1
- Initial version of `LookupMap`

26
pkg/lookup_map/LICENSE Normal file
View file

@ -0,0 +1,26 @@
Copyright 2015, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

29
pkg/lookup_map/README.md Normal file
View file

@ -0,0 +1,29 @@
# Lookup maps
This package contains the definition of `LookupMap`: a simple, but very
restricted map. The map can only hold constant keys and the only way to use the
map is to retrieve values with a key you already have. Expect for lookup, any
other operation in `Map` (like forEach, keys, values, length, etc) is not
available.
Constant `LookupMap`s are understood by dart2js and can be tree-shaken
internally: if a key is not used elsewhere in the program, its entry can be
deleted from the map during compilation without changing the program's behavior.
Currently dart2js supports tree-shaking keys that are Type literals, and any
const expression that can only be created with a const constructor. This means
that primitives, Strings, and constant objects that override the `==` operator
cannot be tree-shaken.
## Examples
`LookupMap` is unlikely going to be useful for individual developers writing
code by hand. It is mainly intended as a helper utility for frameworks that need
to autogenerate data and associate it with a type in the program. For example,
this can be used by a dependency injection system to record how to create
instances of a given type. A dependency injection framework can store in a
`LookupMap` all the information it needs for every injectable type in every
library and package. When compiling a specific application, dart2js can
tree-shake the data of types that are not used by the application. Similarly,
this can also be used by serialization/deserialization packages that can store
in a `LookupMap` the deserialization logic for a given type.

View file

@ -0,0 +1,93 @@
// 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.
/// Defines [LookupMap], a simple map that can be optimized by dart2js.
library lookup_map;
/// [LookupMap] is a simple, but very restricted map. The map can only hold
/// constant keys and the only way to use the map is to retrieve values with a
/// key you already have. Expect for lookup, any other operation in [Map] (like
/// forEach, keys, values, length, etc) is not available.
///
/// Constant [LookupMap]s are understood by dart2js and can be tree-shaken
/// internally: if a key is not used elsewhere in the program, its entry can be
/// deleted from the map during compilation without changing the program's
/// behavior. Currently dart2js supports tree-shaking keys that are `Type`
/// literals, and any const expression that can only be created with a const
/// constructor. This means that primitives, Strings, and constant objects that
/// override the `==` operator cannot be tree-shaken.
///
/// Note: [LookupMap] is unlikely going to be useful for individual developers
/// writing code by hand. It is mainly intended as a helper utility for
/// frameworks that need to autogenerate data and associate it with a type in
/// the program. For example, this can be used by a dependency injection system
/// to record how to create instances of a given type. A dependency injection
/// framework can store in a [LookupMap] all the information it needs for every
/// injectable type in every library and package. When compiling a specific
/// application, dart2js can tree-shake the data of types that are not used by
/// the application. Similarly, this can also be used by
/// serialization/deserialization packages that can store in a [LookupMap] the
/// deserialization logic for a given type.
class LookupMap<K, V> {
/// The key for [LookupMap]s with a single key/value pair.
final K _key;
/// The value for [LookupMap]s with a single key/value pair.
final V _value;
/// List of alternating key-value pairs in the map.
final List _entries;
/// Other maps to which this map delegates lookup operations if the key is not
/// found on [entries]. See [LookupMap]'s constructor for details.
final List<LookupMap<K, V>> _nestedMaps;
/// Creates a lookup-map given a list of key-value pair [entries], and
/// optionally additional entries from other [LookupMap]s.
///
/// When doing a lookup, if the key is not found on [entries]. The lookup will
/// be performed in reverse order of the list of [nestedMaps], so a later
/// entry for a key shadows previous entries. For example, in:
///
/// const map = const LookupMap(const [A, 1],
/// const [const LookupMap(const [A, 2, B, 4]),
/// const LookupMap(const [A, 3, B, 5]));
///
/// `map[A]` returns `1` and `map[B]` returns `5`.
///
/// Note: in the future we expect to change [entries] to be a const map
/// instead of a list of key-value pairs.
// TODO(sigmund): make entries a map once we fix TypeImpl.== (issue #17207).
const LookupMap(List entries, [List<LookupMap<K, V>> nestedMaps = const []])
: _key = null, _value = null, _entries = entries, _nestedMaps = nestedMaps;
/// Creates a lookup map with a single key-value pair.
const LookupMap.pair(K key, V value)
: _key = key, _value = value, _entries = const [], _nestedMaps = const [];
/// Return the data corresponding to [key].
V operator[](K key) {
var map = _flatMap[this];
if (map == null) {
map = {};
_addEntriesTo(map);
_flatMap[this] = map;
}
return map[key];
}
/// Add to [map] entries from [nestedMaps] and from [entries] according to the
/// precedense order described in [nestedMaps].
_addEntriesTo(Map map) {
_nestedMaps.forEach((m) => m._addEntriesTo(map));
for (var i = 0; i < _entries.length; i += 2) {
map[_entries[i]] = _entries[i + 1];
}
if (_key != null) map[_key] = _value;
}
}
/// An expando that stores a flatten version of a [LookupMap], this is
/// computed and stored the first time the map is accessed.
final _flatMap = new Expando('_flat_map');

View file

@ -0,0 +1,3 @@
name: lookup_map
description: a lookup-only map that can be tree-shaken by dart2js
version: 0.0.1

View file

@ -0,0 +1,261 @@
// 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.
library tests.dart2js.lookup_map_test;
import 'package:test/test.dart';
import 'compiler_helper.dart';
main() {
test('live entries are kept', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
const map = const LookupMap(const [
A, "the-text-for-A",
]);
main() => print(map[A]);
""");
expect(generated, contains("the-text-for-A"));
});
test('live entries are kept - single-pair', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
const map = const LookupMap.pair(A, "the-text-for-A");
main() => print(map[A]);
""");
expect(generated, contains("the-text-for-A"));
});
test('unused entries are removed', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const map = const LookupMap(const [
A, "the-text-for-A",
B, "the-text-for-B",
]);
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('unused entries are removed - nested maps', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const map = const LookupMap(const [], const [
const LookupMap(const [
A, "the-text-for-A",
B, "the-text-for-B",
]),
]);
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('unused entries are removed - single-pair', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const map = const LookupMap.pair(A, "the-text-for-A");
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('unused entries are removed - nested single-pair', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const map = const LookupMap(const [], const [
const LookupMap.pair(A, "the-text-for-A"),
const LookupMap.pair(B, "the-text-for-B"),
]);
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('works if entries are declared separate from map', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const entries = const [
A, "the-text-for-A",
B, "the-text-for-B",
];
const map = const LookupMap(entries);
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('escaping entries disable tree-shaking', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
const entries = const [
A, "the-text-for-A",
B, "the-text-for-B",
];
const map = const LookupMap(entries);
main() {
entries.forEach(print);
print(map[A]);
}
""");
expect(generated, contains("the-text-for-B"));
});
test('uses include recursively reachable data', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
class C{}
class D{}
class E{}
const map = const LookupMap(const [
A, const ["the-text-for-A", B],
B, const ["the-text-for-B", C],
C, const ["the-text-for-C"],
D, const ["the-text-for-D", E],
E, const ["the-text-for-E"],
]);
main() => print(map[map[A][1]]);
""");
expect(generated, contains("the-text-for-A"));
expect(generated, contains("the-text-for-B"));
expect(generated, contains("the-text-for-C"));
expect(generated, isNot(contains("the-text-for-D")));
expect(generated, isNot(contains("the-text-for-E")));
});
test('uses are found through newly discovered code', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{ A(B x);}
class B{}
class C{}
class D{}
class E{}
createA() => new A(map[B][1]());
createB() => new B();
const map = const LookupMap(const [
A, const ["the-text-for-A", createA],
B, const ["the-text-for-B", createB],
C, const ["the-text-for-C"],
]);
main() => print(map[A][1]());
""");
expect(generated, contains("the-text-for-A"));
expect(generated, contains("the-text-for-B"));
expect(generated, isNot(contains("the-text-for-C")));
});
test('generic type allocations are considered used', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class M<T>{ get type => T; }
const map = const LookupMap(const [
A, "the-text-for-A",
]);
main() => print(map[new M<A>().type]);
""");
expect(generated, contains("the-text-for-A"));
});
test('generics in type signatures are ignored', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
class M<T>{ get type => T; }
_factory(M<B> t) => t;
const map = const LookupMap(const [
A, const ["the-text-for-A", _factory],
B, "the-text-for-B",
]);
main() => print(map[A]);
""");
expect(generated, isNot(contains("the-text-for-B")));
});
test('metadata is ignored', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{ const A(); }
@A()
class M {}
const map = const LookupMap(const [
A, "the-text-for-A",
]);
main() => print(map[M]);
""");
expect(generated, isNot(contains("the-text-for-A")));
});
test('shared constants used in metadata are ignored', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
const annot = const B(foo: A);
@B(foo: annot)
class A{ const A(); }
class B{ final Type foo; const B({this.foo}); }
class M {}
const map = const LookupMap(const [
A, const ["the-text-for-A", annot]
]);
main() => print(map[M]);
""");
expect(generated, isNot(contains("the-text-for-A")));
});
// regression test for a failure when looking up `dynamic` in a generic.
test('do not choke on dynamic types', () async {
await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class M<T>{ get type => T; }
const map = const LookupMap(const [
A, "the-text-for-A",
]);
main() => print(map[new M<dynamic>().type]);
""");
});
test('support subclassing LookupMap', () async {
String generated = await compileAll(r"""
import 'package:lookup_map/lookup_map.dart';
class A{}
class B{}
class S extends LookupMap {
const S(list) : super(list);
}
const map = const S(const [
A, "the-text-for-A",
B, "the-text-for-B",
]);
main() => print(map[A]);
""");
expect(generated, contains("the-text-for-A"));
expect(generated, isNot(contains("the-text-for-B")));
});
}

View file

@ -134,6 +134,8 @@ class MockCompiler extends Compiler {
}
registerSource(Uris.dart_async,
buildLibrarySource(asyncLibrarySource));
registerSource(JavaScriptBackend.PACKAGE_LOOKUP_MAP,
buildLibrarySource(DEFAULT_LOOKUP_MAP_LIBRARY));
}
String get patchVersion {

View file

@ -398,3 +398,19 @@ const Map<String, String> DEFAULT_MIRRORS_LIBRARY = const <String, String>{
'MirrorSystem': 'class MirrorSystem {}',
'MirrorsUsed': 'class MirrorsUsed {}',
};
const Map<String, String> DEFAULT_LOOKUP_MAP_LIBRARY = const <String, String>{
'LookupMap': r'''
class LookupMap<T> {
final _key;
final _value;
final _entries;
final _nestedMaps;
const LookupMap(this._entries, [this._nestedMaps = const []])
: _key = null, _value = null;
const LookupMap.pair(this._key, this._value)
: _entries = const [], _nestedMaps = const [];
}''',
};

View file

@ -0,0 +1,16 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
const map = const LookupMap(const [], const [
const LookupMap.pair(A, "the-text-for-A"),
const LookupMap.pair(B, "the-text-for-B"),
]);
main() {
Expect.equals(map[A], "the-text-for-A");
}

View file

@ -0,0 +1,16 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
const map = const LookupMap(const [
A, "the-text-for-A",
B, "the-text-for-B",
]);
main() {
Expect.equals(map[A], "the-text-for-A");
}

View file

@ -0,0 +1,23 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{ A(B x);}
class B{}
class C{}
class D{}
class E{}
createA() => new A(map[B][1]());
createB() => new B();
const map = const LookupMap(const [
A, const ["the-text-for-A", createA],
B, const ["the-text-for-B", createB],
C, const ["the-text-for-C"],
]);
main() {
Expect.isTrue(map[A][1]() is A);
}

View file

@ -0,0 +1,18 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
const entries = const [
A, "the-text-for-A",
B, "the-text-for-B",
];
const map = const LookupMap(entries );
main() {
Expect.equals(map[A], 'the-text-for-A');
}

View file

@ -0,0 +1,17 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
const entries = const [
A, "the-text-for-A",
B, "the-text-for-B",
];
const map = const LookupMap(entries);
main() {
entries.forEach(print);
Expect.equals(map[A], 'the-text-for-A');
}

View file

@ -0,0 +1,16 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class M<T>{ get type => T; }
const map = const LookupMap(const [
A, 'the-text-for-A',
]);
main() {
Expect.equals(map[new M<A>().type], 'the-text-for-A');
}

View file

@ -0,0 +1,11 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
const map = const LookupMap.pair(A, "the-text-for-A");
main() {
Expect.equals(map[A], 'the-text-for-A');
}

View file

@ -0,0 +1,13 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
const map = const LookupMap(const [
A, "the-text-for-A",
]);
main() {
Expect.equals(map[A], 'the-text-for-A');
}

View file

@ -0,0 +1,22 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
class C{}
class D{}
class E{}
const map = const LookupMap(const [
A, const ["the-text-for-A", B],
B, const ["the-text-for-B", C],
C, const ["the-text-for-C"],
D, const ["the-text-for-D", E],
E, const ["the-text-for-E"],
]);
main() {
Expect.equals(map[map[A][1]][0], 'the-text-for-B');
}

View file

@ -0,0 +1,19 @@
// 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.
import 'package:lookup_map/lookup_map.dart';
import 'package:expect/expect.dart';
class A{}
class B{}
class S extends LookupMap {
const S(list) : super(list);
}
const map = const S(const [
A, "the-text-for-A",
B, "the-text-for-B",
]);
main() {
Expect.equals(map[A], "the-text-for-A");
}