e30cd0322c
This separates VariableDeclaration from Statement. VariableDeclaration no longer implements Statement and variable declared in a block or in a for-statement are now wrapped by a VariableStatement. Currently there are two VariableStatement implementations; LegacyVariableStatement for variables in the current model, called LegacyVariable, and VariableInitialization for variables used in the new, still experimental, encoding that supports scope computation. This CL is a step towards realigning the AST nodes to the new model in which each kind of variable has its own distinct subclass. (LocalVariable, PositionalParameter, NamedParameter, SyntheticVariable, etc.) Note that it is not the intent to use VariableStatement in ForStatement going forward but that will be handled in a follow-up. TEST=existing. Change-Id: I5b309cd62c9b138f95b74fb054686edffa49a393 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502681 Reviewed-by: Chloe Stefantsova <cstefantsova@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Johnni Winther <johnniwinther@google.com>
1058 lines
29 KiB
Dart
1058 lines
29 KiB
Dart
// Copyright (c) 2022, 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.
|
|
|
|
import 'package:kernel/ast.dart';
|
|
import 'package:kernel/kernel.dart';
|
|
import 'package:kernel/type_environment.dart';
|
|
|
|
/// Implements the `Finalizable` semantics.
|
|
///
|
|
/// Designed to be mixed in. Calls super.visitXXX() to visit all nodes (except
|
|
/// the ones created by this transformation).
|
|
///
|
|
/// This transformation is not AST-node preserving. [Expression]s and
|
|
/// [Statement]s can be replaced by other [Expression]s and [Statement]s
|
|
/// respectively. This means one cannot do `visitX() { super.visitX() as X }`.
|
|
///
|
|
/// This transform must be run on the standard libraries as well. For example
|
|
/// `NativeFinalizer`s `attach` implementation depends on it.
|
|
mixin FinalizableTransformer on Transformer {
|
|
TypeEnvironment get env;
|
|
Procedure get reachabilityFenceFunction;
|
|
Class get finalizableClass;
|
|
|
|
StaticTypeContext? staticTypeContext;
|
|
|
|
_Scope? _currentScope;
|
|
|
|
bool thisIsFinalizable = false;
|
|
|
|
/// Traverses [f] in a newly created [_Scope].
|
|
///
|
|
/// Any declarations added to this new scope will be fenced in
|
|
/// [appendFencesToStatement] and [appendFencesToExpression] if provided.
|
|
///
|
|
/// Captures need to be precomputed (by [FindCaptures]) and can be passed in
|
|
/// through [precomputedCaptureScope].
|
|
///
|
|
/// [declaresThis] is true if `this` in the scope is `Finalizable` and
|
|
/// defined.
|
|
T inScope<T>(
|
|
TreeNode node,
|
|
T Function() f, {
|
|
Statement? appendFencesToStatement,
|
|
Expression? appendFencesToExpression,
|
|
bool? declaresThis,
|
|
_Scope? precomputedCaptureScope,
|
|
Block? addPossiblyUninitializedTo,
|
|
}) {
|
|
final scope = _Scope(
|
|
node,
|
|
parent: _currentScope,
|
|
declaresThis: declaresThis,
|
|
);
|
|
if (precomputedCaptureScope != null) {
|
|
scope._capturesThis = precomputedCaptureScope._capturesThis;
|
|
scope._captures = precomputedCaptureScope._captures;
|
|
}
|
|
_currentScope = scope;
|
|
final result = f();
|
|
if (appendFencesToStatement != null) {
|
|
_appendReachabilityFences(
|
|
appendFencesToStatement,
|
|
scope.toFenceThisScope,
|
|
);
|
|
}
|
|
if (appendFencesToExpression != null) {
|
|
appendFencesToExpression.replaceWith(
|
|
_wrapReachabilityFences(
|
|
appendFencesToExpression,
|
|
scope.toFenceThisScope,
|
|
),
|
|
);
|
|
}
|
|
final possiblyUninitializedDeclarations =
|
|
_currentScope?._possiblyUninitializedDeclarations ?? {};
|
|
for (final entry in possiblyUninitializedDeclarations.entries) {
|
|
final possiblyUninitialized = entry.key;
|
|
final alwaysInitialized = entry.value;
|
|
addPossiblyUninitializedTo!.statements.insert(
|
|
addPossiblyUninitializedTo.statements.indexOf(
|
|
possiblyUninitialized.parent as VariableStatement,
|
|
),
|
|
VariableStatement(alwaysInitialized),
|
|
);
|
|
}
|
|
assert(_currentScope == scope);
|
|
_currentScope = _currentScope!.parent;
|
|
return result;
|
|
}
|
|
|
|
Map<LocalFunction, _Scope> _precomputedCaptures = {};
|
|
|
|
_Scope? _precomputeCaptures(LocalFunction node) {
|
|
if (_currentScope!.allDeclarationsIsEmpty) {
|
|
// There's nothing we can capture.
|
|
return null;
|
|
}
|
|
final lookup = _precomputedCaptures[node];
|
|
if (lookup != null) {
|
|
return lookup;
|
|
}
|
|
final visitor = FindCaptures(
|
|
_currentScope!,
|
|
thisIsFinalizable,
|
|
_isFinalizable,
|
|
);
|
|
visitor.visitLocalFunction(node);
|
|
_precomputedCaptures = visitor.precomputedScopes;
|
|
return _precomputedCaptures[node]!;
|
|
}
|
|
|
|
@override
|
|
visitField(Field node) {
|
|
assert(staticTypeContext == null);
|
|
staticTypeContext = StaticTypeContext(node, env);
|
|
assert(_currentScope == null);
|
|
assert(thisIsFinalizable == false);
|
|
thisIsFinalizable = _thisIsFinalizableFromMember(node);
|
|
final result = inScope(
|
|
node,
|
|
() => super.visitField(node),
|
|
declaresThis: thisIsFinalizable,
|
|
);
|
|
thisIsFinalizable = false;
|
|
staticTypeContext = null;
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
visitConstructor(Constructor node) {
|
|
assert(staticTypeContext == null);
|
|
staticTypeContext = StaticTypeContext(node, env);
|
|
assert(_currentScope == null);
|
|
assert(thisIsFinalizable == false);
|
|
thisIsFinalizable = _thisIsFinalizableFromMember(node);
|
|
final result = inScope(
|
|
node,
|
|
() => super.visitConstructor(node),
|
|
appendFencesToStatement: node.function.body,
|
|
declaresThis: thisIsFinalizable,
|
|
);
|
|
thisIsFinalizable = false;
|
|
staticTypeContext = null;
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
visitProcedure(Procedure node) {
|
|
assert(staticTypeContext == null);
|
|
staticTypeContext = StaticTypeContext(node, env);
|
|
assert(_currentScope == null);
|
|
assert(thisIsFinalizable == false);
|
|
thisIsFinalizable = _thisIsFinalizableFromMember(node);
|
|
final result = inScope(
|
|
node,
|
|
() => super.visitProcedure(node),
|
|
appendFencesToStatement: node.function.body,
|
|
declaresThis: thisIsFinalizable,
|
|
);
|
|
thisIsFinalizable = false;
|
|
staticTypeContext = null;
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitBlock(Block node) {
|
|
return inScope(
|
|
node,
|
|
() => super.visitBlock(node),
|
|
appendFencesToStatement: node,
|
|
addPossiblyUninitializedTo: node,
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitForInStatement(ForInStatement node) {
|
|
// This does not use [inScope], because it would visit [iterable] with
|
|
// [variable] in scope.
|
|
|
|
// First, transform the iterable, which does not have variable in scope.
|
|
node.iterable = transform(node.iterable);
|
|
node.iterable.parent = node;
|
|
|
|
final scope = _Scope(node, parent: _currentScope);
|
|
_currentScope = scope;
|
|
|
|
// Then, transform the variable, adding it to the new scope.
|
|
assert(node.variable.initializer == null);
|
|
node.variable = transform(node.variable);
|
|
node.variable.parent = node;
|
|
|
|
// Then transform the body, with the new variable in scope.
|
|
node.body = transform(node.body);
|
|
node.body.parent = node;
|
|
|
|
_appendReachabilityFences(node.body, scope.toFenceThisScope);
|
|
|
|
_currentScope = _currentScope!.parent;
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitForStatement(ForStatement node) {
|
|
return inScope(
|
|
node,
|
|
() => super.visitForStatement(node),
|
|
appendFencesToStatement: node.body,
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitLet(Let node) {
|
|
return inScope(
|
|
node,
|
|
() => super.visitLet(node),
|
|
appendFencesToExpression: node.body,
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitFunctionDeclaration(FunctionDeclaration node) {
|
|
return inScope(
|
|
node,
|
|
() => super.visitFunctionDeclaration(node),
|
|
appendFencesToStatement: node.function.body,
|
|
precomputedCaptureScope: _precomputeCaptures(node),
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitFunctionExpression(FunctionExpression node) {
|
|
return inScope(
|
|
node,
|
|
() => super.visitFunctionExpression(node),
|
|
appendFencesToStatement: node.function.body,
|
|
precomputedCaptureScope: _precomputeCaptures(node),
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitTryCatch(TryCatch node) {
|
|
return inScope(node, () => super.visitTryCatch(node));
|
|
}
|
|
|
|
@override
|
|
TreeNode visitCatch(Catch node) {
|
|
return inScope(node, () => super.visitCatch(node));
|
|
}
|
|
|
|
@override
|
|
TreeNode visitSwitchStatement(SwitchStatement node) {
|
|
return inScope(node, () => super.visitSwitchStatement(node));
|
|
}
|
|
|
|
bool _possiblyUninitialized(VariableDeclaration declaration) {
|
|
if (declaration.isLate) {
|
|
// Also mark late variables with initializers as uninitialized.
|
|
// Otherwise we would would start running the initializer in a fence.
|
|
return true;
|
|
}
|
|
if (declaration.type.declaredNullability == Nullability.nonNullable &&
|
|
declaration.initializer == null &&
|
|
_currentScope?.node is Block) {
|
|
// Variable declarations in a block without an initializer might be
|
|
// uninitialized. (Variable declarations in function blocks are
|
|
// initialized by the caller.)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
TreeNode defaultVariableDeclaration(VariableDeclaration node) {
|
|
node = super.defaultVariableDeclaration(node) as VariableDeclaration;
|
|
if (_currentScope == null) {
|
|
// Global variable.
|
|
return node;
|
|
}
|
|
if (_isFinalizable(node.type)) {
|
|
if (_possiblyUninitialized(node)) {
|
|
final alwaysInitializedDeclaration = VariableDeclaration(
|
|
':${node.name}:finalizableValue',
|
|
type: node.type.withDeclaredNullability(Nullability.nullable),
|
|
);
|
|
_currentScope!.addPossiblyUninitializedDeclaration(
|
|
node,
|
|
alwaysInitializedDeclaration,
|
|
);
|
|
final initializer = node.initializer;
|
|
if (initializer != null) {
|
|
final newInitializer = VariableSet(
|
|
alwaysInitializedDeclaration,
|
|
initializer,
|
|
);
|
|
node.initializer = newInitializer;
|
|
newInitializer.parent = node;
|
|
}
|
|
} else {
|
|
_currentScope!.addDeclaration(node);
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitVariableSet(VariableSet node) {
|
|
node = super.visitVariableSet(node) as VariableSet;
|
|
final variable = node.variable;
|
|
if (!_isFinalizable(variable.type)) {
|
|
return node;
|
|
}
|
|
|
|
final expression = node.value;
|
|
|
|
// We can't fence late variables, they might not have been set yet.
|
|
// Instead we fence the value variable and assign the late variable
|
|
// value to the value variable.
|
|
final valueVariable = _currentScope?.alwaysInitializedDeclaration(
|
|
variable,
|
|
checkAncestorScopes: true,
|
|
);
|
|
if (valueVariable != null) {
|
|
final newExpression = _wrapReachabilityFences(expression, [
|
|
VariableGet(valueVariable),
|
|
]);
|
|
node.value = newExpression;
|
|
newExpression.parent = node;
|
|
return VariableSet(valueVariable, node);
|
|
}
|
|
|
|
final newExpression = _wrapReachabilityFences(expression, [
|
|
VariableGet(variable),
|
|
]);
|
|
node.value = newExpression;
|
|
newExpression.parent = node;
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitReturnStatement(ReturnStatement node) {
|
|
final declarations = _currentScope!.toFenceReturn;
|
|
node = super.visitReturnStatement(node) as ReturnStatement;
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final expression = node.expression;
|
|
if (expression == null) {
|
|
final newStatement = Block([..._reachabilityFences(declarations), node]);
|
|
return newStatement;
|
|
}
|
|
|
|
final newExpression = _wrapReachabilityFences(expression, declarations);
|
|
|
|
node.expression = newExpression;
|
|
newExpression.parent = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
/// The async transform runs after this transform. It transforms
|
|
/// [YieldStatement]s in async* functions into:
|
|
/// ```
|
|
/// _AsyncStarStreamController controller;
|
|
/// if(controller.add(...){
|
|
/// return ...
|
|
/// } else {
|
|
/// yield ...
|
|
/// }
|
|
/// ```
|
|
/// We don't want to run this transform after the async transform because that
|
|
/// introduces new scoping and control flow and it would create another
|
|
/// traversal over the AST.
|
|
/// So, we need to insert fences for yields as if they were returns in async*
|
|
/// functions.
|
|
///
|
|
/// However, there is more. The body of async* and sync* functions is
|
|
/// transformed into a 'closure', which branches on the yield index and is
|
|
/// executed multiple times. The context of this closure is restored on
|
|
/// re-execution. These two things make it a continuation.
|
|
/// The [YieldStatement]s are compiled into returns from that closure.
|
|
/// When inlining the iterator machinery and eliminating dead code, the
|
|
/// compiler can see that we will never execute a re-entry if we just ask for
|
|
/// only the first value of a stream from a sync* function.
|
|
/// So, we need to insert fences for yields as if they were returns in sync*
|
|
/// functions as well.
|
|
@override
|
|
TreeNode visitYieldStatement(YieldStatement node) {
|
|
final declarations = _currentScope!.toFenceReturn;
|
|
node = super.visitYieldStatement(node) as YieldStatement;
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final newExpression = _wrapReachabilityFences(
|
|
node.expression,
|
|
declarations,
|
|
);
|
|
|
|
node.expression = newExpression;
|
|
newExpression.parent = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
/// [AwaitExpression]s are transformed into [YieldStatement]s by the
|
|
/// async transform. See the comment on [visitYieldStatement].
|
|
@override
|
|
TreeNode visitAwaitExpression(AwaitExpression node) {
|
|
final declarations = _currentScope!.toFenceReturn;
|
|
node = super.visitAwaitExpression(node) as AwaitExpression;
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final newExpression = _wrapReachabilityFences(node.operand, declarations);
|
|
|
|
node.operand = newExpression;
|
|
newExpression.parent = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitThrow(Throw node) {
|
|
final declarations = _currentScope!.toFenceThrow(
|
|
staticTypeContext!.getExpressionType(node.expression),
|
|
env,
|
|
);
|
|
node = super.visitThrow(node) as Throw;
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final newExpression = _wrapReachabilityFences(
|
|
node.expression,
|
|
declarations,
|
|
);
|
|
|
|
node.expression = newExpression;
|
|
newExpression.parent = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitRethrow(Rethrow node) {
|
|
final declarations = _currentScope!.toFenceRethrow(
|
|
_currentScope!.rethrowType,
|
|
env,
|
|
);
|
|
node = super.visitRethrow(node) as Rethrow;
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
return BlockExpression(
|
|
Block(<Statement>[..._reachabilityFences(declarations)]),
|
|
node,
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitBreakStatement(BreakStatement node) {
|
|
final declarations = _currentScope!.toFenceBreak(node.target);
|
|
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final newStatement = Block([..._reachabilityFences(declarations), node]);
|
|
return newStatement;
|
|
}
|
|
|
|
@override
|
|
TreeNode visitLabeledStatement(LabeledStatement node) {
|
|
_currentScope!._labels.add(node);
|
|
return super.visitLabeledStatement(node);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitContinueSwitchStatement(ContinueSwitchStatement node) {
|
|
final switchStatement = node.target.parent as SwitchStatement;
|
|
final declarations = _currentScope!.toFenceSwitchContinue(switchStatement);
|
|
|
|
if (declarations.isEmpty) {
|
|
return node;
|
|
}
|
|
|
|
final newStatement = Block([..._reachabilityFences(declarations), node]);
|
|
return newStatement;
|
|
}
|
|
|
|
/// Cache for [_isFinalizable].
|
|
///
|
|
/// Speeds up the type checks by about a factor of 2 on Flutter Gallery.
|
|
Map<DartType, bool> _isFinalizableCache = {};
|
|
|
|
/// Whether [type] is something that subtypes `FutureOr<Finalizable?>?`.
|
|
bool _isFinalizable(DartType type) => type.isFinalizable(
|
|
finalizableClass: finalizableClass,
|
|
typeEnvironment: env,
|
|
cache: _isFinalizableCache,
|
|
);
|
|
|
|
bool _thisIsFinalizableFromMember(Member member) {
|
|
final enclosingClass_ = member.enclosingClass;
|
|
if (enclosingClass_ == null) {
|
|
return false;
|
|
}
|
|
if (member.isAbstract) {
|
|
return false;
|
|
}
|
|
if (member.isExternal) {
|
|
return false;
|
|
}
|
|
if (member is Constructor && member.isSynthetic) {
|
|
return false;
|
|
}
|
|
if (member is Procedure && member.isStatic) {
|
|
return false;
|
|
}
|
|
return _isFinalizable(
|
|
InterfaceType(enclosingClass_, Nullability.nonNullable),
|
|
);
|
|
}
|
|
|
|
List<Statement> _reachabilityFences(List<Expression> declarations) =>
|
|
<Statement>[
|
|
for (var declaration in declarations)
|
|
ExpressionStatement(
|
|
StaticInvocation(
|
|
reachabilityFenceFunction,
|
|
Arguments(<Expression>[declaration]),
|
|
),
|
|
),
|
|
];
|
|
|
|
/// Turns an [expression] into a block expression with reachability fences.
|
|
///
|
|
/// ```
|
|
/// block {
|
|
/// final <expression type> #t1 = <expression>;
|
|
/// _in::reachabilityFence(finalizable0);
|
|
/// _in::reachabilityFence(finalizable1);
|
|
/// // ..
|
|
/// } =>#t1
|
|
/// ```
|
|
///
|
|
/// Note that this modifies the parent of [expression].
|
|
Expression _wrapReachabilityFences(
|
|
Expression expression,
|
|
List<Expression> declarations,
|
|
) {
|
|
final resultVariable = VariableDeclaration(
|
|
":expressionValueWrappedFinalizable",
|
|
initializer: expression,
|
|
type: staticTypeContext!.getExpressionType(expression),
|
|
isFinal: true,
|
|
isSynthesized: true,
|
|
);
|
|
return BlockExpression(
|
|
Block(<Statement>[
|
|
VariableStatement(resultVariable),
|
|
..._reachabilityFences(declarations),
|
|
]),
|
|
VariableGet(resultVariable),
|
|
);
|
|
}
|
|
|
|
Statement _appendReachabilityFences(
|
|
Statement statement,
|
|
List<Expression> declarations,
|
|
) {
|
|
if (declarations.isEmpty) {
|
|
return statement;
|
|
}
|
|
if (statement is! Block && statement.endsWithAbnormalControlFlow) {
|
|
// This would just wrap the statement in a block for no reason.
|
|
return statement;
|
|
}
|
|
Block block = () {
|
|
if (statement is Block) {
|
|
return statement;
|
|
}
|
|
final replacement = Block(<Statement>[]);
|
|
statement.replaceWith(replacement);
|
|
replacement.statements.add(statement);
|
|
return replacement;
|
|
}();
|
|
if (block.statements.isEmpty ||
|
|
(!block.statements.last.endsWithAbnormalControlFlow)) {
|
|
block.statements.addAll(_reachabilityFences(declarations));
|
|
}
|
|
return block;
|
|
}
|
|
}
|
|
|
|
/// A lightweight version of the above transform that precomputes scopes.
|
|
///
|
|
/// We need to precompute scopes and captures because a variable can be captured
|
|
/// later in a closure than the first return.
|
|
///
|
|
/// We cannot use the precomputed scopes for their declarations, because we
|
|
/// could see returns in a scope before a declaration.
|
|
class FindCaptures extends RecursiveVisitor {
|
|
final bool Function(DartType) _isFinalizable;
|
|
|
|
final bool thisIsFinalizable;
|
|
|
|
final Map<LocalFunction, _Scope> precomputedScopes = {};
|
|
|
|
_Scope _currentScope;
|
|
|
|
FindCaptures(this._currentScope, this.thisIsFinalizable, this._isFinalizable);
|
|
|
|
void inScope(LocalFunction node, void Function() f) {
|
|
final scope = _Scope(node, parent: _currentScope, declaresThis: false);
|
|
assert(precomputedScopes[node] == null);
|
|
precomputedScopes[node] = scope;
|
|
_currentScope = scope;
|
|
final result = f();
|
|
assert(_currentScope == scope);
|
|
_currentScope = _currentScope.parent!;
|
|
return result;
|
|
}
|
|
|
|
void visitLocalFunction(LocalFunction node) {
|
|
if (node is FunctionDeclaration) {
|
|
return visitFunctionDeclaration(node);
|
|
}
|
|
if (node is FunctionExpression) {
|
|
return visitFunctionExpression(node);
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
@override
|
|
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
inScope(node, () => super.visitFunctionDeclaration(node));
|
|
}
|
|
|
|
@override
|
|
void visitFunctionExpression(FunctionExpression node) {
|
|
inScope(node, () => super.visitFunctionExpression(node));
|
|
}
|
|
|
|
@override
|
|
void defaultVariableDeclaration(VariableDeclaration node) {
|
|
if (_isFinalizable(node.type)) {
|
|
_currentScope.addDeclaration(node);
|
|
}
|
|
super.defaultVariableDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitVariableGet(VariableGet node) {
|
|
super.visitVariableGet(node);
|
|
if (_isFinalizable(node.variable.type)) {
|
|
_currentScope.addCapture(node.variable);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitVariableSet(VariableSet node) {
|
|
super.visitVariableSet(node);
|
|
if (_isFinalizable(node.variable.type)) {
|
|
_currentScope.addCapture(node.variable);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitThisExpression(ThisExpression node) {
|
|
if (thisIsFinalizable) {
|
|
_currentScope.addCaptureThis();
|
|
}
|
|
super.visitThisExpression(node);
|
|
}
|
|
}
|
|
|
|
/// A scope contains all `Finalizable` declarations and captures.
|
|
class _Scope {
|
|
/// Parent scope if any.
|
|
final _Scope? parent;
|
|
|
|
/// The [node] introducing this scope.
|
|
final TreeNode node;
|
|
|
|
/// The declarations in this scope.
|
|
///
|
|
/// The list is mutable, because we populate it during visiting statements.
|
|
///
|
|
/// We use a list rather than a set because declarations are unique and we'd
|
|
/// like to prevent arbitrary reorderings when generating code from this.
|
|
///
|
|
/// Includes [_possiblyUninitializedDeclarations] keys.
|
|
final List<VariableDeclaration> _declarations = [];
|
|
|
|
/// The late and non-nullable Finalizable declarations in this scope mapped
|
|
/// to nullable non-late variables that contain the same value.
|
|
///
|
|
/// The map is mutable, because we populate it during visiting statements.
|
|
final Map<VariableDeclaration, VariableDeclaration>
|
|
_possiblyUninitializedDeclarations = {};
|
|
|
|
/// [ThisExpression] is not a [VariableDeclaration] and needs to be tracked
|
|
/// separately.
|
|
final bool declaresThis;
|
|
|
|
/// Labels defined in this scope.
|
|
///
|
|
/// Used for seeing which declarations need to be fenced when encountering
|
|
/// a [BreakStatement];
|
|
final Set<LabeledStatement> _labels = {};
|
|
|
|
_Scope(this.node, {this.parent, bool? declaresThis})
|
|
: this.declaresThis = declaresThis ?? false,
|
|
this.allDeclarationsIsEmpty =
|
|
(parent?.allDeclarationsIsEmpty ?? true) && !(declaresThis ?? false);
|
|
|
|
@override
|
|
String toString() => toStringIndented();
|
|
|
|
toStringIndented({int indentation = 0}) {
|
|
final nonIndented =
|
|
'''node: $node
|
|
declarations:${_declarations.map((e) => '''
|
|
$e''').join()}
|
|
declaresThis: $declaresThis
|
|
labels:${_labels.map((e) => '''
|
|
$e''').join()}
|
|
parent:
|
|
${parent?.toStringIndented(indentation: indentation + 2)}
|
|
''';
|
|
return nonIndented.replaceAll('\n', (' ' * indentation) + '\n');
|
|
}
|
|
|
|
void addDeclaration(VariableDeclaration declaration) {
|
|
_declarations.add(declaration);
|
|
allDeclarationsIsEmpty = false;
|
|
}
|
|
|
|
void addPossiblyUninitializedDeclaration(
|
|
VariableDeclaration possiblyUninitialized,
|
|
VariableDeclaration nullableValue,
|
|
) {
|
|
assert(possiblyUninitialized.parent is VariableStatement);
|
|
_possiblyUninitializedDeclarations[possiblyUninitialized] = nullableValue;
|
|
addDeclaration(possiblyUninitialized);
|
|
}
|
|
|
|
VariableDeclaration? alwaysInitializedDeclaration(
|
|
VariableDeclaration possiblyUninitialized, {
|
|
required bool checkAncestorScopes,
|
|
}) {
|
|
final resultThisScope =
|
|
_possiblyUninitializedDeclarations[possiblyUninitialized];
|
|
if (resultThisScope != null) {
|
|
return resultThisScope;
|
|
}
|
|
if (!checkAncestorScopes) {
|
|
return null;
|
|
}
|
|
return parent?.alwaysInitializedDeclaration(
|
|
possiblyUninitialized,
|
|
checkAncestorScopes: checkAncestorScopes,
|
|
);
|
|
}
|
|
|
|
VariableDeclaration variableToFence(
|
|
VariableDeclaration declaration, {
|
|
required bool checkAncestorScopes,
|
|
}) {
|
|
final possibleValueToFence = alwaysInitializedDeclaration(
|
|
declaration,
|
|
checkAncestorScopes: checkAncestorScopes,
|
|
);
|
|
if (possibleValueToFence != null) {
|
|
return possibleValueToFence;
|
|
}
|
|
|
|
return declaration;
|
|
}
|
|
|
|
/// Whether [allDeclarations] is empty.
|
|
///
|
|
/// Manually cached for performance.
|
|
bool allDeclarationsIsEmpty;
|
|
|
|
/// All declarations in this and parent scopes.
|
|
///
|
|
/// Excluding `this`.
|
|
List<VariableDeclaration> get allDeclarations => [
|
|
...?parent?.allDeclarations,
|
|
..._declarations,
|
|
];
|
|
|
|
bool get canCapture => node is LocalFunction;
|
|
|
|
/// Which of the ancestor scopes (or this) captures variables.
|
|
late final _Scope? capturingScope = () {
|
|
if (canCapture) {
|
|
return this;
|
|
}
|
|
return parent?.capturingScope;
|
|
}();
|
|
|
|
Map<VariableDeclaration, bool>? _captures;
|
|
|
|
Map<VariableDeclaration, bool> get captures {
|
|
if (_captures != null) {
|
|
return _captures!;
|
|
}
|
|
|
|
assert(canCapture);
|
|
_captures = {for (var d in parent!.allDeclarations) d: false};
|
|
return _captures!;
|
|
}
|
|
|
|
bool _capturesThis = false;
|
|
|
|
void addCapture(VariableDeclaration declaration) {
|
|
final capturingScope_ = capturingScope;
|
|
if (capturingScope_ == null) {
|
|
// We're not in a nested closure.
|
|
return;
|
|
}
|
|
|
|
final captures = capturingScope_.captures;
|
|
if (!captures.containsKey(declaration)) {
|
|
// This is a local variable, not a captured one.
|
|
return;
|
|
}
|
|
captures[declaration] = true;
|
|
|
|
capturingScope_.parent?.addCapture(declaration);
|
|
}
|
|
|
|
void addCaptureThis() {
|
|
final capturingScope_ = capturingScope;
|
|
if (capturingScope_ == null) {
|
|
// We're not in a nested closure.
|
|
return;
|
|
}
|
|
|
|
capturingScope_._capturesThis = true;
|
|
|
|
capturingScope_.parent?.addCaptureThis();
|
|
}
|
|
|
|
/// Get declarations in this scope.
|
|
List<Expression> get toFenceThisScope {
|
|
final captures = _captures;
|
|
return [
|
|
if (declaresThis || _capturesThis) ThisExpression(),
|
|
for (var d in _declarations)
|
|
VariableGet(variableToFence(d, checkAncestorScopes: false)),
|
|
if (captures != null)
|
|
for (var d in captures.entries.where((e) => e.value).map((e) => e.key))
|
|
VariableGet(variableToFence(d, checkAncestorScopes: true)),
|
|
];
|
|
}
|
|
|
|
/// Whether when a return is found, this is the last ancestor of which
|
|
/// declarations should be considered.
|
|
bool get scopesReturn {
|
|
assert(
|
|
node is Block ||
|
|
node is Catch ||
|
|
node is ForInStatement ||
|
|
node is ForStatement ||
|
|
node is Let ||
|
|
node is LocalFunction ||
|
|
node is Member ||
|
|
node is SwitchStatement ||
|
|
node is TryCatch,
|
|
);
|
|
return node is Member || node is LocalFunction;
|
|
}
|
|
|
|
/// Get all declarations that should stay alive on a return.
|
|
///
|
|
/// This include all declarations in scopes until we see a function scope.
|
|
List<Expression> get toFenceReturn {
|
|
return [if (!scopesReturn) ...parent!.toFenceReturn, ...toFenceThisScope];
|
|
}
|
|
|
|
List<Expression> toFenceBreak(LabeledStatement label) {
|
|
if (_labels.contains(label)) {
|
|
return [];
|
|
}
|
|
return [...parent!.toFenceBreak(label), ...toFenceThisScope];
|
|
}
|
|
|
|
List<Expression> toFenceSwitchContinue(SwitchStatement switchStatement) {
|
|
if (node == switchStatement) {
|
|
return [];
|
|
}
|
|
return [
|
|
...parent!.toFenceSwitchContinue(switchStatement),
|
|
...toFenceThisScope,
|
|
];
|
|
}
|
|
|
|
bool scopesThrow(DartType exceptionType, TypeEnvironment typeEnvironment) {
|
|
final node_ = node;
|
|
if (node_ is! TryCatch) {
|
|
return false;
|
|
}
|
|
final catches = node_.catches;
|
|
for (final catch_ in catches) {
|
|
if (typeEnvironment.isSubtypeOf(exceptionType, catch_.guard)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
List<Expression> toFenceThrow(
|
|
DartType exceptionType,
|
|
TypeEnvironment typeEnvironment,
|
|
) => [
|
|
if (!scopesThrow(exceptionType, typeEnvironment))
|
|
...?parent?.toFenceThrow(exceptionType, typeEnvironment),
|
|
...toFenceThisScope,
|
|
];
|
|
|
|
DartType get rethrowType {
|
|
final node_ = node;
|
|
if (node_ is Catch) {
|
|
return node_.guard;
|
|
}
|
|
return parent!.rethrowType;
|
|
}
|
|
|
|
List<Expression> toFenceRethrow(
|
|
DartType exceptionType,
|
|
TypeEnvironment typeEnvironment,
|
|
) {
|
|
return [
|
|
if (!scopesThrow(exceptionType, typeEnvironment))
|
|
...?parent?.toFenceRethrow(exceptionType, typeEnvironment),
|
|
if (scopesThrow(exceptionType, typeEnvironment))
|
|
...?parent?.toFenceThrow(exceptionType, typeEnvironment),
|
|
...toFenceThisScope,
|
|
];
|
|
}
|
|
}
|
|
|
|
extension on Statement {
|
|
/// Whether this statement ends with abnormal control flow.
|
|
///
|
|
/// Used to avoid inserting definitely dead reachabilityFences.
|
|
///
|
|
/// Recurses into [Block]s to inspect their last statement.
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// ```dart
|
|
/// {
|
|
/// // ...
|
|
/// return 5;
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// returns true.
|
|
///
|
|
/// ```dart
|
|
/// {
|
|
/// {
|
|
/// break L2;
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// returns true.
|
|
///
|
|
/// ```dart
|
|
/// print(foo);
|
|
/// ```
|
|
///
|
|
/// returns false.
|
|
///
|
|
/// Does not take into consideration full control flow, rather this is best
|
|
/// effort:
|
|
///
|
|
/// ```dart
|
|
/// {
|
|
/// return 42;
|
|
/// var unreachable = true;
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// returns false, even though inserting fences is superfluous.
|
|
///
|
|
/// These extra fences are not unsound.
|
|
bool get endsWithAbnormalControlFlow {
|
|
if (this is ReturnStatement) {
|
|
return true;
|
|
}
|
|
if (this is BreakStatement) {
|
|
return true;
|
|
}
|
|
if (this is ContinueSwitchStatement) {
|
|
return true;
|
|
}
|
|
if (this is Throw) {
|
|
return true;
|
|
}
|
|
if (this is Rethrow) {
|
|
return true;
|
|
}
|
|
final this_ = this;
|
|
if (this_ is Block) {
|
|
final statements = this_.statements;
|
|
if (statements.isEmpty) {
|
|
return false;
|
|
}
|
|
return statements.last.endsWithAbnormalControlFlow;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
extension FinalizableDartType on DartType {
|
|
/// Whether `this` is something that subtypes `FutureOr<Finalizable?>?`.
|
|
bool isFinalizable({
|
|
required Class finalizableClass,
|
|
required TypeEnvironment typeEnvironment,
|
|
Map<DartType, bool>? cache,
|
|
}) {
|
|
final type = this;
|
|
final cached = cache?[type];
|
|
if (cached != null) {
|
|
return cached;
|
|
}
|
|
|
|
final finalizableType = FutureOrType(
|
|
InterfaceType(finalizableClass, Nullability.nullable),
|
|
Nullability.nullable,
|
|
);
|
|
if (!typeEnvironment.isSubtypeOf(type, finalizableType)) {
|
|
cache?[type] = false;
|
|
return false;
|
|
}
|
|
|
|
// Exclude never types.
|
|
final futureOfNeverType = FutureOrType(
|
|
NeverType.nullable(),
|
|
Nullability.nullable,
|
|
);
|
|
final result = !typeEnvironment.isSubtypeOf(type, futureOfNeverType);
|
|
cache?[type] = result;
|
|
return result;
|
|
}
|
|
}
|