From 436c488ee31fa7082f0ecdd7d0c72b2a669250a5 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 18 Jan 2015 18:22:37 +1100 Subject: [PATCH] revamp let scoping transformer - closes #510 --- .../transformers/es6-let-scoping.js | 335 ++++++++---------- .../exec-block-scoped-2/exec.js | 12 + 2 files changed, 151 insertions(+), 196 deletions(-) create mode 100644 test/fixtures/transformation/es6-let-scoping/exec-block-scoped-2/exec.js diff --git a/lib/6to5/transformation/transformers/es6-let-scoping.js b/lib/6to5/transformation/transformers/es6-let-scoping.js index 65f8cf9d9c..8675611244 100644 --- a/lib/6to5/transformation/transformers/es6-let-scoping.js +++ b/lib/6to5/transformation/transformers/es6-let-scoping.js @@ -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. diff --git a/test/fixtures/transformation/es6-let-scoping/exec-block-scoped-2/exec.js b/test/fixtures/transformation/es6-let-scoping/exec-block-scoped-2/exec.js new file mode 100644 index 0000000000..e98bf47f5e --- /dev/null +++ b/test/fixtures/transformation/es6-let-scoping/exec-block-scoped-2/exec.js @@ -0,0 +1,12 @@ +assert.equal(() => { + let sum = 0; + let a = 0; + { + let a = 10; + for (let i = 0; i < a; i++) { + let a = 1; + sum += (() => a)(); + } + } + return sum; +}(), 10);