Linter Demo Errors: 1Warnings: 73File: /home/fstrocco/Dart/dart/benchmark/compiler/lib/src/js/rewrite_async.dart // 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; 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. abstract class AsyncRewriterBase extends js.NodeVisitor { // Local variables are hoisted to the top of the function, so they are // collected here. List localVariables = new List(); Map continueLabels = new Map(); Map breakLabels = new Map(); /// The label of a finally part. Map finallyLabels = new Map(); /// 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 handlerLabels = new Map(); 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 jumpTargets = new List(); List continueStack = new List(); List breakStack = new List(); List returnStack = new List(); List> variableRenamings = new List>(); PreTranslationAnalysis analysis; final Function safeVariableName; // All the 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 [body] function, so that [awaitStatement] can /// call [body] with the result of an awaited Future. js.VariableUse get result => new js.VariableUse(resultName); String resultName; /// A parameter to the [bodyName] function. Indicating if we are in success /// or error case. String errorCodeName; final String suggestedBodyName; /// The inner function that is scheduled to do each await/yield, /// and called to do a new iteration for sync*. js.VariableUse get body => new js.VariableUse(bodyName); String bodyName; /// 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] js.VariableUse get goto => new js.VariableUse(gotoName); String gotoName; /// Variable containing the label of the current error handler. js.VariableUse get handler => new js.VariableUse(handlerName); String handlerName; /// A stack of labels of finally blocks to visit, and the label to go to after /// the last. js.VariableUse get next => new js.VariableUse(nextName); String nextName; /// The current returned value (a finally block may overwrite it). js.VariableUse get returnValue => new js.VariableUse(returnValueName); String returnValueName; /// Stores the current error when we are in the process of handling an error. js.VariableUse get currentError => new js.VariableUse(currentErrorName); 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. js.VariableUse get self => new js.VariableUse(selfName); String selfName; 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 tempVarNames = new Map(); bool get isAsync => false; bool get isSyncStar => false; bool get isAsyncStar => false; AsyncRewriterBase(this.diagnosticListener, spannable, this.safeVariableName, this.suggestedBodyName) : _spannable = spannable; /// Initialize names used by the subClass. void initializeNames(); /// 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; 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"); bodyName = freshName(suggestedBodyName); gotoName = freshName("goto"); handlerName = freshName("handler"); nextName = freshName("next"); returnValueName = freshName("returnValue"); currentErrorName = freshName("currentError"); outerLabelName = freshName("outer"); selfName = freshName("self"); // Initialize names specific to the subclass. initializeNames(); 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> labelledParts = new LinkedHashMap>(); /// Description of each label for readability of the non-minified output. Map labelComments = new Map(); /// True if the function has any try blocks containing await. bool hasTryBlocks = 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 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(); 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 js.js.statement('# = #;', [goto, 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 statements = new List(); 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()); addStatement(js.js.statement('# = #;', [tempVar, result])); return tempVar; } // TODO(sigurdm): This is obsolete - all calls use store: false. Replace with // visitExpression(node); 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 result of evaluating [node]. Taking special care of /// property accesses. /// /// If [store] is true the result of evaluating [node] is stored in a /// temporary. /// /// We cannot rewrite `.m()` to: /// temp = .m; /// temp(); /// Because this leaves `this` unbound in the call. But because of dart /// evaluation order we can write: /// temp = ; /// temp.m(); withCallTargetExpression(js.Expression node, fn(js.Expression result), {bool store}) { int oldTempVarIndex = currentTempVarIndex; js.Expression visited = visitExpression(node); js.Expression selector; js.Expression storedIfNeeded; if (store) { if (visited is js.PropertyAccess) { js.PropertyAccess propertyAccess = visited; selector = propertyAccess.selector; visited = propertyAccess.receiver; } storedIfNeeded = _storeIfNecessary(visited); } else { storedIfNeeded = visited; } js.Expression result; if (selector == null) { result = fn(storedIfNeeded); } else { result = fn(new js.PropertyAccess(storedIfNeeded, selector)); } 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 nodes, fn(List 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 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 jump to (after going /// through all the enclosing finally blocks). The jump to here is made in /// [visitReturn]. void addSuccesExit(); /// Emits the block that control flows to if an error has been thrown /// but not caught. (after going through all the enclosing finally blocks). void addErrorExit(); void addFunctionExits() { addSuccesExit(); addErrorExit(); } /// Returns the rewritten function. js.Fun finishFunction(List parameters, js.Statement rewrittenBody, js.VariableDeclarationList variableDeclarations); Iterable variableInitializations(); /// 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 [goto] inside a nested function [body] /// that can be called back by [asyncStarHelper]/[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 /// [awaitStatement]. The helper sets up the waiting for the awaited /// value and returns a future which is immediately returned by the /// [awaitStatement]. /// /// Yields in sync*/async* are translated to a calls to helper functions. /// (see [visitYield]) /// /// 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 body(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 [handler] to contain the label /// of the current handler. If [body] throws, the caller should catch the /// error and recall [body] with first argument [error_codes.ERROR] and /// second argument the error. /// /// 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 [next] 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 currentError; /// // The result can be either the result of an awaited future, or an /// // error if the future completed with an error. /// function body(errorCode, result) { /// if (errorCode == 1) { /// currentError = result; /// goto = handler; /// } /// while (true) { /// 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(currentError, 1, completer); /// } /// } /// return thenHelper(null, helper, completer); /// } /// } /// @override js.Expression visitFun(js.Fun 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(); addFunctionExits(); List clauses = labelledParts.keys.map((label) { return new js.Case(js.number(label), new js.Block(labelledParts[label])); }).toList(); js.Statement rewrittenBody = new js.Switch(goto, clauses); if (hasJumpThoughOuterLabel) { rewrittenBody = new js.LabeledStatement(outerLabelName, rewrittenBody); } rewrittenBody = js.js.statement('while (true) {#}', rewrittenBody); List variables = new List(); variables.add(_makeVariableInitializer(goto, js.number(0))); variables.addAll(variableInitializations()); variables.add( _makeVariableInitializer(handler, js.number(rethrowLabel))); variables.add(_makeVariableInitializer(currentError, null)); if (analysis.hasFinally || (isAsyncStar && analysis.hasYield)) { variables.add(_makeVariableInitializer(next, new js.ArrayInitializer([]))); } if (analysis.hasThis && !isSyncStar) { // Sync* functions must remember `this` on the level of the outer // function. variables.add(_makeVariableInitializer(self, js.js('this'))); } variables.addAll(localVariables.map( (js.VariableDeclaration declaration) { return new js.VariableInitialization(declaration, null); })); variables.addAll(new Iterable.generate(tempVarHighWaterMark, (int i) => _makeVariableInitializer(useTempVar(i + 1).name, null))); js.VariableDeclarationList variableDeclarations = new js.VariableDeclarationList(variables); return finishFunction(node.params, rewrittenBody, variableDeclarations); } @override js.Expression visitAccess(js.PropertyAccess node) { return withExpression2(node.receiver, node.selector, (receiver, selector) => js.js('#[#]', [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) { // A non-compound [js.Assignment] has `op==null`. So it works out to // use [js.Assignment.compound] for all cases. // Visit the [js.VariableUse] to ensure renaming is done correctly. return new js.Assignment.compound(visitExpression(leftHandSide), node.op, 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"; } } js.Statement awaitStatement(js.Expression value); /// An await is translated to an [awaitStatement]. /// /// 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(awaitStatement(value)); }, store: false); beginLabel(afterAwait); return result; } /// Checks if [node] is the variable named [resultName]. /// /// [result] 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 [result], 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() : js.js.statement('# = #;', [result, left]); if (node.op == "&&") { addStatement(js.js.statement('if (#) {#} else #', [left, gotoAndBreak(thenLabel), assignLeft])); } else { assert(node.op == "||"); addStatement(js.js.statement('if (#) {#} else #', [left, assignLeft, gotoAndBreak(thenLabel)])); } }, store: true); addGoto(joinLabel); beginLabel(thenLabel); withExpression(node.right, (js.Expression value) { if (!isResult(value)) { addStatement(js.js.statement('# = #;', [result, value])); } }, store: false); beginLabel(joinLabel); return result; } 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 withCallTargetExpression(node.target, (target) { return withExpressions(node.arguments, (List 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 js.js('# ? # : #', [visitExpression(node.condition), visitExpression(node.then), visitExpression(node.otherwise)]); } int thenLabel = newLabel("then"); int joinLabel = newLabel("join"); int elseLabel = newLabel("else"); withExpression(node.condition, (js.Expression condition) { addStatement(js.js.statement('# = # ? # : #;', [goto, condition, js.number(thenLabel), js.number(elseLabel)])); }, store: false); addBreak(); beginLabel(thenLabel); withExpression(node.then, (js.Expression value) { if (!isResult(value)) { addStatement(js.js.statement('# = #;', [result, value])); } }, store: false); addGoto(joinLabel); beginLabel(elseLabel); withExpression(node.otherwise, (js.Expression value) { if (!isResult(value)) { addStatement(js.js.statement('# = #;', [result, value])); } }, store: false); beginLabel(joinLabel); return result; } @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 [next] 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 jumpStack = new List(); 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) { js.Expression jsJumpStack = new js.ArrayInitializer( jumpStack.map((int label) => js.number(label)).toList()); addStatement(js.js.statement("# = #;", [next, 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; addStatement(js.js.statement('do {#} while (#)', [translateInBlock(node.body), visitExpression(node.condition)])); 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(js.js.statement('if (#) #', [condition, gotoAndBreak(startLabel)])); }, store: false); beginLabel(afterLabel); } @override void visitEmptyStatement(js.EmptyStatement node) { addStatement(node); } @override void visitExpressionStatement(js.ExpressionStatement node) { visitExpressionIgnoreResult(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 transformed) { addStatement(new js.For(transformed[0], transformed[1], transformed[2], translateInBlock(node.body))); }); insideUntranslatedBreakable = oldInsideUntranslated; return; } if (node.init != null) { addExpressionStatement(visitExpression(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 oldBuffer = currentStatementBuffer; currentStatementBuffer = new List(); List 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( goto, 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 visitInterpolatedDeclaration(js.InterpolatedDeclaration 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; } // `continue label` is really continuing the nested loop. // This is set up in [PreTranslationAnalysis.visitContinue]. // Here we only need a breakLabel: int breakLabel = newLabel("break ${node.label}"); breakLabels[node] = breakLabel; 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 withCallTargetExpression(node.target, (target) { return withExpressions(node.arguments, (List 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 values) { List 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, visitExpression(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, visitExpression(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("# = #;", [returnValue, 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 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 labels = new List(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 clauses = new List(); for (js.SwitchClause clause in node.cases) { if (clause is js.Case) { labels[i] = newLabel("case"); clauses.add(new js.Case(visitExpression(clause.expression), gotoAndBreak(labels[i]))); } else if (clause 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++; } if (!hasDefault) { clauses.add(new js.Default(gotoAndBreak(after))); } withExpression(node.key, (js.Expression key) { addStatement(new js.Switch(key, clauses)); }, store: false); addBreak(); } 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 self; } @override void visitThrow(js.Throw node) { withExpression(node.expression, (js.Expression expression) { addStatement(new js.Throw(expression)); }, store: false); } setErrorHandler([int errorHandler]) { js.Expression label = (errorHandler == null) ? currentErrorHandler : js.number(errorHandler); addStatement(js.js.statement('# = #;',[handler, label])); } List _finalliesUpToAndEnclosingHandler() { List result = new List(); 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("#.push(#);", [next, 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)); addStatement(js.js.statement("# = #;", [errorRename, currentError])); 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("#.push(#);", [next, 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 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("# = #;", [next, 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("# = #.pop();", [goto, next])); addBreak(); } beginLabel(afterFinallyLabel); } @override visitVariableDeclaration(js.VariableDeclaration node) { unreachable(node); } @override js.Expression visitVariableDeclarationList(js.VariableDeclarationList node) { List initializations = new List(); // 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) { initializations.add( new js.Assignment(new js.VariableUse(declaration.name), value)); }, store: false); } } if (initializations.isEmpty) { // Dummy expression. Will be dropped by [visitExpressionIgnoreResult]. return js.number(0); } else { return initializations.reduce( (js.Expression first, js.Expression second) { return new js.Binary(",", first, second); }); } } @override void visitVariableInitialization(js.VariableInitialization node) { unreachable(node); } @override js.Expression visitVariableUse(js.VariableUse node) { Pair 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); } addYield(js.DartYield node, js.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)); addYield(node, expression); }, store: false); beginLabel(label); } } js.VariableInitialization _makeVariableInitializer(dynamic variable, js.Expression initValue) { js.VariableDeclaration declaration; if (variable is js.VariableUse) { declaration = new js.VariableDeclaration(variable.name); } else if (variable is String) { declaration = new js.VariableDeclaration(variable); } else { assert(variable is js.VariableDeclaration); declaration = variable; } return new js.VariableInitialization(declaration, initValue); } class AsyncRewriter extends AsyncRewriterBase { bool get isAsync => true; /// The Completer that will finish an async function. /// /// Not used for sync* or async* functions. String completerName; js.VariableUse get completer => new js.VariableUse(completerName); /// 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 [completer] /// /// For a return it is called with: /// /// - The value to complete the completer with. /// - [error_codes.SUCCESS] /// - The completer object [completer] /// /// For a throw it is called with: /// /// - The error to complete the completer with. /// - [error_codes.ERROR] /// - The completer object [completer] final js.Expression asyncHelper; /// Contructor used to initialize the [completer] variable. /// /// Specific to async methods. final js.Expression newCompleter; AsyncRewriter(DiagnosticListener diagnosticListener, spannable, {this.asyncHelper, this.newCompleter, String safeVariableName(String proposedName), String bodyName}) : super(diagnosticListener, spannable, safeVariableName, bodyName); @override void addYield(js.DartYield node, js.Expression expression) { diagnosticListener.internalError(spannable, "Yield in non-generating async function"); } void addErrorExit() { beginLabel(rethrowLabel); addStatement(js.js.statement( "return #thenHelper(#currentError, #errorCode, #completer);", { "thenHelper": asyncHelper, "errorCode": js.number(error_codes.ERROR), "currentError": currentError, "completer": completer})); } /// Returning from an async method calls [asyncStarHelper] with the result. /// (the result might have been stored in [returnValue] by some finally /// block). void addSuccesExit() { if (analysis.hasExplicitReturns) { beginLabel(exitLabel); } else { addStatement(new js.Comment("implicit return")); } addStatement(js.js.statement( "return #runtimeHelper(#returnValue, #successCode, " "#completer, null);", { "runtimeHelper": asyncHelper, "successCode": js.number(error_codes.SUCCESS), "returnValue": analysis.hasExplicitReturns ? returnValue : new js.LiteralNull(), "completer": completer})); } @override Iterable variableInitializations() { List variables = new List(); variables.add(_makeVariableInitializer(completer, new js.New(newCompleter, []))); if (analysis.hasExplicitReturns) { variables.add(_makeVariableInitializer(returnValue, null)); } return variables; } @override void initializeNames() { completerName = freshName("completer"); } @override js.Statement awaitStatement(js.Expression value) { return js.js.statement(""" return #asyncHelper(#value, #body, #completer); """, { "asyncHelper": asyncHelper, "value": value, "body": body, "completer": completer}); } @override js.Fun finishFunction(List parameters, js.Statement rewrittenBody, js.VariableDeclarationList variableDeclarations) { return js.js(""" function (#parameters) { #variableDeclarations; function #bodyName(#errorCode, #result) { if (#errorCode === #ERROR) { #currentError = #result; #goto = #handler; } #rewrittenBody; } return #asyncHelper(null, #bodyName, #completer, null); }""", { "parameters": parameters, "variableDeclarations": variableDeclarations, "ERROR": js.number(error_codes.ERROR), "rewrittenBody": rewrittenBody, "bodyName": bodyName, "currentError": currentError, "goto": goto, "handler": handler, "errorCode": errorCodeName, "result": resultName, "asyncHelper": asyncHelper, "completer": completer, }); } } class SyncStarRewriter extends AsyncRewriterBase { bool get isSyncStar => true; /// 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 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; SyncStarRewriter(DiagnosticListener diagnosticListener, spannable, {this.endOfIteration, this.newIterable, this.yieldStarExpression, this.uncaughtErrorExpression, String safeVariableName(String proposedName), String bodyName}) : super(diagnosticListener, spannable, safeVariableName, bodyName); /// Translates a yield/yield* in an sync*. /// /// `yield` in a sync* function just returns [value]. /// `yield*` wraps [value] in a [yieldStarExpression] and returns it. @override void addYield(js.DartYield node, js.Expression expression) { if (node.hasStar) { addStatement( new js.Return(new js.Call(yieldStarExpression, [expression]))); } else { addStatement(new js.Return(expression)); } } @override js.Fun finishFunction(List parameters, js.Statement rewrittenBody, js.VariableDeclarationList variableDeclarations) { // Each iterator invocation on the iterable should work on its own copy of // the parameters. // TODO(sigurdm): We only need to do this copying for parameters that are // mutated. List declarations = new List(); List renamedParameters = new List(); for (js.Parameter parameter in parameters) { String name = parameter.name; String renamedName = freshName(name); renamedParameters.add(new js.Parameter(renamedName)); declarations.add( new js.VariableInitialization(new js.VariableDeclaration(name), new js.VariableUse(renamedName))); } js.VariableDeclarationList copyParameters = new js.VariableDeclarationList(declarations); return js.js(""" function (#renamedParameters) { if (#needsThis) var #self = this; return new #newIterable(function () { if (#hasParameters) { #copyParameters; } #varDecl; return function #body(#errorCode, #result) { if (#errorCode === #ERROR) { #currentError = #result; #goto = #handler; } #helperBody; }; }); } """, { "renamedParameters": renamedParameters, "needsThis": analysis.hasThis, "helperBody": rewrittenBody, "hasParameters": parameters.isNotEmpty, "copyParameters": copyParameters, "varDecl": variableDeclarations, "errorCode": errorCodeName, "newIterable": newIterable, "body": bodyName, "self": selfName, "result": resultName, "goto": goto, "handler": handler, "currentError": currentErrorName, "ERROR": js.number(error_codes.ERROR), }); } void addErrorExit() { beginLabel(rethrowLabel); addStatement(js.js.statement('return #(#);', [uncaughtErrorExpression, currentError])); } /// Returning from a sync* function returns an [endOfIteration] marker. void addSuccesExit() { if (analysis.hasExplicitReturns) { beginLabel(exitLabel); } else { addStatement(new js.Comment("implicit return")); } addStatement(js.js.statement('return #();', [endOfIteration])); } @override Iterable variableInitializations() { List variables = new List(); return variables; } @override js.Statement awaitStatement(js.Expression value) { throw diagnosticListener.internalError(spannable, "Sync* functions cannot contain await statements."); } @override void initializeNames() {} } class AsyncStarRewriter extends AsyncRewriterBase { bool get isAsyncStar => true; /// The stack of labels of finally blocks to assign to [next] if the /// async* [StreamSubscription] was canceled during a yield. js.VariableUse get nextWhenCanceled { return new js.VariableUse(nextWhenCanceledName); } String nextWhenCanceledName; /// The StreamController that controls an async* function. String controllerName; js.VariableUse get controller => new js.VariableUse(controllerName); /// 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 asyncStarHelper; /// 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. final js.Expression streamOfController; /// 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; AsyncStarRewriter(DiagnosticListener diagnosticListener, spannable, {this.asyncStarHelper, this.streamOfController, this.newController, this.yieldExpression, this.yieldStarExpression, String safeVariableName(String proposedName), String bodyName}) : super(diagnosticListener, spannable, safeVariableName, bodyName); /// 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 [nextWhenCanceled] is set up to contain the finally blocks that /// must be run in case the stream was canceled. @override void addYield(js.DartYield node, js.Expression expression) { // 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 enclosingFinallyLabels = [exitLabel]; enclosingFinallyLabels.addAll(jumpTargets .where((js.Node node) => finallyLabels[node] != null) .map((js.Block node) => finallyLabels[node])); addStatement(js.js.statement("# = #;", [nextWhenCanceled, new js.ArrayInitializer( enclosingFinallyLabels.map(js.number).toList())])); addStatement(js.js.statement(""" return #asyncStarHelper(#yieldExpression(#expression), #body, #controller);""", { "asyncStarHelper": asyncStarHelper, "yieldExpression": node.hasStar ? yieldStarExpression : yieldExpression, "expression": expression, "body": body, "controller": controllerName, })); } @override js.Fun finishFunction(List parameters, js.Statement rewrittenBody, js.VariableDeclarationList variableDeclarations) { return js.js(""" function (#parameters) { #variableDeclarations; function #bodyName(#errorCode, #result) { if (#hasYield) { switch (#errorCode) { case #STREAM_WAS_CANCELED: #next = #nextWhenCanceled; #goto = #next.pop(); break; case #ERROR: #currentError = #result; #goto = #handler; } } else { if (#errorCode === #ERROR) { #currentError = #result; #goto = #handler; } } #rewrittenBody; } return #streamOfController(#controller); }""", { "parameters": parameters, "variableDeclarations": variableDeclarations, "STREAM_WAS_CANCELED": js.number(error_codes.STREAM_WAS_CANCELED), "ERROR": js.number(error_codes.ERROR), "hasYield": analysis.hasYield, "rewrittenBody": rewrittenBody, "bodyName": bodyName, "currentError": currentError, "goto": goto, "handler": handler, "next": next, "nextWhenCanceled": nextWhenCanceled, "errorCode": errorCodeName, "result": resultName, "streamOfController": streamOfController, "controller": controllerName, }); } @override void addErrorExit() { beginLabel(rethrowLabel); addStatement(js.js.statement( "return #asyncHelper(#currentError, #errorCode, #controller);", { "asyncHelper": asyncStarHelper, "errorCode": js.number(error_codes.ERROR), "currentError": currentError, "controller": controllerName})); } /// Returning from an async* function calls the [streamHelper] with an /// [endOfIteration] marker. @override void addSuccesExit() { beginLabel(exitLabel); addStatement(js.js.statement( "return #streamHelper(null, #successCode, #controller);", { "streamHelper": asyncStarHelper, "successCode": js.number(error_codes.SUCCESS), "controller": controllerName})); } @override Iterable variableInitializations() { List variables = new List(); variables.add(_makeVariableInitializer(controller, js.js('#(#)', [newController, bodyName]))); if (analysis.hasYield) { variables.add(_makeVariableInitializer(nextWhenCanceled, null)); } return variables; } @override void initializeNames() { controllerName = freshName("controller"); nextWhenCanceledName = freshName("nextWhenCanceled"); } @override js.Statement awaitStatement(js.Expression value) { return js.js.statement(""" return #asyncHelper(#value, #body, #controller); """, { "asyncHelper": asyncStarHelper, "value": value, "body": body, "controller": controllerName}); } } /// 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 { Set hasAwaitOrYield = new Set(); Map targets = new Map(); List loopsAndSwitches = new List(); List labelledStatements = new List(); Set usedNames = new Set(); bool hasExplicitReturns = false; bool hasThis = false; bool hasYield = false; bool hasFinally = 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) { js.LabeledStatement targetLabel = labelledStatements.lastWhere( (js.LabeledStatement stm) => stm.label == node.targetLabel); js.Loop targetStatement = targetLabel.body; targets[node] = targetStatement; } 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 visitInterpolatedDeclaration(js.InterpolatedDeclaration 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); // If the key has an `await` expression, do not transform the // body of the switch. visit(node.key); bool result = false; 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); if (finallyPart != null) hasFinally = true; 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; } }