| // 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 'dart:convert' show |
| UTF8; |
| |
| import 'package:compiler/compiler.dart' as api; |
| |
| import 'package:compiler/src/dart2jslib.dart' show |
| Compiler, |
| Script; |
| |
| import 'package:compiler/src/elements/elements.dart' show |
| Element, |
| FunctionElement, |
| LibraryElement, |
| ScopeContainerElement; |
| |
| import 'package:compiler/src/scanner/scannerlib.dart' show |
| EOF_TOKEN, |
| PartialClassElement, |
| PartialElement, |
| PartialFunctionElement, |
| Token; |
| |
| import 'package:compiler/src/source_file.dart' show |
| StringSourceFile; |
| |
| import 'package:compiler/src/tree/tree.dart' show |
| ClassNode, |
| FunctionExpression, |
| NodeList; |
| |
| 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, |
| MemberInfo; |
| |
| import 'package:_internal/compiler/js_lib/shared/embedded_names.dart' |
| as embeddedNames; |
| |
| import 'package:compiler/src/js_backend/js_backend.dart' show |
| JavaScriptBackend, |
| Namer; |
| |
| import 'diff.dart' show |
| Difference, |
| computeDifference; |
| |
| 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'; |
| } |
| } |
| |
| // TODO(ahe): Generalize this class. For now only works for Compiler.mainApp, |
| // and only if that library has exactly one compilation unit. |
| class LibraryUpdater { |
| final Compiler compiler; |
| |
| final api.CompilerInputProvider inputProvider; |
| |
| final Logger logTime; |
| |
| final Logger logVerbose; |
| |
| // TODO(ahe): Get rid of this field. It assumes that only one library has |
| // changed. |
| final Uri uri; |
| |
| final List<Update> updates = <Update>[]; |
| |
| final List<FailedUpdate> _failedUpdates = <FailedUpdate>[]; |
| |
| LibraryUpdater( |
| this.compiler, |
| this.inputProvider, |
| this.uri, |
| this.logTime, |
| this.logVerbose); |
| |
| /// 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; |
| |
| JavaScriptBackend get backend => compiler.backend; |
| |
| Namer get namer => backend.namer; |
| |
| CodeEmitterTask get emitter => backend.emitter; |
| |
| /// Used as tear-off passed to [LibraryLoaderTask.resetAsync]. |
| Future<bool> reuseLibrary(LibraryElement library) { |
| assert(compiler != null); |
| if (library.isPlatformLibrary || library.isPackageLibrary) { |
| logTime('Reusing $library.'); |
| return new Future.value(true); |
| } else if (library != compiler.mainApp) { |
| return new Future.value(false); |
| } |
| return inputProvider(uri).then((bytes) { |
| return canReuseLibrary(library, bytes); |
| }); |
| } |
| |
| /// Returns true if [library] can be reused. |
| /// |
| /// This methods also computes the [updates] (patches) needed to have |
| /// [library] reflect the modifications in [bytes]. |
| bool canReuseLibrary(LibraryElement library, bytes) { |
| logTime('Attempting to reuse mainApp.'); |
| String newSource = bytes is String ? bytes : UTF8.decode(bytes); |
| logTime('Decoded UTF8'); |
| |
| // TODO(ahe): Can't use compiler.mainApp in general. |
| if (false && newSource == compiler.mainApp.compilationUnit.script.text) { |
| // TODO(ahe): Need to update the compilationUnit's source code when |
| // doing incremental analysis for this to work. |
| logTime("Source didn't change"); |
| return true; |
| } |
| |
| logTime("Source did change"); |
| Script sourceScript = new Script( |
| uri, uri, new StringSourceFile('$uri', newSource)); |
| var dartPrivacyIsBroken = compiler.libraryLoader; |
| LibraryElement newLibrary = dartPrivacyIsBroken.createLibrarySync( |
| null, sourceScript, uri); |
| 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 == null) { |
| cannotReuse(difference, "Can't reuse; Scope changed."); |
| 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; |
| } |
| |
| /// 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).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)) { |
| return cannotReuse(after, 'Signature changed.'); |
| } |
| 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).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)) { |
| return cannotReuse(after, "Class header changed."); |
| } |
| logVerbose('Simple modification of ${after} detected'); |
| 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}).'); |
| } |
| |
| List<Element> applyUpdates() { |
| if (!_failedUpdates.isEmpty) { |
| throw new StateError( |
| "Can't compute update.\n\n${_failedUpdates.join('\n\n')}"); |
| } |
| return updates.map((Update update) => update.apply()).toList(); |
| } |
| |
| String computeUpdateJs() { |
| List<Element> updatedElements = applyUpdates(); |
| if (compiler.progress != null) { |
| compiler.progress.reset(); |
| } |
| for (Element element in updatedElements) { |
| compiler.enqueuer.resolution.addToWorkList(element); |
| } |
| compiler.processQueue(compiler.enqueuer.resolution, null); |
| |
| compiler.phase = Compiler.PHASE_DONE_RESOLVING; |
| |
| for (Element element in updatedElements) { |
| compiler.enqueuer.codegen.addToWorkList(element); |
| } |
| compiler.processQueue(compiler.enqueuer.codegen, null); |
| |
| List<jsAst.Statement> updates = <jsAst.Statement>[]; |
| for (Element element in compiler.enqueuer.codegen.newlyEnqueuedElements) { |
| if (!element.isField) { |
| updates.add(computeMemberUpdateJs(element)); |
| } |
| } |
| |
| if (updates.length == 1) { |
| return prettyPrintJs(updates.single); |
| } else { |
| return prettyPrintJs(js.statement('{#}', [updates])); |
| } |
| } |
| |
| jsAst.Node computeMemberUpdateJs(Element element) { |
| MemberInfo info = emitter.oldEmitter.containerBuilder |
| .analyzeMemberMethod(element); |
| if (info == null) { |
| compiler.internalError(element, '${element.runtimeType}'); |
| } |
| String name = info.name; |
| jsAst.Node function = info.code; |
| List<jsAst.Statement> statements = <jsAst.Statement>[]; |
| if (element.isInstanceMember) { |
| jsAst.Node elementAccess = namer.elementAccess(element.enclosingClass); |
| statements.add( |
| js.statement('#.prototype.# = f', [elementAccess, name])); |
| } else { |
| jsAst.Node elementAccess = namer.elementAccess(element); |
| jsAst.Expression globalFunctionsAccess = |
| emitter.generateEmbeddedGlobalAccess(embeddedNames.GLOBAL_FUNCTIONS); |
| statements.add( |
| js.statement( |
| '#.# = # = f', |
| [globalFunctionsAccess, name, elementAccess])); |
| if (info.canTearOff) { |
| String globalName = namer.globalObjectFor(element); |
| statements.add( |
| js.statement( |
| '#.#().# = f', |
| [globalName, info.tearOffName, callNameFor(element)])); |
| } |
| } |
| // Create a scope by creating a new function. The updated function literal |
| // is passed as an argument to this function which ensures that temporary |
| // names in updateScope don't shadow global names. |
| jsAst.Fun updateScope = js('function (f) { # }', [statements]); |
| return js.statement('(#)(#)', [updateScope, function]); |
| } |
| |
| String prettyPrintJs(jsAst.Node node) { |
| jsAst.Printer printer = new jsAst.Printer(compiler, null); |
| printer.blockOutWithoutBraces(node); |
| return printer.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'; |
| } |
| } |
| |
| /// 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. |
| PartialElement apply(); |
| } |
| |
| /// Represents an update of a function element. |
| class FunctionUpdate extends Update { |
| 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; |
| } |
| |
| /// Reset various caches and remove this element from the compiler's internal |
| /// state. |
| void reuseElement() { |
| compiler.forgetElement(before); |
| before.reuseElement(); |
| } |
| } |