retain binding values on iteration if reassigned - fixes #1078

This commit is contained in:
Sebastian McKenzie
2015-03-30 18:02:21 +11:00
parent 3d6e722fc7
commit 1641caedc4
5 changed files with 88 additions and 11 deletions

View File

@@ -122,13 +122,13 @@ var letReferenceFunctionVisitor = {
// not a direct reference
if (!this.isReferencedIdentifier()) return;
// not a part of our scope
if (!state.letReferences[node.name]) return;
// this scope has a variable with the same name so it couldn't belong
// to our let scope
if (scope.hasOwnBinding(node.name)) return;
// not a part of our scope
if (!state.letReferences[node.name]) return;
state.closurify = true;
}
};
@@ -159,6 +159,18 @@ var loopLabelVisitor = {
}
};
var continuationVisitor = {
enter(node, parent, scope, state) {
if (this.isAssignmentExpression() || this.isUpdateExpression()) {
var bindings = this.getBindingIdentifiers();
for (var name in bindings) {
if (state.outsideReferences[name] !== scope.getBindingIdentifier(name)) continue;
state.reassignments[name] = true;
}
}
}
};
var loopNodeTo = function (node) {
if (t.isBreakStatement(node)) {
return "break";
@@ -352,16 +364,20 @@ class BlockScoping {
// turn outsideLetReferences into an array
var params = values(outsideRefs);
var args = values(outsideRefs);
// build the closure that we're going to wrap the block with
var fn = t.functionExpression(null, params, t.blockStatement(block.body));
fn.shadow = true;
// continuation
this.addContinuations(fn);
// replace the current block body with the one we're going to build
block.body = this.body;
// build a call and a unique id that we can assign the return value to
var call = t.callExpression(fn, params);
var call = t.callExpression(fn, args);
var ret = this.scope.generateUidIdentifier("ret");
// handle generators
@@ -381,6 +397,36 @@ class BlockScoping {
this.build(ret, call);
}
/**
* If any of the outer let variables are reassigned then we need to rename them in
* the closure so we can get direct access to the outer variable to continue the
* iteration with bindings based on each iteration.
*
* Reference: https://github.com/babel/babel/issues/1078
*/
addContinuations(fn) {
var state = {
reassignments: {},
outsideReferences: this.outsideLetReferences
};
this.scope.traverse(fn, continuationVisitor, state);
for (var i = 0; i < fn.params.length; i++) {
var param = fn.params[i];
if (!state.reassignments[param.name]) continue;
var newParam = this.scope.generateUidIdentifier(param.name);
fn.params[i] = newParam;
this.scope.rename(param.name, newParam.name, fn);
// assign outer reference as it's been modified internally and needs to be retained
fn.body.body.push(t.expressionStatement(t.assignmentExpression("=", param, newParam)));
}
}
/**
* Description
*/
@@ -542,7 +588,7 @@ class BlockScoping {
single.consequent[0]
)));
} else {
// #998
// https://github.com/babel/babel/issues/998
for (var i = 0; i < cases.length; i++) {
var caseConsequent = cases[i].consequent[0];
if (t.isBreakStatement(caseConsequent) && !caseConsequent.label) {

View File

@@ -238,16 +238,16 @@ export default class Scope {
* Description
*/
rename(oldName: string, newName: string) {
rename(oldName: string, newName: string, block?) {
newName ||= this.generateUidIdentifier(oldName).name;
var info = this.getBinding(oldName);
if (!info) return;
var binding = info.identifier;
var scope = info.scope;
var scope = info.scope;
scope.traverse(scope.block, {
scope.traverse(block || scope.block, {
enter(node, parent, scope) {
if (t.isReferencedIdentifier(node, parent) && node.name === oldName) {
node.name = newName;
@@ -264,10 +264,12 @@ export default class Scope {
}
});
scope.removeOwnBinding(oldName);
scope.bindings[newName] = info;
if (!block) {
scope.removeOwnBinding(oldName);
scope.bindings[newName] = info;
binding.name = newName;
binding.name = newName;
}
}
/**