Skip to content

Commit 252aada

Browse files
committed
Merge pull request #6910 from Microsoft/port-6898
Ports #6898 into release-1.8
2 parents 3d01190 + a2774c1 commit 252aada

35 files changed

+2063
-45
lines changed

src/compiler/checker.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7242,6 +7242,15 @@ namespace ts {
72427242
// mark iteration statement as containing block-scoped binding captured in some function
72437243
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
72447244
}
7245+
7246+
// mark variables that are declared in loop initializer and reassigned inside the body of ForStatement.
7247+
// if body of ForStatement will be converted to function then we'll need a extra machinery to propagate reassigned values back.
7248+
if (container.kind === SyntaxKind.ForStatement &&
7249+
getAncestor(symbol.valueDeclaration, SyntaxKind.VariableDeclarationList).parent === container &&
7250+
isAssignedInBodyOfForStatement(node, <ForStatement>container)) {
7251+
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.NeedsLoopOutParameter;
7252+
}
7253+
72457254
// set 'declared inside loop' bit on the block-scoped binding
72467255
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
72477256
}
@@ -7251,6 +7260,41 @@ namespace ts {
72517260
}
72527261
}
72537262

7263+
function isAssignedInBodyOfForStatement(node: Identifier, container: ForStatement): boolean {
7264+
let current: Node = node;
7265+
// skip parenthesized nodes
7266+
while (current.parent.kind === SyntaxKind.ParenthesizedExpression) {
7267+
current = current.parent;
7268+
}
7269+
7270+
// check if node is used as LHS in some assignment expression
7271+
let isAssigned = false;
7272+
if (current.parent.kind === SyntaxKind.BinaryExpression) {
7273+
isAssigned = (<BinaryExpression>current.parent).left === current && isAssignmentOperator((<BinaryExpression>current.parent).operatorToken.kind);
7274+
}
7275+
7276+
if ((current.parent.kind === SyntaxKind.PrefixUnaryExpression || current.parent.kind === SyntaxKind.PostfixUnaryExpression)) {
7277+
const expr = <PrefixUnaryExpression | PostfixUnaryExpression>current.parent;
7278+
isAssigned = expr.operator === SyntaxKind.PlusPlusToken || expr.operator === SyntaxKind.MinusMinusToken;
7279+
}
7280+
7281+
if (!isAssigned) {
7282+
return false;
7283+
}
7284+
7285+
// at this point we know that node is the target of assignment
7286+
// now check that modification happens inside the statement part of the ForStatement
7287+
while (current !== container) {
7288+
if (current === container.statement) {
7289+
return true;
7290+
}
7291+
else {
7292+
current = current.parent;
7293+
}
7294+
}
7295+
return false;
7296+
}
7297+
72547298
function captureLexicalThis(node: Node, container: Node): void {
72557299
getNodeLinks(node).flags |= NodeCheckFlags.LexicalThis;
72567300
if (container.kind === SyntaxKind.PropertyDeclaration || container.kind === SyntaxKind.Constructor) {

src/compiler/emitter.ts

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,54 @@ namespace ts {
287287
_i = 0x10000000, // Use/preference flag for '_i'
288288
}
289289

290+
const enum CopyDirection {
291+
ToOriginal,
292+
ToOutParameter
293+
}
294+
295+
/**
296+
* If loop contains block scoped binding captured in some function then loop body is converted to a function.
297+
* Lexical bindings declared in loop initializer will be passed into the loop body function as parameters,
298+
* however if this binding is modified inside the body - this new value should be propagated back to the original binding.
299+
* This is done by declaring new variable (out parameter holder) outside of the loop for every binding that is reassigned inside the body.
300+
* On every iteration this variable is initialized with value of corresponding binding.
301+
* At every point where control flow leaves the loop either explicitly (break/continue) or implicitly (at the end of loop body)
302+
* we copy the value inside the loop to the out parameter holder.
303+
*
304+
* for (let x;;) {
305+
* let a = 1;
306+
* let b = () => a;
307+
* x++
308+
* if (...) break;
309+
* ...
310+
* }
311+
*
312+
* will be converted to
313+
*
314+
* var out_x;
315+
* var loop = function(x) {
316+
* var a = 1;
317+
* var b = function() { return a; }
318+
* x++;
319+
* if (...) return out_x = x, "break";
320+
* ...
321+
* out_x = x;
322+
* }
323+
* for (var x;;) {
324+
* out_x = x;
325+
* var state = loop(x);
326+
* x = out_x;
327+
* if (state === "break") break;
328+
* }
329+
*
330+
* NOTE: values to out parameters are not copies if loop is abrupted with 'return' - in this case this will end the entire enclosing function
331+
* so nobody can observe this new value.
332+
*/
333+
interface LoopOutParameter {
334+
originalName: Identifier;
335+
outParamName: string;
336+
}
337+
290338
// targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature
291339
export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile): EmitResult {
292340
// emit output for the __extends helper function
@@ -419,6 +467,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
419467
* for (var x;;) loop(x);
420468
*/
421469
hoistedLocalVariables?: Identifier[];
470+
471+
/**
472+
* List of loop out parameters - detailed descripion can be found in the comment to LoopOutParameter
473+
*/
474+
loopOutParameters?: LoopOutParameter[];
422475
}
423476

424477
function setLabeledJump(state: ConvertedLoopState, isBreak: boolean, labelText: string, labelMarker: string): void {
@@ -2944,11 +2997,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
29442997
}
29452998

29462999
let loopParameters: string[];
3000+
let loopOutParameters: LoopOutParameter[];
29473001
if (loopInitializer && (getCombinedNodeFlags(loopInitializer) & NodeFlags.BlockScoped)) {
29483002
// if loop initializer contains block scoped variables - they should be passed to converted loop body as parameters
29493003
loopParameters = [];
29503004
for (const varDeclaration of loopInitializer.declarations) {
2951-
collectNames(varDeclaration.name);
3005+
processVariableDeclaration(varDeclaration.name);
29523006
}
29533007
}
29543008

@@ -2958,14 +3012,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
29583012
writeLine();
29593013
write(`var ${functionName} = function(${paramList})`);
29603014

2961-
if (!bodyIsBlock) {
2962-
write(" {");
2963-
writeLine();
2964-
increaseIndent();
2965-
}
2966-
29673015
const convertedOuterLoopState = convertedLoopState;
2968-
convertedLoopState = {};
3016+
convertedLoopState = { loopOutParameters };
29693017

29703018
if (convertedOuterLoopState) {
29713019
// convertedOuterLoopState !== undefined means that this converted loop is nested in another converted loop.
@@ -2989,16 +3037,38 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
29893037
}
29903038
}
29913039

2992-
emitEmbeddedStatement(node.statement);
3040+
write(" {");
3041+
writeLine();
3042+
increaseIndent();
29933043

2994-
if (!bodyIsBlock) {
2995-
decreaseIndent();
2996-
writeLine();
2997-
write("}");
3044+
if (bodyIsBlock) {
3045+
emitLines((<Block>node.statement).statements);
29983046
}
2999-
write(";");
3047+
else {
3048+
emit(node.statement);
3049+
}
3050+
3051+
writeLine();
3052+
// end of loop body -> copy out parameter
3053+
copyLoopOutParameters(convertedLoopState, CopyDirection.ToOutParameter, /*emitAsStatements*/true);
3054+
3055+
decreaseIndent();
3056+
writeLine();
3057+
write("};");
30003058
writeLine();
30013059

3060+
if (loopOutParameters) {
3061+
// declare variables to hold out params for loop body
3062+
write(`var `);
3063+
for (let i = 0; i < loopOutParameters.length; i++) {
3064+
if (i !== 0) {
3065+
write(", ");
3066+
}
3067+
write(loopOutParameters[i].outParamName);
3068+
}
3069+
write(";");
3070+
writeLine();
3071+
}
30023072
if (convertedLoopState.argumentsName) {
30033073
// if alias for arguments is set
30043074
if (convertedOuterLoopState) {
@@ -3062,14 +3132,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
30623132

30633133
return { functionName, paramList, state: currentLoopState };
30643134

3065-
function collectNames(name: Identifier | BindingPattern): void {
3135+
function processVariableDeclaration(name: Identifier | BindingPattern): void {
30663136
if (name.kind === SyntaxKind.Identifier) {
3067-
const nameText = isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(<Identifier>name) ? getGeneratedNameForNode(name) : (<Identifier>name).text;
3137+
const nameText = isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(<Identifier>name)
3138+
? getGeneratedNameForNode(name)
3139+
: (<Identifier>name).text;
3140+
30683141
loopParameters.push(nameText);
3142+
if (resolver.getNodeCheckFlags(name.parent) & NodeCheckFlags.NeedsLoopOutParameter) {
3143+
const reassignedVariable = { originalName: <Identifier>name, outParamName: makeUniqueName(`out_${nameText}`) };
3144+
(loopOutParameters || (loopOutParameters = [])).push(reassignedVariable);
3145+
}
30693146
}
30703147
else {
30713148
for (const element of (<BindingPattern>name).elements) {
3072-
collectNames(element.name);
3149+
processVariableDeclaration(element.name);
30733150
}
30743151
}
30753152
}
@@ -3100,6 +3177,28 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
31003177
}
31013178
}
31023179

3180+
function copyLoopOutParameters(state: ConvertedLoopState, copyDirection: CopyDirection, emitAsStatements: boolean) {
3181+
if (state.loopOutParameters) {
3182+
for (const outParam of state.loopOutParameters) {
3183+
if (copyDirection === CopyDirection.ToOriginal) {
3184+
emitIdentifier(outParam.originalName);
3185+
write(` = ${outParam.outParamName}`);
3186+
}
3187+
else {
3188+
write(`${outParam.outParamName} = `);
3189+
emitIdentifier(outParam.originalName);
3190+
}
3191+
if (emitAsStatements) {
3192+
write(";");
3193+
writeLine();
3194+
}
3195+
else {
3196+
write(", ");
3197+
}
3198+
}
3199+
}
3200+
}
3201+
31033202
function emitConvertedLoopCall(loop: ConvertedLoop, emitAsBlock: boolean): void {
31043203
if (emitAsBlock) {
31053204
write(" {");
@@ -3120,6 +3219,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
31203219
}
31213220

31223221
write(`${loop.functionName}(${loop.paramList});`);
3222+
writeLine();
3223+
3224+
copyLoopOutParameters(loop.state, CopyDirection.ToOriginal, /*emitAsStatements*/ true);
31233225

31243226
if (!isSimpleLoop) {
31253227
// for non simple loops we need to store result returned from converted loop function and use it to do dispatching
@@ -3138,7 +3240,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
31383240
}
31393241
else {
31403242
// top level converted loop - return unwrapped value
3141-
write(`return ${loopResult}.value`);
3243+
write(`return ${loopResult}.value;`);
31423244
}
31433245
writeLine();
31443246
}
@@ -3439,14 +3541,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
34393541
(!node.label && (convertedLoopState.allowedNonLabeledJumps & jump));
34403542

34413543
if (!canUseBreakOrContinue) {
3544+
write ("return ");
3545+
// explicit exit from loop -> copy out parameters
3546+
copyLoopOutParameters(convertedLoopState, CopyDirection.ToOutParameter, /*emitAsStatements*/ false);
34423547
if (!node.label) {
34433548
if (node.kind === SyntaxKind.BreakStatement) {
34443549
convertedLoopState.nonLocalJumps |= Jump.Break;
3445-
write(`return "break";`);
3550+
write(`"break";`);
34463551
}
34473552
else {
34483553
convertedLoopState.nonLocalJumps |= Jump.Continue;
3449-
write(`return "continue";`);
3554+
write(`"continue";`);
34503555
}
34513556
}
34523557
else {
@@ -3459,7 +3564,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
34593564
labelMarker = `continue-${node.label.text}`;
34603565
setLabeledJump(convertedLoopState, /*isBreak*/ false, node.label.text, labelMarker);
34613566
}
3462-
write(`return "${labelMarker}";`);
3567+
write(`"${labelMarker}";`);
34633568
}
34643569

34653570
return;

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,6 +2071,7 @@ namespace ts {
20712071
HasSeenSuperCall = 0x00080000, // Set during the binding when encounter 'super'
20722072
ClassWithBodyScopedClassBinding = 0x00100000, // Decorated class that contains a binding to itself inside of the class body.
20732073
BodyScopedClassBinding = 0x00200000, // Binding to a decorated class inside of the class's body.
2074+
NeedsLoopOutParameter = 0x00400000, // Block scoped binding whose value should be explicitly copied outside of the converted loop
20742075
}
20752076

20762077
/* @internal */
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//// [blockScopedBindingsReassignedInLoop1.ts]
2+
declare function use(n: number): void;
3+
(function () {
4+
'use strict'
5+
for (let i = 0; i < 9; ++i) {
6+
(() => use(++i))();
7+
}
8+
})();
9+
10+
//// [blockScopedBindingsReassignedInLoop1.js]
11+
(function () {
12+
'use strict';
13+
var _loop_1 = function(i) {
14+
(function () { return use(++i); })();
15+
out_i_1 = i;
16+
};
17+
var out_i_1;
18+
for (var i = 0; i < 9; ++i) {
19+
_loop_1(i);
20+
i = out_i_1;
21+
}
22+
})();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
=== tests/cases/compiler/blockScopedBindingsReassignedInLoop1.ts ===
2+
declare function use(n: number): void;
3+
>use : Symbol(use, Decl(blockScopedBindingsReassignedInLoop1.ts, 0, 0))
4+
>n : Symbol(n, Decl(blockScopedBindingsReassignedInLoop1.ts, 0, 21))
5+
6+
(function () {
7+
'use strict'
8+
for (let i = 0; i < 9; ++i) {
9+
>i : Symbol(i, Decl(blockScopedBindingsReassignedInLoop1.ts, 3, 10))
10+
>i : Symbol(i, Decl(blockScopedBindingsReassignedInLoop1.ts, 3, 10))
11+
>i : Symbol(i, Decl(blockScopedBindingsReassignedInLoop1.ts, 3, 10))
12+
13+
(() => use(++i))();
14+
>use : Symbol(use, Decl(blockScopedBindingsReassignedInLoop1.ts, 0, 0))
15+
>i : Symbol(i, Decl(blockScopedBindingsReassignedInLoop1.ts, 3, 10))
16+
}
17+
})();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
=== tests/cases/compiler/blockScopedBindingsReassignedInLoop1.ts ===
2+
declare function use(n: number): void;
3+
>use : (n: number) => void
4+
>n : number
5+
6+
(function () {
7+
>(function () { 'use strict' for (let i = 0; i < 9; ++i) { (() => use(++i))(); }})() : void
8+
>(function () { 'use strict' for (let i = 0; i < 9; ++i) { (() => use(++i))(); }}) : () => void
9+
>function () { 'use strict' for (let i = 0; i < 9; ++i) { (() => use(++i))(); }} : () => void
10+
11+
'use strict'
12+
>'use strict' : string
13+
14+
for (let i = 0; i < 9; ++i) {
15+
>i : number
16+
>0 : number
17+
>i < 9 : boolean
18+
>i : number
19+
>9 : number
20+
>++i : number
21+
>i : number
22+
23+
(() => use(++i))();
24+
>(() => use(++i))() : void
25+
>(() => use(++i)) : () => void
26+
>() => use(++i) : () => void
27+
>use(++i) : void
28+
>use : (n: number) => void
29+
>++i : number
30+
>i : number
31+
}
32+
})();

0 commit comments

Comments
 (0)