blob: 4147190a78800b52f1fa462e532156e6ac8e3301 [file] [log] [blame] [edit]
// 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);
}
}