blob: 87bb36be645548cdcdaa4f0024c788dc18737ae7 [file] [log] [blame]
// Copyright (c) 2019, 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.10
/// TypeReferences are 'holes' in the generated JavaScript that are filled in by
/// the emitter with code to access a type.
///
/// The Dart code
///
/// foo1() => bar<int>(X<String>());
///
/// might be compiled to something like the following, with TypeReference1
/// referring to the constructed type `X<String>`, and TypeReference2 referring
/// to the method type argument `int`:
///
/// foo1: function() {
/// return bar(new X(TypeReference1), TypeReference2);
/// }
///
/// The dart method `foo2` would be compiled separately, with the generated code
/// containing TypeReference3 referring to `int`:
///
/// foo2() => bar<int>(null);
/// -->
/// foo2: function() {
/// return bar(null, TypeReference3);
/// }
///
/// When the code for an output unit (main unit or deferred loaded unit) is
/// assembled, there will also be a TypeReferenceResource 'hole', so the
/// assembled looks something like
///
/// foo: function() {
/// return bar(new X(TypeReference1), TypeReference2);
/// }
/// foo2: function() {
/// return bar(null, TypeReference3);
/// }
/// ...
/// TypeReferenceResource
///
/// The TypeReferenceFinalizer decides on a strategy for accessing the types. In
/// most cases it is best to precompute the types and access them via a
/// object. The TypeReference nodes are filled in with property access
/// expressions and the TypeReferenceResource is filled in with the precomputed
/// data, something like:
///
/// foo1: function() {
/// return bar(new X(type$.X_String), type$.int);
/// }
/// foo2: function() {
/// return bar(null, type$.int);
/// }
/// ...
/// var type$ = {
/// int: findType("int"),
/// X_String: findType("X<String>")
/// };
///
/// In minified mode, the properties `int` and `X_String` can be replaced by
/// shorter names.
library js_backend.type_reference;
import 'package:front_end/src/api_unstable/dart2js.dart'
show $0, $9, $A, $Z, $_, $a, $z;
import '../common/elements.dart' show CommonElements;
import '../elements/types.dart';
import '../js/js.dart' as js;
import '../js_emitter/code_emitter_task.dart' show Emitter;
import '../js_model/type_recipe.dart'
show
TypeRecipe,
TypeExpressionRecipe,
SingletonTypeEnvironmentRecipe,
FullTypeEnvironmentRecipe;
import '../serialization/serialization.dart';
import '../util/util.dart' show Hashing;
import 'frequency_assignment.dart';
import 'namer.dart';
import 'runtime_types_new.dart' show RecipeEncoder;
/// Run the minifier for 'type$' property names even in non-minified mode,
/// making a name from minified name and the readable name. Usage:
///
/// DART_VM_OPTIONS='-DDebugMinifyTypesHolder=true' dart2js ...
///
const _debugMinify = bool.fromEnvironment('DebugMinifyTypesHolder');
/// A [TypeReference] is a deferred JavaScript expression that refers to the
/// runtime representation of a ground type or ground type environment. The
/// deferred expression is filled in by the TypeReferenceFinalizer which is
/// called from the fragment emitter. The replacement expression could be any
/// expression, e.g. a call, or a reference to a variable, or property of a
/// variable.
class TypeReference extends js.DeferredExpression implements js.AstContainer {
static const String tag = 'type-reference';
/// [typeRecipe] is a recipe for a ground type or type environment.
final TypeRecipe typeRecipe;
// TODO(sra): Refine the concept of reference context and replace the
// 'forConstant' and 'forLazyInitializer' flags.
// `true` if TypeReference is in code that initializes constant value.
bool forConstant = false;
// `true` if TypeReference is in code for a lazy initializer of a static
// variable.
bool forLazyInitializer = false;
js.Expression _value;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
TypeReference(this.typeRecipe) : sourceInformation = null;
TypeReference._(this.typeRecipe, this._value, this.sourceInformation);
factory TypeReference.readFromDataSource(DataSourceReader source) {
source.begin(tag);
TypeRecipe recipe = source.readTypeRecipe();
bool forLazyInitializer = source.readBool();
source.end(tag);
return TypeReference(recipe)..forLazyInitializer = forLazyInitializer;
}
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeTypeRecipe(typeRecipe);
sink.writeBool(forLazyInitializer);
sink.end(tag);
}
set value(js.Expression value) {
assert(!isFinalized && value != null);
_value = value;
}
@override
js.Expression get value {
assert(isFinalized, 'TypeReference is unassigned');
return _value;
}
@override
bool get isFinalized => _value != null;
// Precedence will be CALL or LEFT_HAND_SIDE depending on what expression the
// reference is resolved to.
@override
int get precedenceLevel => value.precedenceLevel;
@override
TypeReference withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return TypeReference._(typeRecipe, _value, newSourceInformation);
}
@override
Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
@override
String nonfinalizedDebugText() {
TypeRecipe typeRecipe = this.typeRecipe;
if (typeRecipe is TypeExpressionRecipe) {
return 'TypeReference"${typeRecipe.type.toString()}"';
}
return super.nonfinalizedDebugText();
}
}
/// A [TypeReferenceResource] is a deferred JavaScript statement determined by
/// the finalization of type references. It is the injection point for data or
/// code to support type references. For example, if the
/// [TypeReferenceFinalizer] decides that type should be referred to via a
/// variable, the [TypeReferenceResource] would be set to code that declares and
/// initializes the variable.
class TypeReferenceResource extends js.DeferredStatement
implements js.AstContainer {
js.Statement _statement;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
TypeReferenceResource() : sourceInformation = null;
TypeReferenceResource._(this._statement, this.sourceInformation);
set statement(js.Statement statement) {
assert(!isFinalized && statement != null);
_statement = statement;
}
@override
js.Statement get statement {
assert(isFinalized, 'TypeReferenceResource is unassigned');
return _statement;
}
@override
bool get isFinalized => _statement != null;
@override
TypeReferenceResource withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return TypeReferenceResource._(_statement, newSourceInformation);
}
@override
Iterable<js.Node> get containedNodes => isFinalized ? [_statement] : const [];
@override
void visitChildren<T>(js.NodeVisitor<T> visitor) {
_statement?.accept<T>(visitor);
}
@override
void visitChildren1<R, A>(js.NodeVisitor1<R, A> visitor, A arg) {
_statement?.accept1<R, A>(visitor, arg);
}
}
abstract class TypeReferenceFinalizer {
/// Collects TypeReference and TypeReferenceResource nodes from the JavaScript
/// AST [code];
void addCode(js.Node code);
/// Performs analysis on all collected TypeReference nodes finalizes the
/// values to expressions to access the types.
void finalize();
}
class TypeReferenceFinalizerImpl implements TypeReferenceFinalizer {
final Emitter _emitter;
final CommonElements _commonElements;
final RecipeEncoder _recipeEncoder;
final bool _minify;
/*late final*/ _TypeReferenceCollectorVisitor _visitor;
TypeReferenceResource _resource;
/// Maps the recipe (type expression) to the references with the same recipe.
/// Much of the algorithm's state is stored in the _ReferenceSet objects.
final Map<TypeRecipe, _ReferenceSet> _referencesByRecipe = {};
TypeReferenceFinalizerImpl(
this._emitter, this._commonElements, this._recipeEncoder, this._minify) {
_visitor = _TypeReferenceCollectorVisitor(this);
}
@override
void addCode(js.Node code) {
code.accept(_visitor);
}
@override
void finalize() {
assert(_resource != null, 'TypeReferenceFinalizer needs resource');
_allocateNames();
_updateReferences();
}
// Called from collector visitor.
void _registerTypeReference(TypeReference node) {
TypeRecipe recipe = node.typeRecipe;
_ReferenceSet refs = _referencesByRecipe[recipe] ??= _ReferenceSet(recipe);
refs.count++;
if (node.forConstant) refs.countInConstant++;
if (node.forLazyInitializer) refs.countInLazyInitializer++;
refs._references.add(node);
}
// Called from collector visitor.
void _registerTypeReferenceResource(TypeReferenceResource node) {
assert(_resource == null);
_resource = node;
}
void _updateReferences() {
js.Expression helperAccess =
_emitter.staticFunctionAccess(_commonElements.findType);
js.Expression loadTypeCall(TypeRecipe recipe, String helperLocal) {
js.Expression recipeExpression =
_recipeEncoder.encodeGroundRecipe(_emitter, recipe);
return js.js(r'#(#)', [helperLocal ?? helperAccess, recipeExpression]);
}
// Emit generate-at-use references.
for (_ReferenceSet referenceSet in _referencesByRecipe.values) {
if (referenceSet.generateAtUse) {
TypeRecipe recipe = referenceSet.recipe;
js.Expression reference = loadTypeCall(recipe, null);
for (TypeReference ref in referenceSet._references) {
ref.value = reference;
}
}
}
List<_ReferenceSet> referenceSetsUsingProperties =
_referencesByRecipe.values.where((ref) => !ref.generateAtUse).toList();
// Sort by name (which is unique and mostly stable) so that similar recipes
// are grouped together.
referenceSetsUsingProperties.sort((a, b) => a.name.compareTo(b.name));
// We can generate a literal with calls to H.findType (minified to typically
// e.g. H.xy) or cache H.findType in a local in a scope created by an IIFE.
// Doing so saves 2-3 bytes per entry, but with an overhead of 30+ bytes for
// the IIFE. So it is smaller to use the IIFE only for over 10 or so types.
const minUseIIFE = 10;
String helperLocal =
referenceSetsUsingProperties.length < minUseIIFE ? null : 'findType';
List<js.Property> properties = [];
for (_ReferenceSet referenceSet in referenceSetsUsingProperties) {
TypeRecipe recipe = referenceSet.recipe;
var propertyName = js.string(referenceSet.propertyName);
properties
.add(js.Property(propertyName, loadTypeCall(recipe, helperLocal)));
var access = js.js('#.#', [typesHolderLocalName, propertyName]);
for (TypeReference ref in referenceSet._references) {
ref.value = access;
}
}
if (properties.isEmpty) {
_resource.statement = js.Block.empty();
} else {
js.Expression initializer =
js.ObjectInitializer(properties, isOneLiner: false);
if (helperLocal != null) {
// A named IIFE helps attribute startup time in profiling.
var function = js.js(r'function rtii(){var # = #; return #}',
[js.VariableDeclaration(helperLocal), helperAccess, initializer]);
initializer = js.js('#()', js.Parentheses(function));
}
_resource.statement = js.js.statement(r'var # = #',
[js.VariableDeclaration(typesHolderLocalName), initializer]);
}
}
// This is a top-level local name in the generated JavaScript top-level
// function, so will be minified automatically. The name should not collide
// with any other locals.
static const typesHolderLocalName = r'type$';
void _allocateNames() {
// Filter out generate-at-use cases and allocate unique names to the rest.
List<_ReferenceSet> referencesInTable = [];
Set<String> usedNames = {};
for (_ReferenceSet referenceSet in _referencesByRecipe.values) {
// - If a type is used only once from a constant then the findType can be
// a subexpression of constant since it will be evaluated exactly once and
// need not be stored anywhere else.
if (referenceSet.count == 1 && referenceSet.countInConstant == 1) {
continue;
}
// - Lazy initializers are usually evaluated on demand and only once, so
// it is worth deferrering evaluating the type references until the lazy
// initializer is executed, provided it does not increase program
// size too much.
//
// Assuming minification in a large program, the size for precomputed is
//
// abc:f("TTT"),/*once*/ + type$.abc/*N times*/
//
// i.e. 13 + 5N. The size for repeated generate-at-use is
//
// H.lookupType("TTT") /*N times*/
//
// i.e. 10N. Repeated is smaller when 10N < 13+5N, or N < 2.6. Since we
// get a startup benefit of not evaluating the type recipe, lets round
// up. Note that we don't know the size of the recipe ("TTT" assumes an
// infrequently referenced non-generic interface type), but if the recipe
// is larger, it is in a string so the program has the same number of
// tokens and the extra bytes will be parsed efficiently.
const int maxRepeatedLookups = 3;
if (referenceSet.countInLazyInitializer == referenceSet.count &&
referenceSet.count <= maxRepeatedLookups) {
continue;
}
// TODO(sra): There are other contexts that would be beneficial, e.g. a
// type reference occurring only in a throw expression.
String suggestedName = _RecipeToIdentifier().run(referenceSet.recipe);
if (usedNames.contains(suggestedName)) {
for (int i = 2; true; i++) {
String next = '${suggestedName}_$i';
if (usedNames.contains(next)) continue;
suggestedName = next;
break;
}
}
usedNames.add(suggestedName);
referenceSet.name = suggestedName;
referencesInTable.add(referenceSet);
}
if (!_minify && !_debugMinify) {
// For unminified code, use the characteristic names as property names.
// TODO(sra): Some of these names are long. We could truncate the names
// after the unique prefix.
for (_ReferenceSet referenceSet in referencesInTable) {
referenceSet.propertyName = referenceSet.name;
}
return;
}
// Step 2. Sort by frequency to arrange common entries have shorter property
// names.
List<_ReferenceSet> referencesByFrequency = referencesInTable.toList()
..sort((a, b) {
assert(a.name != b.name);
int r = b.count.compareTo(a.count); // Decreasing frequency.
if (r != 0) return r;
return a.name.compareTo(b.name); // Tie-break with characteristic name.
});
for (var referenceSet in referencesByFrequency) {
referenceSet.hash = _hashCharacteristicString(referenceSet.name);
}
int hashOf(int index) => referencesByFrequency[index].hash;
int countOf(int index) => referencesByFrequency[index].count;
void assign(int index, String name) {
if (_minify) {
referencesByFrequency[index].propertyName = name;
} else {
var refSet = referencesByFrequency[index];
refSet.propertyName = name + '_' + refSet.name;
}
}
//naiveFrequencyAssignment(
semistableFrequencyAssignment(referencesByFrequency.length,
minifiedNameSequence(), hashOf, countOf, assign);
}
static int _hashCharacteristicString(String s) {
int hash = 0;
for (int i = 0; i < s.length; i++) {
hash = Hashing.mixHashCodeBits(hash, s.codeUnitAt(i));
}
return hash;
}
/// Returns an infinite sequence of property names in increasing size.
static Iterable<String> minifiedNameSequence() sync* {
List<int> nextName = [$a];
/// Increments the letter at [pos] in the current name. Also takes care of
/// overflows to the left. Returns the carry bit, i.e., it returns `true`
/// if all positions to the left have wrapped around.
///
/// If [nextName] is initially 'a', this will generate the sequence
///
/// [a-zA-Z_]
/// [a-zA-Z_][0-9a-zA-Z_]
/// [a-zA-Z_][0-9a-zA-Z_][0-9a-zA-Z_]
/// ...
bool incrementPosition(int pos) {
bool overflow = false;
if (pos < 0) return true;
int value = nextName[pos];
if (value == $9) {
value = $a;
} else if (value == $z) {
value = $A;
} else if (value == $Z) {
value = $_;
} else if (value == $_) {
overflow = incrementPosition(pos - 1);
value = (pos > 0) ? $0 : $a;
} else {
value++;
}
nextName[pos] = value;
return overflow;
}
while (true) {
yield String.fromCharCodes(nextName);
if (incrementPosition(nextName.length - 1)) {
nextName.add($0);
}
}
}
}
/// Set of references to a single recipe.
class _ReferenceSet {
final TypeRecipe recipe;
// Number of times a TypeReference for [recipe] occurs in the tree-scan of the
// JavaScript ASTs.
int count = 0;
// Number tree-scan occurrences in a constant initializer.
int countInConstant = 0;
// Number tree-scan occurrences in a static lazy initializer.
int countInLazyInitializer = 0;
// It is possible for the JavaScript AST to be a DAG, so collect
// [TypeReference]s as set so we don't try to update one twice.
final Set<TypeReference> _references = Set.identity();
/// Characteristic name of the recipe - this can be used as a property name
/// for emitting unminified code, and as a stable hash source for minified
/// names. [name] is `null` if [recipe] should always be generated at use.
String name;
/// Property name for 'indexing' into the precomputed types.
String propertyName;
/// A stable hash code that can be used for picking stable minified names.
int hash = 0;
_ReferenceSet(this.recipe);
// If we don't assign a name it means we should not precompute the recipe.
bool get generateAtUse => name == null;
}
/// Scans a JavaScript AST to collect all the TypeReference nodes.
///
/// The state is kept in the finalizer so that this scan could be extended to
/// look for other deferred expressions in one pass.
class _TypeReferenceCollectorVisitor extends js.BaseVisitorVoid {
final TypeReferenceFinalizerImpl _finalizer;
_TypeReferenceCollectorVisitor(this._finalizer);
@override
void visitNode(js.Node node) {
assert(node is! TypeReference);
assert(node is! TypeReferenceResource);
if (node is js.AstContainer) {
for (js.Node element in node.containedNodes) {
element.accept(this);
}
} else {
super.visitNode(node);
}
}
@override
void visitDeferredExpression(js.DeferredExpression node) {
if (node is TypeReference) {
_finalizer._registerTypeReference(node);
} else {
visitNode(node);
}
}
@override
void visitDeferredStatement(js.DeferredStatement node) {
if (node is TypeReferenceResource) {
_finalizer._registerTypeReferenceResource(node);
} else {
visitNode(node);
}
}
}
/// Returns a valid JavaScript identifier characterizing the recipe. The names
/// tend to look like the type expression with the non-identifier characters
/// removed. Separators 'of' and 'and' are sometimes used to separate complex
/// types.
///
/// Map<int,int> "Map_int_int"
/// Map<int,List<int>> "Map_of_int_and_List_int"
///
///
/// For many common types the strings are unique, but this is not
/// guaranteed. This needs to be disambiguated at a higher level.
///
/// Different types can have the same string if the types contain different
/// interface types with the same name (i.e. from different libraries), or types
/// with names that contain underscores or dollar signs. There is also some
/// ambiguity in the generated names in the interest of keeping most names
/// short, e.g. "FutureOr_int_Function" could be "FutureOr<int> Function()" or
/// "FutureOr<int Function()>".
class _RecipeToIdentifier extends DartTypeVisitor<void, DartType> {
final Map<DartType, int> _backrefs = Map.identity();
final List<String> _fragments = [];
String run(TypeRecipe recipe) {
if (recipe is TypeExpressionRecipe) {
_visit(recipe.type, null);
} else if (recipe is SingletonTypeEnvironmentRecipe) {
_add(r'$env');
_visit(recipe.type, null);
} else if (recipe is FullTypeEnvironmentRecipe) {
_add(r'$env');
if (recipe.classType != null) _visit(recipe.classType, null);
_add('${recipe.types.length}');
int index = 0;
for (DartType type in recipe.types) {
++index;
_add('${index}');
_visit(type, null);
}
} else {
throw StateError('Unexpected recipe: $recipe');
}
String result = _fragments.join('_');
if (Namer.startsWithIdentifierCharacter(result)) return result;
return 'z' + result;
}
void _add(String text) {
_fragments.add(text);
}
void _identifier(String text) {
_add(Namer.replaceNonIdentifierCharacters(text));
}
bool _comma(bool needsComma) {
if (needsComma) _add('and');
return true;
}
void _visit(DartType type, DartType parent) {
type.accept(this, parent);
}
@override
void visitLegacyType(covariant LegacyType type, _) {
_add('legacy');
_visit(type.baseType, type);
}
@override
void visitNullableType(covariant NullableType type, _) {
_add('nullable');
_visit(type.baseType, type);
}
@override
void visitNeverType(covariant NeverType type, _) {
_add('Never');
}
@override
void visitVoidType(covariant VoidType type, _) {
_add('void');
}
@override
void visitDynamicType(covariant DynamicType type, _) {
_add('dynamic');
}
@override
void visitErasedType(covariant ErasedType type, _) {
_add('erased');
}
@override
void visitAnyType(covariant AnyType type, _) {
_add('any');
}
@override
void visitTypeVariableType(covariant TypeVariableType type, DartType parent) {
_identifier(type.element.typeDeclaration.name);
_identifier(type.element.name);
}
@override
void visitFunctionTypeVariable(covariant FunctionTypeVariable type, _) {
int index = type.index;
String name = index < 26 ? String.fromCharCode($A + index) : 'v\$${index}';
_add(name);
}
@override
void visitFunctionType(covariant FunctionType type, DartType parent) {
if (_dagCheck(type)) return;
_visit(type.returnType, type);
_add('Function');
var typeVariables = type.typeVariables;
if (typeVariables.isNotEmpty) {
bool needsComma = false;
for (FunctionTypeVariable typeVariable in typeVariables) {
needsComma = _comma(needsComma);
_visit(typeVariable, type);
DartType bound = typeVariable.bound;
if (!bound.isObject) {
_add('extends');
_visit(typeVariable.bound, typeVariable);
}
}
}
var parameterTypes = type.parameterTypes;
var optionalParameterTypes = type.optionalParameterTypes;
var namedParameters = type.namedParameters;
var requiredNamedParameters = type.requiredNamedParameters;
if (optionalParameterTypes.isEmpty &&
namedParameters.isEmpty &&
parameterTypes.every(_isSimple)) {
// e.g. "void_Function_int_int"
for (DartType parameterType in parameterTypes) {
_visit(parameterType, type);
}
return;
}
if (parameterTypes.length > 1) {
_add('${parameterTypes.length}');
}
bool needsComma = false;
for (DartType parameterType in parameterTypes) {
needsComma = _comma(needsComma);
_visit(parameterType, type);
}
if (optionalParameterTypes.isNotEmpty) {
_add(r'$opt');
bool needsOptionalComma = false;
for (DartType typeArgument in optionalParameterTypes) {
needsOptionalComma = _comma(needsOptionalComma);
_visit(typeArgument, type);
}
}
if (namedParameters.isNotEmpty) {
_add(r'$named');
bool needsNamedComma = false;
for (int index = 0; index < namedParameters.length; index++) {
needsNamedComma = _comma(needsNamedComma);
if (requiredNamedParameters.contains(namedParameters[index])) {
_add(r'$req');
}
_identifier(namedParameters[index]);
_visit(type.namedParameterTypes[index], type);
}
}
}
@override
void visitInterfaceType(covariant InterfaceType type, _) {
var arguments = type.typeArguments;
// Don't bother DAG-checking (generating back-ref encodings) for interface
// types which 'print' as a single identifier.
if (arguments.isNotEmpty && _dagCheck(type)) return;
_identifier(type.element.name);
if (arguments.isEmpty) return;
if (arguments.length == 1) {
// e.g. "List_of_int_Function"
if (arguments.first.withoutNullability is FunctionType) {
_add('of');
}
// e.g. "List_int"
_visit(arguments.first, type);
return;
}
if (arguments.every(_isSimple)) {
// e.g. "Map_String_String"
for (DartType argument in arguments) {
_visit(argument, type);
}
return;
}
// e.g "Map_of_String_and_int_Function"
_add('of');
bool needsComma = false;
for (DartType argument in arguments) {
needsComma = _comma(needsComma);
_visit(argument, type);
}
}
bool _dagCheck(DartType type) {
int /*?*/ ref = _backrefs[type];
if (ref != null) {
_add('\$$ref');
return true;
}
_backrefs[type] = _backrefs.length;
return false;
}
/// Returns `true` for types which print as a single identifier.
static bool _isSimple(DartType type) {
return type is DynamicType ||
type is VoidType ||
type is AnyType ||
(type is InterfaceType && type.typeArguments.isEmpty);
}
@override
void visitFutureOrType(covariant FutureOrType type, _) {
_identifier('FutureOr');
_visit(type.typeArgument, type);
}
}