blob: 2e03a6372fcd8d268c2088caf90b54884e2779b9 [file] [log] [blame]
// Copyright (c) 2020, 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.
// @dart = 2.9
import '../compiler/js_names.dart' as js_ast;
import '../js_ast/js_ast.dart' as js_ast;
import '../js_ast/js_ast.dart' show js;
/// Represents a top-level property hoisted to a top-level object.
class ModuleItemData {
/// The container that holds this module item in the emitted JS.
js_ast.Identifier id;
/// This module item's key in the emitted JS.
///
/// A LiteralString if this object is backed by a JS Object/Map.
/// A LiteralNumber if this object is backed by a JS Array.
js_ast.Literal jsKey;
/// This module item's value in the emitted JS.
js_ast.Expression jsValue;
ModuleItemData(this.id, this.jsKey, this.jsValue);
}
/// Holds variables emitted during code gen.
///
/// Associates a [K] with a container-unique JS key and arbitrary JS value.
/// The container is emitted as a single object:
/// ```
/// var C = {
/// jsKey: jsValue,
/// ...
/// };
/// ```
abstract class ModuleItemContainer<K> {
/// Name of the container in the emitted JS.
String name;
/// Indicates if this table is being used in an incremental context (such as
/// during expression evaluation).
///
/// Set by `emitFunctionIncremental` in kernel/compiler.dart.
bool incrementalMode = false;
/// Refers to the latest container if this container is sharded.
js_ast.Identifier containerId;
/// Refers to the aggregated entrypoint into this container.
///
/// Should only be accessed during expression evaluation since lookups are
/// deoptimized in V8..
js_ast.Identifier aggregatedContainerId;
final Map<K, ModuleItemData> moduleItems = {};
/// Holds keys that will not be emitted when calling [emit].
final Set<K> _noEmit = {};
/// Creates a container with a name, ID, and incremental ID used for
/// expression evaluation.
///
/// If [aggregatedId] is null, the container is not sharded, so the
/// containerId is safe to use during eval.
ModuleItemContainer._(
this.name, this.containerId, js_ast.Identifier aggregatedId)
: aggregatedContainerId = aggregatedId ?? containerId;
/// Creates an automatically sharding container backed by JS Objects.
factory ModuleItemContainer.asObject(String name,
{String Function(K) keyToString}) {
return ModuleItemObjectContainer<K>(name, keyToString);
}
/// Creates a container backed by a JS Array.
factory ModuleItemContainer.asArray(String name) {
return ModuleItemArrayContainer<K>(name);
}
bool get isNotEmpty => moduleItems.isNotEmpty;
Iterable<K> get keys => moduleItems.keys;
int get length => moduleItems.keys.length;
bool get isEmpty => moduleItems.isEmpty;
js_ast.Expression operator [](K key) => moduleItems[key]?.jsValue;
void operator []=(K key, js_ast.Expression value);
/// Returns the expression that retrieves [key]'s corresponding JS value via
/// a property access through its container.
js_ast.Expression access(K key);
bool contains(K key) => moduleItems.containsKey(key);
bool canEmit(K key) => !_noEmit.contains(key);
/// Indicates that [K] should be treated as if it weren't hoisted.
///
/// Used when we are managing the variable declarations manually (such as
/// unhoisting specific symbols for performance reasons).
void setNoEmit(K key) {
_noEmit.add(key);
}
/// Emit the container declaration/initializer, using multiple statements if
/// necessary.
List<js_ast.Statement> emit();
/// Emit the container declaration/initializer incrementally.
///
/// Used during expression evaluation. Appends all newly added types to the
/// aggregated container.
List<js_ast.Statement> emitIncremental();
}
/// Associates a [K] with a container-unique JS key and arbitrary JS value.
///
/// Emitted as a series of JS Objects, splitting them into groups of 500 for
/// JS optimization purposes:
/// ```
/// var C = {
/// jsKey: jsValue,
/// ...
/// };
/// var C$1 = { ... };
/// ```
class ModuleItemObjectContainer<K> extends ModuleItemContainer<K> {
/// Tracks how often JS emitted field names appear.
///
/// [keyToString] may resolve multiple unique keys to the same JS string.
/// When this occurs, the resolved JS string will automatically be renamed.
final Map<String, int> _nameFrequencies = {};
/// Transforms a [K] into a valid name for a JS object property key.
///
/// Non-unique generated strings are automatically renamed.
String Function(K) keyToString;
ModuleItemObjectContainer(String name, this.keyToString)
: super._(
name, js_ast.TemporaryId(name), js_ast.Identifier('${name}\$Eval'));
@override
void operator []=(K key, js_ast.Expression value) {
if (contains(key)) {
moduleItems[key].jsValue = value;
return;
}
// Create a unique name for K when emitted as a JS field.
var fieldString = keyToString(key);
_nameFrequencies.update(fieldString, (v) {
fieldString += '\$${v + 1}';
return v + 1;
}, ifAbsent: () {
// Avoid shadowing common JS properties.
if (js_ast.objectProperties.contains(fieldString)) {
fieldString += '\$';
}
return 0;
});
moduleItems[key] = ModuleItemData(
containerId, js_ast.LiteralString("'$fieldString'"), value);
if (length % 500 == 0) containerId = js_ast.TemporaryId(name);
}
@override
js_ast.Expression access(K key) {
var id = incrementalMode ? aggregatedContainerId : moduleItems[key].id;
return js.call('#.#', [id, moduleItems[key].jsKey]);
}
@override
List<js_ast.Statement> emit() {
var containersToProperties = <js_ast.Identifier, List<js_ast.Property>>{};
moduleItems.forEach((k, v) {
if (_noEmit.contains(k)) return;
if (!containersToProperties.containsKey(v.id)) {
containersToProperties[v.id] = <js_ast.Property>[];
}
containersToProperties[v.id].add(js_ast.Property(v.jsKey, v.jsValue));
});
// Emit a self-reference for the next container so V8 does not optimize it
// away. Required for expression evaluation.
if (containersToProperties[containerId] == null) {
containersToProperties[containerId] = [
js_ast.Property(
js_ast.LiteralString('_'), js.call('() => #', [containerId]))
];
}
var statements = <js_ast.Statement>[];
var aggregatedContainers = <js_ast.Expression>[];
containersToProperties.forEach((containerId, properties) {
var containerObject = js_ast.ObjectInitializer(properties,
multiline: properties.length > 1);
statements.add(js.statement('var # = #', [containerId, containerObject]));
aggregatedContainers.add(js.call('#', [containerId]));
});
// Create an aggregated access point over all containers for eval.
statements.add(js.statement('var # = Object.assign({_ : () => #}, #)',
[aggregatedContainerId, aggregatedContainerId, aggregatedContainers]));
return statements;
}
/// Appends all newly added types to the most recent container.
@override
List<js_ast.Statement> emitIncremental() {
assert(incrementalMode);
var statements = <js_ast.Statement>[];
moduleItems.forEach((k, v) {
if (_noEmit.contains(k)) return;
statements.add(js
.statement('#[#] = #', [aggregatedContainerId, v.jsKey, v.jsValue]));
});
return statements;
}
}
/// Associates a unique [K] with an arbitrary JS value.
///
/// Emitted as a JS Array:
/// ```
/// var C = [
/// jsValue,
/// ...
/// ];
/// ```
class ModuleItemArrayContainer<K> extends ModuleItemContainer<K> {
ModuleItemArrayContainer(String name)
: super._(name, js_ast.TemporaryId(name), null);
@override
void operator []=(K key, js_ast.Expression value) {
if (moduleItems.containsKey(key)) {
moduleItems[key].jsValue = value;
return;
}
moduleItems[key] =
ModuleItemData(containerId, js_ast.LiteralNumber('$length'), value);
}
@override
js_ast.Expression access(K key) {
var id = incrementalMode ? aggregatedContainerId : containerId;
return js.call('#[#]', [id, moduleItems[key].jsKey]);
}
@override
List<js_ast.Statement> emit() {
var properties = List<js_ast.Expression>.filled(length, null);
// If the entire array holds just one value, generate a short initializer.
var valueSet = <js_ast.Expression>{};
moduleItems.forEach((k, v) {
if (_noEmit.contains(k)) return;
valueSet.add(v.jsValue);
properties[int.parse((v.jsKey as js_ast.LiteralNumber).value)] =
v.jsValue;
});
if (valueSet.length == 1 && moduleItems.length > 1) {
return [
js.statement('var # = Array(#).fill(#)', [
containerId,
js_ast.LiteralNumber('${properties.length}'),
valueSet.first
])
];
}
// Array containers are not sharded, as we do not expect to hit V8's
// dictionary-mode limit of 99999 elements.
return [
js.statement('var # = #', [
containerId,
js_ast.ArrayInitializer(properties, multiline: properties.length > 1)
])
];
}
@override
List<js_ast.Statement> emitIncremental() {
assert(incrementalMode);
var statements = <js_ast.Statement>[];
moduleItems.forEach((k, v) {
if (_noEmit.contains(k)) return;
statements.add(js
.statement('#[#] = #', [aggregatedContainerId, v.jsKey, v.jsValue]));
});
return statements;
}
}