revamp let scoping transformer - closes #510

This commit is contained in:
Sebastian McKenzie
2015-01-18 18:22:37 +11:00
parent 93d99077e5
commit 436c488ee3
2 changed files with 151 additions and 196 deletions

View File

@@ -1,4 +1,5 @@
var traverse = require("../../traverse");
var Scope = require("../../traverse/scope");
var util = require("../../util");
var t = require("../../types");
var _ = require("lodash");
@@ -79,8 +80,9 @@ function LetScoping(loopParent, block, parent, scope, file) {
this.block = block;
this.file = file;
this.letReferences = {};
this.body = [];
this.outsideLetReferences = {};
this.letReferences = {};
this.body = [];
}
/**
@@ -89,27 +91,91 @@ function LetScoping(loopParent, block, parent, scope, file) {
LetScoping.prototype.run = function () {
var block = this.block;
if (block._letDone) return;
block._letDone = true;
this.info = this.getInfo();
// remap all let references that exist in upper scopes to their uid
this.remap();
// this is a block within a `Function` so we can safely leave it be
if (t.isFunction(this.parent)) return this.noClosure();
if (t.isFunction(this.parent)) return;
// this block has no let references so let's clean up
if (!this.info.keys.length) return this.noClosure();
var needsClosure = this.getLetReferences();
if (needsClosure) {
this.needsClosure();
} else {
this.remap();
}
};
// returns whether or not there are any outside let references within any
// functions
var referencesInClosure = this.getLetReferences();
/**
* Description
*/
// no need for a closure so let's clean up
if (!referencesInClosure) return this.noClosure();
LetScoping.prototype.remap = function () {
var letRefs = this.letReferences;
var scope = this.scope;
var file = this.file;
// alright, so since we aren't wrapping this block in a closure
// we have to check if any of our let variables collide with
// those in upper scopes and then if they do, generate a uid
// for them and replace all references with it
var remaps = {};
for (var key in letRefs) {
// just an Identifier node we collected in `getLetReferences`
// this is the defining identifier of a declaration
var ref = letRefs[key];
if (scope.parentHas(key)) {
var uid = file.generateUidIdentifier(ref.name, scope).name;
ref.name = uid;
remaps[key] = {
node: ref,
uid: uid
};
}
}
//
var replace = function (node, parent, scope, context, remaps) {
if (!t.isIdentifier(node)) return;
if (!t.isReferenced(node, parent)) return;
var remap = remaps[node.name];
if (!remap) return;
var own = scope.get(node.name, true);
if (own === remap.node) {
node.name = remap.uid;
} else {
// scope already has it's own declaration that doesn't
// match the one we have a stored replacement for
if (context) context.skip();
}
};
var traverseReplace = function (node, parent) {
replace(node, parent, scope, null, remaps);
traverse(node, { enter: replace }, scope, remaps);
};
var loopParent = this.loopParent;
if (loopParent) {
traverseReplace(loopParent.right, loopParent);
traverseReplace(loopParent.test, loopParent);
traverseReplace(loopParent.update, loopParent);
}
traverse(this.block, { enter: replace }, scope, remaps);
};
/**
* Description
*/
LetScoping.prototype.needsClosure = function () {
var block = this.block;
// if we're inside of a for loop then we search to see if there are any
// `break`s, `continue`s, `return`s etc
@@ -118,22 +184,16 @@ LetScoping.prototype.run = function () {
// hoist var references to retain scope
this.hoistVarDeclarations();
// set let references to plain var references
standardiseLets(this.info.declarators);
// turn letReferences into an array
var letReferences = _.values(this.letReferences);
// turn outsideLetReferences into an array
var params = _.values(this.outsideLetReferences);
// build the closure that we're going to wrap the block with
var fn = t.functionExpression(null, letReferences, t.blockStatement(block.body));
var fn = t.functionExpression(null, params, t.blockStatement(block.body));
fn._aliasFunction = true;
// replace the current block body with the one we're going to build
block.body = this.body;
// change upper scope references with their uid if they have one
var params = this.getParams(letReferences);
// build a call and a unique id that we can assign the return value to
var call = t.callExpression(fn, params);
var ret = this.file.generateUidIdentifier("ret", this.scope);
@@ -147,130 +207,77 @@ LetScoping.prototype.run = function () {
this.build(ret, call);
};
/**
* There are no let references accessed within a closure so we can just turn the
* lets into vars.
*/
LetScoping.prototype.noClosure = function () {
standardiseLets(this.info.declarators);
};
/**
* Traverse through block and replace all references that exist in a higher
* scope to their uids.
*/
LetScoping.prototype.remap = function () {
var duplicates = this.info.duplicates;
var block = this.block;
var scope = this.scope;
if (!this.info.hasDuplicates) return;
var replace = function (node, parent, scope, context, duplicates) {
if (!t.isIdentifier(node)) return;
if (!t.isReferenced(node, parent)) return;
var duplicate = duplicates[node.name];
if (!duplicate) return;
var own = scope.get(node.name, true);
if (own === duplicate.node) {
node.name = duplicate.uid;
} else {
// scope already has it's own declaration that doesn't
// match the one we have a stored replacement for
context.skip();
}
};
var traverseReplace = function (node, parent) {
replace(node, parent);
traverse(node, { enter: replace }, scope, duplicates);
};
var loopParent = this.loopParent;
if (loopParent) {
traverseReplace(loopParent.right, loopParent);
traverseReplace(loopParent.test, loopParent);
traverseReplace(loopParent.update, loopParent);
}
traverse(block, { enter: replace }, scope, duplicates);
};
/**
* Description
*
* @returns {Object}
*/
LetScoping.prototype.getInfo = function () {
LetScoping.prototype.getLetReferences = function () {
var block = this.block;
var scope = this.scope;
var file = this.file;
var opts = {
// array of `Identifier` names of let variables that appear lexically out of
// this scope but should be accessible - eg. `ForOfStatement`.left
outsideKeys: [],
// array of let `VariableDeclarator`s that are a part of this block
declarators: block._letDeclars || [],
// object of duplicate ids and their aliases - if there's an `Identifier`
// name that's used in an upper scope we generate a unique id and replace
// all references with it
duplicates: {},
hasDuplicates: false,
// array of `Identifier` names of let variables that are accessible within
// the current block
keys: []
};
var duplicates = function (id, key) {
// there's a variable with this exact name in an upper scope so we need
// to generate a new name
if (scope.parentHas(key, true)) {
var duplicate = opts.duplicates[key] = {
uid: file.generateUidIdentifier(key, scope).name,
node: id
};
id.name = duplicate.uid;
opts.hasDuplicates = true;
}
};
var declarators = block._letDeclars || [];
var declar;
for (var i = 0; i < opts.declarators.length; i++) {
declar = opts.declarators[i];
var keys = t.getIds(declar, true);
_.each(keys, duplicates);
keys = Object.keys(keys);
opts.outsideKeys = opts.outsideKeys.concat(keys);
opts.keys = opts.keys.concat(keys);
//
for (var i = 0; i < declarators.length; i++) {
declar = declarators[i];
_.extend(this.outsideLetReferences, t.getIds(declar, true));
}
//
if (block.body) {
for (i = 0; i < block.body.length; i++) {
declar = block.body[i];
if (!isLet(declar, block)) continue;
var declars = t.getIds(declar, true);
for (var key in declars) {
duplicates(declars[key], key);
opts.keys.push(key);
if (isLet(declar, block)) {
declarators = declarators.concat(declar.declarations);
}
}
}
return opts;
//
for (var i = 0; i < declarators.length; i++) {
var declar = declarators[i];
var keys = t.getIds(declar, true);
_.extend(this.letReferences, keys);
}
// set let references to plain var references
standardiseLets(declarators);
var state = {
letReferences: this.letReferences,
closurify: false
};
// traverse through this block, stopping on functions and checking if they
// contain any local let references
traverse(this.block, {
enter: function (node, parent, scope, context, state) {
if (t.isFunction(node)) {
traverse(node, {
enter: function (node, parent) {
// not an identifier so we have no use
if (!t.isIdentifier(node)) return;
// not a direct reference
if (!t.isReferenced(node, parent)) return;
// this scope has a variable with the same name so it couldn't belong
// to our let scope
if (scope.hasOwn(node.name, true)) return;
// not a part of our scope
if (!state.letReferences[node.name]) return;
state.closurify = true;
}
}, scope, state);
return context.skip();
}
}
}, this.scope, state);
return state.closurify;
};
/**
@@ -348,70 +355,6 @@ LetScoping.prototype.hoistVarDeclarations = function () {
}, this.scope, this);
};
/**
* Build up a parameter list that we'll call our closure wrapper with, replacing
* all duplicate ids with their uid.
*
* @param {Array} params
* @returns {Array}
*/
LetScoping.prototype.getParams = function (params) {
var info = this.info;
params = _.cloneDeep(params);
_.each(params, function (param) {
var duplicate = info.duplicates[param.name];
if (duplicate) param.name = duplicate.uid;
});
return params;
};
/**
* Get all let references within this block. Stopping whenever we reach another
* block.
*/
LetScoping.prototype.getLetReferences = function () {
var state = {
self: this,
closurify: false
};
// traverse through this block, stopping on functions and checking if they
// contain any outside let references
traverse(this.block, {
enter: function (node, parent, scope, context, state) {
if (t.isFunction(node)) {
traverse(node, {
enter: function (node, parent) {
// not an identifier so we have no use
if (!t.isIdentifier(node)) return;
// not a direct reference
if (!t.isReferenced(node, parent)) return;
// this scope has a variable with the same name so it couldn't belong
// to our let scope
if (scope.hasOwn(node.name, true)) return;
state.closurify = true;
// this key doesn't appear just outside our scope
if (!_.contains(state.self.info.outsideKeys, node.name)) return;
// push this badboy
state.self.letReferences[node.name] = node;
}
}, scope, state);
return context.skip();
}
}
}, this.scope, state);
return state.closurify;
};
/**
* Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with
* their declarations hoisted to before the closure wrapper.