blob: f7e5ff128c65abd8072fd83b195017c6ea8c61e6 [file] [log] [blame]
// Copyright (c) 2021, 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:js_ast/src/precedence.dart' as js show PRIMARY;
import '../common_elements.dart' show JCommonElements;
import '../elements/entities.dart';
import '../js/js.dart' as js;
import '../serialization/serialization.dart';
import '../util/util.dart';
import 'namer.dart';
// TODO(joshualitt): Figure out how to subsume more of the modular naming
// framework into this approach. For example, we are still creating ModularNames
// for the entity referenced in the DeferredHolderExpression.
enum DeferredHolderExpressionKind {
globalObjectForStaticState,
globalObjectForConstants,
globalObjectForLibrary,
globalObjectForClass,
globalObjectForType,
globalObjectForMember,
}
/// A [DeferredHolderExpression] is a deferred JavaScript expression determined
/// by the finalization of holders. It is the injection point for data or
/// code to related to holders. The actual [Expression] contained within the
/// [DeferredHolderExpression] is determined by the
/// [DeferredHolderExpressionKind], eventually, most will be a [PropertyAccess]
/// but currently all are [VariableUse]s.
class DeferredHolderExpression extends js.DeferredExpression
implements js.AstContainer {
static const String tag = 'deferred-holder-expression';
final DeferredHolderExpressionKind kind;
final Entity entity;
js.Expression _value;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
DeferredHolderExpression(this.kind, this.entity) : sourceInformation = null;
DeferredHolderExpression._(
this.kind, this.entity, this._value, this.sourceInformation);
factory DeferredHolderExpression.forConstants() {
return DeferredHolderExpression(
DeferredHolderExpressionKind.globalObjectForConstants, null);
}
factory DeferredHolderExpression.forStaticState() {
return DeferredHolderExpression(
DeferredHolderExpressionKind.globalObjectForStaticState, null);
}
factory DeferredHolderExpression.readFromDataSource(DataSource source) {
source.begin(tag);
var kind = source.readEnum(DeferredHolderExpressionKind.values);
Entity entity;
switch (kind) {
case DeferredHolderExpressionKind.globalObjectForLibrary:
entity = source.readLibrary();
break;
case DeferredHolderExpressionKind.globalObjectForClass:
entity = source.readClass();
break;
case DeferredHolderExpressionKind.globalObjectForType:
entity = source.readClass();
break;
case DeferredHolderExpressionKind.globalObjectForMember:
entity = source.readMember();
break;
case DeferredHolderExpressionKind.globalObjectForStaticState:
case DeferredHolderExpressionKind.globalObjectForConstants:
// no entity.
break;
}
source.end(tag);
return DeferredHolderExpression(kind, entity);
}
void writeToDataSink(DataSink sink) {
sink.begin(tag);
sink.writeEnum(kind);
switch (kind) {
case DeferredHolderExpressionKind.globalObjectForLibrary:
sink.writeLibrary(entity);
break;
case DeferredHolderExpressionKind.globalObjectForClass:
sink.writeClass(entity);
break;
case DeferredHolderExpressionKind.globalObjectForType:
sink.writeClass(entity);
break;
case DeferredHolderExpressionKind.globalObjectForMember:
sink.writeMember(entity);
break;
case DeferredHolderExpressionKind.globalObjectForStaticState:
case DeferredHolderExpressionKind.globalObjectForConstants:
// no entity.
break;
}
sink.end(tag);
}
set value(js.Expression value) {
assert(!isFinalized && value != null);
_value = value;
}
@override
js.Expression get value {
assert(isFinalized, '$this is unassigned');
return _value;
}
@override
bool get isFinalized => _value != null;
@override
DeferredHolderExpression withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return DeferredHolderExpression._(
kind, entity, _value, newSourceInformation);
}
@override
int get precedenceLevel => _value?.precedenceLevel ?? js.PRIMARY;
@override
int get hashCode {
return Hashing.objectsHash(kind, entity);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DeferredHolderExpression &&
kind == other.kind &&
entity == other.entity;
}
@override
String toString() {
StringBuffer sb = new StringBuffer();
sb.write('DeferredHolderExpression(kind=$kind,entity=$entity,');
sb.write('value=$_value)');
return sb.toString();
}
@override
Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
}
/// A [DeferredHolderParameter] is a deferred JavaScript expression determined
/// by the finalization of holders. It is the injection point for data or
/// code to related to holders. This class does not support serialization.
/// TODO(joshualitt): Today this exists just for the static state holder.
/// Ideally we'd be able to treat the static state holder like other holders.
class DeferredHolderParameter extends js.Expression implements js.Parameter {
String _name;
@override
final bool allowRename = false;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
DeferredHolderParameter() : sourceInformation = null;
DeferredHolderParameter._(this._name, this.sourceInformation);
set name(String name) {
assert(!isFinalized && name != null);
_name = name;
}
@override
String get name {
assert(isFinalized, '$this is unassigned');
return _name;
}
@override
bool get isFinalized => _name != null;
@override
DeferredHolderParameter withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return DeferredHolderParameter._(_name, newSourceInformation);
}
@override
int get precedenceLevel => js.PRIMARY;
@override
T accept<T>(js.NodeVisitor<T> visitor) => visitor.visitParameter(this);
@override
R accept1<R, A>(js.NodeVisitor1<R, A> visitor, A arg) =>
visitor.visitParameter(this, arg);
@override
void visitChildren<T>(js.NodeVisitor<T> visitor) {}
@override
void visitChildren1<R, A>(js.NodeVisitor1<R, A> visitor, A arg) {}
@override
String toString() {
StringBuffer sb = new StringBuffer();
sb.write('DeferredHolderParameter(name=$_name)');
return sb.toString();
}
}
enum DeferredHolderResourceKind {
declaration,
update,
}
/// A [DeferredHolderResource] is a deferred JavaScript statement determined by
/// the finalization of holders. It is the injection point for data or
/// code to holders. The actual [Statement] contained with the
/// [DeferredHolderResource] will be determined by the
/// [DeferredHolderResourceKind]. These [Statement]s differ considerably
/// depending on where they are used in the AST. This class does not support
/// serialization.
class DeferredHolderResource extends js.DeferredStatement
implements js.AstContainer {
DeferredHolderResourceKind kind;
Map<Entity, List<js.Property>> holderCode;
bool initializeEmptyHolders;
js.Statement _statement;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
DeferredHolderResource(this.kind,
{this.holderCode: const {}, this.initializeEmptyHolders: false})
: sourceInformation = null;
DeferredHolderResource._(this.kind, this.holderCode,
this.initializeEmptyHolders, this._statement, this.sourceInformation);
set statement(js.Statement statement) {
assert(!isFinalized && statement != null);
_statement = statement;
}
@override
js.Statement get statement {
assert(isFinalized, 'DeferredHolderResource is unassigned');
return _statement;
}
@override
bool get isFinalized => _statement != null;
@override
DeferredHolderResource withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return DeferredHolderResource._(kind, holderCode, initializeEmptyHolders,
_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);
}
}
enum DeferredHolderResourceExpressionKind {
constantHolderReference,
}
/// Similar to [DeferredHolderExpression], [DeferredHolderResourceExpression]
/// is used by resources which want to insert a DeferredExpression into the
/// ast. This class does not support serialization.
class DeferredHolderResourceExpression extends js.DeferredExpression
implements js.AstContainer {
final DeferredHolderResourceExpressionKind kind;
final Entity entity;
js.Expression _value;
@override
final js.JavaScriptNodeSourceInformation sourceInformation;
DeferredHolderResourceExpression(this.kind, this.entity)
: sourceInformation = null;
DeferredHolderResourceExpression._(
this.kind, this.entity, this._value, this.sourceInformation);
factory DeferredHolderResourceExpression.constantHolderReference() {
return DeferredHolderResourceExpression(
DeferredHolderResourceExpressionKind.constantHolderReference, null);
}
set value(js.Expression value) {
assert(!isFinalized && value != null);
_value = value;
}
@override
js.Expression get value {
assert(isFinalized, '$this is unassigned');
return _value;
}
@override
bool get isFinalized => _value != null;
@override
DeferredHolderResourceExpression withSourceInformation(
js.JavaScriptNodeSourceInformation newSourceInformation) {
if (newSourceInformation == sourceInformation) return this;
if (newSourceInformation == null) return this;
return DeferredHolderResourceExpression._(
kind, entity, _value, newSourceInformation);
}
@override
int get precedenceLevel => _value?.precedenceLevel ?? js.PRIMARY;
@override
int get hashCode {
return Hashing.objectsHash(kind, entity);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DeferredHolderExpression &&
kind == other.kind &&
entity == other.entity;
}
@override
String toString() {
StringBuffer sb = new StringBuffer();
sb.write('DeferredHolderResourceExpression(kind=$kind,entity=$entity,');
sb.write('value=$_value)');
return sb.toString();
}
@override
Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
}
abstract class DeferredHolderExpressionFinalizer {
/// Collects DeferredHolderExpressions from the JavaScript
/// AST [code];
void addCode(js.Node code);
/// Performs analysis on all collected DeferredHolderExpression nodes
/// finalizes the values to expressions to access the holders.
void finalize();
}
/// [DeferredHolderExpressionFinalizerImpl] finalizes
/// [DeferredHolderExpression]s, [DeferredHolderParameter]s,
/// [DeferredHolderResource]s, [DeferredHolderResourceExpression]s.
class DeferredHolderExpressionFinalizerImpl
implements DeferredHolderExpressionFinalizer {
_DeferredHolderExpressionCollectorVisitor _visitor;
final List<DeferredHolderExpression> holderReferences = [];
final List<DeferredHolderParameter> holderParameters = [];
final List<DeferredHolderResource> holderResources = [];
final List<DeferredHolderResourceExpression> holderResourceExpressions = [];
final Set<String> _uniqueHolders = {};
final List<String> _holders = [];
final Map<Entity, String> _entityMap = {};
final JCommonElements _commonElements;
DeferredHolderExpressionFinalizerImpl(this._commonElements) {
_visitor = _DeferredHolderExpressionCollectorVisitor(this);
}
@override
void addCode(js.Node code) {
code.accept(_visitor);
}
final List<String> userGlobalObjects =
new List.from(Namer.reservedGlobalObjectNames)
..remove('C')
..remove('H')
..remove('J')
..remove('P')
..remove('W');
/// Returns the [reservedGlobalObjectNames] for [library].
String globalObjectForLibrary(LibraryEntity library) {
if (library == _commonElements.interceptorsLibrary) return 'J';
Uri uri = library.canonicalUri;
if (uri.scheme == 'dart') {
if (uri.path == 'html') return 'W';
if (uri.path.startsWith('_')) return 'H';
return 'P';
}
return userGlobalObjects[library.name.hashCode % userGlobalObjects.length];
}
/// Returns true if [element] is stored in the static state holder
/// ([staticStateHolder]). We intend to store only mutable static state
/// there, whereas constants are stored in 'C'. Functions, accessors,
/// classes, etc. are stored in one of the other objects in
/// [reservedGlobalObjectNames].
bool _isPropertyOfStaticStateHolder(MemberEntity element) {
// TODO(ahe): Make sure this method's documentation is always true and
// remove the word "intend".
return element.isField;
}
String globalObjectForMember(MemberEntity entity) {
if (_isPropertyOfStaticStateHolder(entity)) {
return globalObjectForStaticState();
} else {
return globalObjectForLibrary(entity.library);
}
}
String globalObjectForClass(ClassEntity entity) {
return globalObjectForLibrary(entity.library);
}
String globalObjectForType(Entity entity) {
return globalObjectForClass(entity);
}
String globalObjectForStaticState() => r'$';
String globalObjectForConstants() => 'C';
String globalObjectForEntity(Entity entity) {
if (entity is MemberEntity) {
return globalObjectForMember(entity);
} else if (entity is ClassEntity) {
return globalObjectForLibrary(entity.library);
} else {
assert(entity is LibraryEntity);
return globalObjectForLibrary(entity);
}
}
/// Registers an [Entity] with a specific [holder].
void registerHolderUse(String holder, Entity entity) {
if (_uniqueHolders.add(holder)) _holders.add(holder);
if (entity != null) {
assert(!_entityMap.containsKey(entity) || _entityMap[entity] == holder);
_entityMap[entity] = holder;
}
}
/// Returns a global object for a given [Entity] based on the
/// [DeferredHolderExpressionKind].
String kindToHolder(DeferredHolderExpressionKind kind, Entity entity) {
switch (kind) {
case DeferredHolderExpressionKind.globalObjectForLibrary:
return globalObjectForLibrary(entity);
case DeferredHolderExpressionKind.globalObjectForClass:
return globalObjectForClass(entity);
case DeferredHolderExpressionKind.globalObjectForType:
return globalObjectForType(entity);
case DeferredHolderExpressionKind.globalObjectForMember:
return globalObjectForMember(entity);
case DeferredHolderExpressionKind.globalObjectForConstants:
return globalObjectForConstants();
case DeferredHolderExpressionKind.globalObjectForStaticState:
return globalObjectForStaticState();
}
throw UnsupportedError("Unreachable");
}
/// Finalizes [DeferredHolderExpression]s [DeferredHolderParameter]s.
void finalizeReferences() {
// Finalize [DeferredHolderExpression]s and registers holder usage.
for (var reference in holderReferences) {
if (reference.isFinalized) continue;
Entity entity = reference.entity;
String holder = kindToHolder(reference.kind, entity);
js.Expression value = js.VariableUse(holder);
registerHolderUse(holder, entity);
reference.value =
value.withSourceInformation(reference.sourceInformation);
}
// Finalize [DeferredHolderParameter]s.
for (var parameter in holderParameters) {
if (parameter.isFinalized) continue;
parameter.name = globalObjectForStaticState();
}
}
/// Registers all of the holders used by a given [DeferredHolderResource].
void registerHolders(DeferredHolderResource resource) {
for (var entity in resource.holderCode.keys) {
var holder = globalObjectForEntity(entity);
registerHolderUse(holder, entity);
}
}
/// Returns a [List<String>] containing all of the holders except the static
/// state holder.
List<String> get nonStaticStateHolders {
return _holders
.where((holder) => holder != globalObjectForStaticState())
.toList(growable: false);
}
/// Generates code to declare holders.
HolderCode declareHolders(DeferredHolderResource resource) {
// Collect all holders except the static state holder. Then, create a map of
// holder to list of properties which are associated with that holder, but
// only with respect to a given [DeferredHolderResource]. Each fragment will
// have its own [DeferredHolderResource] and associated code.
Map<String, List<js.Property>> codePerHolder = {};
final holders = nonStaticStateHolders;
for (var holder in holders) {
codePerHolder[holder] = [];
}
final holderCode = resource.holderCode;
holderCode.forEach((entity, properties) {
assert(_entityMap.containsKey(entity));
var holder = _entityMap[entity];
assert(codePerHolder.containsKey(holder));
codePerHolder[holder].addAll(properties);
});
// Create holder initialization code based on the [codePerHolder]. If there
// are no properties associated with a given holder in this specific
// [DeferredHolderResource] then it will be omitted. However, in some cases,
// i.e. the main output unit, we still want to declare the holder with an
// empty object literal which will be filled in later by another
// [DeferredHolderResource], i.e. in a specific deferred fragment.
// The generated code looks like this:
//
// {
// var H = {...}, ..., G = {...};
// var holders = [ H, ..., G ]; // Main unit only.
// }
List<String> activeHolders = [];
List<js.VariableInitialization> holderInitializations = [];
for (var holder in holders) {
List<js.Property> properties = codePerHolder[holder];
if (properties.isEmpty) {
holderInitializations.add(js.VariableInitialization(
js.VariableDeclaration(holder, allowRename: false),
resource.initializeEmptyHolders
? js.ObjectInitializer(properties)
: null));
} else {
activeHolders.add(holder);
holderInitializations.add(js.VariableInitialization(
js.VariableDeclaration(holder, allowRename: false),
js.ObjectInitializer(properties)));
}
}
List<js.Statement> statements = [];
statements.add(js.ExpressionStatement(js.VariableDeclarationList(
holderInitializations,
indentSplits: false)));
if (resource.initializeEmptyHolders) {
statements.add(js.js.statement(
'var holders = #',
js.ArrayInitializer(holders
.map((holder) => js.VariableUse(holder))
.toList(growable: false))));
}
return HolderCode(activeHolders, statements);
}
/// Finalizes [resource] to code that updates holders. [resource] must be in
/// the AST of a deferred fragment.
void updateHolders(DeferredHolderResource resource) {
// Declare holders.
final holderCode = declareHolders(resource);
// Set names if necessary on deferred holders list.
js.Expression deferredHoldersList = js.ArrayInitializer(holderCode
.activeHolders
.map((holder) => js.js("#", holder))
.toList(growable: false));
js.Statement setNames = js.js.statement(
'hunkHelpers.setFunctionNamesIfNecessary(#deferredHoldersList)',
{'deferredHoldersList': deferredHoldersList});
// Update holder assignments.
final holders = nonStaticStateHolders;
List<js.Statement> updateHolderAssignments = [setNames];
for (int i = 0; i < holders.length; i++) {
var holder = holders[i];
if (holderCode.activeHolders.contains(holder)) {
updateHolderAssignments.add(js.js.statement(
'#holder = hunkHelpers.updateHolder(holdersList[#index], #holder)',
{'index': js.number(i), 'holder': js.VariableUse(holder)}));
} else {
// TODO(sra): Change declaration followed by assignments to declarations
// with initialization.
updateHolderAssignments.add(js.js.statement(
'#holder = holdersList[#index]',
{'index': js.number(i), 'holder': js.VariableUse(holder)}));
}
}
// Create a single block of all statements.
List<js.Statement> statements = holderCode.statements
.followedBy(updateHolderAssignments)
.toList(growable: false);
resource.statement = js.Block(statements);
}
/// Creates a reference to the constant holder.
void allocateConstantHolderReference(
DeferredHolderResourceExpression resource) {
String constantHolder = _holders.firstWhere(
(holder) => holder == globalObjectForConstants(),
orElse: () => null);
resource.value = constantHolder == null
? js.LiteralNull()
: js.VariableUse(constantHolder);
}
/// Allocates all [DeferredHolderResource]s and
/// [DeferredHolderResourceExpression]s.
void allocateResources() {
// First ensure all holders used in all [DeferredHolderResource]s have been
// allocated.
for (var resource in holderResources) {
registerHolders(resource);
}
_holders.sort();
// Next finalize all [DeferredHolderResource]s.
for (var resource in holderResources) {
switch (resource.kind) {
case DeferredHolderResourceKind.declaration:
var holderCode = declareHolders(resource);
resource.statement = js.Block(holderCode.statements);
break;
case DeferredHolderResourceKind.update:
updateHolders(resource);
break;
}
}
// Finally, finalize any [DeferredHolderResourceExpression]s.
for (var resource in holderResourceExpressions) {
switch (resource.kind) {
case DeferredHolderResourceExpressionKind.constantHolderReference:
allocateConstantHolderReference(resource);
break;
}
}
}
@override
void finalize() {
finalizeReferences();
allocateResources();
}
void _registerDeferredHolderExpression(DeferredHolderExpression node) {
holderReferences.add(node);
}
void _registerDeferredHolderResource(DeferredHolderResource node) {
holderResources.add(node);
}
void _registerDeferredHolderResourceExpression(
DeferredHolderResourceExpression node) {
holderResourceExpressions.add(node);
}
void _registerDeferredHolderParameter(DeferredHolderParameter node) {
holderParameters.add(node);
}
}
/// Scans a JavaScript AST to collect all the [DeferredHolderExpression],
/// [DeferredHolderParameter], [DeferredHolderResource], and
/// [DeferredHolderResourceExpression] 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 _DeferredHolderExpressionCollectorVisitor extends js.BaseVisitor<void> {
final DeferredHolderExpressionFinalizerImpl _finalizer;
_DeferredHolderExpressionCollectorVisitor(this._finalizer);
@override
void visitNode(js.Node node) {
assert(node is! DeferredHolderExpression);
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 DeferredHolderExpression) {
_finalizer._registerDeferredHolderExpression(node);
} else if (node is DeferredHolderResourceExpression) {
_finalizer._registerDeferredHolderResourceExpression(node);
} else {
visitNode(node);
}
}
@override
void visitDeferredStatement(js.DeferredStatement node) {
if (node is DeferredHolderResource) {
_finalizer._registerDeferredHolderResource(node);
} else {
visitNode(node);
}
}
@override
void visitParameter(js.Parameter node) {
if (node is DeferredHolderParameter) {
_finalizer._registerDeferredHolderParameter(node);
} else {
visitNode(node);
}
}
}
class HolderCode {
final List<String> activeHolders;
final List<js.Statement> statements;
HolderCode(this.activeHolders, this.statements);
}