blob: 961ec7b80f28af26cb94433d0f55431b1129310d [file] [log] [blame]
// Copyright (c) 2015, 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 rewrite_async;
// TODO(sigurdm): Avoid using variables in templates. It could blow up memory
// use.
// TODO(sigurdm): Move the try/catch expression to a js_helper function.
// That would also simplify the sync* case, where the error can just be thrown.
import "dart:math" show max;
import 'dart:collection';
import 'package:_internal/compiler/js_lib/shared/async_await_error_codes.dart'
as error_codes;
import "js.dart" as js;
import '../util/util.dart';
import '../dart2jslib.dart' show DiagnosticListener;
import "../helpers/helpers.dart";
/// Rewrites a [js.Fun] with async/sync*/async* functions and await and yield
/// (with dart-like semantics) to an equivalent function without these.
/// await-for is not handled and must be rewritten before. (Currently handled
/// in ssa/builder.dart).
///
/// When generating the input to this, special care must be taken that
/// parameters to sync* functions that are mutated in the body must be boxed.
/// (Currently handled in closure.dart).
///
/// Look at [visitFun], [visitDartYield] and [visitAwait] for more explanation.
class AsyncRewriter extends js.NodeVisitor {
// Local variables are hoisted to the top of the function, so they are
// collected here.
List<js.VariableDeclaration> localVariables =
new List<js.VariableDeclaration>();
Map<js.Node, int> continueLabels = new Map<js.Node, int>();
Map<js.Node, int> breakLabels = new Map<js.Node, int>();
/// The label of a finally part.
Map<js.Block, int> finallyLabels = new Map<js.Block, int>();
/// The label of the catch handler of a [js.Try] or a [js.Fun] or [js.Catch].
///
/// These mark the points an error can be consumed.
///
/// - The handler of a [js.Fun] is the outermost and will rethrow the error.
/// - The handler of a [js.Try] will run the catch handler.
/// - The handler of a [js.Catch] is a synthetic handler that ensures the
/// right finally blocks are run if an error is thrown inside a
/// catch-handler.
Map<js.Node, int> handlerLabels = new Map<js.Node, int>();
int exitLabel;
int rethrowLabel;
/// A stack of all (surrounding) jump targets.
///
/// Jump targets are:
///
/// * The function, signalling a return or uncaught throw.
/// * Loops.
/// * LabeledStatements (also used for 'continue' when attached to loops).
/// * Try statements, for catch and finally handlers.
/// * Catch handlers, when inside a catch-part of a try, the catch-handler is
/// used to associate with a synthetic handler that will ensure the right
/// finally blocks are visited.
///
/// When jumping to a target it is necessary to visit all finallies that
/// are on the way to target (i.e. more nested than the jump target).
List<js.Node> jumpTargets = new List<js.Node>();
List<int> continueStack = new List<int>();
List<int> breakStack = new List<int>();
List<int> returnStack = new List<int>();
List<Pair<String, String>> variableRenamings =
new List<Pair<String, String>>();
PreTranslationAnalysis analysis;
final Function safeVariableName;
// All the <x>Name variables are names of Javascript variables used in the
// transformed code.
/// Contains the result of an awaited expression, or a conditional or
/// lazy boolean operator.
///
/// For example a conditional expression is roughly translated like:
/// [[cond ? a : b]]
///
/// Becomes:
///
/// while true { // outer while loop
/// switch (goto) { // Simulates goto
/// ...
/// goto = [[cond]] ? thenLabel : elseLabel
/// break;
/// case thenLabel:
/// result = [[a]];
/// goto = joinLabel;
/// case elseLabel:
/// result = [[b]];
/// case joinLabel:
/// // Now the result of computing the condition is in result.
/// ....
/// }
/// }
///
/// It is a parameter to the [bodyName] function, so that [asyncHelper] and
/// [streamHelper] can call [bodyName] with the result of an awaited Future.
String resultName;
/// A parameter to the [bodyName] function. Indicating if we are in success
/// or error case.
String errorCodeName;
/// The name of the inner function that is scheduled to do each await/yield,
/// and called to do a new iteration for sync*.
String bodyName;
/// The Completer that will finish an async function.
///
/// Not used for sync* or async* functions.
String completerName;
/// The StreamController that controls an async* function.
///
/// Not used for async and sync* functions
String controllerName;
/// Used to simulate a goto.
///
/// To "goto" a label, the label is assigned to this
/// variable, and break out of the switch to take another iteration in the
/// while loop. See [addGoto]
String gotoName;
/// The label of the current error handler.
String handlerName;
/// Current caught error.
String errorName;
/// A stack of labels of finally blocks to visit, and the label to go to after
/// the last.
String nextName;
/// The stack of labels of finally blocks to assign to [nextName] if the
/// async* [StreamSubscription] was canceled during a yield.
String nextWhenCanceledName;
/// The current returned value (a finally block may overwrite it).
String returnValueName;
/// If we are in the process of handling an error, stores the current error.
String currentErrorName;
/// The label of the outer loop.
///
/// Used if there are untransformed loops containing break or continues to
/// targets outside the loop.
String outerLabelName;
/// If javascript `this` is used, it is accessed via this variable, in the
/// [bodyName] function.
String selfName;
// These expressions are hooks for communicating with the runtime.
/// The function called by an async function to simulate an await or return.
///
/// For an await it is called with:
///
/// - The value to await
/// - The body function [bodyName]
/// - The completer object [completerName]
///
/// For a return it is called with:
///
/// - The value to complete the completer with.
/// - [error_codes.SUCCESS]
/// - The completer object [completerName]
///
/// For a throw it is called with:
///
/// - The error to complete the completer with.
/// - [error_codes.ERROR]
/// - The completer object [completerName]
final js.Expression asyncHelper;
/// The function called by an async* function to simulate an await, yield or
/// yield*.
///
/// For an await/yield/yield* it is called with:
///
/// - The value to await/yieldExpression(value to yield)/
/// yieldStarExpression(stream to yield)
/// - The body function [bodyName]
/// - The controller object [controllerName]
///
/// For a return it is called with:
///
/// - null
/// - null
/// - The [controllerName]
/// - null.
final js.Expression streamHelper;
/// Contructor used to initialize the [completerName] variable.
///
/// Specific to async methods.
final js.Expression newCompleter;
/// Contructor used to initialize the [controllerName] variable.
///
/// Specific to async* methods.
final js.Expression newController;
/// Used to get the `Stream` out of the [controllerName] variable.
///
/// Specific to async* methods.
final js.Expression streamOfController;
/// Contructor creating the Iterable for a sync* method. Called with
/// [bodyName].
final js.Expression newIterable;
/// A JS Expression that creates a marker showing that iteration is over.
///
/// Called without arguments.
final js.Expression endOfIteration;
/// A JS Expression that creates a marker indicating a 'yield' statement.
///
/// Called with the value to yield.
final js.Expression yieldExpression;
/// A JS Expression that creates a marker indication a 'yield*' statement.
///
/// Called with the stream to yield from.
final js.Expression yieldStarExpression;
/// Used by sync* functions to throw exeptions.
final js.Expression uncaughtErrorExpression;
final DiagnosticListener diagnosticListener;
// For error reporting only.
Spannable get spannable {
return (_spannable == null) ? NO_LOCATION_SPANNABLE : _spannable;
}
Spannable _spannable;
int _currentLabel = 0;
// The highest temporary variable index currently in use.
int currentTempVarIndex = 0;
// The highest temporary variable index ever in use in this function.
int tempVarHighWaterMark = 0;
Map<int, js.Expression> tempVarNames = new Map<int, js.Expression>();
js.AsyncModifier async;
bool get isSync => async == const js.AsyncModifier.sync();
bool get isAsync => async == const js.AsyncModifier.async();
bool get isSyncStar => async == const js.AsyncModifier.syncStar();
bool get isAsyncStar => async == const js.AsyncModifier.asyncStar();
AsyncRewriter(this.diagnosticListener,
spannable,
{this.asyncHelper,
this.streamHelper,
this.streamOfController,
this.newCompleter,
this.newController,
this.endOfIteration,
this.newIterable,
this.yieldExpression,
this.yieldStarExpression,
this.uncaughtErrorExpression,
this.safeVariableName})
: _spannable = spannable;
/// Main entry point.
/// Rewrites a sync*/async/async* function to an equivalent normal function.
///
/// [spannable] can be passed to have a location for error messages.
js.Fun rewrite(js.Fun node, [Spannable spannable]) {
_spannable = spannable;
async = node.asyncModifier;
assert(!isSync);
analysis = new PreTranslationAnalysis(unsupported);
analysis.analyze(node);
// To avoid name collisions with existing names, the fresh names are
// generated after the analysis.
resultName = freshName("result");
errorCodeName = freshName("errorCode");
completerName = freshName("completer");
controllerName = freshName("controller");
bodyName = freshName("body");
gotoName = freshName("goto");
handlerName = freshName("handler");
errorName = freshName("error");
nextName = freshName("next");
nextWhenCanceledName = freshName("nextWhenCanceled");
returnValueName = freshName("returnValue");
currentErrorName = freshName("currentError");
outerLabelName = freshName("outer");
selfName = freshName("self");
return node.accept(this);
}
js.Expression get currentErrorHandler {
return js.number(handlerLabels[jumpTargets.lastWhere(
(node) => handlerLabels[node] != null)]);
}
int allocateTempVar() {
assert(tempVarHighWaterMark >= currentTempVarIndex);
currentTempVarIndex++;
tempVarHighWaterMark = max(currentTempVarIndex, tempVarHighWaterMark);
return currentTempVarIndex;
}
js.VariableUse useTempVar(int i) {
return tempVarNames.putIfAbsent(
i, () => new js.VariableUse(freshName("temp$i")));
}
/// Generates a variable name with [safeVariableName] based on [originalName]
/// with a suffix to guarantee it does not collide with already used names.
String freshName(String originalName) {
String safeName = safeVariableName(originalName);
String result = safeName;
int counter = 1;
while (analysis.usedNames.contains(result)) {
result = "$safeName$counter";
++counter;
}
analysis.usedNames.add(result);
return result;
}
/// All the pieces are collected in this map, to create a switch with a case
/// for each label.
///
/// The order is important, therefore the type is explicitly LinkedHashMap.
LinkedHashMap<int, List<js.Statement>> labelledParts =
new LinkedHashMap<int, List<js.Statement>>();
/// Description of each label for readability of the non-minified output.
Map<int, String> labelComments = new Map<int, String>();
/// True if the function has any try blocks containing await.
bool hasTryBlocks = false;
/// True if any return, break or continue passes through a finally.
bool hasJumpThroughFinally = false;
/// True if the traversion currently is inside a loop or switch for which
/// [shouldTransform] is false.
bool insideUntranslatedBreakable = false;
/// True if a label is used to break to an outer switch-statement.
bool hasJumpThoughOuterLabel = false;
/// True if there is a catch-handler protected by a finally with no enclosing
/// catch-handlers.
bool needsRethrow = false;
/// Buffer for collecting translated statements belonging to the same switch
/// case.
List<js.Statement> currentStatementBuffer;
// Labels will become cases in the big switch expression, and `goto label`
// is expressed by assigning to the switch key [gotoName] and breaking out of
// the switch.
int newLabel([String comment]) {
int result = _currentLabel;
_currentLabel++;
if (comment != null) {
labelComments[result] = comment;
}
return result;
}
/// Begins outputting statements to a new buffer with label [label].
///
/// Each buffer ends up as its own case part in the big state-switch.
void beginLabel(int label) {
assert(!labelledParts.containsKey(label));
currentStatementBuffer = new List<js.Statement>();
labelledParts[label] = currentStatementBuffer;
addStatement(new js.Comment(labelComments[label]));
}
/// Returns a statement assigning to the variable named [gotoName].
/// This should be followed by a break for the goto to be executed. Use
/// [gotoWithBreak] or [addGoto] for this.
js.Statement setGotoVariable(int label) {
return new js.ExpressionStatement(
new js.Assignment(new js.VariableUse(gotoName), js.number(label)));
}
/// Returns a block that has a goto to [label] including the break.
///
/// Also inserts a comment describing the label if available.
js.Block gotoAndBreak(int label) {
List<js.Statement> statements = new List<js.Statement>();
if (labelComments.containsKey(label)) {
statements.add(new js.Comment("goto ${labelComments[label]}"));
}
statements.add(setGotoVariable(label));
if (insideUntranslatedBreakable) {
hasJumpThoughOuterLabel = true;
statements.add(new js.Break(outerLabelName));
} else {
statements.add(new js.Break(null));
}
return new js.Block(statements);
}
/// Adds a goto to [label] including the break.
///
/// Also inserts a comment describing the label if available.
void addGoto(int label) {
if (labelComments.containsKey(label)) {
addStatement(new js.Comment("goto ${labelComments[label]}"));
}
addStatement(setGotoVariable(label));
addBreak();
}
void addStatement(js.Statement node) {
currentStatementBuffer.add(node);
}
void addExpressionStatement(js.Expression node) {
addStatement(new js.ExpressionStatement(node));
}
/// True if there is an await or yield in [node] or some subexpression.
bool shouldTransform(js.Node node) {
return analysis.hasAwaitOrYield.contains(node);
}
void unsupported(js.Node node) {
throw new UnsupportedError(
"Node $node cannot be transformed by the await-sync transformer");
}
void unreachable(js.Node node) {
diagnosticListener.internalError(
spannable, "Internal error, trying to visit $node");
}
visitStatement(js.Statement node) {
node.accept(this);
}
/// Visits [node] to ensure its sideeffects are performed, but throwing away
/// the result.
///
/// If the return value of visiting [node] is an expression guaranteed to have
/// no side effect, it is dropped.
void visitExpressionIgnoreResult(js.Expression node) {
js.Expression result = node.accept(this);
if (!(result is js.Literal || result is js.VariableUse)) {
addExpressionStatement(result);
}
}
js.Expression visitExpression(js.Expression node) {
return node.accept(this);
}
/// Calls [fn] with the value of evaluating [node1] and [node2].
///
/// Both nodes are evaluated in order.
///
/// If node2 must be transformed (see [shouldTransform]), then the evaluation
/// of node1 is added to the current statement-list and the result is stored
/// in a temporary variable. The evaluation of node2 is then free to emit
/// statements without affecting the result of node1.
///
/// This is necessary, because await or yield expressions have to emit
/// statements, and these statements could affect the value of node1.
///
/// For example:
///
/// - _storeIfNecessary(someLiteral) returns someLiteral.
/// - _storeIfNecessary(someVariable)
/// inserts: var tempX = someVariable
/// returns: tempX
/// where tempX is a fresh temporary variable.
js.Expression _storeIfNecessary(js.Expression result) {
// Note that RegExes, js.ArrayInitializer and js.ObjectInitializer are not
// [js.Literal]s.
if (result is js.Literal) return result;
js.Expression tempVar = useTempVar(allocateTempVar());
addExpressionStatement(new js.Assignment(tempVar, result));
return tempVar;
}
withExpression(js.Expression node, fn(js.Expression result), {bool store}) {
int oldTempVarIndex = currentTempVarIndex;
js.Expression visited = visitExpression(node);
if (store) {
visited = _storeIfNecessary(visited);
}
var result = fn(visited);
currentTempVarIndex = oldTempVarIndex;
return result;
}
/// Calls [fn] with the value of evaluating [node1] and [node2].
///
/// If `shouldTransform(node2)` the first expression is stored in a temporary
/// variable.
///
/// This is because node1 must be evaluated before visiting node2,
/// because the evaluation of an await or yield cannot be expressed as
/// an expression, visiting node2 it will output statements that
/// might have an influence on the value of node1.
withExpression2(js.Expression node1, js.Expression node2,
fn(js.Expression result1, js.Expression result2)) {
int oldTempVarIndex = currentTempVarIndex;
js.Expression r1 = visitExpression(node1);
if (shouldTransform(node2)) {
r1 = _storeIfNecessary(r1);
}
js.Expression r2 = visitExpression(node2);
var result = fn(r1, r2);
currentTempVarIndex = oldTempVarIndex;
return result;
}
/// Calls [fn] with the value of evaluating all [nodes].
///
/// All results before the last node where `shouldTransform(node)` are stored
/// in temporary variables.
///
/// See more explanation on [withExpression2].
///
/// If any of the nodes are null, they are ignored, and a null is passed to
/// [fn] in that place.
withExpressions(List<js.Expression> nodes, fn(List<js.Expression> results)) {
int oldTempVarIndex = currentTempVarIndex;
// Find last occurence of a 'transform' expression in [nodes].
// All expressions before that must be stored in temp-vars.
int lastTransformIndex = 0;
for (int i = nodes.length - 1; i >= 0; --i) {
if (nodes[i] == null) continue;
if (shouldTransform(nodes[i])) {
lastTransformIndex = i;
break;
}
}
List<js.Node> visited = nodes.take(lastTransformIndex).map((js.Node node) {
return node == null ? null : _storeIfNecessary(visitExpression(node));
}).toList();
visited.addAll(nodes.skip(lastTransformIndex).map((js.Node node) {
return node == null ? null : visitExpression(node);
}));
var result = fn(visited);
currentTempVarIndex = oldTempVarIndex;
return result;
}
/// Emits the return block that all returns should jump to (after going
/// through all the enclosing finally blocks). The jump to here is made in
/// [visitReturn].
///
/// Returning from an async method calls the [asyncHelper] with the result.
/// (the result might have been stored in [returnValueName] by some finally
/// block).
///
/// Returning from a sync* function returns an [endOfIteration] marker.
///
/// Returning from an async* function calls the [streamHelper] with an
/// [endOfIteration] marker.
void addExit() {
if (analysis.hasExplicitReturns || isAsyncStar) {
beginLabel(exitLabel);
} else {
addStatement(new js.Comment("implicit return"));
}
switch (async) {
case const js.AsyncModifier.async():
String returnValue =
analysis.hasExplicitReturns ? returnValueName : "null";
addStatement(js.js.statement(
"return #thenHelper($returnValue, #successCode, "
"$completerName, null)", {
"thenHelper": asyncHelper,
"successCode": js.number(error_codes.SUCCESS)}));
break;
case const js.AsyncModifier.syncStar():
addStatement(new js.Return(new js.Call(endOfIteration, [])));
break;
case const js.AsyncModifier.asyncStar():
addStatement(js.js.statement(
"return #streamHelper(null, #successCode, $controllerName)", {
"streamHelper": streamHelper,
"successCode": js.number(error_codes.SUCCESS)}));
break;
default:
diagnosticListener.internalError(
spannable, "Internal error, unexpected asyncmodifier $async");
}
if (isAsync || isAsyncStar) {
beginLabel(rethrowLabel);
addStatement(js.js.statement(
"return #thenHelper($currentErrorName, #errorCode, "
"${isAsync ? completerName : controllerName})", {
"thenHelper": isAsync ? asyncHelper : streamHelper,
"errorCode": js.number(error_codes.ERROR)}));
} else {
assert(isSyncStar);
beginLabel(rethrowLabel);
addStatement(new js.Return(new js.Call(uncaughtErrorExpression,
[new js.VariableUse(currentErrorName)])));
}
}
/// The initial call to [asyncHelper]/[streamHelper].
///
/// There is no value to await/yield, so the first argument is `null` and
/// also the errorCallback is `null`.
///
/// Returns the [Future]/[Stream] coming from [completerName]/
/// [controllerName].
js.Statement generateInitializer() {
if (isAsync) {
return js.js.statement(
"return #asyncHelper(null, $bodyName, $completerName, null);", {
"asyncHelper": asyncHelper
});
} else if (isAsyncStar) {
return js.js.statement(
"return #streamOfController($controllerName);", {
"streamOfController": streamOfController
});
} else {
throw diagnosticListener.internalError(
spannable, "Unexpected asyncModifier: $async");
}
}
/// Rewrites an async/sync*/async* function to a normal Javascript function.
///
/// The control flow is flattened by simulating 'goto' using a switch in a
/// loop and a state variable [gotoName] inside a nested function [bodyName]
/// that can be called back by [asyncHelper]/[asyncStarHelper]/the [Iterator].
///
/// Local variables are hoisted outside the helper.
///
/// Awaits in async/async* are translated to code that remembers the current
/// location (so the function can resume from where it was) followed by a call
/// to the [asyncHelper]. The helper sets up the waiting for the awaited value
/// and returns a future which is immediately returned by the translated
/// await.
/// Yields in async* are translated to a call to the [asyncStarHelper]. They,
/// too, need to be prepared to be interrupted in case the stream is paused or
/// canceled. (Currently we always suspend - this is different from the spec,
/// see `streamHelper` in `js_helper.dart`).
///
/// Yield/yield* in a sync* function is translated to a return of the value,
/// wrapped into a "IterationMarker" that signals the type (yield or yield*).
/// Sync* functions are executed on demand (when the user requests a value) by
/// the Iterable that knows how to handle these values.
///
/// Simplified examples (not the exact translation, but intended to show the
/// ideas):
///
/// function (x, y, z) async {
/// var p = await foo();
/// return bar(p);
/// }
///
/// Becomes (without error handling):
///
/// function(x, y, z) {
/// var goto = 0, returnValue, completer = new Completer(), p;
/// function helper(result) {
/// while (true) {
/// switch (goto) {
/// case 0:
/// goto = 1 // Remember where to continue when the future succeeds.
/// return thenHelper(foo(), helper, completer);
/// case 1:
/// p = result;
/// returnValue = bar(p);
/// goto = 2;
/// break;
/// case 2:
/// return thenHelper(returnValue, null, completer)
/// }
/// }
/// return thenHelper(null, helper, completer);
/// }
/// }
///
/// Try/catch is implemented by maintaining [handlerName] to contain the label
/// of the current handler. The switch is nested inside a try/catch that will
/// redirect the flow to the current handler.
///
/// A `finally` clause is compiled similar to normal code, with the additional
/// complexity that `finally` clauses need to know where to jump to after the
/// clause is done. In the translation, each flow-path that enters a `finally`
/// sets up the variable [nextName] with a stack of finally-blocks and a final
/// jump-target (exit, catch, ...).
///
/// function (x, y, z) async {
/// try {
/// try {
/// throw "error";
/// } finally {
/// finalize1();
/// }
/// } catch (e) {
/// handle(e);
/// } finally {
/// finalize2();
/// }
/// }
///
/// Translates into (besides the fact that structures not containing
/// await/yield/yield* are left intact):
///
/// function(x, y, z) {
/// var goto = 0;
/// var returnValue;
/// var completer = new Completer();
/// var handler = 8; // Outside try-blocks go to the rethrow label.
/// var p;
/// var storedError;
/// // The result can be either the result of an awaited future, or an
/// // error if the future completed with an error.
/// function helper(errorCode, result) {
/// if (errorCode == 1) {
/// storedError = result;
/// goto = handler;
/// }
/// while (true) {
/// try {
/// switch (goto) {
/// case 0:
/// handler = 4; // The outer catch-handler
/// handler = 1; // The inner (implicit) catch-handler
/// throw "error";
/// next = [3];
/// // After the finally (2) continue normally after the try.
/// goto = 2;
/// break;
/// case 1: // (implicit) catch handler for inner try.
/// next = [3]; // destination after the finally.
/// // fall-though to the finally handler.
/// case 2: // finally for inner try
/// handler = 4; // catch-handler for outer try.
/// finalize1();
/// goto = next.pop();
/// break;
/// case 3: // exiting inner try.
/// next = [6];
/// goto = 5; // finally handler for outer try.
/// break;
/// case 4: // catch handler for outer try.
/// handler = 5; // If the handler throws, do the finally ..
/// next = [8] // ... and rethrow.
/// e = storedError;
/// handle(e);
/// // Fall through to finally.
/// case 5: // finally handler for outer try.
/// handler = null;
/// finalize2();
/// goto = next.pop();
/// break;
/// case 6: // Exiting outer try.
/// case 7: // return
/// return thenHelper(returnValue, 0, completer);
/// case 8: // Rethrow
/// return thenHelper(storedError, 1, completer);
/// }
/// } catch (error) {
/// storedError = error;
/// goto = handler;
/// }
/// }
/// return thenHelper(null, helper, completer);
/// }
/// }
///
@override
js.Expression visitFun(js.Fun node) {
if (isSync) return node;
beginLabel(newLabel("Function start"));
// AsyncStar needs a returnlabel for its handling of cancelation. See
// [visitDartYield].
exitLabel =
analysis.hasExplicitReturns || isAsyncStar ? newLabel("return") : null;
rethrowLabel = newLabel("rethrow");
handlerLabels[node] = rethrowLabel;
js.Statement body = node.body;
jumpTargets.add(node);
visitStatement(body);
jumpTargets.removeLast();
addExit();
List<js.SwitchClause> clauses = labelledParts.keys.map((label) {
return new js.Case(js.number(label), new js.Block(labelledParts[label]));
}).toList();
js.Statement helperBody =
new js.Switch(new js.VariableUse(gotoName), clauses);
if (hasJumpThoughOuterLabel) {
helperBody = js.js.statement("$outerLabelName: #", [helperBody]);
}
helperBody = js.js.statement("""
try {
#body
} catch ($errorName){
$currentErrorName = $errorName;
$gotoName = $handlerName;
}""", {"body": helperBody});
List<js.VariableInitialization> inits = <js.VariableInitialization>[];
js.VariableInitialization makeInit(String name, js.Expression initValue) {
return new js.VariableInitialization(
new js.VariableDeclaration(name), initValue);
}
inits.add(makeInit(gotoName, js.number(0)));
if (isAsync) {
inits.add(makeInit(completerName, new js.New(newCompleter, [])));
} else if (isAsyncStar) {
inits.add(makeInit(controllerName,
new js.Call(newController, [new js.VariableUse(bodyName)])));
}
inits.add(makeInit(handlerName, js.number(rethrowLabel)));
inits.add(makeInit(currentErrorName, null));
if (hasJumpThroughFinally || analysis.hasYield) {
inits.add(makeInit(nextName, null));
}
if (analysis.hasExplicitReturns && isAsync) {
inits.add(makeInit(returnValueName, null));
}
if (isSyncStar) {
inits.add(makeInit(resultName, null));
}
if (analysis.hasThis && !isSyncStar) {
// Sync* functions must remember `this` on the level of the outer
// function.
inits.add(makeInit(selfName, new js.This()));
}
inits.addAll(localVariables.map((js.VariableDeclaration decl) {
return new js.VariableInitialization(decl, null);
}));
inits.addAll(new Iterable.generate(tempVarHighWaterMark,
(int i) => makeInit(useTempVar(i + 1).name, null)));
js.VariableDeclarationList varDecl = new js.VariableDeclarationList(inits);
// TODO(sigurdm): Explain the difference between these cases.
if (isSyncStar) {
return js.js("""
function (#params) {
if (#needsThis)
var $selfName = this;
return new #newIterable(function () {
#varDecl;
return function $bodyName() {
while (true)
#helperBody;
};
});
}
""", {
"params": node.params,
"needsThis": analysis.hasThis,
"helperBody": helperBody,
"varDecl": varDecl,
"newIterable": newIterable
});
}
return js.js("""
function (#params) {
#varDecl;
function $bodyName($errorCodeName, $resultName) {
if (#hasYield)
switch ($errorCodeName) {
case #streamWasCanceled:
$nextName = $nextWhenCanceledName;
$gotoName = $nextName.pop();
break;
case #errorCode:
$currentErrorName = $resultName;
$gotoName = $handlerName;
}
else
if ($errorCodeName == #errorCode) {
$currentErrorName = $resultName;
$gotoName = $handlerName;
}
while (true)
#helperBody;
}
#init;
}""", {
"params": node.params,
"varDecl": varDecl,
"streamWasCanceled": js.number(error_codes.STREAM_WAS_CANCELED),
"errorCode": js.number(error_codes.ERROR),
"hasYield": analysis.hasYield,
"helperBody": helperBody,
"init": generateInitializer()
});
}
@override
js.Expression visitAccess(js.PropertyAccess node) {
return withExpression2(node.receiver, node.selector,
(receiver, selector) => new js.PropertyAccess(receiver, selector));
}
@override
js.Expression visitArrayHole(js.ArrayHole node) {
return node;
}
@override
js.Expression visitArrayInitializer(js.ArrayInitializer node) {
return withExpressions(node.elements, (elements) {
return new js.ArrayInitializer(elements);
});
}
@override
js.Expression visitAssignment(js.Assignment node) {
if (!shouldTransform(node)) {
return new js.Assignment.compound(visitExpression(node.leftHandSide),
node.op, visitExpression(node.value));
}
js.Expression leftHandSide = node.leftHandSide;
if (leftHandSide is js.VariableUse) {
return withExpression(node.value, (js.Expression value) {
return new js.Assignment(leftHandSide, value);
}, store: false);
} else if (leftHandSide is js.PropertyAccess) {
return withExpressions([
leftHandSide.receiver,
leftHandSide.selector,
node.value
], (evaluated) {
return new js.Assignment.compound(
new js.PropertyAccess(evaluated[0], evaluated[1]), node.op,
evaluated[2]);
});
} else {
throw "Unexpected assignment left hand side $leftHandSide";
}
}
/// An await is translated to a call to [asyncHelper]/[streamHelper].
///
/// See the comments of [visitFun] for an example.
@override
js.Expression visitAwait(js.Await node) {
assert(isAsync || isAsyncStar);
int afterAwait = newLabel("returning from await.");
withExpression(node.expression, (js.Expression value) {
addStatement(setGotoVariable(afterAwait));
addStatement(js.js.statement("""
return #asyncHelper(#value,
$bodyName,
${isAsync ? completerName : controllerName});
""", {
"asyncHelper": isAsync ? asyncHelper : streamHelper,
"value": value,
}));
}, store: false);
beginLabel(afterAwait);
return new js.VariableUse(resultName);
}
/// Checks if [node] is the variable named [resultName].
///
/// [resultName] is used to hold the result of a transformed computation
/// for example the result of awaiting, or the result of a conditional or
/// short-circuiting expression.
/// If the subexpression of some transformed node already is transformed and
/// visiting it returns [resultName], it is not redundantly assigned to itself
/// again.
bool isResult(js.Expression node) {
return node is js.VariableUse && node.name == resultName;
}
@override
js.Expression visitBinary(js.Binary node) {
if (shouldTransform(node.right) && (node.op == "||" || node.op == "&&")) {
int thenLabel = newLabel("then");
int joinLabel = newLabel("join");
withExpression(node.left, (js.Expression left) {
js.Statement assignLeft = isResult(left)
? new js.Block.empty()
: new js.ExpressionStatement(
new js.Assignment(new js.VariableUse(resultName), left));
if (node.op == "||") {
addStatement(new js.If(left, gotoAndBreak(thenLabel), assignLeft));
} else {
assert(node.op == "&&");
addStatement(new js.If(left, assignLeft, gotoAndBreak(thenLabel)));
}
}, store: true);
addGoto(joinLabel);
beginLabel(thenLabel);
withExpression(node.right, (js.Expression value) {
if (!isResult(value)) {
addExpressionStatement(
new js.Assignment(new js.VariableUse(resultName), value));
}
}, store: false);
beginLabel(joinLabel);
return new js.VariableUse(resultName);
}
return withExpression2(node.left, node.right,
(left, right) => new js.Binary(node.op, left, right));
}
@override
void visitBlock(js.Block node) {
for (js.Statement statement in node.statements) {
visitStatement(statement);
}
}
@override
void visitBreak(js.Break node) {
js.Node target = analysis.targets[node];
if (!shouldTransform(target)) {
addStatement(node);
return;
}
translateJump(target, breakLabels[target]);
}
@override
js.Expression visitCall(js.Call node) {
bool storeTarget = node.arguments.any(shouldTransform);
return withExpression(node.target, (target) {
return withExpressions(node.arguments, (List<js.Expression> arguments) {
return new js.Call(target, arguments);
});
}, store: storeTarget);
}
@override
void visitCase(js.Case node) {
return unreachable(node);
}
@override
void visitCatch(js.Catch node) {
return unreachable(node);
}
@override
void visitComment(js.Comment node) {
addStatement(node);
}
@override
js.Expression visitConditional(js.Conditional node) {
if (!shouldTransform(node.then) && !shouldTransform(node.otherwise)) {
return withExpression(node.condition, (js.Expression condition) {
return new js.Conditional(condition, node.then, node.otherwise);
});
}
int thenLabel = newLabel("then");
int joinLabel = newLabel("join");
int elseLabel = newLabel("else");
withExpression(node.condition, (js.Expression condition) {
addExpressionStatement(new js.Assignment(new js.VariableUse(gotoName),
new js.Conditional(
condition, js.number(thenLabel), js.number(elseLabel))));
}, store: false);
addBreak();
beginLabel(thenLabel);
withExpression(node.then, (js.Expression value) {
if (!isResult(value)) {
addExpressionStatement(
new js.Assignment(new js.VariableUse(resultName), value));
}
}, store: false);
addGoto(joinLabel);
beginLabel(elseLabel);
withExpression(node.otherwise, (js.Expression value) {
if (!isResult(value)) {
addExpressionStatement(
new js.Assignment(new js.VariableUse(resultName), value));
}
}, store: false);
beginLabel(joinLabel);
return new js.VariableUse(resultName);
}
@override
void visitContinue(js.Continue node) {
js.Node target = analysis.targets[node];
if (!shouldTransform(target)) {
addStatement(node);
return;
}
translateJump(target, continueLabels[target]);
}
/// Emits a break statement that exits the big switch statement.
void addBreak() {
if (insideUntranslatedBreakable) {
hasJumpThoughOuterLabel = true;
addStatement(new js.Break(outerLabelName));
} else {
addStatement(new js.Break(null));
}
}
/// Common code for handling break, continue, return.
///
/// It is necessary to run all nesting finally-handlers between the jump and
/// the target. For that [nextName] is used as a stack of places to go.
///
/// See also [visitFun].
void translateJump(js.Node target, int targetLabel) {
// Compute a stack of all the 'finally' nodes that must be visited before
// the jump.
// The bottom of the stack is the label where the jump goes to.
List<int> jumpStack = new List<int>();
for (js.Node node in jumpTargets.reversed) {
if (finallyLabels[node] != null) {
jumpStack.add(finallyLabels[node]);
} else if (node == target) {
jumpStack.add(targetLabel);
break;
}
// Ignore other nodes.
}
jumpStack = jumpStack.reversed.toList();
// As the program jumps directly to the top of the stack, it is taken off
// now.
int firstTarget = jumpStack.removeLast();
if (jumpStack.isNotEmpty) {
hasJumpThroughFinally = true;
js.Expression jsJumpStack = new js.ArrayInitializer(
jumpStack.map((int label) => js.number(label)).toList());
addStatement(js.js.statement("$nextName = #", [jsJumpStack]));
}
addGoto(firstTarget);
}
@override
void visitDefault(js.Default node) => unreachable(node);
@override
void visitDo(js.Do node) {
if (!shouldTransform(node)) {
bool oldInsideUntranslatedBreakable = insideUntranslatedBreakable;
insideUntranslatedBreakable = true;
withExpression(node.condition, (js.Expression condition) {
addStatement(new js.Do(translateInBlock(node.body), condition));
}, store: false);
insideUntranslatedBreakable = oldInsideUntranslatedBreakable;
return;
}
int startLabel = newLabel("do body");
int continueLabel = newLabel("do condition");
continueLabels[node] = continueLabel;
int afterLabel = newLabel("after do");
breakLabels[node] = afterLabel;
beginLabel(startLabel);
jumpTargets.add(node);
visitStatement(node.body);
jumpTargets.removeLast();
beginLabel(continueLabel);
withExpression(node.condition, (js.Expression condition) {
addStatement(new js.If.noElse(condition, gotoAndBreak(startLabel)));
}, store: false);
beginLabel(afterLabel);
}
@override
void visitEmptyStatement(js.EmptyStatement node) {
addStatement(node);
}
void visitExpressionInStatementContext(js.Expression node) {
if (node is js.VariableDeclarationList) {
// Treat js.VariableDeclarationList as a statement.
visitVariableDeclarationList(node);
} else {
visitExpressionIgnoreResult(node);
}
}
@override
void visitExpressionStatement(js.ExpressionStatement node) {
visitExpressionInStatementContext(node.expression);
}
@override
void visitFor(js.For node) {
if (!shouldTransform(node)) {
bool oldInsideUntranslated = insideUntranslatedBreakable;
insideUntranslatedBreakable = true;
// Note that node.init, node.condition, node.update all can be null, but
// withExpressions handles that.
withExpressions([
node.init,
node.condition,
node.update
], (List<js.Expression> transformed) {
addStatement(new js.For(transformed[0], transformed[1], transformed[2],
translateInBlock(node.body)));
});
insideUntranslatedBreakable = oldInsideUntranslated;
return;
}
if (node.init != null) {
visitExpressionInStatementContext(node.init);
}
int startLabel = newLabel("for condition");
// If there is no update, continuing the loop is the same as going to the
// start.
int continueLabel =
(node.update == null) ? startLabel : newLabel("for update");
continueLabels[node] = continueLabel;
int afterLabel = newLabel("after for");
breakLabels[node] = afterLabel;
beginLabel(startLabel);
js.Expression condition = node.condition;
if (condition == null ||
(condition is js.LiteralBool && condition.value == true)) {
addStatement(new js.Comment("trivial condition"));
} else {
withExpression(condition, (js.Expression condition) {
addStatement(new js.If.noElse(
new js.Prefix("!", condition), gotoAndBreak(afterLabel)));
}, store: false);
}
jumpTargets.add(node);
visitStatement(node.body);
jumpTargets.removeLast();
if (node.update != null) {
beginLabel(continueLabel);
visitExpressionIgnoreResult(node.update);
}
addGoto(startLabel);
beginLabel(afterLabel);
}
@override
void visitForIn(js.ForIn node) {
// The dart output currently never uses for-in loops.
throw "Javascript for-in not implemented yet in the await transformation";
}
@override
void visitFunctionDeclaration(js.FunctionDeclaration node) {
unsupported(node);
}
// Only used for code where `!shouldTransform(node)`.
js.Block translateInBlock(js.Statement node) {
assert(!shouldTransform(node));
List<js.Statement> oldBuffer = currentStatementBuffer;
currentStatementBuffer = new List();
List<js.Statement> resultBuffer = currentStatementBuffer;
visitStatement(node);
currentStatementBuffer = oldBuffer;
return new js.Block(resultBuffer);
}
@override
void visitIf(js.If node) {
if (!shouldTransform(node.then) && !shouldTransform(node.otherwise)) {
withExpression(node.condition, (js.Expression condition) {
addStatement(new js.If(condition, translateInBlock(node.then),
translateInBlock(node.otherwise)));
}, store: false);
return;
}
int thenLabel = newLabel("then");
int joinLabel = newLabel("join");
int elseLabel =
node.otherwise is js.EmptyStatement ? joinLabel : newLabel("else");
withExpression(node.condition, (js.Expression condition) {
addExpressionStatement(
new js.Assignment(
new js.VariableUse(gotoName),
new js.Conditional(
condition,
js.number(thenLabel),
js.number(elseLabel))));
}, store: false);
addBreak();
beginLabel(thenLabel);
visitStatement(node.then);
if (node.otherwise is! js.EmptyStatement) {
addGoto(joinLabel);
beginLabel(elseLabel);
visitStatement(node.otherwise);
}
beginLabel(joinLabel);
}
@override
visitInterpolatedExpression(js.InterpolatedExpression node) {
return unsupported(node);
}
@override
visitInterpolatedLiteral(js.InterpolatedLiteral node) => unsupported(node);
@override
visitInterpolatedParameter(js.InterpolatedParameter node) {
return unsupported(node);
}
@override
visitInterpolatedSelector(js.InterpolatedSelector node) {
return unsupported(node);
}
@override
visitInterpolatedStatement(js.InterpolatedStatement node) {
return unsupported(node);
}
@override
void visitLabeledStatement(js.LabeledStatement node) {
if (!shouldTransform(node)) {
addStatement(
new js.LabeledStatement(node.label, translateInBlock(node.body)));
return;
}
int breakLabel = newLabel("break ${node.label}");
int continueLabel = newLabel("continue ${node.label}");
breakLabels[node] = breakLabel;
continueLabels[node] = continueLabel;
beginLabel(continueLabel);
jumpTargets.add(node);
visitStatement(node.body);
jumpTargets.removeLast();
beginLabel(breakLabel);
}
@override
js.Expression visitLiteralBool(js.LiteralBool node) => node;
@override
visitLiteralExpression(js.LiteralExpression node) => unsupported(node);
@override
js.Expression visitLiteralNull(js.LiteralNull node) => node;
@override
js.Expression visitLiteralNumber(js.LiteralNumber node) => node;
@override
visitLiteralStatement(js.LiteralStatement node) => unsupported(node);
@override
js.Expression visitLiteralString(js.LiteralString node) => node;
@override
visitNamedFunction(js.NamedFunction node) {
unsupported(node);
}
@override
js.Expression visitNew(js.New node) {
bool storeTarget = node.arguments.any(shouldTransform);
return withExpression(node.target, (target) {
return withExpressions(node.arguments, (List<js.Expression> arguments) {
return new js.New(target, arguments);
});
}, store: storeTarget);
}
@override
js.Expression visitObjectInitializer(js.ObjectInitializer node) {
return withExpressions(
node.properties.map((js.Property property) => property.value).toList(),
(List<js.Node> values) {
List<js.Property> properties = new List.generate(values.length, (int i) {
return new js.Property(node.properties[i].name, values[i]);
});
return new js.ObjectInitializer(properties);
});
}
@override
visitParameter(js.Parameter node) => unreachable(node);
@override
js.Expression visitPostfix(js.Postfix node) {
if (node.op == "++" || node.op == "--") {
js.Expression argument = node.argument;
if (argument is js.VariableUse) {
return new js.Postfix(node.op, argument);
} else if (argument is js.PropertyAccess) {
return withExpression2(argument.receiver, argument.selector,
(receiver, selector) {
return new js.Postfix(
node.op, new js.PropertyAccess(receiver, selector));
});
} else {
throw "Unexpected postfix ${node.op} "
"operator argument ${node.argument}";
}
}
return withExpression(node.argument,
(js.Expression argument) => new js.Postfix(node.op, argument),
store: false);
}
@override
js.Expression visitPrefix(js.Prefix node) {
if (node.op == "++" || node.op == "--") {
js.Expression argument = node.argument;
if (argument is js.VariableUse) {
return new js.Prefix(node.op, argument);
} else if (argument is js.PropertyAccess) {
return withExpression2(argument.receiver, argument.selector,
(receiver, selector) {
return new js.Prefix(
node.op, new js.PropertyAccess(receiver, selector));
});
} else {
throw "Unexpected prefix ${node.op} operator "
"argument ${node.argument}";
}
}
return withExpression(node.argument,
(js.Expression argument) => new js.Prefix(node.op, argument),
store: false);
}
@override
visitProgram(js.Program node) => unsupported(node);
@override
js.Property visitProperty(js.Property node) {
return withExpression(
node.value, (js.Expression value) => new js.Property(node.name, value),
store: false);
}
@override
js.Expression visitRegExpLiteral(js.RegExpLiteral node) => node;
@override
void visitReturn(js.Return node) {
assert(node.value == null || !isSyncStar && !isAsyncStar);
js.Node target = analysis.targets[node];
if (node.value != null) {
withExpression(node.value, (js.Expression value) {
addStatement(js.js.statement("$returnValueName = #", [value]));
}, store: false);
}
translateJump(target, exitLabel);
}
@override
void visitSwitch(js.Switch node) {
if (!node.cases.any(shouldTransform)) {
// If only the key has an await, translation can be simplified.
bool oldInsideUntranslated = insideUntranslatedBreakable;
insideUntranslatedBreakable = true;
withExpression(node.key, (js.Expression key) {
List<js.SwitchClause> cases = node.cases.map((js.SwitchClause clause) {
if (clause is js.Case) {
return new js.Case(
clause.expression, translateInBlock(clause.body));
} else if (clause is js.Default) {
return new js.Default(translateInBlock(clause.body));
}
}).toList();
addStatement(new js.Switch(key, cases));
}, store: false);
insideUntranslatedBreakable = oldInsideUntranslated;
return;
}
int before = newLabel("switch");
int after = newLabel("after switch");
breakLabels[node] = after;
beginLabel(before);
List<int> labels = new List<int>(node.cases.length);
bool anyCaseExpressionTransformed = node.cases.any(
(js.SwitchClause x) => x is js.Case && shouldTransform(x.expression));
if (anyCaseExpressionTransformed) {
int defaultIndex = null; // Null means no default was found.
// If there is an await in one of the keys, a chain of ifs has to be used.
withExpression(node.key, (js.Expression key) {
int i = 0;
for (js.SwitchClause clause in node.cases) {
if (clause is js.Default) {
// The goto for the default case is added after all non-default
// clauses have been handled.
defaultIndex = i;
labels[i] = newLabel("default");
continue;
} else if (clause is js.Case) {
labels[i] = newLabel("case");
withExpression(clause.expression, (expression) {
addStatement(new js.If.noElse(
new js.Binary("===", key, expression),
gotoAndBreak(labels[i])));
}, store: false);
}
i++;
}
}, store: true);
if (defaultIndex == null) {
addGoto(after);
} else {
addGoto(labels[defaultIndex]);
}
} else {
bool hasDefault = false;
int i = 0;
List<js.SwitchClause> clauses = new List<js.SwitchClause>();
for (js.SwitchClause clause in node.cases) {
if (clause is js.Case) {
labels[i] = newLabel("case");
clauses.add(new js.Case(clause.expression, gotoAndBreak(labels[i])));
} else if (i is js.Default) {
labels[i] = newLabel("default");
clauses.add(new js.Default(gotoAndBreak(labels[i])));
hasDefault = true;
} else {
diagnosticListener.internalError(
spannable, "Unknown clause type $clause");
}
i++;
}
withExpression(node.key, (js.Expression key) {
addStatement(new js.Switch(key, clauses));
}, store: false);
if (!hasDefault) {
addGoto(after);
}
}
jumpTargets.add(node);
for (int i = 0; i < labels.length; i++) {
beginLabel(labels[i]);
visitStatement(node.cases[i].body);
}
beginLabel(after);
jumpTargets.removeLast();
}
@override
js.Expression visitThis(js.This node) {
return new js.VariableUse(selfName);
}
@override
void visitThrow(js.Throw node) {
withExpression(node.expression, (js.Expression expression) {
addStatement(new js.Throw(expression));
}, store: false);
}
setErrorHandler([int errorHandler]) {
addExpressionStatement(new js.Assignment(
new js.VariableUse(handlerName),
errorHandler == null ? currentErrorHandler : js.number(errorHandler)));
}
List<int> _finalliesUpToAndEnclosingHandler() {
List<int> result = new List<int>();
for (int i = jumpTargets.length - 1; i >= 0; i--) {
js.Node node = jumpTargets[i];
int handlerLabel = handlerLabels[node];
if (handlerLabel != null) {
result.add(handlerLabel);
break;
}
int finallyLabel = finallyLabels[node];
if (finallyLabel != null) {
result.add(finallyLabel);
}
}
return result.reversed.toList();
}
/// See the comments of [visitFun] for more explanation.
void visitTry(js.Try node) {
if (!shouldTransform(node)) {
js.Block body = translateInBlock(node.body);
js.Catch catchPart = (node.catchPart == null)
? null
: new js.Catch(node.catchPart.declaration,
translateInBlock(node.catchPart.body));
js.Block finallyPart = (node.finallyPart == null)
? null
: translateInBlock(node.finallyPart);
addStatement(new js.Try(body, catchPart, finallyPart));
return;
}
hasTryBlocks = true;
int uncaughtLabel = newLabel("uncaught");
int handlerLabel = (node.catchPart == null)
? uncaughtLabel
: newLabel("catch");
int finallyLabel = newLabel("finally");
int afterFinallyLabel = newLabel("after finally");
if (node.finallyPart != null) {
finallyLabels[node.finallyPart] = finallyLabel;
jumpTargets.add(node.finallyPart);
}
handlerLabels[node] = handlerLabel;
jumpTargets.add(node);
// Set the error handler here. It must be cleared on every path out;
// normal and error exit.
setErrorHandler();
visitStatement(node.body);
js.Node last = jumpTargets.removeLast();
assert(last == node);
if (node.finallyPart == null) {
setErrorHandler();
addGoto(afterFinallyLabel);
} else {
// The handler is reset as the first thing in the finally block.
addStatement(
js.js.statement("$nextName = [#];", [js.number(afterFinallyLabel)]));
addGoto(finallyLabel);
}
if (node.catchPart != null) {
beginLabel(handlerLabel);
// [uncaughtLabel] is the handler for the code in the catch-part.
// It ensures that [nextName] is set up to run the right finally blocks.
handlerLabels[node.catchPart] = uncaughtLabel;
jumpTargets.add(node.catchPart);
setErrorHandler();
// The catch declaration name can shadow outer variables, so a fresh name
// is needed to avoid collisions. See Ecma 262, 3rd edition,
// section 12.14.
String errorRename = freshName(node.catchPart.declaration.name);
localVariables.add(new js.VariableDeclaration(errorRename));
variableRenamings
.add(new Pair(node.catchPart.declaration.name, errorRename));
addExpressionStatement(new js.Assignment(
new js.VariableUse(errorRename),
new js.VariableUse(currentErrorName)));
visitStatement(node.catchPart.body);
variableRenamings.removeLast();
if (node.finallyPart != null) {
// The error has been caught, so after the finally, continue after the
// try.
addStatement(js.js.statement("$nextName = [#];",
[js.number(afterFinallyLabel)]));
addGoto(finallyLabel);
} else {
addGoto(afterFinallyLabel);
}
js.Node last = jumpTargets.removeLast();
assert(last == node.catchPart);
}
// The "uncaught"-handler tells the finally-block to continue with
// the enclosing finally-blocks until the current catch-handler.
beginLabel(uncaughtLabel);
List<int> enclosingFinallies = _finalliesUpToAndEnclosingHandler();
int nextLabel = enclosingFinallies.removeLast();
if (enclosingFinallies.isNotEmpty) {
// [enclosingFinallies] can be empty if there is no surrounding finally
// blocks. Then [nextLabel] will be [rethrowLabel].
addStatement(
js.js.statement("$nextName = #;", new js.ArrayInitializer(
enclosingFinallies.map(js.number).toList())));
}
if (node.finallyPart == null) {
// The finally-block belonging to [node] will be visited because of
// fallthrough. If it does not exist, add an explicit goto.
addGoto(nextLabel);
}
if (node.finallyPart != null) {
js.Node last = jumpTargets.removeLast();
assert(last == node.finallyPart);
beginLabel(finallyLabel);
setErrorHandler();
visitStatement(node.finallyPart);
addStatement(new js.Comment("// goto the next finally handler"));
addStatement(js.js.statement("$gotoName = $nextName.pop();"));
addBreak();
}
beginLabel(afterFinallyLabel);
}
@override
visitVariableDeclaration(js.VariableDeclaration node) {
unreachable(node);
}
@override
void visitVariableDeclarationList(js.VariableDeclarationList node) {
// Declaration of local variables is hoisted outside the helper but the
// initialization is done here.
for (js.VariableInitialization initialization in node.declarations) {
js.VariableDeclaration declaration = initialization.declaration;
localVariables.add(declaration);
if (initialization.value != null) {
withExpression(initialization.value, (js.Expression value) {
addStatement(new js.ExpressionStatement(
new js.Assignment(new js.VariableUse(declaration.name), value)));
}, store: false);
}
}
}
@override
void visitVariableInitialization(js.VariableInitialization node) {
unreachable(node);
}
@override
js.Expression visitVariableUse(js.VariableUse node) {
Pair<String, String> renaming = variableRenamings.lastWhere(
(Pair renaming) => renaming.a == node.name, orElse: () => null);
if (renaming == null) return node;
return new js.VariableUse(renaming.b);
}
@override
void visitWhile(js.While node) {
if (!shouldTransform(node)) {
bool oldInsideUntranslated = insideUntranslatedBreakable;
insideUntranslatedBreakable = true;
withExpression(node.condition, (js.Expression condition) {
addStatement(new js.While(condition, translateInBlock(node.body)));
}, store: false);
insideUntranslatedBreakable = oldInsideUntranslated;
return;
}
int continueLabel = newLabel("while condition");
continueLabels[node] = continueLabel;
beginLabel(continueLabel);
int afterLabel = newLabel("after while");
breakLabels[node] = afterLabel;
js.Expression condition = node.condition;
// If the condition is `true`, a test is not needed.
if (!(condition is js.LiteralBool && condition.value == true)) {
withExpression(node.condition, (js.Expression condition) {
addStatement(new js.If.noElse(
new js.Prefix("!", condition), gotoAndBreak(afterLabel)));
}, store: false);
}
jumpTargets.add(node);
visitStatement(node.body);
jumpTargets.removeLast();
addGoto(continueLabel);
beginLabel(afterLabel);
}
/// Translates a yield/yield* in an sync*.
///
/// `yield` in a sync* function just returns [value].
/// `yield*` wraps [value] in a [yieldStarExpression] and returns it.
void addSyncYield(js.DartYield node, js.Expression expression) {
assert(isSyncStar);
if (node.hasStar) {
addStatement(
new js.Return(new js.Call(yieldStarExpression, [expression])));
} else {
addStatement(new js.Return(expression));
}
}
/// Translates a yield/yield* in an async* function.
///
/// yield/yield* in an async* function is translated much like the `await` is
/// translated in [visitAwait], only the object is wrapped in a
/// [yieldExpression]/[yieldStarExpression] to let [asyncStarHelper]
/// distinguish them.
/// Also [nextWhenCanceledName] is set up to contain the finally blocks that
/// must be run in case the stream was canceled.
void addAsyncYield(js.DartYield node, js.Expression expression) {
assert(isAsyncStar);
// Find all the finally blocks that should be performed if the stream is
// canceled during the yield.
// At the bottom of the stack is the return label.
List<int> enclosingFinallyLabels = <int>[exitLabel];
enclosingFinallyLabels.addAll(jumpTargets
.where((js.Node node) => finallyLabels[node] != null)
.map((js.Block node) => finallyLabels[node]));
addStatement(js.js.statement("$nextWhenCanceledName = #",
[new js.ArrayInitializer(enclosingFinallyLabels.map(js.number)
.toList())]));
addStatement(js.js.statement("""
return #streamHelper(#yieldExpression(#expression),
$bodyName, $controllerName);""", {
"streamHelper": streamHelper,
"yieldExpression": node.hasStar ? yieldStarExpression : yieldExpression,
"expression": expression,
}));
}
@override
void visitDartYield(js.DartYield node) {
assert(isSyncStar || isAsyncStar);
int label = newLabel("after yield");
// Don't do a break here for the goto, but instead a return in either
// addSynYield or addAsyncYield.
withExpression(node.expression, (js.Expression expression) {
addStatement(setGotoVariable(label));
if (isSyncStar) {
addSyncYield(node, expression);
} else {
addAsyncYield(node, expression);
}
}, store: false);
beginLabel(label);
}
}
/// Finds out
///
/// - which expressions have yield or await nested in them.
/// - targets of jumps
/// - a set of used names.
/// - if any [This]-expressions are used.
class PreTranslationAnalysis extends js.NodeVisitor<bool> {
Set<js.Node> hasAwaitOrYield = new Set<js.Node>();
Map<js.Node, js.Node> targets = new Map<js.Node, js.Node>();
List<js.Node> loopsAndSwitches = new List<js.Node>();
List<js.LabeledStatement> labelledStatements =
new List<js.LabeledStatement>();
Set<String> usedNames = new Set<String>();
bool hasExplicitReturns = false;
bool hasThis = false;
bool hasYield = false;
// The function currently being analyzed.
js.Fun currentFunction;
// For error messages.
final Function unsupported;
PreTranslationAnalysis(void this.unsupported(js.Node node));
bool visit(js.Node node) {
bool containsAwait = node.accept(this);
if (containsAwait) {
hasAwaitOrYield.add(node);
}
return containsAwait;
}
analyze(js.Fun node) {
currentFunction = node;
node.params.forEach(visit);
visit(node.body);
}
@override
bool visitAccess(js.PropertyAccess node) {
bool receiver = visit(node.receiver);
bool selector = visit(node.selector);
return receiver || selector;
}
@override
bool visitArrayHole(js.ArrayHole node) {
return false;
}
@override
bool visitArrayInitializer(js.ArrayInitializer node) {
bool containsAwait = false;
for (js.Expression element in node.elements) {
if (visit(element)) containsAwait = true;
}
return containsAwait;
}
@override
bool visitAssignment(js.Assignment node) {
bool leftHandSide = visit(node.leftHandSide);
bool value = (node.value == null) ? false : visit(node.value);
return leftHandSide || value;
}
@override
bool visitAwait(js.Await node) {
visit(node.expression);
return true;
}
@override
bool visitBinary(js.Binary node) {
bool left = visit(node.left);
bool right = visit(node.right);
return left || right;
}
@override
bool visitBlock(js.Block node) {
bool containsAwait = false;
for (js.Statement statement in node.statements) {
if (visit(statement)) containsAwait = true;
}
return containsAwait;
}
@override
bool visitBreak(js.Break node) {
if (node.targetLabel != null) {
targets[node] = labelledStatements.lastWhere(
(js.LabeledStatement statement) {
return statement.label == node.targetLabel;
});
} else {
targets[node] = loopsAndSwitches.last;
}
return false;
}
@override
bool visitCall(js.Call node) {
bool containsAwait = visit(node.target);
for (js.Expression argument in node.arguments) {
if (visit(argument)) containsAwait = true;
}
return containsAwait;
}
@override
bool visitCase(js.Case node) {
bool expression = visit(node.expression);
bool body = visit(node.body);
return expression || body;
}
@override
bool visitCatch(js.Catch node) {
bool declaration = visit(node.declaration);
bool body = visit(node.body);
return declaration || body;
}
@override
bool visitComment(js.Comment node) {
return false;
}
@override
bool visitConditional(js.Conditional node) {
bool condition = visit(node.condition);
bool then = visit(node.then);
bool otherwise = visit(node.otherwise);
return condition || then || otherwise;
}
@override
bool visitContinue(js.Continue node) {
if (node.targetLabel != null) {
targets[node] = labelledStatements.lastWhere(
(js.LabeledStatement stm) => stm.label == node.targetLabel);
} else {
targets[node] =
loopsAndSwitches.lastWhere((js.Node node) => node is! js.Switch);
}
assert(() {
js.Node target = targets[node];
return target is js.Loop ||
(target is js.LabeledStatement && target.body is js.Loop);
});
return false;
}
@override
bool visitDefault(js.Default node) {
return visit(node.body);
}
@override
bool visitDo(js.Do node) {
loopsAndSwitches.add(node);
bool body = visit(node.body);
bool condition = visit(node.condition);
loopsAndSwitches.removeLast();
return body || condition;
}
@override
bool visitEmptyStatement(js.EmptyStatement node) {
return false;
}
@override
bool visitExpressionStatement(js.ExpressionStatement node) {
return visit(node.expression);
}
@override
bool visitFor(js.For node) {
bool init = (node.init == null) ? false : visit(node.init);
bool condition = (node.condition == null) ? false : visit(node.condition);
bool update = (node.update == null) ? false : visit(node.update);
loopsAndSwitches.add(node);
bool body = visit(node.body);
loopsAndSwitches.removeLast();
return init || condition || update || body;
}
@override
bool visitForIn(js.ForIn node) {
bool object = visit(node.object);
loopsAndSwitches.add(node);
bool body = visit(node.body);
loopsAndSwitches.removeLast();
return object || body;
}
@override
bool visitFun(js.Fun node) {
return false;
}
@override
bool visitFunctionDeclaration(js.FunctionDeclaration node) {
return false;
}
@override
bool visitIf(js.If node) {
bool condition = visit(node.condition);
bool then = visit(node.then);
bool otherwise = visit(node.otherwise);
return condition || then || otherwise;
}
@override
bool visitInterpolatedExpression(js.InterpolatedExpression node) {
return unsupported(node);
}
@override
bool visitInterpolatedLiteral(js.InterpolatedLiteral node) {
return unsupported(node);
}
@override
bool visitInterpolatedParameter(js.InterpolatedParameter node) {
return unsupported(node);
}
@override
bool visitInterpolatedSelector(js.InterpolatedSelector node) {
return unsupported(node);
}
@override
bool visitInterpolatedStatement(js.InterpolatedStatement node) {
return unsupported(node);
}
@override
bool visitLabeledStatement(js.LabeledStatement node) {
usedNames.add(node.label);
labelledStatements.add(node);
bool containsAwait = visit(node.body);
labelledStatements.removeLast();
return containsAwait;
}
@override
bool visitLiteralBool(js.LiteralBool node) {
return false;
}
@override
bool visitLiteralExpression(js.LiteralExpression node) {
return unsupported(node);
}
@override
bool visitLiteralNull(js.LiteralNull node) {
return false;
}
@override
bool visitLiteralNumber(js.LiteralNumber node) {
return false;
}
@override
bool visitLiteralStatement(js.LiteralStatement node) {
return unsupported(node);
}
@override
bool visitLiteralString(js.LiteralString node) {
return false;
}
@override
bool visitNamedFunction(js.NamedFunction node) {
return false;
}
@override
bool visitNew(js.New node) {
return visitCall(node);
}
@override
bool visitObjectInitializer(js.ObjectInitializer node) {
bool containsAwait = false;
for (js.Property property in node.properties) {
if (visit(property)) containsAwait = true;
}
return containsAwait;
}
@override
bool visitParameter(js.Parameter node) {
usedNames.add(node.name);
return false;
}
@override
bool visitPostfix(js.Postfix node) {
return visit(node.argument);
}
@override
bool visitPrefix(js.Prefix node) {
return visit(node.argument);
}
@override
bool visitProgram(js.Program node) {
throw "Unexpected";
}
@override
bool visitProperty(js.Property node) {
return visit(node.value);
}
@override
bool visitRegExpLiteral(js.RegExpLiteral node) {
return false;
}
@override
bool visitReturn(js.Return node) {
hasExplicitReturns = true;
targets[node] = currentFunction;
if (node.value == null) return false;
return visit(node.value);
}
@override
bool visitSwitch(js.Switch node) {
loopsAndSwitches.add(node);
bool result = visit(node.key);
for (js.SwitchClause clause in node.cases) {
if (visit(clause)) result = true;
}
loopsAndSwitches.removeLast();
return result;
}
@override
bool visitThis(js.This node) {
hasThis = true;
return false;
}
@override
bool visitThrow(js.Throw node) {
return visit(node.expression);
}
@override
bool visitTry(js.Try node) {
bool body = visit(node.body);
bool catchPart = (node.catchPart == null) ? false : visit(node.catchPart);
bool finallyPart =
(node.finallyPart == null) ? false : visit(node.finallyPart);
return body || catchPart || finallyPart;
}
@override
bool visitVariableDeclaration(js.VariableDeclaration node) {
usedNames.add(node.name);
return false;
}
@override
bool visitVariableDeclarationList(js.VariableDeclarationList node) {
bool result = false;
for (js.VariableInitialization init in node.declarations) {
if (visit(init)) result = true;
}
return result;
}
@override
bool visitVariableInitialization(js.VariableInitialization node) {
return visitAssignment(node);
}
@override
bool visitVariableUse(js.VariableUse node) {
usedNames.add(node.name);
return false;
}
@override
bool visitWhile(js.While node) {
loopsAndSwitches.add(node);
bool condition = visit(node.condition);
bool body = visit(node.body);
loopsAndSwitches.removeLast();
return condition || body;
}
@override
bool visitDartYield(js.DartYield node) {
hasYield = true;
visit(node.expression);
return true;
}
}