Files
sdk/pkg/vm/lib/modular/transformations/ffi/finalizable.dart
T
Johnni Winther e30cd0322c [cfe][Contexts] Split VariableDeclaration and VariableStatement
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>
2026-05-18 05:49:28 -07:00

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;
}
}