| // 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?.parent as VariableStatement, | |
| ), | |
| VariableStatement(VariableDeclaration(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(Variable 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 defaultVariable(Variable node) { | |
| node = super.defaultVariable(node) as Variable; | |
| if (_currentScope == null) { | |
| // Global variable. | |
| return node; | |
| } | |
| if (_isFinalizable(node.type)) { | |
| if (_possiblyUninitialized(node)) { | |
| node as DeclaredVariable; | |
| final alwaysInitializedDeclaration = SyntheticVariable( | |
| cosmeticName: ':${node.cosmeticName}:finalizableValue', | |
| type: node.type.withDeclaredNullability(Nullability.nullable), | |
| isSynthesized: false, | |
| ); | |
| _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(ExpressionList(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 = SyntheticVariable( | |
| cosmeticName: ":expressionValueWrappedFinalizable", | |
| initializer: expression, | |
| type: staticTypeContext!.getExpressionType(expression), | |
| isFinal: true, | |
| ); | |
| return BlockExpression( | |
| Block(<Statement>[ | |
| VariableStatement(VariableDeclaration(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 defaultVariable(Variable node) { | |
| if (_isFinalizable(node.type)) { | |
| _currentScope.addDeclaration(node); | |
| } | |
| super.defaultVariable(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<Variable> _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<DeclaredVariable, DeclaredVariable> | |
| _possiblyUninitializedDeclarations = {}; | |
| /// [ThisExpression] is not a [Variable] 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(Variable declaration) { | |
| _declarations.add(declaration); | |
| allDeclarationsIsEmpty = false; | |
| } | |
| void addPossiblyUninitializedDeclaration( | |
| DeclaredVariable possiblyUninitialized, | |
| DeclaredVariable nullableValue, | |
| ) { | |
| assert(possiblyUninitialized.parent?.parent is VariableStatement); | |
| _possiblyUninitializedDeclarations[possiblyUninitialized] = nullableValue; | |
| addDeclaration(possiblyUninitialized); | |
| } | |
| Variable? alwaysInitializedDeclaration( | |
| Variable possiblyUninitialized, { | |
| required bool checkAncestorScopes, | |
| }) { | |
| final resultThisScope = | |
| _possiblyUninitializedDeclarations[possiblyUninitialized]; | |
| if (resultThisScope != null) { | |
| return resultThisScope; | |
| } | |
| if (!checkAncestorScopes) { | |
| return null; | |
| } | |
| return parent?.alwaysInitializedDeclaration( | |
| possiblyUninitialized, | |
| checkAncestorScopes: checkAncestorScopes, | |
| ); | |
| } | |
| Variable variableToFence( | |
| Variable 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<Variable> 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<Variable, bool>? _captures; | |
| Map<Variable, 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(Variable 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; | |
| } | |
| } |