Files
sdk/pkg/kernel/lib/transformations/async.dart
T
Clement Skau ffde1585da [VM] Removes types from reused temps in async.
When we detect that a temporary variable is being reused
with a different type, we change the type of the temp
to dynamic.
This allows us to retain types for any non-reused temps.

For any dynamic temps we still have type information available
for use sites, which we can pass on via unsafeCast.

One drawback of this approach is that we don't know ahead
of time which temps are reused, and get turned into dynamic,
so we have to unsafeCast all uses.


This change is similar to what was done in:
https://dart-review.googlesource.com/c/sdk/+/138500

TEST=Adds tests/language/vm/regress_flutter_85311_test.dart,
updates relevant expect files.

Bug: https://github.com/flutter/flutter/issues/85311
Change-Id: I821c5266327892d5c3fd5bae1bebba7f3fe3931b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/205647
Commit-Queue: Clement Skau <cskau@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2021-07-12 08:55:53 +00:00

655 lines
24 KiB
Dart

// 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 '../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);
StatefulStaticTypeContext 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 = transform(expression);
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;
}
// Wraps VariableGet in an unsafeCast if `type` isn't dynamic.
Expression unsafeCastVariableGet(
VariableDeclaration variable, DartType type) {
if (type != const DynamicType()) {
return StaticInvocation(
continuationRewriter.helper.unsafeCast,
Arguments(<Expression>[VariableGet(variable)],
types: <DartType>[type]));
}
return VariableGet(variable);
}
// Name an expression by emitting an assignment to a temporary variable.
Expression name(Expression expr) {
DartType type = expr.getStaticType(_staticTypeContext);
VariableDeclaration temp = allocateTemporary(nameIndex, type);
statements.add(ExpressionStatement(VariableSet(temp, expr)));
// Wrap in unsafeCast to make sure we pass type information even if we later
// have to re-type the temporary variable to dynamic.
return unsafeCastVariableGet(temp, type);
}
VariableDeclaration allocateTemporary(int index,
[DartType type = const DynamicType()]) {
if (variables.length > index) {
// Re-type temporary to dynamic if we detect reuse with different type.
// Note: We should make sure all uses use `unsafeCast(...)` to pass their
// type information on, as that is lost otherwise.
if (variables[index].type != const DynamicType() &&
variables[index].type != type) {
variables[index].type = const DynamicType();
}
return variables[index];
}
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 visitStaticTearOff(StaticTearOff 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);
});
}
@override
TreeNode visitVariableSet(VariableSet expr) => unary(expr);
@override
TreeNode visitInstanceGet(InstanceGet expr) => unary(expr);
@override
TreeNode visitDynamicGet(DynamicGet expr) => unary(expr);
@override
TreeNode visitInstanceTearOff(InstanceTearOff expr) => unary(expr);
@override
TreeNode visitFunctionTearOff(FunctionTearOff expr) => unary(expr);
@override
TreeNode visitSuperPropertySet(SuperPropertySet expr) => unary(expr);
@override
TreeNode visitStaticSet(StaticSet expr) => unary(expr);
@override
TreeNode visitNot(Not expr) => unary(expr);
@override
TreeNode visitIsExpression(IsExpression expr) => unary(expr);
@override
TreeNode visitAsExpression(AsExpression expr) => unary(expr);
@override
TreeNode visitThrow(Throw expr) => unary(expr);
@override
TreeNode visitInstanceSet(InstanceSet expr) {
return transformTreeNode(expr, () {
expr.value = transform(expr.value)..parent = expr;
expr.receiver = transform(expr.receiver)..parent = expr;
});
}
@override
TreeNode visitDynamicSet(DynamicSet expr) {
return transformTreeNode(expr, () {
expr.value = transform(expr.value)..parent = expr;
expr.receiver = transform(expr.receiver)..parent = expr;
});
}
TreeNode visitArguments(Arguments args) {
for (var named in args.named.reversed) {
named.value = transform(named.value)..parent = named;
}
var positional = args.positional;
for (var i = positional.length - 1; i >= 0; --i) {
positional[i] = transform(positional[i])..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;
}
@override
TreeNode visitInstanceInvocation(InstanceInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
expr.receiver = transform(expr.receiver)..parent = expr;
});
}
@override
TreeNode visitLocalFunctionInvocation(LocalFunctionInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
});
}
@override
TreeNode visitDynamicInvocation(DynamicInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
expr.receiver = transform(expr.receiver)..parent = expr;
});
}
@override
TreeNode visitFunctionInvocation(FunctionInvocation expr) {
return transformTreeNode(expr, () {
visitArguments(expr.arguments);
expr.receiver = transform(expr.receiver)..parent = expr;
});
}
@override
TreeNode visitEqualsNull(EqualsNull expr) => unary(expr);
@override
TreeNode visitEqualsCall(EqualsCall expr) {
return transformTreeNode(expr, () {
expr.right = transform(expr.right)..parent = expr;
expr.left = transform(expr.left)..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] = transform(expressions[i])..parent = expr;
}
});
}
TreeNode visitListLiteral(ListLiteral expr) {
return transformTreeNode(expr, () {
var expressions = expr.expressions;
for (var i = expressions.length - 1; i >= 0; --i) {
expressions[i] = transform(expr.expressions[i])..parent = expr;
}
});
}
TreeNode visitMapLiteral(MapLiteral expr) {
return transformTreeNode(expr, () {
for (var entry in expr.entries.reversed) {
entry.value = transform(entry.value)..parent = entry;
entry.key = transform(entry.key)..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(() => transform(expr.right), rightStatements)
..parent = expr;
var rightAwait = seenAwait;
if (rightStatements.isEmpty) {
// Easy case: right did not emit any statements.
seenAwait = shouldName;
return transformTreeNode(expr, () {
expr.left = transform(expr.left)..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);
final type = _staticTypeContext.typeEnvironment.coreTypes
.boolRawType(_staticTypeContext.nonNullable);
final result = allocateTemporary(nameIndex, type);
final objectEquals = continuationRewriter.helper.coreTypes.objectEquals;
rightBody.addStatement(new ExpressionStatement(new VariableSet(
result,
new EqualsCall(expr.right, new BoolLiteral(true),
interfaceTarget: objectEquals,
functionType: objectEquals.getterType as FunctionType))));
var then, otherwise;
if (expr.operatorEnum == LogicalExpressionOperator.AND) {
then = rightBody;
otherwise = null;
} else {
then = new EmptyStatement();
otherwise = rightBody;
}
statements.add(
new IfStatement(unsafeCastVariableGet(result, type), then, otherwise));
final test = new EqualsCall(expr.left, new BoolLiteral(true),
interfaceTarget: objectEquals,
functionType: objectEquals.getterType as FunctionType);
statements.add(new ExpressionStatement(new VariableSet(result, test)));
seenAwait = false;
test.left = transform(test.left)..parent = test;
++nameIndex;
seenAwait = seenAwait || rightAwait;
return unsafeCastVariableGet(result, type);
}
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(() => transform(expr.then), thenStatements)
..parent = expr;
var thenAwait = seenAwait;
final thenNameIndex = nameIndex;
nameIndex = savedNameIndex;
var otherwiseStatements = <Statement>[];
seenAwait = false;
expr.otherwise =
delimit(() => transform(expr.otherwise), 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 = transform(expr.condition)..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];
// }
final 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 = transform(branch.condition)..parent = branch;
++nameIndex;
seenAwait = seenAwait || thenAwait || otherwiseAwait;
return unsafeCastVariableGet(result, expr.staticType);
}
// 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] = transform(expr.operand)..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 = transform(expr.body);
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 = transform(variable.initializer!)
..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 = transform(variable.initializer!)
..parent = variable;
});
}
}
visitFunctionNode(FunctionNode node) {
var nestedRewriter = new RecursiveContinuationRewriter(
continuationRewriter.helper, _staticTypeContext);
return nestedRewriter.transform(node);
}
TreeNode visitBlockExpression(BlockExpression expr) {
return transformTreeNode(expr, () {
expr.value = transform(expr.value)..parent = expr;
List<Statement> body = <Statement>[];
for (Statement stmt in expr.body.statements.reversed) {
Statement? translation = _rewriteStatement(stmt);
if (translation != null) body.add(translation);
}
expr.body = new Block(body.reversed.toList())..parent = expr;
});
}
Statement? _rewriteStatement(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>[];
continuationRewriter.transform(stmt);
List<Statement> results = continuationRewriter.statements;
statements = savedInner;
continuationRewriter.statements = savedOuter;
if (!seenAwait && results.length == 1) return results.first;
statements.addAll(results.reversed);
return null;
}
TreeNode defaultStatement(Statement stmt) {
throw new UnsupportedError(
"Use _rewriteStatement to transform statement: ${stmt}");
}
}