| // Copyright (c) 2024, 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:js_shared/synced/async_status_codes.dart' as status_codes; |
| |
| import '../js_ast/js_ast.dart' as js_ast; |
| import 'js_names.dart'; |
| |
| /// Rewrites a [js_ast.Fun] with async/sync*/async* functions and await and |
| /// yield (with dart-like semantics) to an equivalent function without these. |
| /// await-for is not handled and must be rewritten before (currently lowered to |
| /// a normal for loop in compiler.dart). |
| /// |
| /// Look at [_rewriteFunction], [visitDartYield] and [visitAwait] for more |
| /// explanation. |
| abstract class AsyncRewriterBase extends js_ast.NodeVisitor<Object?> { |
| // Local variables are hoisted to the top of the function, so they are |
| // collected here. |
| final List<js_ast.VariableBinding> _localVariables = []; |
| |
| final Map<js_ast.Node, int> _continueLabels = {}; |
| final Map<js_ast.Node, int> _breakLabels = {}; |
| |
| /// The label of a finally part. |
| final Map<js_ast.Block, int> _finallyLabels = {}; |
| |
| /// The label of the catch handler of a [js_ast.Try] or a [js_ast.Fun] or |
| /// [js_ast.Catch]. |
| /// |
| /// These mark the points an error can be consumed. |
| /// |
| /// - The handler of a [js_ast.Fun] is the outermost and will rethrow the |
| /// error. |
| /// - The handler of a [js_ast.Try] will run the catch handler. |
| /// - The handler of a [js_ast.Catch] is a synthetic handler that ensures the |
| /// right finally blocks are run if an error is thrown inside a |
| /// catch-handler. |
| final Map<js_ast.Node, int> _handlerLabels = {}; |
| |
| /// The label index for the return clause. Only included in functions that |
| /// have one or more explicit return statements or any async* function to |
| /// handle the finally clause clean up. |
| late final int _exitLabel = _newLabel('return'); |
| |
| /// The label exit for the error case. If an async/sync*/async* function |
| /// throws this label captures the error and rethrows it to the correct |
| /// context. |
| late final int _rethrowLabel = _newLabel('rethrow'); |
| |
| /// A stack of all (surrounding) jump targets. |
| /// |
| /// Jump targets are: |
| /// |
| /// * The function, signalling a return or uncaught throw. |
| /// * Loops. |
| /// * LabeledStatements (also used for 'continue' when attached to loops). |
| /// * Try statements, for catch and finally handlers. |
| /// * Catch handlers, when inside a catch-part of a try, the catch-handler is |
| /// used to associate with a synthetic handler that will ensure the right |
| /// finally blocks are visited. |
| /// |
| /// When jumping to a target it is necessary to visit all finallies that |
| /// are on the way to target (i.e. more nested than the jump target). |
| final List<js_ast.Node> _jumpTargets = []; |
| |
| late final PreTranslationAnalysis _analysis; |
| |
| /// Contains the result of an awaited expression, or a conditional or |
| /// lazy boolean operator. |
| /// |
| /// For example a conditional expression is roughly translated like: |
| /// [[cond ? a : b]] |
| /// |
| /// Becomes: |
| /// |
| /// while true { // outer while loop |
| /// switch (goto) { // Simulates goto |
| /// ... |
| /// goto = [[cond]] ? thenLabel : elseLabel |
| /// break; |
| /// case thenLabel: |
| /// result = [[a]]; |
| /// goto = joinLabel; |
| /// break; |
| /// case elseLabel: |
| /// result = [[b]]; |
| /// case joinLabel: |
| /// // Now the result of computing the condition is in result. |
| /// .... |
| /// } |
| /// } |
| /// |
| /// It is a parameter to the [body] function, so that [awaitStatement] can |
| /// call [body] with the result of an awaited Future. |
| late final js_ast.Identifier _result = ScopedId('t\$result'); |
| |
| /// A parameter to the [bodyName] function. Indicating if we are in success |
| /// or error case. |
| late final js_ast.Identifier _errorCode = ScopedId('t\$errorCode'); |
| |
| /// The inner function that is scheduled to do each await/yield, |
| /// and called to do a new iteration for sync*. |
| final js_ast.Identifier bodyName; |
| |
| /// Used to simulate a goto. |
| /// |
| /// To "goto" a label, the label is assigned to this variable, and break out |
| /// of the switch to take another iteration in the while loop. See [_addGoto] |
| late final js_ast.Identifier _goto = ScopedId('t\$goto'); |
| |
| /// Variable containing the label of the current error handler. |
| late final js_ast.Identifier _handler = ScopedId('t\$handler'); |
| |
| /// Set to `true` if any of the switch statement labels is a handler. At the |
| /// end of rewriting this is used to see if a shorter form of error handling |
| /// can be used. The shorter form could be a change in the method boilerplate, |
| /// in the state machine wrapper, or not implemented. [addErrorExit] can test |
| /// this to elide the error exit handler when there are no other handlers, or |
| /// set it to `true` if there is no shorter form. |
| bool _hasHandlerLabels = false; |
| |
| /// A stack of labels of finally blocks to visit, and the label to go to after |
| /// the last. |
| late final js_ast.Identifier _next = ScopedId('t\$next'); |
| |
| /// The current returned value (a finally block may overwrite it). |
| late final js_ast.Identifier _returnValue = ScopedId('t\$returnValue'); |
| |
| /// Stores a stack of the current set of errors when we are in the process of |
| /// handling an error. Errors are pushed onto this stack when error handling |
| /// begins and the current error is popped off when error handling ends. This |
| /// prevents nested error handling from overwriting state. |
| late final js_ast.Identifier _errorStack = ScopedId('t\$errorStack'); |
| |
| /// The label of the outer loop. |
| /// |
| /// Used if there are untransformed loops containing break or continues to |
| /// targets outside the loop. |
| late final String _outerLabelName; |
| |
| int _currentLabel = 0; |
| |
| bool get _isAsync => false; |
| bool get _isSyncStar => false; |
| bool get _isAsyncStar => false; |
| |
| /// Visitor that collects scopes for the function passed to [rewrite]. Used |
| /// to initialize and reset scope objects where necessary. |
| late _ScopeCollector _scopeCollector; |
| |
| AsyncRewriterBase({required this.bodyName}); |
| |
| /// Main entry point. Rewrites a sync*/async/async* function to an equivalent |
| /// normal function. |
| /// |
| /// [bodyPrefix] will get prepended to the body of the rewritten function and |
| /// any references to parameters within it will be replaced with the correct |
| /// temporary ID for that parameter. |
| js_ast.Fun rewrite( |
| js_ast.Fun node, |
| Object? bodySourceInformation, |
| Object? exitSourceInformation, { |
| List<js_ast.Statement>? bodyPrefix, |
| }) { |
| _analysis = PreTranslationAnalysis(_unsupported, node)..analyze(); |
| _scopeCollector = _ScopeCollector(_analysis)..collect(node); |
| |
| _outerLabelName = _freshLabelName('outer'); |
| |
| final rewrittenFunction = _rewriteFunction( |
| node, |
| bodySourceInformation, |
| exitSourceInformation, |
| bodyPrefix: bodyPrefix, |
| ); |
| if (bodyPrefix != null) { |
| // Prepend the body prefix to the start of the rewritten function. |
| rewrittenFunction.body.statements.insertAll(0, bodyPrefix); |
| } |
| return rewrittenFunction; |
| } |
| |
| js_ast.Expression get _currentErrorHandler { |
| return js_ast.number( |
| _handlerLabels[_jumpTargets.lastWhere( |
| (node) => _handlerLabels[node] != null, |
| )]!, |
| ); |
| } |
| |
| /// Generates a label name based on [originalName] with a suffix to |
| /// guarantee it does not collide with already used names. |
| String _freshLabelName(String originalName) { |
| var result = originalName; |
| var counter = 1; |
| while (_analysis.usedLabelNames.contains(result)) { |
| result = '$counter'; |
| ++counter; |
| } |
| _analysis.usedLabelNames.add(result); |
| return result; |
| } |
| |
| /// All the pieces are collected in this map, to create a switch with a case |
| /// for each label. |
| /// |
| /// The order is important due to fall-through control flow, therefore the |
| /// type is explicitly LinkedHashMap. |
| Map<int, List<js_ast.Statement>> labelledParts = {}; |
| |
| /// Description of each label for readability of the non-minified output. |
| Map<int, String> labelComments = {}; |
| |
| /// True if the function has any try blocks containing await. |
| bool hasTryBlocks = false; |
| |
| /// True if the traversion currently is inside a loop or switch for which |
| /// [_shouldTransform] is false. |
| bool insideUntranslatedBreakable = false; |
| |
| /// True if a label is used to break to an outer switch-statement. |
| bool hasJumpThoughOuterLabel = false; |
| |
| /// True if there is a catch-handler protected by a finally with no enclosing |
| /// catch-handlers. |
| bool needsRethrow = false; |
| |
| /// Buffer for collecting translated statements belonging to the same switch |
| /// case. |
| List<js_ast.Statement> currentStatementBuffer = []; |
| |
| /// Hoisted variables get declared in the outer scope of the function body |
| /// being rewritten. Most variables get hoisted via a scope object. See |
| /// [_ScopeCollector] for more info on scope objects. Temporary ids are |
| /// already unique to a given scope so we can just hoist them directly. |
| void _hoistIfNecessary(js_ast.Expression node) { |
| if (node is ScopedId) { |
| _localVariables.add(node); |
| } |
| } |
| |
| // Labels will become cases in the big switch expression, and `goto label` |
| // is expressed by assigning to the switch key [gotoName] and breaking out of |
| // the switch. |
| |
| int _newLabel(String comment) { |
| var result = _currentLabel++; |
| labelComments[result] = comment; |
| return result; |
| } |
| |
| /// Begins outputting statements to a new buffer with label [label]. |
| /// |
| /// Each buffer ends up as its own case part in the big state-switch. |
| void _beginLabel(int label) { |
| assert(!labelledParts.containsKey(label)); |
| currentStatementBuffer = []; |
| labelledParts[label] = currentStatementBuffer; |
| _addStatement(js_ast.Comment(labelComments[label]!)); |
| } |
| |
| /// Returns a statement assigning to the variable named [gotoName]. |
| /// This should be followed by a break for the goto to be executed. Use |
| /// [_gotoAndBreak] or [_addGoto] for this. |
| js_ast.Statement _setGotoVariable(int label, Object? sourceInformation) { |
| return js_ast.ExpressionStatement( |
| js_ast |
| .js('# = #', [_goto, js_ast.number(label)]) |
| .withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| /// Returns a block that has a goto to [label] including the break. |
| /// |
| /// Also inserts a comment describing the label if available. |
| js_ast.Block _gotoAndBreak(int label, Object? sourceInformation) { |
| var statements = <js_ast.Statement>[]; |
| if (labelComments.containsKey(label)) { |
| statements.add(js_ast.Comment('goto ${labelComments[label]}')); |
| } |
| statements.add(_setGotoVariable(label, sourceInformation)); |
| if (insideUntranslatedBreakable) { |
| hasJumpThoughOuterLabel = true; |
| statements.add( |
| js_ast.Break(_outerLabelName).withSourceInformation(sourceInformation), |
| ); |
| } else { |
| statements.add( |
| js_ast.Break(null).withSourceInformation(sourceInformation), |
| ); |
| } |
| return js_ast.Block(statements); |
| } |
| |
| /// Adds a goto to [label] including the break. |
| /// |
| /// Also inserts a comment describing the label if available. |
| void _addGoto(int label, Object? sourceInformation) { |
| if (labelComments.containsKey(label)) { |
| _addStatement(js_ast.Comment('goto ${labelComments[label]}')); |
| } |
| _addStatement(_setGotoVariable(label, sourceInformation)); |
| |
| _addBreak(sourceInformation); |
| } |
| |
| void _addStatement(js_ast.Statement node) { |
| currentStatementBuffer.add(node); |
| } |
| |
| void _addExpressionStatement( |
| js_ast.Expression node, [ |
| Object? sourceInformation, |
| ]) { |
| _addStatement( |
| js_ast.ExpressionStatement(node)..sourceInformation = sourceInformation, |
| ); |
| } |
| |
| /// True if there is an await or yield in [node] or some subexpression. |
| bool _shouldTransform(js_ast.Node? node) { |
| return _analysis.hasAwaitOrYield.contains(node); |
| } |
| |
| Never _unsupported(js_ast.Node node) { |
| throw UnsupportedError( |
| 'Node $node cannot be transformed by the await-sync transformer', |
| ); |
| } |
| |
| Never _unreachable(js_ast.Node node) { |
| throw StateError('Internal error, trying to visit $node'); |
| } |
| |
| void _visitStatement(js_ast.Statement node) { |
| node.accept(this); |
| } |
| |
| /// Visits [node] to ensure its side effects are performed, but throwing away |
| /// the result. |
| /// |
| /// If the return value of visiting [node] is an expression guaranteed to have |
| /// no side effect, it is dropped. |
| void _visitExpressionIgnoreResult(js_ast.Expression node) { |
| var result = node.accept(this) as js_ast.Expression; |
| if (!(result is js_ast.Literal || result is js_ast.Identifier)) { |
| _addExpressionStatement(result); |
| } |
| } |
| |
| js_ast.Expression visitExpression(js_ast.Expression node) { |
| return node.accept(this) as js_ast.Expression; |
| } |
| |
| /// Calls [fn] with the value of evaluating [node1] and [node2]. |
| /// |
| /// Both nodes are evaluated in order. |
| /// |
| /// If node2 must be transformed (see [_shouldTransform]), then the evaluation |
| /// of node1 is added to the current statement-list and the result is stored |
| /// in a temporary variable. The evaluation of node2 is then free to emit |
| /// statements without affecting the result of node1. |
| /// |
| /// This is necessary, because await or yield expressions have to emit |
| /// statements, and these statements could affect the value of node1. |
| /// |
| /// For example: |
| /// |
| /// - _storeIfNecessary(someLiteral) returns someLiteral. |
| /// - _storeIfNecessary(someVariable) |
| /// inserts: var tempX = someVariable |
| /// returns: tempX |
| /// where tempX is a fresh temporary variable. |
| js_ast.Expression _storeIfNecessary(js_ast.Expression result) { |
| // Note that RegExes, js_ast.ArrayInitializer and js_ast.ObjectInitializer |
| // are not [js_ast.Literal]s. |
| if (result is js_ast.Literal) return result; |
| |
| var tempVar = ScopedId('t\$temp'); |
| _localVariables.add(tempVar); |
| _addStatement(js_ast.js.statement('# = #;', [tempVar, result])); |
| return tempVar; |
| } |
| |
| // TODO(sra): Many calls to this method use `store: false`, and could be |
| // replaced with calls to `visitExpression`. |
| T _withExpression<T>( |
| js_ast.Expression node, |
| T Function(js_ast.Expression result) fn, { |
| required bool store, |
| }) { |
| var visited = visitExpression(node); |
| if (store) { |
| visited = _storeIfNecessary(visited); |
| } |
| var result = fn(visited); |
| return result; |
| } |
| |
| /// Calls [fn] with the result of evaluating [node]. Taking special care of |
| /// property accesses. |
| /// |
| /// If [store] is true the result of evaluating [node] is stored in a |
| /// temporary. |
| /// |
| /// We might need to compute and store the receiver of a call expression if |
| /// the arguments include an 'await' expression. Due to expression evaluation |
| /// order we must first evaluate the receiver, then the arguments, and finally |
| /// invoke the call. With this async lowering the argument evaluation might |
| /// cause us to break out of the current function. We therefore need to store |
| /// the receiver in a temporary variable to use after we re-enter the function |
| /// body. |
| /// |
| /// We cannot simply rewrite `<receiver>.m()` to: |
| /// |
| /// temp = <receiver>.m; |
| /// temp(); |
| /// |
| /// Because this leaves `this` unbound in the call. To solve this we `bind` |
| /// the receiver to the tear-off to re-establish the `this` context. |
| /// |
| /// [isCall] determines if the node is a [js_ast.Call] or a [js_ast.New]. We |
| /// cannot `bind` to a constructor tear-off as it would no longer be a |
| /// constructor. However, constructors have no `this` context anyway so they |
| /// are safe to tear-off without binding. |
| js_ast.Expression withCallTargetExpression( |
| js_ast.Expression node, |
| js_ast.Expression Function(js_ast.Expression result) fn, { |
| required bool store, |
| required bool isCall, |
| }) { |
| var visited = visitExpression(node); |
| js_ast.Expression storedIfNeeded; |
| if (store) { |
| if (visited is js_ast.PropertyAccess) { |
| final storedReceiver = _storeIfNecessary(visited.receiver); |
| // We handle the `super` literal specially since the bound object in |
| // that case is `this`. `super` cannot be passed to `bind`. |
| final bindTarget = storedReceiver is js_ast.Super |
| ? js_ast.This() |
| : storedReceiver; |
| final jsTearOff = isCall |
| ? js_ast.Call( |
| js_ast.PropertyAccess.field( |
| js_ast.PropertyAccess(storedReceiver, visited.selector), |
| 'bind', |
| ), |
| [bindTarget], |
| ) |
| : visited; |
| storedIfNeeded = _storeIfNecessary(jsTearOff); |
| } else { |
| storedIfNeeded = _storeIfNecessary(visited); |
| } |
| } else { |
| storedIfNeeded = visited; |
| } |
| return fn(storedIfNeeded); |
| } |
| |
| /// Calls [fn] with the value of evaluating [node1] and [node2]. |
| /// |
| /// If `shouldTransform(node2)` the first expression is stored in a temporary |
| /// variable. |
| /// |
| /// This is because node1 must be evaluated before visiting node2, |
| /// because the evaluation of an await or yield cannot be expressed as |
| /// an expression, visiting node2 it will output statements that |
| /// might have an influence on the value of node1. |
| js_ast.Expression withExpression2( |
| js_ast.Expression node1, |
| js_ast.Expression node2, |
| js_ast.Expression Function( |
| js_ast.Expression result1, |
| js_ast.Expression result2, |
| ) |
| fn, |
| ) { |
| var r1 = visitExpression(node1); |
| if (_shouldTransform(node2)) { |
| r1 = _storeIfNecessary(r1); |
| } |
| var r2 = visitExpression(node2); |
| var result = fn(r1, r2); |
| return result; |
| } |
| |
| /// Calls [fn] with the value of evaluating all [nodes]. |
| /// |
| /// All results before the last node where `shouldTransform(node)` are stored |
| /// in temporary variables. |
| /// |
| /// See more explanation on [withExpression2]. |
| T withExpressions<T>( |
| List<js_ast.Expression> nodes, |
| T Function(List<js_ast.Expression> results) fn, |
| ) { |
| var visited = <js_ast.Expression>[]; |
| _collectVisited(nodes, visited); |
| final result = fn(visited); |
| return result; |
| } |
| |
| /// Like [withExpressions], but permitting `null` nodes. If any of the nodes |
| /// are null, they are ignored, and a null is passed to [fn] in that place. |
| T withNullableExpressions<T>( |
| List<js_ast.Expression?> nodes, |
| T Function(List<js_ast.Expression?> results) fn, |
| ) { |
| var visited = <js_ast.Expression?>[]; |
| _collectVisited(nodes, visited); |
| final result = fn(visited); |
| return result; |
| } |
| |
| void _collectVisited( |
| List<js_ast.Expression?> nodes, |
| List<js_ast.Expression?> visited, |
| ) { |
| // Find last occurrence of a 'transform' expression in [nodes]. |
| // All expressions before that must be stored in temp-vars. |
| var lastTransformIndex = 0; |
| for (var i = nodes.length - 1; i >= 0; --i) { |
| if (nodes[i] == null) continue; |
| if (_shouldTransform(nodes[i])) { |
| lastTransformIndex = i; |
| break; |
| } |
| } |
| for (var i = 0; i < nodes.length; i++) { |
| var node = nodes[i]; |
| if (node != null) { |
| node = visitExpression(node); |
| if (i < lastTransformIndex) { |
| node = _storeIfNecessary(node); |
| } |
| } |
| visited.add(node); |
| } |
| } |
| |
| /// Makes an empty scope object for captured variables. |
| /// |
| /// Uses `Object.create(null)` to ensure none of the JS Object prototype chain |
| /// pollutes the namespace. |
| js_ast.Expression _makeEmptyScopeObject() { |
| return js_ast.js('Object.create(null)'); |
| } |
| |
| /// Creates a new scope object for [node] if it needs one. |
| /// |
| /// Only scopes that are captured need to be reset on re-entry. Otherwise the |
| /// scope object becomes obsolete when the end of the scope is reached as |
| /// there is no way to reference it anymore. |
| /// |
| /// This should be invoked whenever a scope is collected by [_ScopeCollector] |
| /// and the scope would be re-entered by a loop in control flow. |
| void _resetScopeIfNecessary(js_ast.Node node) { |
| final nodeScope = _scopeCollector.scopeMapping[node]; |
| // Also exclude scopes with no declarations, these don't even have an |
| // associated object. |
| if (nodeScope != null && |
| nodeScope.isCaptured && |
| nodeScope.hasDeclarations) { |
| _addExpressionStatement( |
| js_ast.Assignment(nodeScope.scopeObject, _makeEmptyScopeObject()), |
| ); |
| } |
| } |
| |
| /// Emits the return block that all returns jump to (after going |
| /// through all the enclosing finally blocks). The jump to here is made in |
| /// [visitReturn]. |
| void addSuccessExit(Object? sourceInformation); |
| |
| /// Emits the block that control flows to if an error has been thrown |
| /// but not caught. (after going through all the enclosing finally blocks). |
| void addErrorExit(Object? sourceInformation); |
| |
| void addFunctionExits(Object? sourceInformation) { |
| addSuccessExit(sourceInformation); |
| addErrorExit(sourceInformation); |
| } |
| |
| /// Returns the rewritten function. |
| js_ast.Fun _finishFunction( |
| List<js_ast.Parameter> parameters, |
| js_ast.Statement rewrittenBody, |
| js_ast.VariableDeclarationList variableDeclarationLists, |
| Object? functionSourceInformation, |
| Object? bodySourceInformation, |
| ); |
| |
| Iterable<js_ast.VariableInitialization> variableInitializations( |
| Object? sourceInformation, |
| ); |
| |
| /// Rewrites an async/sync*/async* function to a normal JavaScript function. |
| /// |
| /// The control flow is flattened by simulating 'goto' using a switch in a |
| /// loop and a state variable [_goto] inside a nested function [body] |
| /// that can be called back by [asyncStarHelper]/[asyncStarHelper]/the |
| /// [Iterator]. |
| /// |
| /// Local variables are hoisted outside the helper. |
| /// |
| /// Awaits in async/async* are translated to code that remembers the current |
| /// location (so the function can resume from where it was) followed by a |
| /// [awaitStatement]. The helper sets up the waiting for the awaited |
| /// value and returns a future which is immediately returned by the |
| /// [awaitStatement]. |
| /// |
| /// Yields in sync*/async* are translated to a calls to helper functions. |
| /// (see [visitYield]) |
| /// |
| /// Simplified examples (not the exact translation, but intended to show the |
| /// ideas): |
| /// |
| /// function (x, y, z) async { |
| /// var p = await foo(); |
| /// return bar(p); |
| /// } |
| /// |
| /// Becomes (without error handling): |
| /// |
| /// function(x, y, z) { |
| /// var goto = 0, returnValue, completer = new Completer(), p; |
| /// function body(result) { |
| /// while (true) { |
| /// switch (goto) { |
| /// case 0: |
| /// goto = 1 // Remember where to continue when the future succeeds. |
| /// return thenHelper(foo(), helper, completer); |
| /// case 1: |
| /// p = result; |
| /// returnValue = bar(p); |
| /// goto = 2; |
| /// break; |
| /// case 2: |
| /// return thenHelper(returnValue, null, completer) |
| /// } |
| /// } |
| /// return thenHelper(null, helper, completer); |
| /// } |
| /// } |
| /// |
| /// Try/catch is implemented by maintaining [_handler] to contain the label |
| /// of the current handler. If [body] throws, the caller should catch the |
| /// error and recall [body] with first argument [status_codes.ERROR] and |
| /// second argument the error. |
| /// |
| /// A `finally` clause is compiled similar to normal code, with the additional |
| /// complexity that `finally` clauses need to know where to jump to after the |
| /// clause is done. In the translation, each flow-path that enters a `finally` |
| /// sets up the variable [_next] with a stack of finally-blocks and a final |
| /// jump-target (exit, catch, ...). |
| /// |
| /// function(x, y, z) async { |
| /// try { |
| /// try { |
| /// throw "error"; |
| /// } finally { |
| /// finalize1(); |
| /// } |
| /// } catch (e) { |
| /// handle(e); |
| /// } finally { |
| /// finalize2(); |
| /// } |
| /// } |
| /// |
| /// Translates into (besides the fact that structures not containing |
| /// await/yield/yield* are left intact): |
| /// |
| /// function(x, y, z) { |
| /// var goto = 0; |
| /// var returnValue; |
| /// var completer = new Completer(); |
| /// var handler = 8; // Outside try-blocks go to the rethrow label. |
| /// var p; |
| /// var currentError; |
| /// // The result can be either the result of an awaited future, or an |
| /// // error if the future completed with an error. |
| /// function body(errorCode, result) { |
| /// if (errorCode == 1) { |
| /// currentError = result; |
| /// goto = handler; |
| /// } |
| /// while (true) { |
| /// switch (goto) { |
| /// case 0: |
| /// handler = 4; // The outer catch-handler |
| /// handler = 1; // The inner (implicit) catch-handler |
| /// throw "error"; |
| /// next = [3]; |
| /// // After the finally (2) continue normally after the try. |
| /// goto = 2; |
| /// break; |
| /// case 1: // (implicit) catch handler for inner try. |
| /// next = [3]; // destination after the finally. |
| /// // fall-though to the finally handler. |
| /// case 2: // finally for inner try |
| /// handler = 4; // catch-handler for outer try. |
| /// finalize1(); |
| /// goto = next.pop(); |
| /// break; |
| /// case 3: // exiting inner try. |
| /// next = [6]; |
| /// goto = 5; // finally handler for outer try. |
| /// break; |
| /// case 4: // catch handler for outer try. |
| /// handler = 5; // If the handler throws, do the finally .. |
| /// next = [8] // ... and rethrow. |
| /// e = storedError; |
| /// handle(e); |
| /// // Fall through to finally. |
| /// case 5: // finally handler for outer try. |
| /// handler = null; |
| /// finalize2(); |
| /// goto = next.pop(); |
| /// break; |
| /// case 6: // Exiting outer try. |
| /// case 7: // return |
| /// return thenHelper(returnValue, 0, completer); |
| /// case 8: // Rethrow |
| /// return thenHelper(currentError, 1, completer); |
| /// } |
| /// } |
| /// return thenHelper(null, helper, completer); |
| /// } |
| /// } |
| /// |
| /// [bodySourceInformation] is used on code generated to execute the function |
| /// body and [exitSourceInformation] is used on code generated to exit the |
| /// function. |
| js_ast.Fun _rewriteFunction( |
| js_ast.Fun node, |
| Object? bodySourceInformation, |
| Object? exitSourceInformation, { |
| List<js_ast.Statement>? bodyPrefix, |
| }) { |
| _beginLabel(_newLabel('Function start')); |
| _handlerLabels[node] = _rethrowLabel; |
| var body = node.body; |
| _jumpTargets.add(node); |
| _visitStatement(body); |
| _jumpTargets.removeLast(); |
| addFunctionExits(exitSourceInformation); |
| |
| var clauses = <js_ast.SwitchClause>[ |
| for (final entry in labelledParts.entries) |
| js_ast.Case(js_ast.number(entry.key), js_ast.Block(entry.value)), |
| ]; |
| var rewrittenBody = js_ast.Switch( |
| _goto, |
| clauses, |
| ).withSourceInformation(bodySourceInformation); |
| if (hasJumpThoughOuterLabel) { |
| rewrittenBody = js_ast.LabeledStatement(_outerLabelName, rewrittenBody); |
| } |
| rewrittenBody = js_ast.js |
| .statement('while (true) #', rewrittenBody) |
| .withSourceInformation(bodySourceInformation); |
| var variables = <js_ast.VariableInitialization>[]; |
| |
| variables.add( |
| _makeVariableInitializer( |
| _goto, |
| js_ast.number(0).withSourceInformation(bodySourceInformation), |
| bodySourceInformation, |
| ), |
| ); |
| variables.addAll(variableInitializations(bodySourceInformation)); |
| if (_hasHandlerLabels) { |
| variables.add( |
| _makeVariableInitializer( |
| _handler, |
| js_ast.number(_rethrowLabel), |
| bodySourceInformation, |
| ), |
| ); |
| variables.add( |
| _makeVariableInitializer( |
| _errorStack, |
| js_ast.ArrayInitializer(const []), |
| bodySourceInformation, |
| ), |
| ); |
| } |
| if (_analysis.hasFinally || (_isAsyncStar && _analysis.hasYield)) { |
| variables.add( |
| _makeVariableInitializer( |
| _next, |
| js_ast.ArrayInitializer([]), |
| bodySourceInformation, |
| ), |
| ); |
| } |
| variables.addAll( |
| _localVariables.map((js_ast.VariableBinding declaration) { |
| return js_ast.VariableInitialization(declaration, null); |
| }), |
| ); |
| variables.addAll( |
| [ |
| for (final scope in _scopeCollector.scopeMapping.values) |
| if (scope.hasDeclarations) |
| js_ast.VariableInitialization( |
| scope.scopeObject, |
| _makeEmptyScopeObject(), |
| ), |
| ].reversed, |
| ); |
| var variableDeclarationLists = js_ast.VariableDeclarationList( |
| 'let', |
| variables, |
| ); |
| |
| // Names are already safe when added. |
| return _finishFunction( |
| node.params, |
| rewrittenBody, |
| variableDeclarationLists, |
| exitSourceInformation, |
| bodySourceInformation, |
| ); |
| } |
| |
| js_ast.Expression _visitFunctionExpression(js_ast.FunctionExpression node) { |
| if (node.asyncModifier.isAsync || node.asyncModifier.isYielding) { |
| // The translation does not handle nested functions that are generators |
| // or asynchronous. These functions should only be ones that are |
| // introduced by JS foreign code from our own libraries. |
| throw StateError('Nested function is a generator or asynchronous.'); |
| } |
| |
| final captureInfo = _scopeCollector.scopeCaptures[node]!; |
| // If this closure does not capture any variables from an outside scope |
| // then we can leave it as-is. |
| if (!captureInfo.hasCapture) return node; |
| |
| // Rename any references to captured variables so they are instead looked |
| // up via the captured scope object. |
| node = _ClosureRenamer(_scopeCollector, captureInfo).visit(node); |
| final scopeVariableList = <js_ast.Expression>[]; |
| final capturedScopeVariableList = <js_ast.Parameter>[]; |
| |
| captureInfo.usedScopes.forEach((scope, capturedScopeVariable) { |
| scopeVariableList.add(scope.scopeObject); |
| capturedScopeVariableList.add(capturedScopeVariable); |
| }); |
| |
| // Wrap the closure in an IIFE that captures the necessary scope objects. |
| // This ensures the closure grabs the scope before it gets reset (e.g. by |
| // a loop iteration). |
| // |
| // Code that originally looked like: |
| // var foo = 3; |
| // function(x) { |
| // console.log(foo); |
| // } |
| // |
| // Would be transformed to: |
| // var asyncScope = {}; |
| // asyncScope.foo = 3; |
| // ((capturedAsyncScope) => |
| // function (x) { |
| // console.log(capturedAsyncScope.foo); |
| // })(asyncScope); |
| return js_ast.Call( |
| js_ast.ArrowFun(capturedScopeVariableList, node), |
| scopeVariableList, |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitFun(js_ast.Fun node) { |
| return _visitFunctionExpression(node); |
| } |
| |
| @override |
| js_ast.Expression visitArrowFun(js_ast.ArrowFun node) { |
| return _visitFunctionExpression(node); |
| } |
| |
| @override |
| js_ast.Expression visitAccess(js_ast.PropertyAccess node) { |
| return withExpression2( |
| node.receiver, |
| node.selector, |
| (receiver, selector) => js_ast.PropertyAccess( |
| receiver, |
| selector, |
| ).withSourceInformation(node.sourceInformation), |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitArrayHole(js_ast.ArrayHole node) { |
| return node; |
| } |
| |
| @override |
| js_ast.Expression visitArrayInitializer(js_ast.ArrayInitializer node) { |
| return withExpressions(node.elements, (elements) { |
| return js_ast.ArrayInitializer(elements); |
| }); |
| } |
| |
| @override |
| js_ast.Expression visitAssignment(js_ast.Assignment node) { |
| if (!_shouldTransform(node)) { |
| return js_ast.Assignment.compound( |
| visitExpression(node.leftHandSide), |
| node.op, |
| visitExpression(node.value), |
| ); |
| } |
| var leftHandSide = node.leftHandSide; |
| if (leftHandSide is js_ast.Identifier) { |
| return _withExpression(node.value, (js_ast.Expression value) { |
| // A non-compound [js_ast.Assignment] has `op==null`. So it works out to |
| // use [js_ast.Assignment.compound] for all cases. |
| // Visit the [js_ast.Identifier] to ensure renaming is done correctly. |
| return js_ast.Assignment.compound( |
| visitExpression(leftHandSide), |
| node.op, |
| value, |
| ); |
| }, store: false); |
| } else if (leftHandSide is js_ast.PropertyAccess) { |
| return withExpressions( |
| [leftHandSide.receiver, leftHandSide.selector, node.value], |
| (evaluated) { |
| return js_ast.Assignment.compound( |
| js_ast.PropertyAccess(evaluated[0], evaluated[1]), |
| node.op, |
| evaluated[2], |
| ); |
| }, |
| ); |
| } else { |
| throw 'Unexpected assignment left hand side $leftHandSide'; |
| } |
| } |
| |
| js_ast.Statement awaitStatement( |
| js_ast.Expression value, |
| Object? sourceInformation, |
| ); |
| |
| /// An await is translated to an [awaitStatement]. |
| /// |
| /// See the comments of [_rewriteFunction] for an example. |
| @override |
| js_ast.Expression visitAwait(js_ast.Await node) { |
| assert(_isAsync || _isAsyncStar); |
| var afterAwait = _newLabel('returning from await.'); |
| _withExpression(node.expression, (js_ast.Expression value) { |
| _addStatement(_setGotoVariable(afterAwait, node.sourceInformation)); |
| _addStatement(awaitStatement(value, node.sourceInformation)); |
| }, store: false); |
| _beginLabel(afterAwait); |
| return _result; |
| } |
| |
| /// Checks if [node] is the variable for [_result]. |
| /// |
| /// [_result] is used to hold the result of a transformed computation |
| /// for example the result of awaiting, or the result of a conditional or |
| /// short-circuiting expression. |
| /// If the subexpression of some transformed node already is transformed and |
| /// visiting it returns [_result], it is not redundantly assigned to itself |
| /// again. |
| bool isResult(js_ast.Expression node) { |
| return node == _result; |
| } |
| |
| @override |
| js_ast.Expression visitBinary(js_ast.Binary node) { |
| if (_shouldTransform(node.right) && (node.op == '||' || node.op == '&&')) { |
| var thenLabel = _newLabel('then'); |
| var joinLabel = _newLabel('join'); |
| _withExpression(node.left, (js_ast.Expression left) { |
| var assignLeft = isResult(left) |
| ? js_ast.Block.empty() |
| : js_ast.js.statement('# = #;', [_result, left]); |
| if (node.op == '&&') { |
| _addStatement( |
| js_ast.js.statement('if (#) #; else #', [ |
| left, |
| _gotoAndBreak(thenLabel, node.sourceInformation), |
| assignLeft, |
| ]), |
| ); |
| } else { |
| assert(node.op == '||'); |
| _addStatement( |
| js_ast.js.statement('if (#) #; else #', [ |
| left, |
| assignLeft, |
| _gotoAndBreak(thenLabel, node.sourceInformation), |
| ]), |
| ); |
| } |
| }, store: true); |
| _addGoto(joinLabel, node.sourceInformation); |
| _beginLabel(thenLabel); |
| _withExpression(node.right, (js_ast.Expression value) { |
| if (!isResult(value)) { |
| _addStatement(js_ast.js.statement('# = #;', [_result, value])); |
| } |
| }, store: false); |
| _beginLabel(joinLabel); |
| return _result; |
| } |
| |
| return withExpression2( |
| node.left, |
| node.right, |
| (left, right) => js_ast.Binary(node.op, left, right), |
| ); |
| } |
| |
| @override |
| void visitBlock(js_ast.Block node) { |
| _resetScopeIfNecessary(node); |
| for (var statement in node.statements) { |
| _visitStatement(statement); |
| } |
| } |
| |
| @override |
| void visitBreak(js_ast.Break node) { |
| var target = _analysis.targets[node]!; |
| if (!_shouldTransform(target)) { |
| _addStatement(node); |
| return; |
| } |
| _translateJump(target, _breakLabels[target], node.sourceInformation); |
| } |
| |
| @override |
| js_ast.Expression visitCall(js_ast.Call node) { |
| var storeTarget = node.arguments.any(_shouldTransform); |
| return withCallTargetExpression( |
| node.target, |
| (target) { |
| return withExpressions(node.arguments, ( |
| List<js_ast.Expression> arguments, |
| ) { |
| return js_ast.Call( |
| target, |
| arguments, |
| ).withSourceInformation(node.sourceInformation); |
| }); |
| }, |
| store: storeTarget, |
| isCall: true, |
| ); |
| } |
| |
| @override |
| void visitCase(js_ast.Case node) { |
| _unreachable(node); |
| } |
| |
| @override |
| void visitCatch(js_ast.Catch node) { |
| _unreachable(node); |
| } |
| |
| @override |
| void visitComment(js_ast.Comment node) { |
| _addStatement(node); |
| } |
| |
| @override |
| js_ast.Expression visitConditional(js_ast.Conditional node) { |
| if (!_shouldTransform(node.then) && !_shouldTransform(node.otherwise)) { |
| return js_ast |
| .js('# ? # : #', [ |
| visitExpression(node.condition), |
| visitExpression(node.then), |
| visitExpression(node.otherwise), |
| ]) |
| .withSourceInformation(node.sourceInformation); |
| } |
| var thenLabel = _newLabel('then'); |
| var joinLabel = _newLabel('join'); |
| var elseLabel = _newLabel('else'); |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| _addStatement( |
| js_ast.js.statement('# = # ? # : #;', [ |
| _goto, |
| condition, |
| js_ast.number(thenLabel), |
| js_ast.number(elseLabel), |
| ]), |
| ); |
| }, store: false); |
| _addBreak(node.sourceInformation); |
| _beginLabel(thenLabel); |
| _withExpression(node.then, (js_ast.Expression value) { |
| if (!isResult(value)) { |
| _addStatement(js_ast.js.statement('# = #;', [_result, value])); |
| } |
| }, store: false); |
| _addGoto(joinLabel, node.sourceInformation); |
| _beginLabel(elseLabel); |
| _withExpression(node.otherwise, (js_ast.Expression value) { |
| if (!isResult(value)) { |
| _addStatement(js_ast.js.statement('# = #;', [_result, value])); |
| } |
| }, store: false); |
| _beginLabel(joinLabel); |
| return _result; |
| } |
| |
| @override |
| void visitContinue(js_ast.Continue node) { |
| var target = _analysis.targets[node]; |
| if (!_shouldTransform(target)) { |
| _addStatement(node); |
| return; |
| } |
| _translateJump(target, _continueLabels[target!], node.sourceInformation); |
| } |
| |
| /// Emits a break statement that exits the big switch statement. |
| void _addBreak(Object? sourceInformation) { |
| if (insideUntranslatedBreakable) { |
| hasJumpThoughOuterLabel = true; |
| _addStatement( |
| js_ast.Break(_outerLabelName).withSourceInformation(sourceInformation), |
| ); |
| } else { |
| _addStatement( |
| js_ast.Break(null).withSourceInformation(sourceInformation), |
| ); |
| } |
| } |
| |
| /// Common code for handling break, continue, return. |
| /// |
| /// It is necessary to run all nesting finally-handlers between the jump and |
| /// the target. For that [_next] is used as a stack of places to go. |
| /// |
| /// See also [_rewriteFunction]. |
| void _translateJump( |
| js_ast.Node? target, |
| int? targetLabel, |
| Object? sourceInformation, |
| ) { |
| // Compute a stack of all the 'finally' nodes that must be visited before |
| // the jump. |
| // The bottom of the stack is the label where the jump goes to. |
| var jumpStack = <int>[]; |
| for (var node in _jumpTargets.reversed) { |
| if (_finallyLabels[node] != null) { |
| jumpStack.add(_finallyLabels[node]!); |
| } else if (node == target) { |
| jumpStack.add(targetLabel!); |
| break; |
| } |
| // Ignore other nodes. |
| } |
| jumpStack = jumpStack.reversed.toList(); |
| // As the program jumps directly to the top of the stack, it is taken off |
| // now. |
| var firstTarget = jumpStack.removeLast(); |
| if (jumpStack.isNotEmpty) { |
| var jsJumpStack = js_ast.ArrayInitializer( |
| jumpStack.map((int label) => js_ast.number(label)).toList(), |
| ); |
| _addStatement( |
| js_ast.ExpressionStatement( |
| js_ast |
| .js('# = #', [_next, jsJumpStack]) |
| .withSourceInformation(sourceInformation), |
| ), |
| ); |
| } |
| _addGoto(firstTarget, sourceInformation); |
| } |
| |
| @override |
| void visitDefault(js_ast.Default node) => _unreachable(node); |
| |
| @override |
| void visitDo(js_ast.Do node) { |
| if (!_shouldTransform(node)) { |
| var oldInsideUntranslatedBreakable = insideUntranslatedBreakable; |
| insideUntranslatedBreakable = true; |
| _addStatement( |
| js_ast.js.statement('do {#} while (#)', [ |
| _translateToStatement(node.body), |
| visitExpression(node.condition), |
| ]), |
| ); |
| insideUntranslatedBreakable = oldInsideUntranslatedBreakable; |
| return; |
| } |
| var startLabel = _newLabel('do body'); |
| |
| var continueLabel = _newLabel('do condition'); |
| _continueLabels[node] = continueLabel; |
| |
| var afterLabel = _newLabel('after do'); |
| _breakLabels[node] = afterLabel; |
| |
| _beginLabel(startLabel); |
| |
| _jumpTargets.add(node); |
| _visitStatement(node.body); |
| _jumpTargets.removeLast(); |
| |
| _beginLabel(continueLabel); |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| _addStatement( |
| js_ast.js.statement('if (#) #', [ |
| condition, |
| _gotoAndBreak(startLabel, node.sourceInformation), |
| ]), |
| ); |
| }, store: false); |
| _beginLabel(afterLabel); |
| } |
| |
| @override |
| void visitEmptyStatement(js_ast.EmptyStatement node) { |
| _addStatement(node); |
| } |
| |
| @override |
| void visitExpressionStatement(js_ast.ExpressionStatement node) { |
| _visitExpressionIgnoreResult(node.expression); |
| } |
| |
| @override |
| void visitFor(js_ast.For node) { |
| if (!_shouldTransform(node)) { |
| var oldInsideUntranslated = insideUntranslatedBreakable; |
| insideUntranslatedBreakable = true; |
| |
| // Handle init specially as it might be a VariableDeclarationList. |
| // These declarations should not be hoisted in an untransformed for loop. |
| final init = node.init; |
| js_ast.Expression? newInit; |
| if (init is js_ast.VariableDeclarationList) { |
| final newInitializationList = <js_ast.VariableInitialization>[]; |
| for (final initialization in init.declarations) { |
| final value = initialization.value; |
| newInitializationList.add( |
| js_ast.VariableInitialization( |
| initialization.declaration, |
| value != null ? visitExpression(value) : null, |
| ), |
| ); |
| } |
| newInit = js_ast.VariableDeclarationList( |
| init.keyword, |
| newInitializationList, |
| ); |
| } else { |
| newInit = init != null ? visitExpression(init) : null; |
| } |
| withNullableExpressions([node.condition, node.update], ( |
| List<js_ast.Expression?> transformed, |
| ) { |
| _addStatement( |
| js_ast.For( |
| newInit, |
| transformed[0], |
| transformed[1], |
| _translateToStatement(node.body), |
| ), |
| ); |
| }); |
| insideUntranslatedBreakable = oldInsideUntranslated; |
| return; |
| } |
| |
| _resetScopeIfNecessary(node); |
| if (node.init != null) { |
| _visitExpressionIgnoreResult(node.init!); |
| } |
| var startLabel = _newLabel('for condition'); |
| // If there is no update, continuing the loop is the same as going to the |
| // start. |
| var continueLabel = (node.update == null) |
| ? startLabel |
| : _newLabel('for update'); |
| _continueLabels[node] = continueLabel; |
| var afterLabel = _newLabel('after for'); |
| _breakLabels[node] = afterLabel; |
| _beginLabel(startLabel); |
| var condition = node.condition; |
| if (condition == null || |
| (condition is js_ast.LiteralBool && condition.value == true)) { |
| _addStatement(js_ast.Comment('trivial condition')); |
| } else { |
| _withExpression(condition, (js_ast.Expression condition) { |
| _addStatement( |
| js_ast.If.noElse( |
| js_ast.Prefix('!', condition), |
| _gotoAndBreak(afterLabel, node.sourceInformation), |
| ), |
| ); |
| }, store: false); |
| } |
| _jumpTargets.add(node); |
| _visitStatement(node.body); |
| _jumpTargets.removeLast(); |
| if (node.update != null) { |
| _beginLabel(continueLabel); |
| _visitExpressionIgnoreResult(node.update!); |
| } |
| _addGoto(startLabel, node.sourceInformation); |
| _beginLabel(afterLabel); |
| } |
| |
| @override |
| void visitForIn(js_ast.ForIn node) { |
| // The dart output currently never uses for-in loops. |
| throw 'JavaScript for-in not implemented yet in the await transformation'; |
| } |
| |
| @override |
| void visitFunctionDeclaration(js_ast.FunctionDeclaration node) { |
| _withExpression(node.function, (js_ast.Expression function) { |
| final name = visitExpression(node.name); |
| _hoistIfNecessary(name); |
| _addExpressionStatement( |
| js_ast.Assignment(visitExpression(name), function), |
| ); |
| }, store: false); |
| } |
| |
| List<js_ast.Statement> _translateToStatementSequence(js_ast.Statement node) { |
| assert(!_shouldTransform(node)); |
| var oldBuffer = currentStatementBuffer; |
| currentStatementBuffer = []; |
| var resultBuffer = currentStatementBuffer; |
| _visitStatement(node); |
| currentStatementBuffer = oldBuffer; |
| return resultBuffer; |
| } |
| |
| js_ast.Statement _translateToStatement(js_ast.Statement node) { |
| var statements = _translateToStatementSequence(node); |
| if (statements.length == 1) return statements.single; |
| return js_ast.Block(statements); |
| } |
| |
| js_ast.Block translateToBlock(js_ast.Statement node) { |
| return js_ast.Block(_translateToStatementSequence(node)); |
| } |
| |
| @override |
| void visitIf(js_ast.If node) { |
| if (!_shouldTransform(node.then) && !_shouldTransform(node.otherwise)) { |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| var translatedThen = _translateToStatement(node.then); |
| var translatedElse = _translateToStatement(node.otherwise); |
| _addStatement(js_ast.If(condition, translatedThen, translatedElse)); |
| }, store: false); |
| return; |
| } |
| var thenLabel = _newLabel('then'); |
| var joinLabel = _newLabel('join'); |
| var elseLabel = (node.otherwise is js_ast.EmptyStatement) |
| ? joinLabel |
| : _newLabel('else'); |
| |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| _addExpressionStatement( |
| js_ast.Assignment( |
| _goto, |
| js_ast.Conditional( |
| condition, |
| js_ast.number(thenLabel), |
| js_ast.number(elseLabel), |
| ), |
| ), |
| ); |
| }, store: false); |
| _addBreak(node.sourceInformation); |
| _beginLabel(thenLabel); |
| _visitStatement(node.then); |
| if (node.otherwise is! js_ast.EmptyStatement) { |
| _addGoto(joinLabel, node.sourceInformation); |
| _beginLabel(elseLabel); |
| _visitStatement(node.otherwise); |
| } |
| _beginLabel(joinLabel); |
| } |
| |
| @override |
| Never visitInterpolatedExpression(js_ast.InterpolatedExpression node) { |
| _unsupported(node); |
| } |
| |
| @override |
| Never visitInterpolatedLiteral(js_ast.InterpolatedLiteral node) { |
| _unsupported(node); |
| } |
| |
| @override |
| Never visitInterpolatedParameter(js_ast.InterpolatedParameter node) { |
| _unsupported(node); |
| } |
| |
| @override |
| Never visitInterpolatedSelector(js_ast.InterpolatedSelector node) { |
| _unsupported(node); |
| } |
| |
| @override |
| Never visitInterpolatedStatement(js_ast.InterpolatedStatement node) { |
| _unsupported(node); |
| } |
| |
| @override |
| void visitLabeledStatement(js_ast.LabeledStatement node) { |
| if (!_shouldTransform(node)) { |
| _addStatement( |
| js_ast.LabeledStatement(node.label, _translateToStatement(node.body)), |
| ); |
| return; |
| } |
| // `continue label` is really continuing the nested loop. |
| // This is set up in [PreTranslationAnalysis.visitContinue]. |
| // Here we only need a breakLabel: |
| var breakLabel = _newLabel('break ${node.label}'); |
| _breakLabels[node] = breakLabel; |
| |
| _jumpTargets.add(node); |
| _visitStatement(node.body); |
| _jumpTargets.removeLast(); |
| _beginLabel(breakLabel); |
| } |
| |
| @override |
| js_ast.Expression visitLiteralBool(js_ast.LiteralBool node) => node; |
| |
| @override |
| Never visitLiteralExpression(js_ast.LiteralExpression node) => |
| _unsupported(node); |
| |
| @override |
| js_ast.Expression visitLiteralNull(js_ast.LiteralNull node) => node; |
| |
| @override |
| js_ast.Expression visitLiteralNumber(js_ast.LiteralNumber node) => node; |
| |
| @override |
| Never visitLiteralStatement(js_ast.LiteralStatement node) => |
| _unsupported(node); |
| |
| @override |
| js_ast.Expression visitLiteralString(js_ast.LiteralString node) => node; |
| |
| @override |
| Never visitNamedFunction(js_ast.NamedFunction node) { |
| _unsupported(node); |
| } |
| |
| @override |
| js_ast.Expression visitNew(js_ast.New node) { |
| var storeTarget = node.arguments.any(_shouldTransform); |
| return withCallTargetExpression( |
| node.target, |
| (target) { |
| return withExpressions(node.arguments, ( |
| List<js_ast.Expression> arguments, |
| ) { |
| return js_ast.New(target, arguments); |
| }); |
| }, |
| store: storeTarget, |
| isCall: false, |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitObjectInitializer(js_ast.ObjectInitializer node) { |
| return withExpressions( |
| node.properties |
| .map((js_ast.Property property) => property.value) |
| .toList(), |
| (List<js_ast.Expression> values) { |
| var properties = List<js_ast.Property>.generate(values.length, (int i) { |
| if (node.properties[i] is js_ast.Method) { |
| return js_ast.Method( |
| node.properties[i].name, |
| values[i] as js_ast.Fun, |
| ); |
| } |
| return js_ast.Property(node.properties[i].name, values[i]); |
| }); |
| return js_ast.ObjectInitializer(properties); |
| }, |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitPostfix(js_ast.Postfix node) { |
| if (node.op == '++' || node.op == '--') { |
| var argument = node.argument; |
| if (argument is js_ast.Identifier) { |
| return js_ast.Postfix(node.op, visitExpression(argument)); |
| } else if (argument is js_ast.PropertyAccess) { |
| return withExpression2(argument.receiver, argument.selector, ( |
| receiver, |
| selector, |
| ) { |
| return js_ast.Postfix( |
| node.op, |
| js_ast.PropertyAccess(receiver, selector), |
| ); |
| }); |
| } else { |
| throw 'Unexpected postfix ${node.op} ' |
| 'operator argument ${node.argument}'; |
| } |
| } |
| return _withExpression( |
| node.argument, |
| (js_ast.Expression argument) => js_ast.Postfix(node.op, argument), |
| store: false, |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitPrefix(js_ast.Prefix node) { |
| if (node.op == '++' || node.op == '--') { |
| var argument = node.argument; |
| if (argument is js_ast.Identifier) { |
| return js_ast.Prefix(node.op, visitExpression(argument)); |
| } else if (argument is js_ast.PropertyAccess) { |
| return withExpression2(argument.receiver, argument.selector, ( |
| receiver, |
| selector, |
| ) { |
| return js_ast.Prefix( |
| node.op, |
| js_ast.PropertyAccess(receiver, selector), |
| ); |
| }); |
| } else { |
| throw 'Unexpected prefix ${node.op} operator ' |
| 'argument ${node.argument}'; |
| } |
| } |
| return _withExpression( |
| node.argument, |
| (js_ast.Expression argument) => js_ast.Prefix(node.op, argument), |
| store: false, |
| ); |
| } |
| |
| @override |
| Never visitProgram(js_ast.Program node) => _unsupported(node); |
| |
| @override |
| js_ast.Property visitProperty(js_ast.Property node) { |
| assert(node.runtimeType == js_ast.Property); |
| return _withExpression( |
| node.value, |
| (js_ast.Expression value) => js_ast.Property(node.name, value), |
| store: false, |
| ); |
| } |
| |
| @override |
| js_ast.Method visitMethod(js_ast.Method node) { |
| return _withExpression( |
| node.function, |
| (js_ast.Expression value) => |
| js_ast.Method(node.name, value as js_ast.Fun), |
| store: false, |
| ); |
| } |
| |
| @override |
| js_ast.Expression visitRegExpLiteral(js_ast.RegExpLiteral node) => node; |
| |
| @override |
| void visitReturn(js_ast.Return node) { |
| var target = _analysis.targets[node]; |
| final expression = node.value; |
| if (expression != null) { |
| if (_isSyncStar || _isAsyncStar) { |
| // Even though `return expr;` is not allowed in the dart sync* and |
| // async* code, the backend sometimes generates code like this, but |
| // only when it is known that the 'expr' throws, and the return is just |
| // to tell the JavaScript VM that the code won't continue here. |
| // It is therefore interpreted as `expr; return;` |
| _visitExpressionIgnoreResult(expression); |
| } else { |
| _withExpression(expression, (js_ast.Expression value) { |
| _addStatement( |
| js_ast.js |
| .statement('# = #;', [_returnValue, value]) |
| .withSourceInformation(node.sourceInformation), |
| ); |
| }, store: false); |
| } |
| } |
| _translateJump(target, _exitLabel, node.sourceInformation); |
| } |
| |
| @override |
| void visitSwitch(js_ast.Switch node) { |
| if (!_shouldTransform(node)) { |
| // TODO(sra): If only the key has an await, translation can be simplified. |
| var oldInsideUntranslated = insideUntranslatedBreakable; |
| insideUntranslatedBreakable = true; |
| _withExpression(node.key, (js_ast.Expression key) { |
| var cases = node.cases.map((js_ast.SwitchClause clause) { |
| if (clause is js_ast.Case) { |
| return js_ast.Case( |
| clause.expression, |
| translateToBlock(clause.body), |
| ); |
| } else { |
| return js_ast.Default( |
| translateToBlock((clause as js_ast.Default).body), |
| ); |
| } |
| }).toList(); |
| _addStatement(js_ast.Switch(key, cases)); |
| }, store: false); |
| insideUntranslatedBreakable = oldInsideUntranslated; |
| return; |
| } |
| var before = _newLabel('switch'); |
| var after = _newLabel('after switch'); |
| _breakLabels[node] = after; |
| |
| _beginLabel(before); |
| var labels = List<int>.filled(node.cases.length, -1); |
| |
| var anyCaseExpressionTransformed = node.cases.any( |
| (js_ast.SwitchClause x) => |
| x is js_ast.Case && _shouldTransform(x.expression), |
| ); |
| if (anyCaseExpressionTransformed) { |
| int? defaultIndex; // Null means no default was found. |
| // If there is an await in one of the keys, a chain of ifs has to be used. |
| |
| _withExpression(node.key, (js_ast.Expression key) { |
| var i = 0; |
| for (var clause in node.cases) { |
| if (clause is js_ast.Default) { |
| // The goto for the default case is added after all non-default |
| // clauses have been handled. |
| defaultIndex = i; |
| labels[i] = _newLabel('default'); |
| continue; |
| } else if (clause is js_ast.Case) { |
| labels[i] = _newLabel('case'); |
| _withExpression(clause.expression, (expression) { |
| _addStatement( |
| js_ast.If.noElse( |
| js_ast.Binary('===', key, expression), |
| _gotoAndBreak(labels[i], clause.sourceInformation), |
| ), |
| ); |
| }, store: false); |
| } |
| i++; |
| } |
| }, store: true); |
| |
| if (defaultIndex == null) { |
| _addGoto(after, node.sourceInformation); |
| } else { |
| _addGoto(labels[defaultIndex!], node.sourceInformation); |
| } |
| } else { |
| var hasDefault = false; |
| var i = 0; |
| var clauses = <js_ast.SwitchClause>[]; |
| for (var clause in node.cases) { |
| if (clause is js_ast.Case) { |
| labels[i] = _newLabel('case'); |
| clauses.add( |
| js_ast.Case( |
| visitExpression(clause.expression), |
| _gotoAndBreak(labels[i], clause.sourceInformation), |
| ), |
| ); |
| } else if (clause is js_ast.Default) { |
| labels[i] = _newLabel('default'); |
| clauses.add( |
| js_ast.Default(_gotoAndBreak(labels[i], clause.sourceInformation)), |
| ); |
| hasDefault = true; |
| } else { |
| throw StateError('Unknown clause type $clause'); |
| } |
| i++; |
| } |
| if (!hasDefault) { |
| clauses.add( |
| js_ast.Default(_gotoAndBreak(after, node.sourceInformation)), |
| ); |
| } |
| _withExpression(node.key, (js_ast.Expression key) { |
| _addStatement(js_ast.Switch(key, clauses)); |
| }, store: false); |
| |
| _addBreak(node.sourceInformation); |
| } |
| |
| _jumpTargets.add(node); |
| for (var i = 0; i < labels.length; i++) { |
| _beginLabel(labels[i]); |
| _visitStatement(node.cases[i].body); |
| } |
| _beginLabel(after); |
| _jumpTargets.removeLast(); |
| } |
| |
| @override |
| js_ast.Expression visitThis(js_ast.This node) => node; |
| |
| @override |
| void visitThrow(js_ast.Throw node) { |
| _withExpression(node.expression, (js_ast.Expression expression) { |
| _addStatement( |
| js_ast.Throw(expression).withSourceInformation(node.sourceInformation), |
| ); |
| }, store: false); |
| } |
| |
| void _setErrorHandler([int? errorHandler]) { |
| _hasHandlerLabels = true; // TODO(sra): Add short form error handler. |
| var label = (errorHandler == null) |
| ? _currentErrorHandler |
| : js_ast.number(errorHandler); |
| _addStatement(js_ast.js.statement('# = #;', [_handler, label])); |
| } |
| |
| List<int> _finalliesUpToAndEnclosingHandler() { |
| var result = <int>[]; |
| for (var i = _jumpTargets.length - 1; i >= 0; i--) { |
| var node = _jumpTargets[i]; |
| var handlerLabel = _handlerLabels[node]; |
| if (handlerLabel != null) { |
| result.add(handlerLabel); |
| break; |
| } |
| var finallyLabel = _finallyLabels[node]; |
| if (finallyLabel != null) { |
| result.add(finallyLabel); |
| } |
| } |
| return result.reversed.toList(); |
| } |
| |
| /// See the comments of [_rewriteFunction] for more explanation. |
| @override |
| void visitTry(js_ast.Try node) { |
| final catchPart = node.catchPart; |
| final finallyPart = node.finallyPart; |
| |
| if (!_shouldTransform(node)) { |
| var body = translateToBlock(node.body); |
| js_ast.Catch? translatedCatchPart; |
| if (catchPart != null) { |
| translatedCatchPart = js_ast.Catch( |
| catchPart.declaration, |
| translateToBlock(catchPart.body), |
| ); |
| } |
| var translatedFinallyPart = (finallyPart == null) |
| ? null |
| : translateToBlock(finallyPart); |
| _addStatement( |
| js_ast.Try(body, translatedCatchPart, translatedFinallyPart), |
| ); |
| return; |
| } |
| |
| hasTryBlocks = true; |
| var uncaughtLabel = _newLabel('uncaught'); |
| var handlerLabel = (catchPart == null) ? uncaughtLabel : _newLabel('catch'); |
| |
| var finallyLabel = _newLabel('finally'); |
| var afterFinallyLabel = _newLabel('after finally'); |
| if (finallyPart != null) { |
| _finallyLabels[finallyPart] = finallyLabel; |
| _jumpTargets.add(finallyPart); |
| } |
| |
| _handlerLabels[node] = handlerLabel; |
| _jumpTargets.add(node); |
| |
| // Set the error handler here. It must be cleared on every path out; |
| // normal and error exit. |
| _setErrorHandler(); |
| |
| _visitStatement(node.body); |
| |
| var last = _jumpTargets.removeLast(); |
| assert(last == node); |
| |
| if (finallyPart == null) { |
| _setErrorHandler(); |
| _addGoto(afterFinallyLabel, node.sourceInformation); |
| } else { |
| // The handler is reset as the first thing in the finally block. |
| _addStatement( |
| js_ast.js.statement('#.push(#);', [ |
| _next, |
| js_ast.number(afterFinallyLabel), |
| ]), |
| ); |
| _addGoto(finallyLabel, node.sourceInformation); |
| } |
| |
| if (catchPart != null) { |
| _beginLabel(handlerLabel); |
| // [uncaughtLabel] is the handler for the code in the catch-part. |
| // It ensures that [nextName] is set up to run the right finally blocks. |
| _handlerLabels[catchPart] = uncaughtLabel; |
| _jumpTargets.add(catchPart); |
| _setErrorHandler(); |
| // The catch declaration name can shadow outer variables, so a fresh name |
| // is needed to avoid collisions. See Ecma 262, 3rd edition, |
| // section 12.14. |
| var errorName = visitExpression(catchPart.declaration); |
| _hoistIfNecessary(errorName); |
| _addStatement( |
| js_ast.js.statement('# = #.pop();', [errorName, _errorStack]), |
| ); |
| _visitStatement(catchPart.body); |
| if (finallyPart != null) { |
| // The error has been caught, so after the finally, continue after the |
| // try. |
| _addStatement( |
| js_ast.js.statement('#.push(#);', [ |
| _next, |
| js_ast.number(afterFinallyLabel), |
| ]), |
| ); |
| _addGoto(finallyLabel, node.sourceInformation); |
| } else { |
| _addGoto(afterFinallyLabel, node.sourceInformation); |
| } |
| var last = _jumpTargets.removeLast(); |
| assert(last == catchPart); |
| } |
| |
| // The "uncaught"-handler tells the finally-block to continue with |
| // the enclosing finally-blocks until the current catch-handler. |
| _beginLabel(uncaughtLabel); |
| |
| var enclosingFinallies = _finalliesUpToAndEnclosingHandler(); |
| |
| var nextLabel = enclosingFinallies.removeLast(); |
| if (enclosingFinallies.isNotEmpty) { |
| // [enclosingFinallies] can be empty if there is no surrounding finally |
| // blocks. Then [nextLabel] will be [rethrowLabel]. |
| _addStatement( |
| js_ast.js.statement('# = #;', [ |
| _next, |
| js_ast.ArrayInitializer( |
| enclosingFinallies.map(js_ast.number).toList(), |
| ), |
| ]), |
| ); |
| } |
| if (finallyPart == null) { |
| // The finally-block belonging to [node] will be visited because of |
| // fallthrough. If it does not exist, add an explicit goto. |
| _addGoto(nextLabel, node.sourceInformation); |
| } |
| if (finallyPart != null) { |
| var last = _jumpTargets.removeLast(); |
| assert(last == finallyPart); |
| |
| _beginLabel(finallyLabel); |
| _setErrorHandler(); |
| _visitStatement(finallyPart); |
| _addStatement(js_ast.Comment('// goto the next finally handler')); |
| _addStatement(js_ast.js.statement('# = #.pop();', [_goto, _next])); |
| _addBreak(node.sourceInformation); |
| } |
| _beginLabel(afterFinallyLabel); |
| } |
| |
| @override |
| js_ast.Expression visitVariableDeclarationList( |
| js_ast.VariableDeclarationList node, |
| ) { |
| for (final initialization in node.declarations) { |
| var declaration = visitExpression(initialization.declaration); |
| _hoistIfNecessary(declaration); |
| if (initialization.value != null) { |
| _withExpression(initialization.value!, (js_ast.Expression value) { |
| _addExpressionStatement( |
| js_ast.Assignment(declaration, value) |
| ..sourceInformation = initialization.sourceInformation, |
| node.sourceInformation, |
| ); |
| }, store: false); |
| } |
| } |
| return js_ast.number(0); // Dummy expression. |
| } |
| |
| @override |
| void visitVariableInitialization(js_ast.VariableInitialization node) { |
| _unreachable(node); |
| } |
| |
| @override |
| js_ast.Expression visitIdentifier(js_ast.Identifier node) { |
| return _scopeCollector.transformIdentifier(node); |
| } |
| |
| @override |
| void visitWhile(js_ast.While node) { |
| if (!_shouldTransform(node)) { |
| var oldInsideUntranslated = insideUntranslatedBreakable; |
| insideUntranslatedBreakable = true; |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| _addStatement( |
| js_ast.While( |
| condition, |
| _translateToStatement(node.body), |
| ).withSourceInformation(node.sourceInformation), |
| ); |
| }, store: false); |
| insideUntranslatedBreakable = oldInsideUntranslated; |
| return; |
| } |
| var continueLabel = _newLabel('while condition'); |
| _continueLabels[node] = continueLabel; |
| _beginLabel(continueLabel); |
| |
| var afterLabel = _newLabel('after while'); |
| _breakLabels[node] = afterLabel; |
| var condition = node.condition; |
| // If the condition is `true`, a test is not needed. |
| if (!(condition is js_ast.LiteralBool && condition.value == true)) { |
| _withExpression(node.condition, (js_ast.Expression condition) { |
| _addStatement( |
| js_ast.If.noElse( |
| js_ast.Prefix('!', condition), |
| _gotoAndBreak(afterLabel, node.sourceInformation), |
| ).withSourceInformation(node.sourceInformation), |
| ); |
| }, store: false); |
| } |
| _jumpTargets.add(node); |
| _visitStatement(node.body); |
| _jumpTargets.removeLast(); |
| _addGoto(continueLabel, node.sourceInformation); |
| _beginLabel(afterLabel); |
| } |
| |
| void addYield( |
| js_ast.DartYield node, |
| js_ast.Expression expression, |
| Object? sourceInformation, |
| ); |
| |
| @override |
| void visitDartYield(js_ast.DartYield node) { |
| assert(_isSyncStar || _isAsyncStar); |
| var label = _newLabel('after yield'); |
| // Don't do a break here for the goto, but instead a return in either |
| // addSynYield or addAsyncYield. |
| _withExpression(node.expression, (js_ast.Expression expression) { |
| _addStatement(_setGotoVariable(label, node.sourceInformation)); |
| addYield(node, expression, node.sourceInformation); |
| }, store: false); |
| _beginLabel(label); |
| } |
| |
| @override |
| void visitForOf(js_ast.ForOf node) { |
| if (!_shouldTransform(node)) { |
| var oldInsideUntranslated = insideUntranslatedBreakable; |
| insideUntranslatedBreakable = true; |
| _addStatement( |
| js_ast.ForOf( |
| node.leftHandSide, |
| visitExpression(node.iterable), |
| _translateToStatement(node.body), |
| ), |
| ); |
| insideUntranslatedBreakable = oldInsideUntranslated; |
| return; |
| } |
| |
| _visitExpressionIgnoreResult(node.leftHandSide); |
| final loopVar = visitExpression( |
| (node.leftHandSide as js_ast.VariableDeclarationList) |
| .declarations |
| .first |
| .declaration, |
| ); |
| |
| final valueWrapperVar = ScopedId('t\$wrappedValue'); |
| final iteratorVar = ScopedId('t\$iterator'); |
| _localVariables.add(valueWrapperVar); |
| _localVariables.add(iteratorVar); |
| |
| // Get the iterator object for the iterable expression. |
| _withExpression(node.iterable, (js_ast.Expression iterable) { |
| _addExpressionStatement( |
| js_ast.Assignment( |
| iteratorVar, |
| js_ast.js('#[Symbol.iterator]()', [iterable]) |
| ..sourceInformation = node.iterable.sourceInformation, |
| ), |
| ); |
| }, store: false); |
| |
| var continueLabel = _newLabel('for-of iterator update'); |
| _continueLabels[node] = continueLabel; |
| |
| var afterLabel = _newLabel('after for-of'); |
| _breakLabels[node] = afterLabel; |
| |
| // At the start of each loop step: |
| // 1) Move the iterator forward. |
| // 2) Check if the current value is marked as done. |
| // 3a) If no: assign the value to the loop variable and execute the body. |
| // 3b) If yes: jump to after the loop body. |
| _beginLabel(continueLabel); |
| _resetScopeIfNecessary(node); |
| _addExpressionStatement( |
| js_ast.Assignment(valueWrapperVar, js_ast.js('#.next()', [iteratorVar])), |
| ); |
| _addStatement( |
| js_ast.If.noElse( |
| js_ast.js('#.done', [valueWrapperVar]), |
| _gotoAndBreak(afterLabel, node.sourceInformation), |
| ), |
| ); |
| _addExpressionStatement( |
| js_ast.Assignment(loopVar, js_ast.js('#.value', [valueWrapperVar])), |
| ); |
| _jumpTargets.add(node); |
| _visitStatement(node.body); |
| _jumpTargets.removeLast(); |
| |
| _addGoto(continueLabel, node.sourceInformation); |
| _beginLabel(afterLabel); |
| } |
| |
| @override |
| js_ast.ArrayBindingPattern visitArrayBindingPattern( |
| js_ast.ArrayBindingPattern node, |
| ) => node; |
| |
| @override |
| Never visitClassDeclaration(js_ast.ClassDeclaration node) => |
| _unreachable(node); |
| |
| @override |
| Never visitClassExpression(js_ast.ClassExpression node) => _unreachable(node); |
| |
| @override |
| js_ast.CommentExpression visitCommentExpression( |
| js_ast.CommentExpression node, |
| ) => node; |
| |
| @override |
| js_ast.DebuggerStatement visitDebuggerStatement( |
| js_ast.DebuggerStatement node, |
| ) => node; |
| |
| @override |
| js_ast.DestructuredVariable visitDestructuredVariable( |
| js_ast.DestructuredVariable node, |
| ) => node; |
| |
| @override |
| Never visitExportClause(js_ast.ExportClause node) => _unreachable(node); |
| |
| @override |
| Never visitExportDeclaration(js_ast.ExportDeclaration node) => |
| _unreachable(node); |
| |
| @override |
| Never visitImportDeclaration(js_ast.ImportDeclaration node) => |
| _unreachable(node); |
| |
| @override |
| js_ast.InterpolatedIdentifier visitInterpolatedIdentifier( |
| js_ast.InterpolatedIdentifier node, |
| ) => node; |
| |
| @override |
| js_ast.InterpolatedMethod visitInterpolatedMethod( |
| js_ast.InterpolatedMethod node, |
| ) => node; |
| |
| @override |
| Never visitNameSpecifier(js_ast.NameSpecifier node) => _unreachable(node); |
| |
| @override |
| js_ast.ObjectBindingPattern visitObjectBindingPattern( |
| js_ast.ObjectBindingPattern node, |
| ) => node; |
| |
| @override |
| js_ast.RestParameter visitRestParameter(js_ast.RestParameter node) => node; |
| |
| @override |
| js_ast.SimpleBindingPattern visitSimpleBindingPattern( |
| js_ast.SimpleBindingPattern node, |
| ) => node; |
| |
| @override |
| js_ast.Spread visitSpread(js_ast.Spread node) => node; |
| |
| @override |
| js_ast.Super visitSuper(js_ast.Super node) => node; |
| |
| @override |
| js_ast.TaggedTemplate visitTaggedTemplate(js_ast.TaggedTemplate node) => node; |
| |
| @override |
| js_ast.TemplateString visitTemplateString(js_ast.TemplateString node) => node; |
| |
| @override |
| Never visitYield(js_ast.Yield node) => _unreachable(node); |
| } |
| |
| js_ast.VariableInitialization _makeVariableInitializer( |
| js_ast.Identifier variable, |
| js_ast.Expression? initValue, |
| Object? sourceInformation, |
| ) { |
| return js_ast.VariableInitialization( |
| variable, |
| initValue, |
| ).withSourceInformation(sourceInformation) |
| as js_ast.VariableInitialization; |
| } |
| |
| class AsyncRewriter extends AsyncRewriterBase { |
| @override |
| bool get _isAsync => true; |
| |
| /// The Completer that will finish an async function. |
| /// |
| /// Not used for sync* or async* functions. |
| late final js_ast.Identifier completer = ScopedId('t\$completer'); |
| |
| /// The function called by an async function to initiate asynchronous |
| /// execution of the body. This is called with: |
| /// |
| /// - The body function [bodyName]. |
| /// - the completer object [completer]. |
| /// |
| /// It returns the completer's future. Passing the completer and returning its |
| /// future is a convenience to allow both the initiation and fetching the |
| /// future to be compactly encoded in a return statement's expression. |
| final js_ast.Expression asyncStart; |
| |
| /// Function called by the async function to simulate an `await` |
| /// expression. It is called with: |
| /// |
| /// - The value to await |
| /// - The body function [bodyName] |
| final js_ast.Expression asyncAwait; |
| |
| /// Function called by the async function to simulate a return. |
| /// It is called with: |
| /// |
| /// - The value to return |
| /// - The completer object [completer] |
| final js_ast.Expression asyncReturn; |
| |
| /// Function called by the async function to simulate a rethrow. |
| /// It is called with: |
| /// |
| /// - The value containing the exception and stack |
| /// - The completer object [completer] |
| final js_ast.Expression asyncRethrow; |
| |
| /// Constructor used to initialize the [completer] variable. |
| /// |
| /// Specific to async methods. |
| final js_ast.Expression completerFactory; |
| final List<js_ast.Expression> completerFactoryTypeArguments; |
| |
| final js_ast.Expression wrapBody; |
| |
| AsyncRewriter({ |
| required this.asyncStart, |
| required this.asyncAwait, |
| required this.asyncReturn, |
| required this.asyncRethrow, |
| required this.completerFactory, |
| required this.completerFactoryTypeArguments, |
| required this.wrapBody, |
| required super.bodyName, |
| }); |
| |
| @override |
| void addYield( |
| js_ast.DartYield node, |
| js_ast.Expression expression, |
| Object? sourceInformation, |
| ) { |
| throw StateError('Yield in non-generating async function'); |
| } |
| |
| @override |
| void addErrorExit(Object? sourceInformation) { |
| if (!_hasHandlerLabels) return; // rethrow handled in method boilerplate. |
| _beginLabel(_rethrowLabel); |
| var thenHelperCall = js_ast |
| .js('#thenHelper(#errorStack.at(-1), #completer)', { |
| 'thenHelper': asyncRethrow, |
| 'errorStack': _errorStack, |
| 'completer': completer, |
| }) |
| .withSourceInformation(sourceInformation); |
| _addStatement( |
| js_ast.Return(thenHelperCall).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| /// Returning from an async method calls [asyncStarHelper] with the result. |
| /// (the result might have been stored in [_returnValue] by some finally |
| /// block). |
| @override |
| void addSuccessExit(Object? sourceInformation) { |
| if (_analysis.hasExplicitReturns) { |
| _beginLabel(_exitLabel); |
| } else { |
| _addStatement(js_ast.Comment('implicit return')); |
| } |
| |
| var runtimeHelperCall = js_ast |
| .js('#runtimeHelper(#returnValue, #completer)', { |
| 'runtimeHelper': asyncReturn, |
| 'returnValue': _analysis.hasExplicitReturns |
| ? _returnValue |
| : js_ast.LiteralNull(), |
| 'completer': completer, |
| }) |
| .withSourceInformation(sourceInformation); |
| _addStatement( |
| js_ast.Return(runtimeHelperCall).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| @override |
| Iterable<js_ast.VariableInitialization> variableInitializations( |
| Object? sourceInformation, |
| ) { |
| var variables = <js_ast.VariableInitialization>[]; |
| variables.add( |
| _makeVariableInitializer( |
| completer, |
| js_ast |
| .js('#(#)', [completerFactory, completerFactoryTypeArguments]) |
| .withSourceInformation(sourceInformation), |
| sourceInformation, |
| ), |
| ); |
| if (_analysis.hasExplicitReturns) { |
| variables.add( |
| _makeVariableInitializer(_returnValue, null, sourceInformation), |
| ); |
| } |
| return variables; |
| } |
| |
| @override |
| js_ast.Statement awaitStatement( |
| js_ast.Expression value, |
| Object? sourceInformation, |
| ) { |
| var asyncHelperCall = js_ast |
| .js('#asyncHelper(#value, #bodyName, #completer)', { |
| 'asyncHelper': asyncAwait, |
| 'value': value, |
| 'bodyName': bodyName, |
| 'completer': completer, |
| }) |
| .withSourceInformation(sourceInformation); |
| return js_ast.Return( |
| asyncHelperCall, |
| ).withSourceInformation(sourceInformation); |
| } |
| |
| @override |
| js_ast.Fun _finishFunction( |
| List<js_ast.Parameter> parameters, |
| js_ast.Statement rewrittenBody, |
| js_ast.VariableDeclarationList variableDeclarationLists, |
| Object? functionSourceInformation, |
| Object? bodySourceInformation, |
| ) { |
| js_ast.Statement errorCheck; |
| if (_hasHandlerLabels) { |
| errorCheck = js_ast.js.statement( |
| ''' |
| if (#errorCode === #ERROR) { |
| #errorStack.push(#result); |
| #goto = #handler; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'ERROR': js_ast.number(status_codes.ERROR), |
| 'errorStack': _errorStack, |
| 'result': _result, |
| 'goto': _goto, |
| 'handler': _handler, |
| }, |
| ); |
| } else { |
| var asyncRethrowCall = js_ast.js('#asyncRethrow(#result, #completer)', { |
| 'result': _result, |
| 'asyncRethrow': asyncRethrow, |
| 'completer': completer, |
| }); |
| var returnAsyncRethrow = js_ast.Return(asyncRethrowCall); |
| errorCheck = js_ast.js.statement( |
| ''' |
| if (#errorCode === #ERROR) |
| #returnAsyncRethrow; |
| ''', |
| { |
| 'errorCode': _errorCode, |
| 'ERROR': js_ast.number(status_codes.ERROR), |
| 'returnAsyncRethrow': returnAsyncRethrow, |
| }, |
| ); |
| } |
| |
| // Use an arrow function so that we can access 'this' from the outer scope. |
| var innerFunction = js_ast |
| .js( |
| ''' |
| (#errorCode, #result) => { |
| #errorCheck; |
| #rewrittenBody; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'result': _result, |
| 'errorCheck': errorCheck, |
| 'rewrittenBody': rewrittenBody, |
| }, |
| ) |
| .withSourceInformation(bodySourceInformation); |
| var asyncStartCall = js_ast |
| .js('#asyncStart(#bodyName, #completer)', { |
| 'asyncStart': asyncStart, |
| 'bodyName': bodyName, |
| 'completer': completer, |
| }) |
| .withSourceInformation(bodySourceInformation); |
| var returnAsyncStart = js_ast.Return(asyncStartCall); |
| var wrapBodyCall = js_ast |
| .js('#wrapBody(#innerFunction)', { |
| 'wrapBody': wrapBody, |
| 'innerFunction': innerFunction, |
| }) |
| .withSourceInformation(bodySourceInformation); |
| return (js_ast |
| .js( |
| ''' |
| function (#parameters) { |
| #variableDeclarationLists; |
| var #bodyName = #wrapBodyCall; |
| #returnAsyncStart; |
| }''', |
| { |
| 'parameters': parameters, |
| 'variableDeclarationLists': variableDeclarationLists, |
| 'bodyName': bodyName, |
| 'wrapBodyCall': wrapBodyCall, |
| 'returnAsyncStart': returnAsyncStart, |
| }, |
| ) |
| .withSourceInformation(functionSourceInformation)) |
| as js_ast.Fun; |
| } |
| } |
| |
| class SyncStarRewriter extends AsyncRewriterBase { |
| @override |
| bool get _isSyncStar => true; |
| |
| /// A parameter to the [bodyName] function that passes the controlling |
| /// `_SyncStarIterator`. This parameter is used to update the state of the |
| /// iterator. |
| late final js_ast.Identifier iterator = ScopedId('t\$iterator'); |
| |
| /// Static method to create a sync star iterable. |
| final js_ast.Expression makeSyncStarIterable; |
| |
| /// The type argument expression to instantiate the sync star iterable. |
| final js_ast.Expression syncStarIterableTypeArgument; |
| |
| /// Property of the iterator that contains the current value. |
| final js_ast.Expression iteratorCurrentValueProperty; |
| |
| /// Property of the iterator that contains the uncaught exception. |
| final js_ast.Expression iteratorDatumProperty; |
| |
| /// Property of the iterator that is bound to the `_yieldStar` method. |
| final js_ast.Expression yieldStarSelector; |
| |
| SyncStarRewriter({ |
| required this.makeSyncStarIterable, |
| required this.syncStarIterableTypeArgument, |
| required this.iteratorCurrentValueProperty, |
| required this.iteratorDatumProperty, |
| required this.yieldStarSelector, |
| required super.bodyName, |
| }); |
| |
| /// Translates a yield/yield* in an sync*. |
| @override |
| void addYield( |
| js_ast.DartYield node, |
| js_ast.Expression expression, |
| Object? sourceInformation, |
| ) { |
| if (node.hasStar) { |
| // ``yield* expression` is translated to: |
| // |
| // return $iterator._yieldStar(expression); |
| // |
| // The `_yieldStar` method updates the state of the Iterator to 'enter' |
| // the expression and returns the SYNC_STAR_YIELD_STAR status code. |
| _addStatement( |
| js_ast.Return( |
| js_ast.Call(js_ast.PropertyAccess(iterator, yieldStarSelector), [ |
| expression, |
| ]).withSourceInformation(sourceInformation), |
| ).withSourceInformation(sourceInformation), |
| ); |
| } else { |
| // `yield expression` is translated to: |
| // |
| // return $iterator._current = expression, SYNC_STAR_YIELD; |
| // |
| // This sets the `_current` field of the Iterator and returns the |
| // SYNC_STAR_YIELD status code. |
| final store = js_ast.Assignment( |
| js_ast.PropertyAccess(iterator, iteratorCurrentValueProperty), |
| expression, |
| ); |
| _addStatement( |
| js_ast.Return( |
| js_ast.Binary( |
| ',', |
| store, |
| js_ast.number(status_codes.SYNC_STAR_YIELD), |
| ), |
| ).withSourceInformation(sourceInformation), |
| ); |
| } |
| } |
| |
| @override |
| js_ast.Fun _finishFunction( |
| List<js_ast.Parameter> parameters, |
| js_ast.Statement rewrittenBody, |
| js_ast.VariableDeclarationList variableDeclarationLists, |
| Object? functionSourceInformation, |
| Object? bodySourceInformation, |
| ) { |
| // Each iterator invocation on the iterable should work on its own copy of |
| // the parameters. Since parameter initialization code at the start of the |
| // function may reference the original parameter names, we create an alias |
| // for each parameter. Then in the async body we shadow each parameter with |
| // a copy of that alias so each iteration of the body works on it's own |
| // version of the parameter in case of modification. |
| var outerDeclarationsList = <js_ast.VariableInitialization>[]; |
| var innerDeclarationsList = <js_ast.VariableInitialization>[]; |
| for (var parameter in parameters) { |
| final name = parameter.parameterName; |
| final renamedIdentifier = ScopedId(name); |
| final parameterRef = switch (parameter) { |
| ScopedId() => ScopedId.from(parameter), |
| js_ast.DestructuredVariable() when parameter.name is ScopedId => |
| ScopedId.from(parameter.name as ScopedId), |
| _ => js_ast.Identifier(name), |
| }; |
| |
| innerDeclarationsList.add( |
| js_ast.VariableInitialization(parameterRef, renamedIdentifier), |
| ); |
| outerDeclarationsList.add( |
| js_ast.VariableInitialization(renamedIdentifier, parameterRef), |
| ); |
| } |
| var outerDeclarations = js_ast.VariableDeclarationList( |
| 'let', |
| outerDeclarationsList, |
| ); |
| var innerDeclarations = js_ast.VariableDeclarationList( |
| 'let', |
| innerDeclarationsList, |
| ); |
| |
| var pushError = js_ast.js('#errorStack.push(#result)', { |
| 'result': _result, |
| 'errorStack': _errorStack, |
| }); |
| var setGoto = js_ast.js('#goto = #handler', { |
| 'goto': _goto, |
| 'handler': _handler, |
| }); |
| var checkErrorCode = js_ast.js.statement( |
| ''' |
| if (#errorCode === #ERROR) { |
| #pushError; |
| #setGoto; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'ERROR': js_ast.number(status_codes.ERROR), |
| 'pushError': pushError, |
| 'setGoto': setGoto, |
| }, |
| ); |
| // Use an arrow function so that we can access 'this' from the outer scope. |
| var innerInnerFunction = js_ast.js( |
| ''' |
| (#iterator, #errorCode, #result) => { |
| #checkErrorCode; |
| #helperBody; |
| }''', |
| { |
| 'helperBody': rewrittenBody, |
| 'errorCode': _errorCode, |
| 'iterator': iterator, |
| 'result': _result, |
| 'checkErrorCode': checkErrorCode, |
| }, |
| ); |
| var returnInnerInnerFunction = js_ast.Return(innerInnerFunction); |
| // Use an arrow function so that we can access 'this' from the outer scope. |
| var innerInnerFunctionInvocation = js_ast.js( |
| ''' |
| #makeSyncStarIterable(#iterableType, () => { |
| if (#hasParameters) { |
| #innerDeclarations; |
| } |
| #varDecl; |
| #returnInnerInnerFunction; |
| })''', |
| { |
| 'hasParameters': parameters.isNotEmpty, |
| 'innerDeclarations': innerDeclarations, |
| 'varDecl': variableDeclarationLists, |
| 'returnInnerInnerFunction': returnInnerInnerFunction, |
| 'makeSyncStarIterable': makeSyncStarIterable, |
| 'iterableType': syncStarIterableTypeArgument, |
| }, |
| ); |
| var returnInnerFunction = js_ast.Return(innerInnerFunctionInvocation); |
| // Add the copied parameter declarations outside the inner function in case |
| // one is a type parameter that gets passed to the inner function. |
| return (js_ast |
| .js( |
| ''' |
| function (#parameters) { |
| if (#hasParameters) { |
| #outerDeclarations; |
| } |
| #returnInnerFunction; |
| } |
| ''', |
| { |
| 'hasParameters': parameters.isNotEmpty, |
| 'outerDeclarations': outerDeclarations, |
| 'parameters': parameters, |
| 'returnInnerFunction': returnInnerFunction, |
| }, |
| ) |
| .withSourceInformation(functionSourceInformation)) |
| as js_ast.Fun; |
| } |
| |
| @override |
| void addErrorExit(Object? sourceInformation) { |
| _hasHandlerLabels = true; // TODO(sra): Add short form error handler. |
| _beginLabel(_rethrowLabel); |
| // Unguarded rethrow is translated to: |
| // |
| // return $iterator._datum = exception, SYNC_STAR_UNCAUGHT_EXCEPTION; |
| // |
| // This stashes the exception on the Iterator and returns the |
| // SYNC_STAR_UNCAUGHT_EXCEPTION status code. |
| final store = js_ast.Assignment( |
| js_ast.PropertyAccess(iterator, iteratorDatumProperty), |
| js_ast.js('#.at(-1)', [_errorStack]), |
| ); |
| _addStatement( |
| js_ast.Return( |
| js_ast.Binary( |
| ',', |
| store, |
| js_ast.number(status_codes.SYNC_STAR_UNCAUGHT_EXCEPTION), |
| ), |
| ).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| /// Returning from a sync* function returns the SYNC_STAR_DONE status code. |
| @override |
| void addSuccessExit(Object? sourceInformation) { |
| if (_analysis.hasExplicitReturns) { |
| _beginLabel(_exitLabel); |
| } else { |
| _addStatement(js_ast.Comment('implicit return')); |
| } |
| _addStatement( |
| js_ast.Return( |
| js_ast.number(status_codes.SYNC_STAR_DONE), |
| ).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| @override |
| Iterable<js_ast.VariableInitialization> variableInitializations( |
| Object? sourceInformation, |
| ) { |
| var variables = <js_ast.VariableInitialization>[]; |
| return variables; |
| } |
| |
| @override |
| js_ast.Statement awaitStatement( |
| js_ast.Expression value, |
| Object? sourceInformation, |
| ) { |
| throw StateError('Sync* functions cannot contain await statements.'); |
| } |
| } |
| |
| class AsyncStarRewriter extends AsyncRewriterBase { |
| @override |
| bool get _isAsyncStar => true; |
| |
| /// The stack of labels of finally blocks to assign to [_next] if the |
| /// async* [StreamSubscription] was canceled during a yield. |
| late final js_ast.Identifier nextWhenCanceled = ScopedId( |
| 't\$nextWhenCanceled', |
| ); |
| |
| /// The StreamController that controls an async* function. |
| late final js_ast.Identifier controller = ScopedId('t\$controller'); |
| |
| /// The function called by an async* function to simulate an await, yield or |
| /// yield*. |
| /// |
| /// For an await/yield/yield* it is called with: |
| /// |
| /// - The value to await/yieldExpression(value to yield)/ |
| /// yieldStarExpression(stream to yield) |
| /// - The body function [bodyName] |
| /// - The controller object [controller] |
| /// |
| /// For a return it is called with: |
| /// |
| /// - null |
| /// - null |
| /// - The [controller] |
| /// - null. |
| final js_ast.Expression asyncStarHelper; |
| |
| /// Constructor used to initialize the [controller] variable. |
| /// |
| /// Specific to async* methods. |
| final js_ast.Expression newController; |
| List<js_ast.Expression> newControllerTypeArguments; |
| |
| /// Used to get the `Stream` out of the [controllerName] variable. |
| final js_ast.Expression streamOfController; |
| |
| /// A JS Expression that creates a marker indicating a 'yield' statement. |
| /// |
| /// Called with the value to yield. |
| final js_ast.Expression yieldExpression; |
| |
| /// A JS Expression that creates a marker indication a 'yield*' statement. |
| /// |
| /// Called with the stream to yield from. |
| final js_ast.Expression yieldStarExpression; |
| |
| final js_ast.Expression wrapBody; |
| |
| AsyncStarRewriter({ |
| required this.asyncStarHelper, |
| required this.streamOfController, |
| required this.newController, |
| required this.newControllerTypeArguments, |
| required this.yieldExpression, |
| required this.yieldStarExpression, |
| required this.wrapBody, |
| required super.bodyName, |
| }); |
| |
| /// Translates a yield/yield* in an async* function. |
| /// |
| /// yield/yield* in an async* function is translated much like the `await` is |
| /// translated in [visitAwait], only the object is wrapped in a |
| /// [yieldExpression]/[yieldStarExpression] to let [asyncStarHelper] |
| /// distinguish them. |
| /// Also [nextWhenCanceled] is set up to contain the finally blocks that |
| /// must be run in case the stream was canceled. |
| @override |
| void addYield( |
| js_ast.DartYield node, |
| js_ast.Expression expression, |
| Object? sourceInformation, |
| ) { |
| // Find all the finally blocks that should be performed if the stream is |
| // canceled during the yield. |
| var enclosingFinallyLabels = <int>[ |
| // At the bottom of the stack is the return label. |
| _exitLabel, |
| for (final node in _jumpTargets) |
| if (_finallyLabels[node] != null) _finallyLabels[node]!, |
| ]; |
| |
| _addStatement( |
| js_ast.js |
| .statement('# = #;', [ |
| nextWhenCanceled, |
| js_ast.ArrayInitializer( |
| enclosingFinallyLabels.map(js_ast.number).toList(), |
| ), |
| ]) |
| .withSourceInformation(sourceInformation), |
| ); |
| var yieldExpressionCall = js_ast |
| .js('#yieldExpression(#expression)', { |
| 'yieldExpression': node.hasStar |
| ? yieldStarExpression |
| : yieldExpression, |
| 'expression': expression, |
| }) |
| .withSourceInformation(sourceInformation); |
| var asyncStarHelperCall = js_ast |
| .js('#asyncStarHelper(#yieldExpressionCall, #bodyName, #controller)', { |
| 'asyncStarHelper': asyncStarHelper, |
| 'yieldExpressionCall': yieldExpressionCall, |
| 'bodyName': bodyName, |
| 'controller': controller, |
| }) |
| .withSourceInformation(sourceInformation); |
| _addStatement( |
| js_ast.Return( |
| asyncStarHelperCall, |
| ).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| @override |
| js_ast.Fun _finishFunction( |
| List<js_ast.Parameter> parameters, |
| js_ast.Statement rewrittenBody, |
| js_ast.VariableDeclarationList variableDeclarationLists, |
| Object? functionSourceInformation, |
| Object? bodySourceInformation, |
| ) { |
| var updateNext = js_ast.js('#next = #nextWhenCanceled', { |
| 'next': _next, |
| 'nextWhenCanceled': nextWhenCanceled, |
| }); |
| var callPop = js_ast.js('#next.pop()', {'next': _next}); |
| var gotoCancelled = js_ast.js('#goto = #callPop', { |
| 'goto': _goto, |
| 'callPop': callPop, |
| }); |
| var pushError = js_ast.js('#errorStack.push(#result)', { |
| 'errorStack': _errorStack, |
| 'result': _result, |
| }); |
| var gotoError = js_ast.js('#goto = #handler', { |
| 'goto': _goto, |
| 'handler': _handler, |
| }); |
| var breakStatement = js_ast.Break(null); |
| var switchCase = js_ast.js.statement( |
| ''' |
| switch (#errorCode) { |
| case #STREAM_WAS_CANCELED: |
| #updateNext; |
| #gotoCancelled; |
| #break; |
| case #ERROR: |
| #pushError; |
| #gotoError; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'STREAM_WAS_CANCELED': js_ast.number(status_codes.STREAM_WAS_CANCELED), |
| 'updateNext': updateNext, |
| 'gotoCancelled': gotoCancelled, |
| 'break': breakStatement, |
| 'ERROR': js_ast.number(status_codes.ERROR), |
| 'pushError': pushError, |
| 'gotoError': gotoError, |
| }, |
| ); |
| var ifError = js_ast.js.statement( |
| ''' |
| if (#errorCode === #ERROR) { |
| #pushError; |
| #gotoError; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'ERROR': js_ast.number(status_codes.ERROR), |
| 'pushError': pushError, |
| 'gotoError': gotoError, |
| }, |
| ); |
| var ifHasYield = js_ast.js.statement( |
| ''' |
| if (#hasYield) { |
| #switchCase |
| } else { |
| #ifError; |
| } |
| ''', |
| { |
| 'hasYield': _analysis.hasYield, |
| 'switchCase': switchCase, |
| 'ifError': ifError, |
| }, |
| ); |
| // Use an arrow function so that we can access 'this' from the outer scope. |
| var innerFunction = js_ast |
| .js( |
| ''' |
| (#errorCode, #result) => { |
| #ifHasYield; |
| #rewrittenBody; |
| }''', |
| { |
| 'errorCode': _errorCode, |
| 'result': _result, |
| 'ifHasYield': ifHasYield, |
| 'rewrittenBody': rewrittenBody, |
| }, |
| ) |
| .withSourceInformation(functionSourceInformation); |
| var wrapBodyCall = js_ast |
| .js('#wrapBody(#innerFunction)', { |
| 'wrapBody': wrapBody, |
| 'innerFunction': innerFunction, |
| }) |
| .withSourceInformation(bodySourceInformation); |
| var declareBodyName = js_ast.js.statement( |
| 'var #bodyName = #wrapBodyCall;', |
| {'bodyName': bodyName, 'wrapBodyCall': wrapBodyCall}, |
| ); |
| var streamOfControllerCall = js_ast.js('#streamOfController(#controller)', { |
| 'streamOfController': streamOfController, |
| 'controller': controller, |
| }); |
| var returnStreamOfControllerCall = js_ast.Return(streamOfControllerCall); |
| return (js_ast |
| .js( |
| ''' |
| function (#parameters) { |
| #declareBodyName; |
| #variableDeclarationLists; |
| #returnStreamOfControllerCall; |
| }''', |
| { |
| 'parameters': parameters, |
| 'declareBodyName': declareBodyName, |
| 'variableDeclarationLists': variableDeclarationLists, |
| 'returnStreamOfControllerCall': returnStreamOfControllerCall, |
| }, |
| ) |
| .withSourceInformation(functionSourceInformation)) |
| as js_ast.Fun; |
| } |
| |
| @override |
| void addErrorExit(Object? sourceInformation) { |
| _hasHandlerLabels = true; |
| _beginLabel(_rethrowLabel); |
| var asyncHelperCall = js_ast |
| .js('#asyncHelper(#errorStack.at(-1), #errorCode, #controller)', { |
| 'asyncHelper': asyncStarHelper, |
| 'errorCode': js_ast.number(status_codes.ERROR), |
| 'errorStack': _errorStack, |
| 'controller': controller, |
| }) |
| .withSourceInformation(sourceInformation); |
| _addStatement( |
| js_ast.Return(asyncHelperCall).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| /// Returning from an async* function calls the [streamHelper] with an |
| /// [endOfIteration] marker. |
| @override |
| void addSuccessExit(Object? sourceInformation) { |
| _beginLabel(_exitLabel); |
| |
| var streamHelperCall = js_ast |
| .js('#streamHelper(null, #successCode, #controller)', { |
| 'streamHelper': asyncStarHelper, |
| 'successCode': js_ast.number(status_codes.SUCCESS), |
| 'controller': controller, |
| }) |
| .withSourceInformation(sourceInformation); |
| _addStatement( |
| js_ast.Return(streamHelperCall).withSourceInformation(sourceInformation), |
| ); |
| } |
| |
| @override |
| Iterable<js_ast.VariableInitialization> variableInitializations( |
| Object? sourceInformation, |
| ) { |
| var variables = <js_ast.VariableInitialization>[]; |
| variables.add( |
| _makeVariableInitializer( |
| controller, |
| js_ast |
| .js('#(#, #)', [ |
| newController, |
| newControllerTypeArguments, |
| bodyName, |
| ]) |
| .withSourceInformation(sourceInformation), |
| sourceInformation, |
| ), |
| ); |
| if (_analysis.hasYield) { |
| variables.add( |
| _makeVariableInitializer(nextWhenCanceled, null, sourceInformation), |
| ); |
| } |
| return variables; |
| } |
| |
| @override |
| js_ast.Statement awaitStatement( |
| js_ast.Expression value, |
| Object? sourceInformation, |
| ) { |
| var asyncHelperCall = js_ast |
| .js('#asyncHelper(#value, #bodyName, #controller)', { |
| 'asyncHelper': asyncStarHelper, |
| 'value': value, |
| 'bodyName': bodyName, |
| 'controller': controller, |
| }) |
| .withSourceInformation(sourceInformation); |
| return js_ast.Return( |
| asyncHelperCall, |
| ).withSourceInformation(sourceInformation); |
| } |
| } |
| |
| /// Finds out |
| /// |
| /// - which expressions have yield or await nested in them. |
| /// - targets of jumps |
| /// - a set of used label names. |
| class PreTranslationAnalysis extends js_ast.NodeVisitor<bool> { |
| final Set<js_ast.Node> hasAwaitOrYield = {}; |
| final Map<js_ast.Node, js_ast.Node> targets = {}; |
| final List<js_ast.Node> loopsAndSwitches = []; |
| final List<js_ast.LabeledStatement> labelledStatements = []; |
| final Set<String> usedLabelNames = {}; |
| |
| bool hasExplicitReturns = false; |
| |
| bool hasYield = false; |
| |
| bool hasFinally = false; |
| |
| // The function currently being analyzed. |
| final js_ast.FunctionExpression currentFunction; |
| |
| // For error messages. |
| final Never Function(js_ast.Node) unsupported; |
| |
| PreTranslationAnalysis(this.unsupported, this.currentFunction); |
| |
| bool visit(js_ast.Node node) { |
| var containsAwait = node.accept(this); |
| if (containsAwait) { |
| hasAwaitOrYield.add(node); |
| } |
| return containsAwait; |
| } |
| |
| void analyze() { |
| currentFunction.params.forEach(visit); |
| visit(currentFunction.body); |
| } |
| |
| @override |
| bool visitAccess(js_ast.PropertyAccess node) { |
| var receiver = visit(node.receiver); |
| var selector = visit(node.selector); |
| return receiver || selector; |
| } |
| |
| @override |
| bool visitArrayHole(js_ast.ArrayHole node) { |
| return false; |
| } |
| |
| @override |
| bool visitArrayInitializer(js_ast.ArrayInitializer node) { |
| var containsAwait = false; |
| for (var element in node.elements) { |
| if (visit(element)) containsAwait = true; |
| } |
| return containsAwait; |
| } |
| |
| @override |
| bool visitAssignment(js_ast.Assignment node) { |
| var leftHandSide = visit(node.leftHandSide); |
| var value = visit(node.value); |
| return leftHandSide || value; |
| } |
| |
| @override |
| bool visitAwait(js_ast.Await node) { |
| visit(node.expression); |
| return true; |
| } |
| |
| @override |
| bool visitBinary(js_ast.Binary node) { |
| var left = visit(node.left); |
| var right = visit(node.right); |
| return left || right; |
| } |
| |
| @override |
| bool visitBlock(js_ast.Block node) { |
| var containsAwait = false; |
| for (var statement in node.statements) { |
| if (visit(statement)) containsAwait = true; |
| } |
| return containsAwait; |
| } |
| |
| @override |
| bool visitBreak(js_ast.Break node) { |
| if (node.targetLabel != null) { |
| targets[node] = labelledStatements.lastWhere(( |
| js_ast.LabeledStatement statement, |
| ) { |
| return statement.label == node.targetLabel; |
| }); |
| } else { |
| targets[node] = loopsAndSwitches.last; |
| } |
| return false; |
| } |
| |
| @override |
| bool visitCall(js_ast.Call node) { |
| var containsAwait = visit(node.target); |
| for (var argument in node.arguments) { |
| if (visit(argument)) containsAwait = true; |
| } |
| return containsAwait; |
| } |
| |
| @override |
| bool visitCase(js_ast.Case node) { |
| var expression = visit(node.expression); |
| var body = visit(node.body); |
| return expression || body; |
| } |
| |
| @override |
| bool visitCatch(js_ast.Catch node) { |
| var declaration = visit(node.declaration); |
| var body = visit(node.body); |
| return declaration || body; |
| } |
| |
| @override |
| bool visitComment(js_ast.Comment node) { |
| return false; |
| } |
| |
| @override |
| bool visitConditional(js_ast.Conditional node) { |
| var condition = visit(node.condition); |
| var then = visit(node.then); |
| var otherwise = visit(node.otherwise); |
| return condition || then || otherwise; |
| } |
| |
| @override |
| bool visitContinue(js_ast.Continue node) { |
| if (node.targetLabel != null) { |
| var targetLabel = labelledStatements.lastWhere( |
| (js_ast.LabeledStatement stm) => stm.label == node.targetLabel, |
| ); |
| targets[node] = targetLabel.body; |
| } else { |
| targets[node] = loopsAndSwitches.lastWhere( |
| (js_ast.Node node) => node is! js_ast.Switch, |
| ); |
| } |
| assert(() { |
| var target = targets[node]; |
| return target is js_ast.Loop || |
| (target is js_ast.LabeledStatement && target.body is js_ast.Loop); |
| }()); |
| return false; |
| } |
| |
| @override |
| bool visitDefault(js_ast.Default node) { |
| return visit(node.body); |
| } |
| |
| @override |
| bool visitDo(js_ast.Do node) { |
| loopsAndSwitches.add(node); |
| var body = visit(node.body); |
| var condition = visit(node.condition); |
| loopsAndSwitches.removeLast(); |
| return body || condition; |
| } |
| |
| @override |
| bool visitEmptyStatement(js_ast.EmptyStatement node) { |
| return false; |
| } |
| |
| @override |
| bool visitExpressionStatement(js_ast.ExpressionStatement node) { |
| return visit(node.expression); |
| } |
| |
| @override |
| bool visitFor(js_ast.For node) { |
| var init = (node.init == null) ? false : visit(node.init!); |
| var condition = (node.condition == null) ? false : visit(node.condition!); |
| var update = (node.update == null) ? false : visit(node.update!); |
| loopsAndSwitches.add(node); |
| var body = visit(node.body); |
| loopsAndSwitches.removeLast(); |
| return init || condition || update || body; |
| } |
| |
| @override |
| bool visitForIn(js_ast.ForIn node) { |
| var object = visit(node.object); |
| loopsAndSwitches.add(node); |
| var body = visit(node.body); |
| loopsAndSwitches.removeLast(); |
| return object || body; |
| } |
| |
| @override |
| bool visitFun(js_ast.Fun node) { |
| return false; |
| } |
| |
| @override |
| bool visitFunctionDeclaration(js_ast.FunctionDeclaration node) { |
| return false; |
| } |
| |
| @override |
| bool visitArrowFun(js_ast.ArrowFun node) { |
| return false; |
| } |
| |
| @override |
| bool visitIf(js_ast.If node) { |
| var condition = visit(node.condition); |
| var then = visit(node.then); |
| var otherwise = visit(node.otherwise); |
| return condition || then || otherwise; |
| } |
| |
| @override |
| bool visitInterpolatedExpression(js_ast.InterpolatedExpression node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitInterpolatedLiteral(js_ast.InterpolatedLiteral node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitInterpolatedParameter(js_ast.InterpolatedParameter node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitInterpolatedSelector(js_ast.InterpolatedSelector node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitInterpolatedStatement(js_ast.InterpolatedStatement node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitLabeledStatement(js_ast.LabeledStatement node) { |
| usedLabelNames.add(node.label); |
| labelledStatements.add(node); |
| var containsAwait = visit(node.body); |
| labelledStatements.removeLast(); |
| return containsAwait; |
| } |
| |
| @override |
| bool visitLiteralBool(js_ast.LiteralBool node) { |
| return false; |
| } |
| |
| @override |
| bool visitLiteralExpression(js_ast.LiteralExpression node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitLiteralNull(js_ast.LiteralNull node) { |
| return false; |
| } |
| |
| @override |
| bool visitLiteralNumber(js_ast.LiteralNumber node) { |
| return false; |
| } |
| |
| @override |
| bool visitLiteralStatement(js_ast.LiteralStatement node) { |
| unsupported(node); |
| } |
| |
| @override |
| bool visitLiteralString(js_ast.LiteralString node) { |
| return false; |
| } |
| |
| @override |
| bool visitNamedFunction(js_ast.NamedFunction node) { |
| return false; |
| } |
| |
| @override |
| bool visitNew(js_ast.New node) { |
| return visitCall(node); |
| } |
| |
| @override |
| bool visitObjectInitializer(js_ast.ObjectInitializer node) { |
| var containsAwait = false; |
| for (var property in node.properties) { |
| if (visit(property)) containsAwait = true; |
| } |
| return containsAwait; |
| } |
| |
| @override |
| bool visitPostfix(js_ast.Postfix node) { |
| return visit(node.argument); |
| } |
| |
| @override |
| bool visitPrefix(js_ast.Prefix node) { |
| return visit(node.argument); |
| } |
| |
| @override |
| bool visitProgram(js_ast.Program node) { |
| throw 'Unexpected'; |
| } |
| |
| @override |
| bool visitProperty(js_ast.Property node) { |
| return visit(node.value); |
| } |
| |
| @override |
| bool visitMethod(js_ast.Method node) { |
| return false; |
| } |
| |
| @override |
| bool visitRegExpLiteral(js_ast.RegExpLiteral node) { |
| return false; |
| } |
| |
| @override |
| bool visitReturn(js_ast.Return node) { |
| hasExplicitReturns = true; |
| targets[node] = currentFunction; |
| if (node.value == null) return false; |
| return visit(node.value!); |
| } |
| |
| @override |
| bool visitSwitch(js_ast.Switch node) { |
| loopsAndSwitches.add(node); |
| // TODO(sra): If just the key has an `await` expression, do not transform |
| // the body of the switch. |
| var result = visit(node.key); |
| for (var clause in node.cases) { |
| if (visit(clause)) result = true; |
| } |
| loopsAndSwitches.removeLast(); |
| return result; |
| } |
| |
| @override |
| bool visitThis(js_ast.This node) { |
| return false; |
| } |
| |
| @override |
| bool visitThrow(js_ast.Throw node) { |
| return visit(node.expression); |
| } |
| |
| @override |
| bool visitTry(js_ast.Try node) { |
| if (node.finallyPart != null) hasFinally = true; |
| var body = visit(node.body); |
| var catchPart = (node.catchPart == null) ? false : visit(node.catchPart!); |
| var finallyPart = (node.finallyPart == null) |
| ? false |
| : visit(node.finallyPart!); |
| return body || catchPart || finallyPart; |
| } |
| |
| @override |
| bool visitVariableDeclarationList(js_ast.VariableDeclarationList node) { |
| var result = false; |
| for (var init in node.declarations) { |
| if (visit(init)) result = true; |
| } |
| return result; |
| } |
| |
| @override |
| bool visitVariableInitialization(js_ast.VariableInitialization node) { |
| var leftHandSide = visit(node.declaration); |
| var value = (node.value == null) ? false : visit(node.value!); |
| return leftHandSide || value; |
| } |
| |
| @override |
| bool visitIdentifier(js_ast.Identifier node) { |
| return false; |
| } |
| |
| @override |
| bool visitWhile(js_ast.While node) { |
| loopsAndSwitches.add(node); |
| var condition = visit(node.condition); |
| var body = visit(node.body); |
| loopsAndSwitches.removeLast(); |
| return condition || body; |
| } |
| |
| @override |
| bool visitDartYield(js_ast.DartYield node) { |
| hasYield = true; |
| visit(node.expression); |
| return true; |
| } |
| |
| @override |
| bool visitCommentExpression(js_ast.CommentExpression node) { |
| return false; |
| } |
| |
| @override |
| bool visitArrayBindingPattern(js_ast.ArrayBindingPattern node) { |
| return false; |
| } |
| |
| @override |
| bool visitClassDeclaration(js_ast.ClassDeclaration node) { |
| return false; |
| } |
| |
| @override |
| bool visitClassExpression(js_ast.ClassExpression node) { |
| return false; |
| } |
| |
| @override |
| bool visitDebuggerStatement(js_ast.DebuggerStatement node) { |
| return false; |
| } |
| |
| @override |
| bool visitDestructuredVariable(js_ast.DestructuredVariable node) { |
| return false; |
| } |
| |
| @override |
| bool visitExportClause(js_ast.ExportClause node) { |
| return false; |
| } |
| |
| @override |
| bool visitExportDeclaration(js_ast.ExportDeclaration node) { |
| return false; |
| } |
| |
| @override |
| bool visitForOf(js_ast.ForOf node) { |
| node.leftHandSide.accept(this); |
| var iterable = node.iterable.accept(this); |
| loopsAndSwitches.add(node); |
| var body = node.body.accept(this); |
| loopsAndSwitches.removeLast(); |
| return iterable || body; |
| } |
| |
| @override |
| bool visitImportDeclaration(js_ast.ImportDeclaration node) { |
| return false; |
| } |
| |
| @override |
| bool visitInterpolatedIdentifier(js_ast.InterpolatedIdentifier node) { |
| return false; |
| } |
| |
| @override |
| bool visitInterpolatedMethod(js_ast.InterpolatedMethod node) { |
| return false; |
| } |
| |
| @override |
| bool visitNameSpecifier(js_ast.NameSpecifier node) { |
| return false; |
| } |
| |
| @override |
| bool visitObjectBindingPattern(js_ast.ObjectBindingPattern node) { |
| return false; |
| } |
| |
| @override |
| bool visitRestParameter(js_ast.RestParameter node) { |
| return false; |
| } |
| |
| @override |
| bool visitSimpleBindingPattern(js_ast.SimpleBindingPattern node) { |
| return false; |
| } |
| |
| @override |
| bool visitSpread(js_ast.Spread node) { |
| return false; |
| } |
| |
| @override |
| bool visitSuper(js_ast.Super node) { |
| return false; |
| } |
| |
| @override |
| bool visitTaggedTemplate(js_ast.TaggedTemplate node) { |
| return false; |
| } |
| |
| @override |
| bool visitTemplateString(js_ast.TemplateString node) { |
| return node.interpolations.any((e) => e.accept(this)); |
| } |
| |
| @override |
| bool visitYield(js_ast.Yield node) { |
| unsupported(node); |
| } |
| } |
| |
| /// Defines a scope in the async body of a function tracking all variables |
| /// available in the scope. |
| /// |
| /// We maintain a mapping from each variable name to the scope it is declared |
| /// in. This allows us to refer to the correct scope object for uses of that |
| /// variable. |
| /// |
| /// Each scope also tracks if it was captured. Only captured scopes need to be |
| /// reset upon re-entry, otherwise the values within them cannot leak out. |
| class _ScopeInfo { |
| late final ScopedId scopeObject = ScopedId('asyncScope'); |
| bool isCaptured = false; |
| bool hasDeclarations = false; |
| final Map<String, _ScopeInfo> _nameDeclarations; |
| |
| _ScopeInfo([Map<String, _ScopeInfo>? nameDeclarations]) |
| : _nameDeclarations = {...?nameDeclarations}; |
| |
| _ScopeInfo childScope() { |
| return _ScopeInfo(_nameDeclarations); |
| } |
| |
| void declare(js_ast.Identifier node, bool isUntrackedDeclaration) { |
| final key = node.name; |
| assert( |
| _nameDeclarations[key] != this, |
| 'Name "$node" already declared in scope.', |
| ); |
| if (isUntrackedDeclaration) { |
| _nameDeclarations.remove(key); |
| } else { |
| _nameDeclarations[key] = this; |
| hasDeclarations = true; |
| } |
| } |
| |
| _ScopeInfo? getDeclaringScope(js_ast.Identifier node) { |
| return _nameDeclarations[node.name]; |
| } |
| } |
| |
| /// Tracks [_ScopeInfo] are captured by this closure. |
| /// |
| /// We use an IIFE to capture scope objects used within this closure. Capture |
| /// names are assigned to each captured scope, this is the parameter name in the |
| /// IIFE. Within the body of this closure, captured scopes will be referred to |
| /// by their capture name. |
| class _ClosureCaptureInfo { |
| final _ScopeInfo scopeInfo; |
| final Map<_ScopeInfo, ScopedId> usedScopes = {}; |
| bool get hasCapture => usedScopes.isNotEmpty; |
| |
| _ClosureCaptureInfo(this.scopeInfo); |
| |
| ScopedId useScope(_ScopeInfo scope) { |
| scope.isCaptured = true; |
| return usedScopes[scope] ??= ScopedId('capturedAsyncScope'); |
| } |
| } |
| |
| /// Updates references to captured variables to read the value from the |
| /// appropriate captured scope name. |
| class _ClosureRenamer extends js_ast.Transformer { |
| final _ScopeCollector scopeCollector; |
| final _ClosureCaptureInfo closureInfo; |
| |
| _ClosureRenamer(this.scopeCollector, this.closureInfo); |
| |
| @override |
| js_ast.Node visitIdentifier(js_ast.Identifier node) { |
| final declaringScope = scopeCollector.useToDeclaringScope[node]; |
| if (declaringScope == null) return node; |
| final captureVariable = closureInfo.usedScopes[declaringScope]; |
| return captureVariable != null |
| ? (js_ast.PropertyAccess.field(captureVariable, node.name) |
| ..sourceInformation = node.sourceInformation) |
| : node; |
| } |
| } |
| |
| /// Collects scoped names for each variable declared within the scope of the |
| /// given function. |
| /// |
| /// In order to support scope capture we define a [_ScopeInfo] for each scope |
| /// we enter. Each one will be a JS Object that we can capture in inner |
| /// functions. This object can also be reset when we re-enter a scoped |
| /// construct (e.g. different iterations of a for loop). |
| /// |
| /// The [_ScopeInfo] object will get hoisted to the top of the async body so it |
| /// can be accessed where needed across async gaps. |
| /// |
| /// This approach also works well for debugging, users will see "asyncScope" |
| /// objects. Since we have one scope object (roughly) per Dart scope, users will |
| /// see variables matching the names they've used in the source code. In the |
| /// future DevTools can even recognize these objects and flatten them into their |
| /// appropriate scopes. |
| /// |
| /// We also track [_ClosureCaptureInfo] for each capturing function so that we |
| /// can maintain the correct captured scopes. |
| /// |
| /// Variables that are not hoisted (i.e. we can maintain their control flow |
| /// constructs because they don't contain awaits or yields) do not need to be |
| /// referenced via scope objects. |
| class _ScopeCollector extends js_ast.VariableDeclarationVisitor { |
| final PreTranslationAnalysis _analysis; |
| |
| _ScopeInfo _currentScope = _ScopeInfo(null); |
| _ClosureCaptureInfo? _currentOuterClosure; |
| bool skipHoisting = false; |
| final Map<js_ast.Identifier, _ScopeInfo> useToDeclaringScope = {}; |
| final Map<js_ast.Node, _ScopeInfo> scopeMapping = {}; |
| final Map<js_ast.FunctionExpression, _ClosureCaptureInfo> scopeCaptures = {}; |
| bool get inClosure => _currentOuterClosure != null; |
| |
| _ScopeCollector(this._analysis); |
| |
| void collect(js_ast.FunctionExpression node) { |
| node.body.accept(this); |
| scopeMapping[node.body] = _currentScope; |
| } |
| |
| js_ast.Expression transformIdentifier(js_ast.Identifier node) { |
| final declaringScope = useToDeclaringScope[node]; |
| if (declaringScope == null) return node; |
| return (js_ast.PropertyAccess.field(declaringScope.scopeObject, node.name) |
| ..sourceInformation = node.sourceInformation); |
| } |
| |
| void registerUsed(js_ast.Identifier node) { |
| final declaringScope = _currentScope.getDeclaringScope(node); |
| if (declaringScope != null) { |
| useToDeclaringScope[node] = declaringScope; |
| _currentOuterClosure?.useScope(declaringScope); |
| } |
| } |
| |
| void withNewScope(js_ast.Node node, void Function() f) { |
| final savedScope = _currentScope; |
| _currentScope = _currentScope.childScope(); |
| scopeMapping[node] = _currentScope; |
| f(); |
| _currentScope = savedScope; |
| } |
| |
| @override |
| void declare(js_ast.Identifier node) { |
| if (node is ScopedId && !node.needsCapture) return; |
| _currentScope.declare(node, inClosure || skipHoisting); |
| registerUsed(node); |
| } |
| |
| @override |
| void visitIdentifier(js_ast.Identifier node) { |
| if (node is ScopedId && !node.needsCapture) return; |
| registerUsed(node); |
| } |
| |
| @override |
| void visitFunctionExpression(js_ast.FunctionExpression node) { |
| withNewScope(node, () { |
| if (!inClosure) { |
| _currentOuterClosure = _ClosureCaptureInfo(_currentScope); |
| scopeCaptures[node] = _currentOuterClosure!; |
| super.visitFunctionExpression(node); |
| _currentOuterClosure = null; |
| } else { |
| super.visitFunctionExpression(node); |
| } |
| }); |
| } |
| |
| @override |
| void visitBlock(js_ast.Block node) { |
| if (node.isScope) { |
| withNewScope(node, () => super.visitBlock(node)); |
| } else { |
| super.visitBlock(node); |
| } |
| } |
| |
| @override |
| void visitForIn(js_ast.ForIn node) { |
| node.object.accept(this); |
| withNewScope(node, () { |
| final savedSkipHoisting = skipHoisting; |
| skipHoisting = !_analysis.hasAwaitOrYield.contains(node); |
| node.leftHandSide.accept(this); |
| skipHoisting = savedSkipHoisting; |
| node.body.accept(this); |
| }); |
| } |
| |
| @override |
| void visitForOf(js_ast.ForOf node) { |
| node.iterable.accept(this); |
| withNewScope(node, () { |
| final savedSkipHoisting = skipHoisting; |
| skipHoisting = !_analysis.hasAwaitOrYield.contains(node); |
| node.leftHandSide.accept(this); |
| skipHoisting = savedSkipHoisting; |
| node.body.accept(this); |
| }); |
| } |
| |
| @override |
| void visitFor(js_ast.For node) { |
| // Make sure any declared variables are scoped to this loop. |
| withNewScope(node, () { |
| final savedSkipHoisting = skipHoisting; |
| skipHoisting = !_analysis.hasAwaitOrYield.contains(node); |
| node.init?.accept(this); |
| skipHoisting = savedSkipHoisting; |
| node.condition?.accept(this); |
| node.update?.accept(this); |
| node.body.accept(this); |
| }); |
| } |
| |
| @override |
| void visitTry(js_ast.Try node) { |
| node.body.accept(this); |
| final savedSkipHoisting = skipHoisting; |
| skipHoisting = !_analysis.hasAwaitOrYield.contains(node); |
| node.catchPart?.declaration.accept(this); |
| skipHoisting = savedSkipHoisting; |
| node.catchPart?.body.accept(this); |
| node.finallyPart?.accept(this); |
| } |
| } |