blob: eb4bbd1e904d587eac110084458f2ab5829dbbd6 [file] [log] [blame]
// Copyright (c) 2014, 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 dart2js_incremental.library_updater;
import 'dart:async' show
Future;
import 'package:compiler/compiler.dart' as api;
import 'package:compiler/src/compiler.dart' show
Compiler;
import 'package:compiler/src/diagnostics/messages.dart' show
MessageKind;
import 'package:compiler/src/elements/elements.dart' show
ClassElement,
CompilationUnitElement,
Element,
FunctionElement,
LibraryElement,
STATE_NOT_STARTED,
ScopeContainerElement;
import 'package:compiler/src/enqueue.dart' show
EnqueueTask;
import 'package:compiler/src/parser/listener.dart' show
Listener;
import 'package:compiler/src/parser/node_listener.dart' show
NodeListener;
import 'package:compiler/src/parser/partial_elements.dart' show
PartialClassElement,
PartialElement,
PartialFieldList,
PartialFunctionElement;
import 'package:compiler/src/parser/parser.dart' show
Parser;
import 'package:compiler/src/scanner/scanner.dart' show
Scanner;
import 'package:compiler/src/tokens/token.dart' show
Token;
import 'package:compiler/src/tokens/token_constants.dart' show
EOF_TOKEN;
import 'package:compiler/src/script.dart' show
Script;
import 'package:compiler/src/io/source_file.dart' show
CachingUtf8BytesSourceFile,
SourceFile,
StringSourceFile;
import 'package:compiler/src/tree/tree.dart' show
ClassNode,
FunctionExpression,
LibraryTag,
NodeList,
Part,
StringNode,
unparse;
import 'package:compiler/src/js/js.dart' show
js;
import 'package:compiler/src/js/js.dart' as jsAst;
import 'package:compiler/src/js_emitter/js_emitter.dart' show
CodeEmitterTask,
computeMixinClass;
import 'package:compiler/src/js_emitter/full_emitter/emitter.dart'
as full show Emitter;
import 'package:compiler/src/js_emitter/model.dart' show
Class,
Method;
import 'package:compiler/src/js_emitter/program_builder/program_builder.dart'
show ProgramBuilder;
import 'package:js_runtime/shared/embedded_names.dart'
as embeddedNames;
import 'package:compiler/src/js_backend/js_backend.dart' show
JavaScriptBackend,
Namer;
import 'package:compiler/src/util/util.dart' show
Link,
LinkBuilder;
import 'package:compiler/src/elements/modelx.dart' show
ClassElementX,
CompilationUnitElementX,
DeclarationSite,
ElementX,
FieldElementX,
LibraryElementX;
import 'package:compiler/src/universe/selector.dart' show
Selector;
import 'package:compiler/src/constants/values.dart' show
ConstantValue;
import 'package:compiler/src/library_loader.dart' show
TagState;
import 'diff.dart' show
Difference,
computeDifference;
import 'dart2js_incremental.dart' show
IncrementalCompilationFailed,
IncrementalCompiler;
typedef void Logger(message);
typedef bool Reuser(
Token diffToken,
PartialElement before,
PartialElement after);
class FailedUpdate {
/// Either an [Element] or a [Difference].
final context;
final String message;
FailedUpdate(this.context, this.message);
String toString() {
if (context == null) return '$message';
return 'In $context:\n $message';
}
}
abstract class _IncrementalCompilerContext {
IncrementalCompiler incrementalCompiler;
Set<ClassElementX> _emittedClasses;
Set<ClassElementX> _directlyInstantiatedClasses;
Set<ConstantValue> _compiledConstants;
}
class IncrementalCompilerContext extends _IncrementalCompilerContext {
final Set<Uri> _uriWithUpdates = new Set<Uri>();
void set incrementalCompiler(IncrementalCompiler value) {
if (super.incrementalCompiler != null) {
throw new StateError("Can't set [incrementalCompiler] more than once.");
}
super.incrementalCompiler = value;
}
void registerUriWithUpdates(Iterable<Uri> uris) {
_uriWithUpdates.addAll(uris);
}
void _captureState(Compiler compiler) {
JavaScriptBackend backend = compiler.backend;
Set neededClasses = backend.emitter.neededClasses;
if (neededClasses == null) {
neededClasses = new Set();
}
_emittedClasses = new Set.from(neededClasses);
_directlyInstantiatedClasses =
new Set.from(compiler.codegenWorld.directlyInstantiatedClasses);
// This breaks constant tracking of the incremental compiler. It would need
// to capture the emitted constants.
List<ConstantValue> constants = null;
if (constants == null) constants = <ConstantValue>[];
_compiledConstants = new Set<ConstantValue>.identity()..addAll(constants);
}
bool _uriHasUpdate(Uri uri) => _uriWithUpdates.contains(uri);
}
class LibraryUpdater extends JsFeatures {
final Compiler compiler;
final api.CompilerInputProvider inputProvider;
final Logger logTime;
final Logger logVerbose;
final List<Update> updates = <Update>[];
final List<FailedUpdate> _failedUpdates = <FailedUpdate>[];
final Set<ElementX> _elementsToInvalidate = new Set<ElementX>();
final Set<ElementX> _removedElements = new Set<ElementX>();
final Set<ClassElementX> _classesWithSchemaChanges =
new Set<ClassElementX>();
final IncrementalCompilerContext _context;
final Map<Uri, Future> _sources = <Uri, Future>{};
/// Cached tokens of entry compilation units.
final Map<LibraryElementX, Token> _entryUnitTokens =
<LibraryElementX, Token>{};
/// Cached source files for entry compilation units.
final Map<LibraryElementX, SourceFile> _entrySourceFiles =
<LibraryElementX, SourceFile>{};
bool _hasComputedNeeds = false;
bool _hasCapturedCompilerState = false;
LibraryUpdater(
this.compiler,
this.inputProvider,
this.logTime,
this.logVerbose,
this._context) {
// TODO(ahe): Would like to remove this from the constructor. However, the
// state must be captured before calling [reuseCompiler].
// Proper solution might be: [reuseCompiler] should not clear the sets that
// are captured in [IncrementalCompilerContext._captureState].
_ensureCompilerStateCaptured();
}
/// Returns the classes emitted by [compiler].
Set<ClassElementX> get _emittedClasses => _context._emittedClasses;
/// Returns the directly instantantiated classes seen by [compiler] (this
/// includes interfaces and may be different from [_emittedClasses] that only
/// includes interfaces used in type tests).
Set<ClassElementX> get _directlyInstantiatedClasses {
return _context._directlyInstantiatedClasses;
}
/// Returns the constants emitted by [compiler].
Set<ConstantValue> get _compiledConstants => _context._compiledConstants;
/// When [true], updates must be applied (using [applyUpdates]) before the
/// [compiler]'s state correctly reflects the updated program.
bool get hasPendingUpdates => !updates.isEmpty;
bool get failed => !_failedUpdates.isEmpty;
/// Used as tear-off passed to [LibraryLoaderTask.resetAsync].
Future<bool> reuseLibrary(LibraryElement library) {
_ensureCompilerStateCaptured();
assert(compiler != null);
if (library.isPlatformLibrary) {
logTime('Reusing $library (assumed read-only).');
return new Future.value(true);
}
return _haveTagsChanged(library).then((bool haveTagsChanged) {
if (haveTagsChanged) {
cannotReuse(
library,
"Changes to library, import, export, or part declarations not"
" supported.");
return true;
}
bool isChanged = false;
List<Future<Script>> futureScripts = <Future<Script>>[];
for (CompilationUnitElementX unit in library.compilationUnits) {
Uri uri = unit.script.resourceUri;
if (_context._uriHasUpdate(uri)) {
isChanged = true;
futureScripts.add(_updatedScript(unit.script, library));
} else {
futureScripts.add(new Future.value(unit.script));
}
}
if (!isChanged) {
logTime("Reusing $library, source didn't change.");
return true;
}
return Future.wait(futureScripts).then(
(List<Script> scripts) => canReuseLibrary(library, scripts));
}).whenComplete(() => _cleanUp(library));
}
void _cleanUp(LibraryElementX library) {
_entryUnitTokens.remove(library);
_entrySourceFiles.remove(library);
}
Future<Script> _updatedScript(Script before, LibraryElementX library) {
if (before == library.entryCompilationUnit.script &&
_entrySourceFiles.containsKey(library)) {
return new Future.value(before.copyWithFile(_entrySourceFiles[library]));
}
return _readUri(before.resourceUri).then((bytes) {
Uri uri = before.file.uri;
String filename = before.file.filename;
SourceFile sourceFile = bytes is String
? new StringSourceFile(uri, filename, bytes)
: new CachingUtf8BytesSourceFile(uri, filename, bytes);
return before.copyWithFile(sourceFile);
});
}
Future<bool> _haveTagsChanged(LibraryElement library) {
Script before = library.entryCompilationUnit.script;
if (!_context._uriHasUpdate(before.resourceUri)) {
// The entry compilation unit hasn't been updated. So the tags aren't
// changed.
return new Future<bool>.value(false);
}
return _updatedScript(before, library).then((Script script) {
_entrySourceFiles[library] = script.file;
Token token = new Scanner(_entrySourceFiles[library]).tokenize();
_entryUnitTokens[library] = token;
// Using two parsers to only create the nodes we want ([LibraryTag]).
Parser parser = new Parser(new Listener());
NodeListener listener = new NodeListener(
compiler, library.entryCompilationUnit);
Parser nodeParser = new Parser(listener);
Iterator<LibraryTag> tags = library.tags.iterator;
while (token.kind != EOF_TOKEN) {
token = parser.parseMetadataStar(token);
if (parser.optional('library', token) ||
parser.optional('import', token) ||
parser.optional('export', token) ||
parser.optional('part', token)) {
if (!tags.moveNext()) return true;
token = nodeParser.parseTopLevelDeclaration(token);
LibraryTag tag = listener.popNode();
assert(listener.nodes.isEmpty);
if (unparse(tags.current) != unparse(tag)) {
return true;
}
} else {
break;
}
}
return tags.moveNext();
});
}
Future _readUri(Uri uri) {
return _sources.putIfAbsent(uri, () => inputProvider(uri));
}
void _ensureCompilerStateCaptured() {
// TODO(ahe): [compiler] shouldn't be null, remove the following line.
if (compiler == null) return;
if (_hasCapturedCompilerState) return;
_context._captureState(compiler);
_hasCapturedCompilerState = true;
}
/// Returns true if [library] can be reused.
///
/// This methods also computes the [updates] (patches) needed to have
/// [library] reflect the modifications in [scripts].
bool canReuseLibrary(LibraryElement library, List<Script> scripts) {
logTime('Attempting to reuse ${library}.');
Uri entryUri = library.entryCompilationUnit.script.resourceUri;
Script entryScript =
scripts.singleWhere((Script script) => script.resourceUri == entryUri);
LibraryElement newLibrary =
new LibraryElementX(entryScript, library.canonicalUri);
if (_entryUnitTokens.containsKey(library)) {
compiler.dietParser.dietParse(
newLibrary.entryCompilationUnit, _entryUnitTokens[library]);
} else {
compiler.scanner.scanLibrary(newLibrary);
}
TagState tagState = new TagState();
for (LibraryTag tag in newLibrary.tags) {
if (tag.isImport) {
tagState.checkTag(TagState.IMPORT_OR_EXPORT, tag, compiler);
} else if (tag.isExport) {
tagState.checkTag(TagState.IMPORT_OR_EXPORT, tag, compiler);
} else if (tag.isLibraryName) {
tagState.checkTag(TagState.LIBRARY, tag, compiler);
if (newLibrary.libraryTag == null) {
// Use the first if there are multiple (which is reported as an
// error in [TagState.checkTag]).
newLibrary.libraryTag = tag;
}
} else if (tag.isPart) {
tagState.checkTag(TagState.PART, tag, compiler);
}
}
// TODO(ahe): Process tags using TagState, not
// LibraryLoaderTask.processLibraryTags.
Link<CompilationUnitElement> units = library.compilationUnits;
for (Script script in scripts) {
CompilationUnitElementX unit = units.head;
units = units.tail;
if (script != entryScript) {
// TODO(ahe): Copied from library_loader.
CompilationUnitElement newUnit =
new CompilationUnitElementX(script, newLibrary);
compiler.withCurrentElement(newUnit, () {
compiler.scanner.scan(newUnit);
if (unit.partTag == null) {
compiler.reportError(unit, MessageKind.MISSING_PART_OF_TAG);
}
});
}
}
logTime('New library synthesized.');
return canReuseScopeContainerElement(library, newLibrary);
}
bool cannotReuse(context, String message) {
_failedUpdates.add(new FailedUpdate(context, message));
logVerbose(message);
return false;
}
bool canReuseScopeContainerElement(
ScopeContainerElement element,
ScopeContainerElement newElement) {
List<Difference> differences = computeDifference(element, newElement);
logTime('Differences computed.');
for (Difference difference in differences) {
logTime('Looking at difference: $difference');
if (difference.before == null && difference.after is PartialElement) {
canReuseAddedElement(difference.after, element, newElement);
continue;
}
if (difference.after == null && difference.before is PartialElement) {
canReuseRemovedElement(difference.before, element);
continue;
}
Token diffToken = difference.token;
if (diffToken == null) {
cannotReuse(difference, "No difference token.");
continue;
}
if (difference.after is! PartialElement &&
difference.before is! PartialElement) {
cannotReuse(difference, "Don't know how to recompile.");
continue;
}
PartialElement before = difference.before;
PartialElement after = difference.after;
Reuser reuser;
if (before is PartialFunctionElement && after is PartialFunctionElement) {
reuser = canReuseFunction;
} else if (before is PartialClassElement &&
after is PartialClassElement) {
reuser = canReuseClass;
} else {
reuser = unableToReuse;
}
if (!reuser(diffToken, before, after)) {
assert(!_failedUpdates.isEmpty);
continue;
}
}
return _failedUpdates.isEmpty;
}
bool canReuseAddedElement(
PartialElement element,
ScopeContainerElement container,
ScopeContainerElement syntheticContainer) {
if (element is PartialFunctionElement) {
addFunction(element, container);
return true;
} else if (element is PartialClassElement) {
addClass(element, container);
return true;
} else if (element is PartialFieldList) {
addFields(element, container, syntheticContainer);
return true;
}
return cannotReuse(element, "Adding ${element.runtimeType} not supported.");
}
void addFunction(
PartialFunctionElement element,
/* ScopeContainerElement */ container) {
invalidateScopesAffectedBy(element, container);
updates.add(new AddedFunctionUpdate(compiler, element, container));
}
void addClass(
PartialClassElement element,
LibraryElementX library) {
invalidateScopesAffectedBy(element, library);
updates.add(new AddedClassUpdate(compiler, element, library));
}
/// Called when a field in [definition] has changed.
///
/// There's no direct link from a [PartialFieldList] to its implied
/// [FieldElementX], so instead we use [syntheticContainer], the (synthetic)
/// container created by [canReuseLibrary], or [canReuseClass] (through
/// [PartialClassElement.parseNode]). This container is scanned looking for
/// fields whose declaration site is [definition].
// TODO(ahe): It would be nice if [computeDifference] returned this
// information directly.
void addFields(
PartialFieldList definition,
ScopeContainerElement container,
ScopeContainerElement syntheticContainer) {
List<FieldElementX> fields = <FieldElementX>[];
syntheticContainer.forEachLocalMember((ElementX member) {
if (member.declarationSite == definition) {
fields.add(member);
}
});
for (FieldElementX field in fields) {
// TODO(ahe): This only works when there's one field per
// PartialFieldList.
addField(field, container);
}
}
void addField(FieldElementX element, ScopeContainerElement container) {
invalidateScopesAffectedBy(element, container);
if (element.isInstanceMember) {
_classesWithSchemaChanges.add(container);
}
updates.add(new AddedFieldUpdate(compiler, element, container));
}
bool canReuseRemovedElement(
PartialElement element,
ScopeContainerElement container) {
if (element is PartialFunctionElement) {
removeFunction(element);
return true;
} else if (element is PartialClassElement) {
removeClass(element);
return true;
} else if (element is PartialFieldList) {
removeFields(element, container);
return true;
}
return cannotReuse(
element, "Removing ${element.runtimeType} not supported.");
}
void removeFunction(PartialFunctionElement element) {
logVerbose("Removed method $element.");
invalidateScopesAffectedBy(element, element.enclosingElement);
_removedElements.add(element);
updates.add(new RemovedFunctionUpdate(compiler, element));
}
void removeClass(PartialClassElement element) {
logVerbose("Removed class $element.");
invalidateScopesAffectedBy(element, element.library);
_removedElements.add(element);
element.forEachLocalMember((ElementX member) {
_removedElements.add(member);
});
updates.add(new RemovedClassUpdate(compiler, element));
}
void removeFields(
PartialFieldList definition,
ScopeContainerElement container) {
List<FieldElementX> fields = <FieldElementX>[];
container.forEachLocalMember((ElementX member) {
if (member.declarationSite == definition) {
fields.add(member);
}
});
for (FieldElementX field in fields) {
// TODO(ahe): This only works when there's one field per
// PartialFieldList.
removeField(field);
}
}
void removeField(FieldElementX element) {
logVerbose("Removed field $element.");
if (!element.isInstanceMember) {
cannotReuse(element, "Not an instance field.");
} else {
removeInstanceField(element);
}
}
void removeInstanceField(FieldElementX element) {
PartialClassElement cls = element.enclosingClass;
_classesWithSchemaChanges.add(cls);
invalidateScopesAffectedBy(element, cls);
_removedElements.add(element);
updates.add(new RemovedFieldUpdate(compiler, element));
}
void invalidateScopesAffectedBy(
ElementX element,
/* ScopeContainerElement */ container) {
for (ScopeContainerElement scope in scopesAffectedBy(element, container)) {
scanSites(scope, (Element member, DeclarationSite site) {
// TODO(ahe): Cache qualifiedNamesIn to avoid quadratic behavior.
Set<String> names = qualifiedNamesIn(site);
if (canNamesResolveStaticallyTo(names, element, container)) {
_elementsToInvalidate.add(member);
}
});
}
}
/// Invoke [f] on each [DeclarationSite] in [element]. If [element] is a
/// [ScopeContainerElement], invoke f on all local members as well.
void scanSites(
Element element,
void f(ElementX element, DeclarationSite site)) {
DeclarationSite site = declarationSite(element);
if (site != null) {
f(element, site);
}
if (element is ScopeContainerElement) {
element.forEachLocalMember((member) { scanSites(member, f); });
}
}
/// Assume [element] is either removed from or added to [container], and
/// return all [ScopeContainerElement] that can see this change.
List<ScopeContainerElement> scopesAffectedBy(
Element element,
/* ScopeContainerElement */ container) {
// TODO(ahe): Use library export graph to compute this.
// TODO(ahe): Should return all user-defined libraries and packages.
LibraryElement library = container.library;
List<ScopeContainerElement> result = <ScopeContainerElement>[library];
if (!container.isClass) return result;
ClassElement cls = container;
var externalSubtypes =
compiler.world.subtypesOf(cls).where((e) => e.library != library);
return result..addAll(externalSubtypes);
}
/// Returns true if function [before] can be reused to reflect the changes in
/// [after].
///
/// If [before] can be reused, an update (patch) is added to [updates].
bool canReuseFunction(
Token diffToken,
PartialFunctionElement before,
PartialFunctionElement after) {
FunctionExpression node =
after.parseNode(compiler.parsing).asFunctionExpression();
if (node == null) {
return cannotReuse(after, "Not a function expression: '$node'");
}
Token last = after.endToken;
if (node.body != null) {
last = node.body.getBeginToken();
}
if (isTokenBetween(diffToken, after.beginToken, last)) {
removeFunction(before);
addFunction(after, before.enclosingElement);
return true;
}
logVerbose('Simple modification of ${after} detected');
updates.add(new FunctionUpdate(compiler, before, after));
return true;
}
bool canReuseClass(
Token diffToken,
PartialClassElement before,
PartialClassElement after) {
ClassNode node = after.parseNode(compiler.parsing).asClassNode();
if (node == null) {
return cannotReuse(after, "Not a ClassNode: '$node'");
}
NodeList body = node.body;
if (body == null) {
return cannotReuse(after, "Class has no body.");
}
if (isTokenBetween(diffToken, node.beginToken, body.beginToken)) {
logVerbose('Class header modified in ${after}');
updates.add(new ClassUpdate(compiler, before, after));
before.forEachLocalMember((ElementX member) {
// TODO(ahe): Quadratic.
invalidateScopesAffectedBy(member, before);
});
}
return canReuseScopeContainerElement(before, after);
}
bool isTokenBetween(Token token, Token first, Token last) {
Token current = first;
while (current != last && current.kind != EOF_TOKEN) {
if (current == token) {
return true;
}
current = current.next;
}
return false;
}
bool unableToReuse(
Token diffToken,
PartialElement before,
PartialElement after) {
return cannotReuse(
after,
'Unhandled change:'
' ${before} (${before.runtimeType} -> ${after.runtimeType}).');
}
/// Apply the collected [updates]. Return a list of elements that needs to be
/// recompiled after applying the updates. Any elements removed as a
/// consequence of applying the patches are added to [removals] if provided.
List<Element> applyUpdates([List<Update> removals]) {
for (Update update in updates) {
update.captureState();
}
if (!_failedUpdates.isEmpty) {
throw new IncrementalCompilationFailed(_failedUpdates.join('\n\n'));
}
for (ElementX element in _elementsToInvalidate) {
compiler.forgetElement(element);
element.reuseElement();
}
List<Element> elementsToInvalidate = <Element>[];
for (ElementX element in _elementsToInvalidate) {
if (!_removedElements.contains(element)) {
elementsToInvalidate.add(element);
}
}
for (Update update in updates) {
Element element = update.apply();
if (update.isRemoval) {
if (removals != null) {
removals.add(update);
}
} else {
elementsToInvalidate.add(element);
}
}
return elementsToInvalidate;
}
String computeUpdateJs() {
List<Update> removals = <Update>[];
List<Element> updatedElements = applyUpdates(removals);
if (compiler.progress != null) {
compiler.progress.reset();
}
for (Element element in updatedElements) {
if (!element.isClass) {
enqueuer.resolution.addToWorkList(element);
} else {
NO_WARN(element).ensureResolved(compiler);
}
}
compiler.processQueue(enqueuer.resolution, null);
compiler.phase = Compiler.PHASE_DONE_RESOLVING;
// TODO(ahe): Clean this up. Don't call this method in analyze-only mode.
if (compiler.analyzeOnly) return "/* analyze only */";
Set<ClassElementX> changedClasses =
new Set<ClassElementX>.from(_classesWithSchemaChanges);
for (Element element in updatedElements) {
if (!element.isClass) {
enqueuer.codegen.addToWorkList(element);
} else {
changedClasses.add(element);
}
}
compiler.processQueue(enqueuer.codegen, null);
// Run through all compiled methods and see if they may apply to
// newlySeenSelectors.
for (Element e in enqueuer.codegen.generatedCode.keys) {
if (e.isFunction && !e.isConstructor &&
e.functionSignature.hasOptionalParameters) {
for (Selector selector in enqueuer.codegen.newlySeenSelectors) {
// TODO(ahe): Group selectors by name at this point for improved
// performance.
if (e.isInstanceMember && selector.applies(e, compiler.world)) {
// TODO(ahe): Don't use
// enqueuer.codegen.newlyEnqueuedElements directly like
// this, make a copy.
enqueuer.codegen.newlyEnqueuedElements.add(e);
}
if (selector.name == namer.closureInvocationSelectorName) {
selector = new Selector.call(
e.name, e.library,
selector.argumentCount, selector.namedArguments);
if (selector.appliesUnnamed(e, compiler.world)) {
// TODO(ahe): Also make a copy here.
enqueuer.codegen.newlyEnqueuedElements.add(e);
}
}
}
}
}
List<jsAst.Statement> updates = <jsAst.Statement>[];
Set<ClassElementX> newClasses = new Set.from(
compiler.codegenWorld.directlyInstantiatedClasses);
newClasses.removeAll(_directlyInstantiatedClasses);
if (!newClasses.isEmpty) {
// Ask the emitter to compute "needs" (only) if new classes were
// instantiated.
_ensureAllNeededEntitiesComputed();
newClasses = new Set.from(emitter.neededClasses);
newClasses.removeAll(_emittedClasses);
} else {
// Make sure that the set of emitted classes is preserved for subsequent
// updates.
// TODO(ahe): This is a bit convoluted, find a better approach.
emitter.neededClasses
..clear()
..addAll(_emittedClasses);
}
List<jsAst.Statement> inherits = <jsAst.Statement>[];
for (ClassElementX cls in newClasses) {
jsAst.Node classAccess = emitter.constructorAccess(cls);
String name = namer.className(cls);
updates.add(
js.statement(
r'# = #', [classAccess, invokeDefineClass(cls)]));
ClassElement superclass = cls.superclass;
if (superclass != null) {
jsAst.Node superAccess = emitter.constructorAccess(superclass);
inherits.add(
js.statement(
r'this.inheritFrom(#, #)', [classAccess, superAccess]));
}
}
// Call inheritFrom after all classes have been created. This way we don't
// need to sort the classes by having superclasses defined before their
// subclasses.
updates.addAll(inherits);
for (ClassElementX cls in changedClasses) {
ClassElement superclass = cls.superclass;
jsAst.Node superAccess =
superclass == null ? js('null')
: emitter.constructorAccess(superclass);
jsAst.Node classAccess = emitter.constructorAccess(cls);
updates.add(
js.statement(
r'# = this.schemaChange(#, #, #)',
[classAccess, invokeDefineClass(cls), classAccess, superAccess]));
}
for (RemovalUpdate update in removals) {
update.writeUpdateJsOn(updates);
}
for (Element element in enqueuer.codegen.newlyEnqueuedElements) {
if (element.isField) {
updates.addAll(computeFieldUpdateJs(element));
} else {
updates.add(computeMethodUpdateJs(element));
}
}
Set<ConstantValue> newConstants = new Set<ConstantValue>.identity()..addAll(
compiler.backend.constants.compiledConstants);
newConstants.removeAll(_compiledConstants);
if (!newConstants.isEmpty) {
_ensureAllNeededEntitiesComputed();
List<ConstantValue> constants =
emitter.outputConstantLists[compiler.deferredLoadTask.mainOutputUnit];
if (constants != null) {
for (ConstantValue constant in constants) {
if (!_compiledConstants.contains(constant)) {
full.Emitter fullEmitter = emitter.emitter;
jsAst.Statement constantInitializer =
fullEmitter.buildConstantInitializer(constant).toStatement();
updates.add(constantInitializer);
}
}
}
}
updates.add(js.statement(r'''
if (this.pendingStubs) {
this.pendingStubs.map(function(e) { return e(); });
this.pendingStubs = void 0;
}
'''));
if (updates.length == 1) {
return prettyPrintJs(updates.single);
} else {
return prettyPrintJs(js.statement('{#}', [updates]));
}
}
jsAst.Expression invokeDefineClass(ClassElementX cls) {
String name = namer.className(cls);
var descriptor = js('Object.create(null)');
return js(
r'''
(new Function(
"$collectedClasses", "$desc",
this.defineClass(#name, #computeFields) +"\n;return " + #name))(
{#name: [,#descriptor]})''',
{'name': js.string(name),
'computeFields': js.stringArray(computeFields(cls)),
'descriptor': descriptor});
}
jsAst.Node computeMethodUpdateJs(Element element) {
Method member = new ProgramBuilder(compiler, namer, emitter)
.buildMethodHackForIncrementalCompilation(element);
if (member == null) {
compiler.internalError(element, '${element.runtimeType}');
}
ClassBuilder builder = new ClassBuilder(element, namer);
containerBuilder.addMemberMethod(member, builder);
jsAst.Node partialDescriptor =
builder.toObjectInitializer(emitClassDescriptor: false);
String name = member.name;
jsAst.Node function = member.code;
bool isStatic = !element.isInstanceMember;
/// Either a global object (non-instance members) or a prototype (instance
/// members).
jsAst.Node holder;
if (element.isInstanceMember) {
holder = emitter.prototypeAccess(element.enclosingClass);
} else {
holder = js('#', namer.globalObjectFor(element));
}
jsAst.Expression globalFunctionsAccess =
emitter.generateEmbeddedGlobalAccess(embeddedNames.GLOBAL_FUNCTIONS);
return js.statement(
r'this.addMethod(#, #, #, #, #)',
[partialDescriptor, js.string(name), holder,
new jsAst.LiteralBool(isStatic), globalFunctionsAccess]);
}
List<jsAst.Statement> computeFieldUpdateJs(FieldElementX element) {
if (element.isInstanceMember) {
// Any initializers are inlined in factory methods, and the field is
// declared by adding its class to [_classesWithSchemaChanges].
return const <jsAst.Statement>[];
}
// A static (or top-level) field.
if (backend.constants.lazyStatics.contains(element)) {
full.Emitter fullEmitter = emitter.emitter;
jsAst.Expression init =
fullEmitter.buildLazilyInitializedStaticField(
element, isolateProperties: namer.staticStateHolder);
if (init == null) {
throw new StateError("Initializer optimized away for $element");
}
return <jsAst.Statement>[init.toStatement()];
} else {
// TODO(ahe): When a field is referenced it is enqueued. If the field has
// no initializer, it will not have any associated code, so it will
// appear as if it was newly enqueued.
if (element.initializer == null) {
return const <jsAst.Statement>[];
} else {
throw new StateError("Don't know how to compile $element");
}
}
}
String prettyPrintJs(jsAst.Node node) {
jsAst.JavaScriptPrintingOptions options =
new jsAst.JavaScriptPrintingOptions();
jsAst.JavaScriptPrintingContext context =
new jsAst.Dart2JSJavaScriptPrintingContext(compiler, null);
jsAst.Printer printer = new jsAst.Printer(options, context);
printer.blockOutWithoutBraces(node);
return context.outBuffer.getText();
}
String callNameFor(FunctionElement element) {
// TODO(ahe): Call a method in the compiler to obtain this name.
String callPrefix = namer.callPrefix;
int parameterCount = element.functionSignature.parameterCount;
return '$callPrefix\$$parameterCount';
}
List<String> computeFields(ClassElement cls) {
return new EmitterHelper(compiler).computeFields(cls);
}
void _ensureAllNeededEntitiesComputed() {
if (_hasComputedNeeds) return;
emitter.computeAllNeededEntities();
_hasComputedNeeds = true;
}
}
/// Represents an update (aka patch) of [before] to [after]. We use the word
/// "update" to avoid confusion with the compiler feature of "patch" methods.
abstract class Update {
final Compiler compiler;
PartialElement get before;
PartialElement get after;
Update(this.compiler);
/// Applies the update to [before] and returns that element.
Element apply();
bool get isRemoval => false;
/// Called before any patches are applied to capture any state that is needed
/// later.
void captureState() {
}
}
/// Represents an update of a function element.
class FunctionUpdate extends Update with ReuseFunction {
final PartialFunctionElement before;
final PartialFunctionElement after;
FunctionUpdate(Compiler compiler, this.before, this.after)
: super(compiler);
PartialFunctionElement apply() {
patchElement();
reuseElement();
return before;
}
/// Destructively change the tokens in [before] to match those of [after].
void patchElement() {
before.beginToken = after.beginToken;
before.endToken = after.endToken;
before.getOrSet = after.getOrSet;
}
}
abstract class ReuseFunction {
Compiler get compiler;
PartialFunctionElement get before;
/// Reset various caches and remove this element from the compiler's internal
/// state.
void reuseElement() {
compiler.forgetElement(before);
before.reuseElement();
}
}
abstract class RemovalUpdate extends Update {
ElementX get element;
RemovalUpdate(Compiler compiler)
: super(compiler);
bool get isRemoval => true;
void writeUpdateJsOn(List<jsAst.Statement> updates);
void removeFromEnclosing() {
// TODO(ahe): Need to recompute duplicated elements logic again. Simplest
// solution is probably to remove all elements from enclosing scope and add
// them back.
if (element.isTopLevel) {
removeFromLibrary(element.library);
} else {
removeFromEnclosingClass(element.enclosingClass);
}
}
void removeFromEnclosingClass(PartialClassElement cls) {
cls.localMembersCache = null;
cls.localMembersReversed = cls.localMembersReversed.copyWithout(element);
cls.localScope.contents.remove(element.name);
}
void removeFromLibrary(LibraryElementX library) {
library.localMembers = library.localMembers.copyWithout(element);
library.localScope.contents.remove(element.name);
}
}
class RemovedFunctionUpdate extends RemovalUpdate
with JsFeatures, ReuseFunction {
final PartialFunctionElement element;
/// Name of property to remove using JavaScript "delete". Null for
/// non-instance methods.
String name;
/// For instance methods, access to class object. Otherwise, access to the
/// method itself.
jsAst.Node elementAccess;
bool wasStateCaptured = false;
RemovedFunctionUpdate(Compiler compiler, this.element)
: super(compiler);
PartialFunctionElement get before => element;
PartialFunctionElement get after => null;
void captureState() {
if (wasStateCaptured) throw "captureState was called twice.";
wasStateCaptured = true;
if (element.isInstanceMember) {
elementAccess = emitter.constructorAccess(element.enclosingClass);
name = namer.instanceMethodName(element);
} else {
elementAccess = emitter.staticFunctionAccess(element);
}
}
PartialFunctionElement apply() {
if (!wasStateCaptured) throw "captureState must be called before apply.";
removeFromEnclosing();
reuseElement();
return null;
}
void writeUpdateJsOn(List<jsAst.Statement> updates) {
if (elementAccess == null) {
compiler.internalError(
element, 'No elementAccess for ${element.runtimeType}');
}
if (element.isInstanceMember) {
if (name == null) {
compiler.internalError(element, 'No name for ${element.runtimeType}');
}
updates.add(
js.statement('delete #.prototype.#', [elementAccess, name]));
} else {
updates.add(js.statement('delete #', [elementAccess]));
}
}
}
class RemovedClassUpdate extends RemovalUpdate with JsFeatures {
final PartialClassElement element;
bool wasStateCaptured = false;
final List<jsAst.Node> accessToStatics = <jsAst.Node>[];
RemovedClassUpdate(Compiler compiler, this.element)
: super(compiler);
PartialClassElement get before => element;
PartialClassElement get after => null;
void captureState() {
if (wasStateCaptured) throw "captureState was called twice.";
wasStateCaptured = true;
accessToStatics.add(emitter.constructorAccess(element));
element.forEachLocalMember((ElementX member) {
if (!member.isInstanceMember) {
accessToStatics.add(emitter.staticFunctionAccess(member));
}
});
}
PartialClassElement apply() {
if (!wasStateCaptured) {
throw new StateError("captureState must be called before apply.");
}
removeFromEnclosing();
element.forEachLocalMember((ElementX member) {
compiler.forgetElement(member);
member.reuseElement();
});
compiler.forgetElement(element);
element.reuseElement();
return null;
}
void writeUpdateJsOn(List<jsAst.Statement> updates) {
if (accessToStatics.isEmpty) {
throw
new StateError("captureState must be called before writeUpdateJsOn.");
}
for (jsAst.Node access in accessToStatics) {
updates.add(js.statement('delete #', [access]));
}
}
}
class RemovedFieldUpdate extends RemovalUpdate with JsFeatures {
final FieldElementX element;
bool wasStateCaptured = false;
jsAst.Node prototypeAccess;
String getterName;
String setterName;
RemovedFieldUpdate(Compiler compiler, this.element)
: super(compiler);
PartialFieldList get before => element.declarationSite;
PartialFieldList get after => null;
void captureState() {
if (wasStateCaptured) throw "captureState was called twice.";
wasStateCaptured = true;
prototypeAccess = emitter.prototypeAccess(element.enclosingClass);
getterName = namer.getterForElement(element);
setterName = namer.setterForElement(element);
}
FieldElementX apply() {
if (!wasStateCaptured) {
throw new StateError("captureState must be called before apply.");
}
removeFromEnclosing();
return element;
}
void writeUpdateJsOn(List<jsAst.Statement> updates) {
if (!wasStateCaptured) {
throw new StateError(
"captureState must be called before writeUpdateJsOn.");
}
updates.add(
js.statement('delete #.#', [prototypeAccess, getterName]));
updates.add(
js.statement('delete #.#', [prototypeAccess, setterName]));
}
}
class AddedFunctionUpdate extends Update with JsFeatures {
final PartialFunctionElement element;
final /* ScopeContainerElement */ container;
AddedFunctionUpdate(Compiler compiler, this.element, this.container)
: super(compiler) {
if (container == null) {
throw "container is null";
}
}
PartialFunctionElement get before => null;
PartialFunctionElement get after => element;
PartialFunctionElement apply() {
Element enclosing = container;
if (enclosing.isLibrary) {
// TODO(ahe): Reuse compilation unit of element instead?
enclosing = enclosing.compilationUnit;
}
PartialFunctionElement copy = element.copyWithEnclosing(enclosing);
NO_WARN(container).addMember(copy, compiler);
return copy;
}
}
class AddedClassUpdate extends Update with JsFeatures {
final PartialClassElement element;
final LibraryElementX library;
AddedClassUpdate(Compiler compiler, this.element, this.library)
: super(compiler);
PartialClassElement get before => null;
PartialClassElement get after => element;
PartialClassElement apply() {
// TODO(ahe): Reuse compilation unit of element instead?
CompilationUnitElementX compilationUnit = library.compilationUnit;
PartialClassElement copy = element.copyWithEnclosing(compilationUnit);
compilationUnit.addMember(copy, compiler);
return copy;
}
}
class AddedFieldUpdate extends Update with JsFeatures {
final FieldElementX element;
final ScopeContainerElement container;
AddedFieldUpdate(Compiler compiler, this.element, this.container)
: super(compiler);
PartialFieldList get before => null;
PartialFieldList get after => element.declarationSite;
FieldElementX apply() {
Element enclosing = container;
if (enclosing.isLibrary) {
// TODO(ahe): Reuse compilation unit of element instead?
enclosing = enclosing.compilationUnit;
}
FieldElementX copy = element.copyWithEnclosing(enclosing);
NO_WARN(container).addMember(copy, compiler);
return copy;
}
}
class ClassUpdate extends Update with JsFeatures {
final PartialClassElement before;
final PartialClassElement after;
ClassUpdate(Compiler compiler, this.before, this.after)
: super(compiler);
PartialClassElement apply() {
patchElement();
reuseElement();
return before;
}
/// Destructively change the tokens in [before] to match those of [after].
void patchElement() {
before.cachedNode = after.cachedNode;
before.beginToken = after.beginToken;
before.endToken = after.endToken;
}
void reuseElement() {
before.supertype = null;
before.interfaces = null;
before.nativeTagInfo = null;
before.supertypeLoadState = STATE_NOT_STARTED;
before.resolutionState = STATE_NOT_STARTED;
before.isProxy = false;
before.hasIncompleteHierarchy = false;
before.backendMembers = const Link<Element>();
before.allSupertypesAndSelf = null;
}
}
/// Returns all qualified names in [element] with less than four identifiers. A
/// qualified name is an identifier followed by a sequence of dots and
/// identifiers, for example, "x", and "x.y.z". But not "x.y.z.w" ("w" is the
/// fourth identifier).
///
/// The longest possible name that can be resolved is three identifiers, for
/// example, "prefix.MyClass.staticMethod". Since four or more identifiers
/// cannot resolve to anything statically, they're not included in the returned
/// value of this method.
Set<String> qualifiedNamesIn(PartialElement element) {
Token beginToken = element.beginToken;
Token endToken = element.endToken;
Token token = beginToken;
if (element is PartialClassElement) {
ClassNode node = element.cachedNode;
if (node != null) {
NodeList body = node.body;
if (body != null) {
endToken = body.beginToken;
}
}
}
Set<String> names = new Set<String>();
do {
if (token.isIdentifier()) {
String name = token.value;
// [name] is a single "identifier".
names.add(name);
if (identical('.', token.next.stringValue) &&
token.next.next.isIdentifier()) {
token = token.next.next;
name += '.${token.value}';
// [name] is "idenfifier.idenfifier".
names.add(name);
if (identical('.', token.next.stringValue) &&
token.next.next.isIdentifier()) {
token = token.next.next;
name += '.${token.value}';
// [name] is "idenfifier.idenfifier.idenfifier".
names.add(name);
while (identical('.', token.next.stringValue) &&
token.next.next.isIdentifier()) {
// Skip remaining identifiers, they cannot statically resolve to
// anything, and must be dynamic sends.
token = token.next.next;
}
}
}
}
token = token.next;
} while (token.kind != EOF_TOKEN && token != endToken);
return names;
}
/// Returns true if one of the qualified names in names (as computed by
/// [qualifiedNamesIn]) could be a static reference to [element].
bool canNamesResolveStaticallyTo(
Set<String> names,
Element element,
/* ScopeContainerElement */ container) {
if (names.contains(element.name)) return true;
if (container != null && container.isClass) {
// [names] contains C.m, where C is the name of [container], and m is the
// name of [element].
if (names.contains("${container.name}.${element.name}")) return true;
}
// TODO(ahe): Check for prefixes as well.
return false;
}
DeclarationSite declarationSite(Element element) {
return element is ElementX ? element.declarationSite : null;
}
abstract class JsFeatures {
Compiler get compiler;
JavaScriptBackend get backend => compiler.backend;
Namer get namer => backend.namer;
CodeEmitterTask get emitter => backend.emitter;
ContainerBuilder get containerBuilder {
full.Emitter fullEmitter = emitter.emitter;
return fullEmitter.containerBuilder;
}
EnqueueTask get enqueuer => compiler.enqueuer;
}
class EmitterHelper extends JsFeatures {
final Compiler compiler;
EmitterHelper(this.compiler);
ClassEmitter get classEmitter {
full.Emitter fullEmitter = emitter.emitter;
return fullEmitter.classEmitter;
}
List<String> computeFields(ClassElement classElement) {
Class cls = new ProgramBuilder(compiler, namer, emitter)
.buildFieldsHackForIncrementalCompilation(classElement);
// TODO(ahe): Rewrite for new emitter.
ClassBuilder builder = new ClassBuilder(classElement, namer);
classEmitter.emitFields(cls, builder);
return builder.fields;
}
}
// TODO(ahe): Remove this method.
NO_WARN(x) => x;