blob: b59786bfc492721cff417d8fa1cce0a21af25227 [file] [log] [blame]
// 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 libaries 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,
}) {
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));
}
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,
);
}
@override
TreeNode visitForInStatement(ForInStatement node) {
return inScope(
node,
() => super.visitForInStatement(node),
appendFencesToStatement: node.body,
);
}
@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),
);
}
@override
TreeNode visitVariableDeclaration(VariableDeclaration node) {
node = super.visitVariableDeclaration(node) as VariableDeclaration;
if (_currentScope == null) {
// Global variable.
return node;
}
if (_isFinalizable(node.type)) {
_currentScope!.addDeclaration(node);
}
return node;
}
@override
TreeNode visitVariableSet(VariableSet node) {
node = super.visitVariableSet(node) as VariableSet;
if (!_isFinalizable(node.variable.type)) {
return node;
}
final expression = node.value;
final newExpression = _wrapReachabilityFences(
expression,
[VariableGet(node.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 eleminating 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);
return BlockExpression(
Block(<Statement>[
resultVariable,
..._reachabilityFences(declarations),
]),
VariableGet(resultVariable),
);
}
Statement _appendReachabilityFences(
Statement statement, List<Expression> declarations) {
if (declarations.isEmpty) {
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<void> {
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.
final List<VariableDeclaration> _declarations = [];
/// [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);
void addDeclaration(VariableDeclaration declaration) {
_declarations.add(declaration);
allDeclarationsIsEmpty = false;
}
/// 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(d),
if (captures != null)
for (var d in captures.entries.where((e) => e.value).map((e) => e.key))
VariableGet(d),
];
}
/// 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, SubtypeCheckMode.withNullabilities)) {
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,
SubtypeCheckMode.withNullabilities,
)) {
cache?[type] = false;
return false;
}
// Exclude never types.
final futureOfNeverType =
FutureOrType(NeverType.nullable(), Nullability.nullable);
final result = !typeEnvironment.isSubtypeOf(
type,
futureOfNeverType,
SubtypeCheckMode.ignoringNullabilities,
);
cache?[type] = result;
return result;
}
}