blob: 736c9cff94bb351a48045fe71d42996070d52757 [file] [log] [blame]
// 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.
// @dart = 2.9
library kernel.transformations.async;
import '../kernel.dart';
import '../type_environment.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<Statement> statements = <Statement>[];
/// 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<VariableDeclaration> variables = <VariableDeclaration>[];
ExpressionLifter(this.continuationRewriter);
StaticTypeContext get _staticTypeContext =>
continuationRewriter.staticTypeContext;
Block blockOf(List<Statement> statements) {
return new Block(statements.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<Statement> outer) {
assert(statements.isEmpty);
var saved = seenAwait;
seenAwait = false;
Expression result = expression.accept<TreeNode>(this);
outer.addAll(statements.reversed);
statements.clear();
seenAwait = seenAwait || saved;
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<Statement> inner) {
var outer = statements;
statements = inner;
Expression result = action();
statements = outer;
return result;
}
// Name an expression by emitting an assignment to a temporary variable.
Expression name(Expression expr) {
// Allocate as dynamic as temps might be reused with different types.
VariableDeclaration temp =
allocateTemporary(nameIndex, const DynamicType());
statements.add(ExpressionStatement(VariableSet(temp, expr)));
// Type annotate the get via an unsafe cast since all temps are allocated
// as dynamic.
DartType type = expr.getStaticType(_staticTypeContext);
return StaticInvocation(continuationRewriter.helper.unsafeCast,
Arguments(<Expression>[VariableGet(temp)], types: <DartType>[type]));
}
VariableDeclaration allocateTemporary(int index,
[DartType type = const DynamicType()]) {
for (var i = variables.length; i <= index; i++) {
variables.add(VariableDeclaration(":async_temporary_${i}", type: type));
}
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) {
Expression result = expr;
if (seenAwait && !expr.variable.isFinal && !expr.variable.isConst) {
result = name(expr);
++nameIndex;
}
return result;
}
// 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 transformTreeNode(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 transformTreeNode(expr, () {
expr.transformChildren(this);
});
}
TreeNode visitVariableSet(VariableSet expr) => unary(expr);
TreeNode visitPropertyGet(PropertyGet 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 transformTreeNode(expr, () {
expr.value = expr.value.accept<TreeNode>(this)..parent = expr;
expr.receiver = expr.receiver.accept<TreeNode>(this)..parent = expr;
});
}
TreeNode visitArguments(Arguments args) {
for (var named in args.named.reversed) {
named.value = named.value.accept<TreeNode>(this)..parent = named;
}
var positional = args.positional;
for (var i = positional.length - 1; i >= 0; --i) {
positional[i] = positional[i].accept<TreeNode>(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 transformTreeNode(expr, () {
visitArguments(expr.arguments);
expr.receiver = expr.receiver.accept<TreeNode>(this)..parent = expr;
});
}
TreeNode visitSuperMethodInvocation(SuperMethodInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
});
}
TreeNode visitStaticInvocation(StaticInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
});
}
TreeNode visitConstructorInvocation(ConstructorInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
});
}
TreeNode visitStringConcatenation(StringConcatenation expr) {
return transformTreeNode(expr, () {
var expressions = expr.expressions;
for (var i = expressions.length - 1; i >= 0; --i) {
expressions[i] = expressions[i].accept<TreeNode>(this)..parent = expr;
}
});
}
TreeNode visitListLiteral(ListLiteral expr) {
return transformTreeNode(expr, () {
var expressions = expr.expressions;
for (var i = expressions.length - 1; i >= 0; --i) {
expressions[i] = expr.expressions[i].accept<TreeNode>(this)
..parent = expr;
}
});
}
TreeNode visitMapLiteral(MapLiteral expr) {
return transformTreeNode(expr, () {
for (var entry in expr.entries.reversed) {
entry.value = entry.value.accept<TreeNode>(this)..parent = entry;
entry.key = entry.key.accept<TreeNode>(this)..parent = entry;
}
});
}
// Control flow.
TreeNode visitLogicalExpression(LogicalExpression expr) {
var shouldName = seenAwait;
// Right is delimited because it is conditionally evaluated.
var rightStatements = <Statement>[];
seenAwait = false;
expr.right =
delimit(() => expr.right.accept<TreeNode>(this), rightStatements)
..parent = expr;
var rightAwait = seenAwait;
if (rightStatements.isEmpty) {
// Easy case: right did not emit any statements.
seenAwait = shouldName;
return transformTreeNode(expr, () {
expr.left = expr.left.accept<TreeNode>(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,
_staticTypeContext.typeEnvironment.coreTypes
.boolRawType(_staticTypeContext.nonNullable));
rightBody.addStatement(new ExpressionStatement(new VariableSet(
result,
new MethodInvocation(expr.right, new Name('=='),
new Arguments(<Expression>[new BoolLiteral(true)])))));
var then, otherwise;
if (expr.operatorEnum == LogicalExpressionOperator.AND) {
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(<Expression>[new BoolLiteral(true)]));
statements.add(new ExpressionStatement(new VariableSet(result, test)));
seenAwait = false;
test.receiver = test.receiver.accept<TreeNode>(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;
final savedNameIndex = nameIndex;
var thenStatements = <Statement>[];
seenAwait = false;
expr.then = delimit(() => expr.then.accept<TreeNode>(this), thenStatements)
..parent = expr;
var thenAwait = seenAwait;
final thenNameIndex = nameIndex;
nameIndex = savedNameIndex;
var otherwiseStatements = <Statement>[];
seenAwait = false;
expr.otherwise = delimit(
() => expr.otherwise.accept<TreeNode>(this), otherwiseStatements)
..parent = expr;
var otherwiseAwait = seenAwait;
// Only one side of this branch will get executed at a time, so just make
// sure we have enough temps for either, not both at the same time.
if (thenNameIndex > nameIndex) {
nameIndex = thenNameIndex;
}
if (thenStatements.isEmpty && otherwiseStatements.isEmpty) {
// Easy case: neither then nor otherwise emitted any statements.
seenAwait = shouldName;
return transformTreeNode(expr, () {
expr.condition = expr.condition.accept<TreeNode>(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, expr.staticType);
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<TreeNode>(this)..parent = branch;
++nameIndex;
seenAwait = seenAwait || thenAwait || otherwiseAwait;
return new VariableGet(result);
}
// Others.
TreeNode visitAwaitExpression(AwaitExpression expr) {
final R = continuationRewriter;
var shouldName = seenAwait;
var type = expr.getStaticType(_staticTypeContext);
Expression result = new VariableGet(asyncResult);
if (type is! DynamicType) {
int fileOffset = expr.operand.fileOffset;
if (fileOffset == TreeNode.noOffset) {
fileOffset = expr.fileOffset;
}
assert(fileOffset != TreeNode.noOffset);
result = new StaticInvocation(
continuationRewriter.helper.unsafeCast,
new Arguments(<Expression>[result], types: <DartType>[type])
..fileOffset = fileOffset)
..fileOffset = fileOffset;
}
// 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(<Expression>[
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<TreeNode>(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<TreeNode>(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:
//
// <initializer's statements>
// var x = <initializer's value>
// <body's statements>
//
// 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<TreeNode>(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 transformTreeNode(expr, () {
// The body has already been translated.
expr.body = body..parent = expr;
variable.initializer = variable.initializer.accept<TreeNode>(this)
..parent = variable;
});
}
}
visitFunctionNode(FunctionNode node) {
var nestedRewriter = new RecursiveContinuationRewriter(
continuationRewriter.helper, _staticTypeContext);
return node.accept(nestedRewriter);
}
TreeNode visitBlockExpression(BlockExpression expr) {
return transformTreeNode(expr, () {
expr.value = expr.value.accept<TreeNode>(this)..parent = expr;
List<Statement> body = <Statement>[];
for (Statement stmt in expr.body.statements.reversed) {
Statement translation = stmt.accept<TreeNode>(this);
if (translation != null) body.add(translation);
}
expr.body = new Block(body.reversed.toList())..parent = expr;
});
}
TreeNode defaultStatement(Statement stmt) {
// This method translates a statement nested in an expression (e.g., in a
// block expression). It produces a translated statement, a list of
// statements which are side effects necessary for any await, and a flag
// indicating whether there was an await in the statement or to its right.
// The translated statement can be null in the case where there was already
// an await to the right.
// The translation is accumulating two lists of statements, an inner list
// which is a reversed list of effects needed for the current expression and
// an outer list which represents the block containing the current
// statement. We need to preserve both of those from side effects.
List<Statement> savedInner = statements;
List<Statement> savedOuter = continuationRewriter.statements;
statements = <Statement>[];
continuationRewriter.statements = <Statement>[];
stmt.accept(continuationRewriter);
List<Statement> results = continuationRewriter.statements;
statements = savedInner;
continuationRewriter.statements = savedOuter;
if (!seenAwait && results.length == 1) return results.first;
statements.addAll(results.reversed);
return null;
}
}