Skip to content

Fix captured block scope variables in downlevel async. #10890

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 81 additions & 32 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,43 +71,92 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};`;

// The __generator helper is used by down-level transformations to emulate the runtime
// semantics of an ES2015 generator function. When called, this helper returns an
// object that implements the Iterator protocol, in that it has `next`, `return`, and
// `throw` methods that step through the generator when invoked.
//
// parameters:
// thisArg The value to use as the `this` binding for the transformed generator body.
// body A function that acts as the transformed generator body.
//
// variables:
// _ Persistent state for the generator that is shared between the helper and the
// generator body. The state object has the following members:
// sent() - A method that returns or throws the current completion value.
// label - The next point at which to resume evaluation of the generator body.
// trys - A stack of protected regions (try/catch/finally blocks).
// ops - A stack of pending instructions when inside of a finally block.
// f A value indicating whether the generator is executing.
// y An iterator to delegate for a yield*.
// t A temporary variable that holds one of the following values (note that these
// cases do not overlap):
// - The completion value when resuming from a `yield` or `yield*`.
// - The error value for a catch block.
// - The current protected region (array of try/catch/finally/end labels).
// - The verb (`next`, `throw`, or `return` method) to delegate to the expression
// of a `yield*`.
// - The result of evaluating the verb delegated to the expression of a `yield*`.
//
// functions:
// verb(n) Creates a bound callback to the `step` function for opcode `n`.
// step(op) Evaluates opcodes in a generator body until execution is suspended or
// completed.
//
// The __generator helper understands a limited set of instructions:
// 0: next(value?) - Start or resume the generator with the specified value.
// 1: throw(error) - Resume the generator with an exception. If the generator is
// suspended inside of one or more protected regions, evaluates
// any intervening finally blocks between the current label and
// the nearest catch block or function boundary. If uncaught, the
// exception is thrown to the caller.
// 2: return(value?) - Resume the generator as if with a return. If the generator is
// suspended inside of one or more protected regions, evaluates any
// intervening finally blocks.
// 3: break(label) - Jump to the specified label. If the label is outside of the
// current protected region, evaluates any intervening finally
// blocks.
// 4: yield(value?) - Yield execution to the caller with an optional value. When
// resumed, the generator will continue at the next label.
// 5: yield*(value) - Delegates evaluation to the supplied iterator. When
// delegation completes, the generator will continue at the next
// label.
// 6: catch(error) - Handles an exception thrown from within the generator body. If
// the current label is inside of one or more protected regions,
// evaluates any intervening finally blocks between the current
// label and the nearest catch block or function boundary. If
// uncaught, the exception is thrown to the caller.
// 7: endfinally - Ends a finally block, resuming the last instruction prior to
// entering a finally block.
//
// For examples of how these are used, see the comments in ./transformers/generators.ts
const generatorHelper = `
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (sent[0] === 1) throw sent[1]; return sent[1]; }, trys: [], stack: [] }, sent, f;
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t;
return { next: verb(0), "throw": verb(1), "return": verb(2) };
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (1) {
if (_.done) switch (op[0]) {
case 0: return { value: void 0, done: true };
case 1: case 6: throw op[1];
case 2: return { value: op[1], done: true };
}
try {
switch (f = 1, op[0]) {
case 0: case 1: sent = op; break;
case 4: return _.label++, { value: op[1], done: false };
case 7: op = _.stack.pop(), _.trys.pop(); continue;
default:
var r = _.trys.length > 0 && _.trys[_.trys.length - 1];
if (!r && (op[0] === 6 || op[0] === 2)) { _.done = 1; continue; }
if (op[0] === 3 && (!r || (op[1] > r[0] && op[1] < r[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < r[1]) { _.label = r[1], sent = op; break; }
if (r && _.label < r[2]) { _.label = r[2], _.stack.push(op); break; }
if (r[2]) { _.stack.pop(); }
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
}
catch (e) { op = [6, e]; }
finally { f = 0, sent = void 0; }
}
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
return {
next: function (v) { return step([0, v]); },
"throw": function (v) { return step([1, v]); },
"return": function (v) { return step([2, v]); }
};
};`;

// emit output for the __export helper function
Expand Down
39 changes: 29 additions & 10 deletions src/compiler/transformers/es6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2055,26 +2055,39 @@ namespace ts {
if (!isBlock(loopBody)) {
loopBody = createBlock([loopBody], /*location*/ undefined, /*multiline*/ true);
}

const isAsyncBlockContainingAwait =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it be ContainingYield?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no. It contains an await transformed into a yield.

containingNonArrowFunction
&& (containingNonArrowFunction.emitFlags & NodeEmitFlags.AsyncFunctionBody) !== 0
&& (node.statement.transformFlags & TransformFlags.ContainsYield) !== 0;

let loopBodyFlags: NodeEmitFlags = 0;
if (currentState.containsLexicalThis) {
loopBodyFlags |= NodeEmitFlags.CapturesThis;
}

if (isAsyncBlockContainingAwait) {
loopBodyFlags |= NodeEmitFlags.AsyncFunctionBody;
}

const convertedLoopVariable =
createVariableStatement(
/*modifiers*/ undefined,
/*modifiers*/ undefined,
createVariableDeclarationList(
[
createVariableDeclaration(
functionName,
/*type*/ undefined,
setNodeEmitFlags(
createFunctionExpression(
/*asteriskToken*/ undefined,
isAsyncBlockContainingAwait ? createToken(SyntaxKind.AsteriskToken) : undefined,
/*name*/ undefined,
/*typeParameters*/ undefined,
loopParameters,
/*type*/ undefined,
<Block>loopBody
),
currentState.containsLexicalThis
? NodeEmitFlags.CapturesThis
: 0
loopBodyFlags
)
)
]
Expand Down Expand Up @@ -2160,7 +2173,7 @@ namespace ts {
));
}

const convertedLoopBodyStatements = generateCallToConvertedLoop(functionName, loopParameters, currentState);
const convertedLoopBodyStatements = generateCallToConvertedLoop(functionName, loopParameters, currentState, isAsyncBlockContainingAwait);
let loop: IterationStatement;
if (convert) {
loop = convert(node, convertedLoopBodyStatements);
Expand All @@ -2173,12 +2186,17 @@ namespace ts {
loop = visitEachChild(loop, visitor, context);
// set loop statement
loop.statement = createBlock(
generateCallToConvertedLoop(functionName, loopParameters, currentState),
convertedLoopBodyStatements,
/*location*/ undefined,
/*multiline*/ true
);

// reset and re-aggregate the transform flags
loop.transformFlags = 0;
aggregateTransformFlags(loop);
}


statements.push(
currentParent.kind === SyntaxKind.LabeledStatement
? createLabel((<LabeledStatement>currentParent).label, loop)
Expand All @@ -2199,7 +2217,7 @@ namespace ts {
}
}

function generateCallToConvertedLoop(loopFunctionExpressionName: Identifier, parameters: ParameterDeclaration[], state: ConvertedLoopState): Statement[] {
function generateCallToConvertedLoop(loopFunctionExpressionName: Identifier, parameters: ParameterDeclaration[], state: ConvertedLoopState, isAsyncBlockContainingAwait: boolean): Statement[] {
const outerConvertedLoopState = convertedLoopState;

const statements: Statement[] = [];
Expand All @@ -2212,16 +2230,17 @@ namespace ts {
!state.labeledNonLocalContinues;

const call = createCall(loopFunctionExpressionName, /*typeArguments*/ undefined, map(parameters, p => <Identifier>p.name));
const callResult = isAsyncBlockContainingAwait ? createYield(createToken(SyntaxKind.AsteriskToken), call) : call;
if (isSimpleLoop) {
statements.push(createStatement(call));
statements.push(createStatement(callResult));
copyOutParameters(state.loopOutParameters, CopyDirection.ToOriginal, statements);
}
else {
const loopResultName = createUniqueName("state");
const stateVariable = createVariableStatement(
/*modifiers*/ undefined,
createVariableDeclarationList(
[createVariableDeclaration(loopResultName, /*type*/ undefined, call)]
[createVariableDeclaration(loopResultName, /*type*/ undefined, callResult)]
)
);
statements.push(stateVariable);
Expand Down
40 changes: 29 additions & 11 deletions src/compiler/transformers/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
// .brfalse LABEL, (x) - Jump to a label IIF the expression `x` is falsey.
// If jumping out of a protected region, all .finally
// blocks are executed.
// .yield RESUME, (x) - Yield the value of the optional expression `x`.
// Resume at the label RESUME.
// .yieldstar RESUME, (x) - Delegate yield to the value of the optional
// expression `x`. Resume at the label RESUME.
// .yield (x) - Yield the value of the optional expression `x`.
// Resume at the next label.
// .yieldstar (x) - Delegate yield to the value of the optional
// expression `x`. Resume at the next label.
// NOTE: `x` must be an Iterator, not an Iterable.
// .loop CONTINUE, BREAK - Marks the beginning of a loop. Any "continue" or
// "break" abrupt completions jump to the CONTINUE or
// BREAK labels, respectively.
Expand Down Expand Up @@ -80,13 +81,13 @@
// -------------------------------|----------------------------------------------
// .brfalse LABEL, (x) | if (!(x)) return [3, /*break*/, LABEL];
// -------------------------------|----------------------------------------------
// .yield RESUME, (x) | return [4 /*yield*/, x];
// .yield (x) | return [4 /*yield*/, x];
// .mark RESUME | case RESUME:
// a = %sent%; | a = state.sent();
// a = %sent%; | a = state.sent();
// -------------------------------|----------------------------------------------
// .yieldstar RESUME, (X) | return [5 /*yield**/, x];
// .yieldstar (x) | return [5 /*yield**/, x];
// .mark RESUME | case RESUME:
// a = %sent%; | a = state.sent();
// a = %sent%; | a = state.sent();
// -------------------------------|----------------------------------------------
// .with (_a) | with (_a) {
// a(); | a();
Expand All @@ -109,7 +110,7 @@
// .br END | return [3 /*break*/, END];
// .catch (e) |
// .mark CATCH | case CATCH:
// | e = state.error;
// | e = state.sent();
// b(); | b();
// .br END | return [3 /*break*/, END];
// .finally |
Expand Down Expand Up @@ -906,7 +907,7 @@ namespace ts {
*
* @param node The node to visit.
*/
function visitYieldExpression(node: YieldExpression) {
function visitYieldExpression(node: YieldExpression): LeftHandSideExpression {
// [source]
// x = yield a();
//
Expand All @@ -917,7 +918,14 @@ namespace ts {

// NOTE: we are explicitly not handling YieldStar at this time.
const resumeLabel = defineLabel();
emitYield(visitNode(node.expression, visitor, isExpression), /*location*/ node);
const expression = visitNode(node.expression, visitor, isExpression);
if (node.asteriskToken) {
emitYieldStar(expression, /*location*/ node);
}
else {
emitYield(expression, /*location*/ node);
}

markLabel(resumeLabel);
return createGeneratorResume();
}
Expand Down Expand Up @@ -2480,6 +2488,16 @@ namespace ts {
emitWorker(OpCode.BreakWhenFalse, [label, condition], location);
}

/**
* Emits a YieldStar operation for the provided expression.
*
* @param expression An optional value for the yield operation.
* @param location An optional source map location for the assignment.
*/
function emitYieldStar(expression?: Expression, location?: TextRange): void {
emitWorker(OpCode.YieldStar, [expression], location);
}

/**
* Emits a Yield operation for the provided expression.
*
Expand Down
Loading