// Copyright (c) 2016, 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 kernel.transformations.async; import '../kernel.dart'; import 'continuation.dart'; /// A transformer that introduces temporary variables for all subexpressions /// that are alive across yield points (AwaitExpression). /// /// The transformer is invoked by passing [rewrite] a top-level expression. /// /// All intermediate values that are possible live across an await are named in /// local variables. /// /// Await expressions are translated into a call to a helper function and a /// native yield. class ExpressionLifter extends Transformer { final AsyncRewriterBase continuationRewriter; /// Have we seen an await to the right in the expression tree. /// /// Subexpressions are visited right-to-left in the reverse of evaluation /// order. /// /// On entry to an expression's visit method, [seenAwait] indicates whether a /// sibling to the right contains an await. If so the expression will be /// named in a temporary variable because it is potentially live across an /// await. /// /// On exit from an expression's visit method, [seenAwait] indicates whether /// the expression itself or a sibling to the right contains an await. bool seenAwait = false; /// The (reverse order) sequence of statements that have been emitted. /// /// Transformation of an expression produces a transformed expression and a /// sequence of statements which are assignments to local variables, calls to /// helper functions, and yield points. Only the yield points need to be a /// statements, and they are statements so an implementation does not have to /// handle unnamed expression intermediate live across yield points. /// /// The visit methods return the transformed expression and build a sequence /// of statements by emitting statements into this list. This list is built /// in reverse because children are visited right-to-left. /// /// If an expression should be named it is named before visiting its children /// so the naming assignment appears in the list before all statements /// implementing the translation of the children. /// /// Children that are conditionally evaluated, such as some parts of logical /// and conditional expressions, must be delimited so that they do not emit /// unguarded statements into [statements]. This is implemented by setting /// [statements] to a fresh empty list before transforming those children. List statements = []; /// The number of currently live named intermediate values. /// /// This index is used to allocate names to temporary values. Because /// children are visited right-to-left, names are assigned in reverse order of /// index. /// /// When an assignment is emitted into [statements] to name an expression /// before visiting its children, the index is not immediately reserved /// because a child can freely use the same name as its parent. In practice, /// this will be the rightmost named child. /// /// After visiting the children of a named expression, [nameIndex] is set to /// indicate one more live value (the value of the expression) than before /// visiting the expression. /// /// After visiting the children of an expression that is not named, /// [nameIndex] may still account for names of subexpressions. int nameIndex = 0; final VariableDeclaration asyncResult = new VariableDeclaration(':result'); final List variables = []; ExpressionLifter(this.continuationRewriter); Block blockOf(List stmts) => new Block(stmts.reversed.toList()); /// Rewrite a toplevel expression (toplevel wrt. a statement). /// /// Rewriting an expression produces a sequence of statements and an /// expression. The sequence of statements are added to the given list. Pass /// an empty list if the rewritten expression should be delimited from the /// surrounding context. Expression rewrite(Expression expression, List outer) { assert(statements.isEmpty); assert(nameIndex == 0); seenAwait = false; Expression result = expression.accept(this); outer.addAll(statements.reversed); statements.clear(); nameIndex = 0; return result; } // Perform an action with a given list of statements so that it cannot emit // statements into the 'outer' list. Expression delimit(Expression action(), List inner) { var index = nameIndex; var outer = statements; statements = inner; Expression result = action(); nameIndex = index; statements = outer; return result; } // Name an expression by emitting an assignment to a temporary variable. VariableGet name(Expression expr) { VariableDeclaration temp = allocateTemporary(nameIndex); statements.add(new ExpressionStatement(new VariableSet(temp, expr))); return new VariableGet(temp); } VariableDeclaration allocateTemporary(int index) { for (var i = variables.length; i <= index; i++) { variables.add(new VariableDeclaration(":async_temporary_${i}")); } return variables[index]; } // Simple literals. These are pure expressions so they can be evaluated after // an await to their right. TreeNode visitSymbolLiteral(SymbolLiteral expr) => expr; TreeNode visitTypeLiteral(TypeLiteral expr) => expr; TreeNode visitThisExpression(ThisExpression expr) => expr; TreeNode visitStringLiteral(StringLiteral expr) => expr; TreeNode visitIntLiteral(IntLiteral expr) => expr; TreeNode visitDoubleLiteral(DoubleLiteral expr) => expr; TreeNode visitBoolLiteral(BoolLiteral expr) => expr; TreeNode visitNullLiteral(NullLiteral expr) => expr; // Nullary expressions with effects. Expression nullary(Expression expr) { if (seenAwait) { expr = name(expr); ++nameIndex; } return expr; } TreeNode visitInvalidExpression(InvalidExpression expr) => nullary(expr); TreeNode visitSuperPropertyGet(SuperPropertyGet expr) => nullary(expr); TreeNode visitStaticGet(StaticGet expr) => nullary(expr); TreeNode visitRethrow(Rethrow expr) => nullary(expr); // Getting a final or const variable is not an effect so it can be evaluated // after an await to its right. TreeNode visitVariableGet(VariableGet expr) { if (seenAwait && !expr.variable.isFinal && !expr.variable.isConst) { expr = name(expr); ++nameIndex; } return expr; } // Transform an expression given an action to transform the children. For // this purposes of the await transformer the children should generally be // translated from right to left, in the reverse of evaluation order. Expression transform(Expression expr, void action()) { var shouldName = seenAwait; // 1. If there is an await in a sibling to the right, emit an assignment to // a temporary variable before transforming the children. var result = shouldName ? name(expr) : expr; // 2. Remember the number of live temporaries before transforming the // children. var index = nameIndex; // 3. Transform the children. Initially they do not have an await in a // sibling to their right. seenAwait = false; action(); // 4. If the expression was named then the variables used for children are // no longer live but the variable used for the expression is. // On the other hand, a sibling to the left (yet to be processed) cannot // reuse any of the variables used here, as the assignments in the children // (here) would overwrite assignments in the siblings to the left, // possibly before the use of the overwritten values. if (shouldName) { if (index + 1 > nameIndex) nameIndex = index + 1; seenAwait = true; } return result; } // Unary expressions. Expression unary(Expression expr) { return transform(expr, () { expr.transformChildren(this); }); } TreeNode visitVariableSet(VariableSet expr) => unary(expr); TreeNode visitPropertyGet(PropertyGet expr) => unary(expr); TreeNode visitDirectPropertyGet(DirectPropertyGet expr) => unary(expr); TreeNode visitSuperPropertySet(SuperPropertySet expr) => unary(expr); TreeNode visitStaticSet(StaticSet expr) => unary(expr); TreeNode visitNot(Not expr) => unary(expr); TreeNode visitIsExpression(IsExpression expr) => unary(expr); TreeNode visitAsExpression(AsExpression expr) => unary(expr); TreeNode visitThrow(Throw expr) => unary(expr); TreeNode visitPropertySet(PropertySet expr) { return transform(expr, () { expr.value = expr.value.accept(this)..parent = expr; expr.receiver = expr.receiver.accept(this)..parent = expr; }); } TreeNode visitDirectPropertySet(DirectPropertySet expr) { return transform(expr, () { expr.value = expr.value.accept(this)..parent = expr; expr.receiver = expr.receiver.accept(this)..parent = expr; }); } TreeNode visitArguments(Arguments args) { for (var named in args.named.reversed) { named.value = named.value.accept(this)..parent = named; } var positional = args.positional; for (var i = positional.length - 1; i >= 0; --i) { positional[i] = positional[i].accept(this)..parent = args; } // Returns the arguments, which is assumed at the call sites because they do // not replace the arguments or set parent pointers. return args; } TreeNode visitMethodInvocation(MethodInvocation expr) { return transform(expr, () { visitArguments(expr.arguments); expr.receiver = expr.receiver.accept(this)..parent = expr; }); } TreeNode visitDirectMethodInvocation(DirectMethodInvocation expr) { return transform(expr, () { visitArguments(expr.arguments); expr.receiver = expr.receiver.accept(this)..parent = expr; }); } TreeNode visitSuperMethodInvocation(SuperMethodInvocation expr) { return transform(expr, () { visitArguments(expr.arguments); }); } TreeNode visitStaticInvocation(StaticInvocation expr) { return transform(expr, () { visitArguments(expr.arguments); }); } TreeNode visitConstructorInvocation(ConstructorInvocation expr) { return transform(expr, () { visitArguments(expr.arguments); }); } TreeNode visitStringConcatenation(StringConcatenation expr) { return transform(expr, () { var expressions = expr.expressions; for (var i = expressions.length - 1; i >= 0; --i) { expressions[i] = expressions[i].accept(this)..parent = expr; } }); } TreeNode visitListLiteral(ListLiteral expr) { return transform(expr, () { var expressions = expr.expressions; for (var i = expressions.length - 1; i >= 0; --i) { expressions[i] = expr.expressions[i].accept(this)..parent = expr; } }); } TreeNode visitMapLiteral(MapLiteral expr) { return transform(expr, () { for (var entry in expr.entries.reversed) { entry.value = entry.value.accept(this)..parent = entry; entry.key = entry.key.accept(this)..parent = entry; } }); } // Control flow. TreeNode visitLogicalExpression(LogicalExpression expr) { var shouldName = seenAwait; // Right is delimited because it is conditionally evaluated. var rightStatements = []; seenAwait = false; expr.right = delimit(() => expr.right.accept(this), rightStatements) ..parent = expr; var rightAwait = seenAwait; if (rightStatements.isEmpty) { // Easy case: right did not emit any statements. seenAwait = shouldName; return transform(expr, () { expr.left = expr.left.accept(this)..parent = expr; seenAwait = seenAwait || rightAwait; }); } // If right has emitted statements we will produce a temporary t and emit // for && (there is an analogous case for ||): // // t = [left] == true; // if (t) { // t = [right] == true; // } // Recall that statements are emitted in reverse order, so first emit the if // statement, then the assignment of [left] == true, and then translate left // so any statements it emits occur after in the accumulated list (that is, // so they occur before in the corresponding block). var rightBody = blockOf(rightStatements); var result = allocateTemporary(nameIndex); rightBody.addStatement(new ExpressionStatement(new VariableSet( result, new MethodInvocation(expr.right, new Name('=='), new Arguments([new BoolLiteral(true)]))))); var then, otherwise; if (expr.operator == '&&') { then = rightBody; otherwise = null; } else { then = new EmptyStatement(); otherwise = rightBody; } statements.add(new IfStatement(new VariableGet(result), then, otherwise)); var test = new MethodInvocation(expr.left, new Name('=='), new Arguments([new BoolLiteral(true)])); statements.add(new ExpressionStatement(new VariableSet(result, test))); seenAwait = false; test.receiver = test.receiver.accept(this)..parent = test; ++nameIndex; seenAwait = seenAwait || rightAwait; return new VariableGet(result); } TreeNode visitConditionalExpression(ConditionalExpression expr) { // Then and otherwise are delimited because they are conditionally // evaluated. var shouldName = seenAwait; var thenStatements = []; seenAwait = false; expr.then = delimit(() => expr.then.accept(this), thenStatements) ..parent = expr; var thenAwait = seenAwait; var otherwiseStatements = []; seenAwait = false; expr.otherwise = delimit(() => expr.otherwise.accept(this), otherwiseStatements) ..parent = expr; var otherwiseAwait = seenAwait; if (thenStatements.isEmpty && otherwiseStatements.isEmpty) { // Easy case: neither then nor otherwise emitted any statements. seenAwait = shouldName; return transform(expr, () { expr.condition = expr.condition.accept(this)..parent = expr; seenAwait = seenAwait || thenAwait || otherwiseAwait; }); } // If then or otherwise has emitted statements we will produce a temporary t // and emit: // // if ([condition]) { // t = [left]; // } else { // t = [right]; // } var result = allocateTemporary(nameIndex); var thenBody = blockOf(thenStatements); var otherwiseBody = blockOf(otherwiseStatements); thenBody.addStatement( new ExpressionStatement(new VariableSet(result, expr.then))); otherwiseBody.addStatement( new ExpressionStatement(new VariableSet(result, expr.otherwise))); var branch = new IfStatement(expr.condition, thenBody, otherwiseBody); statements.add(branch); seenAwait = false; branch.condition = branch.condition.accept(this)..parent = branch; ++nameIndex; seenAwait = seenAwait || thenAwait || otherwiseAwait; return new VariableGet(result); } // Others. TreeNode visitAwaitExpression(AwaitExpression expr) { final R = continuationRewriter; var shouldName = seenAwait; var result = new VariableGet(asyncResult); // The statements are in reverse order, so name the result first if // necessary and then add the two other statements in reverse. if (shouldName) result = name(result); Arguments arguments = new Arguments([ expr.operand, new VariableGet(R.thenContinuationVariable), new VariableGet(R.catchErrorContinuationVariable), new VariableGet(R.nestedClosureVariable), ]); // We are building // // [yield] (let _ = _awaitHelper(...) in null) // // to ensure that :await_jump_var and :await_jump_ctx are updated // before _awaitHelper is invoked (see BuildYieldStatement in // StreamingFlowGraphBuilder for details of how [yield] is translated to // IL). This guarantees that recursive invocation of the current function // would continue from the correct "jump" position. Recursive invocations // arise if future we are awaiting completes synchronously. Builtin Future // implementation don't complete synchronously, but Flutter's // SynchronousFuture do (see bug http://dartbug.com/32098 for more details). statements.add(R.createContinuationPoint(new Let( new VariableDeclaration(null, initializer: new StaticInvocation(R.helper.awaitHelper, arguments) ..fileOffset = expr.fileOffset), new NullLiteral())) ..fileOffset = expr.fileOffset); seenAwait = false; var index = nameIndex; arguments.positional[0] = expr.operand.accept(this)..parent = arguments; if (shouldName && index + 1 > nameIndex) nameIndex = index + 1; seenAwait = true; return result; } TreeNode visitFunctionExpression(FunctionExpression expr) { expr.transformChildren(this); return expr; } TreeNode visitLet(Let expr) { var body = expr.body.accept(this); VariableDeclaration variable = expr.variable; if (seenAwait) { // There is an await in the body of `let var x = initializer in body` or // to its right. We will produce the sequence of statements: // // // var x = // // // and return the body's value. // // So x is in scope for all the body's statements and the body's value. // This has the unpleasant consequence that all let-bound variables with // await in the let's body will end up hoisted out of the expression and // allocated to the context in the VM, even if they have no uses // (`let _ = e0 in e1` can be used for sequencing of `e0` and `e1`). statements.add(variable); var index = nameIndex; seenAwait = false; variable.initializer = variable.initializer.accept(this) ..parent = variable; // Temporaries used in the initializer or the body are not live but the // temporary used for the body is. if (index + 1 > nameIndex) nameIndex = index + 1; seenAwait = true; return body; } else { // The body in `let x = initializer in body` did not contain an await. We // can leave a let expression. return transform(expr, () { // The body has already been translated. expr.body = body..parent = expr; variable.initializer = variable.initializer.accept(this) ..parent = variable; }); } } visitFunctionNode(FunctionNode node) { var nestedRewriter = new RecursiveContinuationRewriter( continuationRewriter.helper, continuationRewriter.syncAsync); return node.accept(nestedRewriter); } }