39810ffbe4
Under the hood, there is no difference between `isSubtypeWhenUsingNullabilities` and `isSubtypeWhenIgnoringNullabilities` by now. This CL makes the corresponding renames and removals in the CFE and its clients. TEST=existing Change-Id: I22060c29834179c30ba62562aa254146b1d7530d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/433480 Reviewed-by: Ömer Ağacan <omersa@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com> Reviewed-by: Mayank Patke <fishythefish@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com>
1051 lines
29 KiB
Dart
1051 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),
|
|
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 visitVariableDeclaration(VariableDeclaration node) {
|
|
node = super.visitVariableDeclaration(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>[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 visitVariableDeclaration(VariableDeclaration node) {
|
|
if (_isFinalizable(node.type)) {
|
|
_currentScope.addDeclaration(node);
|
|
}
|
|
super.visitVariableDeclaration(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,
|
|
) {
|
|
_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;
|
|
}
|
|
}
|