// 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( 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 _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([..._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 _isFinalizableCache = {}; /// Whether [type] is something that subtypes `FutureOr?`. 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 _reachabilityFences(List declarations) => [ for (var declaration in declarations) ExpressionStatement( StaticInvocation( reachabilityFenceFunction, Arguments([declaration]), ), ), ]; /// Turns an [expression] into a block expression with reachability fences. /// /// ``` /// block { /// final #t1 = ; /// _in::reachabilityFence(finalizable0); /// _in::reachabilityFence(finalizable1); /// // .. /// } =>#t1 /// ``` /// /// Note that this modifies the parent of [expression]. Expression _wrapReachabilityFences( Expression expression, List declarations, ) { final resultVariable = VariableDeclaration( ":expressionValueWrappedFinalizable", initializer: expression, type: staticTypeContext!.getExpressionType(expression), isFinal: true, isSynthesized: true, ); return BlockExpression( Block([ VariableStatement(resultVariable), ..._reachabilityFences(declarations), ]), VariableGet(resultVariable), ); } Statement _appendReachabilityFences( Statement statement, List 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.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 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 _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 _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 _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 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? _captures; Map 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 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 get toFenceReturn { return [if (!scopesReturn) ...parent!.toFenceReturn, ...toFenceThisScope]; } List toFenceBreak(LabeledStatement label) { if (_labels.contains(label)) { return []; } return [...parent!.toFenceBreak(label), ...toFenceThisScope]; } List 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 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 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?`. bool isFinalizable({ required Class finalizableClass, required TypeEnvironment typeEnvironment, Map? 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; } }