diff --git a/src/babel/transformation/transformers/es6/block-scoping.js b/src/babel/transformation/transformers/es6/block-scoping.js index 465f53f562..23ea77a2f3 100644 --- a/src/babel/transformation/transformers/es6/block-scoping.js +++ b/src/babel/transformation/transformers/es6/block-scoping.js @@ -81,53 +81,6 @@ exports.BlockStatement = function (block, parent, scope, file) { } }; -/** - * Description - * - * @param {Boolean|Node} loopParent - * @param {Node} block - * @param {Node} parent - * @param {Scope} scope - * @param {File} file - */ - -function BlockScoping(loopParent, block, parent, scope, file) { - this.loopParent = loopParent; - this.parent = parent; - this.scope = scope; - this.block = block; - this.file = file; - - this.outsideLetReferences = object(); - this.hasLetReferences = false; - this.letReferences = block._letReferences = object(); - this.body = []; -} - -/** - * Start the ball rolling. - */ - -BlockScoping.prototype.run = function () { - var block = this.block; - if (block._letDone) return; - block._letDone = true; - - var needsClosure = this.getLetReferences(); - - // this is a block within a `Function/Program` so we can safely leave it be - if (t.isFunction(this.parent) || t.isProgram(this.block)) return; - - // we can skip everything - if (!this.hasLetReferences) return; - - if (needsClosure) { - this.wrapClosure(); - } else { - this.remap(); - } -}; - function replace(node, parent, scope, remaps) { if (!t.isReferencedIdentifier(node, parent)) return; @@ -153,114 +106,13 @@ function traverseReplace(node, parent, scope, remaps) { scope.traverse(node, replaceVisitor, remaps); } -/** - * Description - */ - -BlockScoping.prototype.remap = function () { - var hasRemaps = false; - var letRefs = this.letReferences; - var scope = this.scope; - - // 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 = object(); - - 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.parentHasBinding(key) || scope.hasGlobal(key)) { - var uid = scope.generateUidIdentifier(ref.name).name; - ref.name = uid; - - hasRemaps = true; - remaps[key] = remaps[uid] = { - binding: ref, - uid: uid - }; +var letReferenceBlockVisitor = { + enter(node, parent, scope, state) { + if (t.isFunction(node)) { + scope.traverse(node, letReferenceFunctionVisitor, state); + return this.skip(); } } - - if (!hasRemaps) return; - - // - - var loopParent = this.loopParent; - if (loopParent) { - traverseReplace(loopParent.right, loopParent, scope, remaps); - traverseReplace(loopParent.test, loopParent, scope, remaps); - traverseReplace(loopParent.update, loopParent, scope, remaps); - } - - scope.traverse(this.block, replaceVisitor, remaps); -}; - -/** - * Description - */ - -BlockScoping.prototype.wrapClosure = function () { - var block = this.block; - - var outsideRefs = this.outsideLetReferences; - - // remap loop heads with colliding variables - if (this.loopParent) { - for (var name in outsideRefs) { - var id = outsideRefs[name]; - - if (this.scope.hasGlobal(id.name)) { - delete outsideRefs[id.name]; - delete this.letReferences[id.name]; - - this.scope.rename(id.name); - - this.letReferences[id.name] = id; - outsideRefs[id.name] = id; - } - } - } - - // if we're inside of a for loop then we search to see if there are any - // `break`s, `continue`s, `return`s etc - this.has = this.checkLoop(); - - // hoist var references to retain scope - this.hoistVarDeclarations(); - - // turn outsideLetReferences into an array - var params = 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._aliasFunction = true; - - // 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 ret = this.scope.generateUidIdentifier("ret"); - - // handle generators - var hasYield = traverse.hasType(fn.body, this.scope, "YieldExpression", t.FUNCTION_TYPES); - if (hasYield) { - fn.generator = true; - call = t.yieldExpression(call, true); - } - - // handlers async functions - var hasAsync = traverse.hasType(fn.body, this.scope, "AwaitExpression", t.FUNCTION_TYPES); - if (hasAsync) { - fn.async = true; - call = t.awaitExpression(call, true); - } - - this.build(ret, call); }; var letReferenceFunctionVisitor = { @@ -279,65 +131,30 @@ var letReferenceFunctionVisitor = { } }; -var letReferenceBlockVisitor = { - enter(node, parent, scope, state) { - if (t.isFunction(node)) { - scope.traverse(node, letReferenceFunctionVisitor, state); +var hoistVarDeclarationsVisitor = { + enter(node, parent, scope, self) { + if (t.isForStatement(node)) { + if (isVar(node.init, node)) { + node.init = t.sequenceExpression(self.pushDeclar(node.init)); + } + } else if (t.isFor(node)) { + if (isVar(node.left, node)) { + node.left = node.left.declarations[0].id; + } + } else if (isVar(node, parent)) { + return self.pushDeclar(node).map(t.expressionStatement); + } else if (t.isFunction(node)) { return this.skip(); } } }; -/** - * Description - */ - -BlockScoping.prototype.getLetReferences = function () { - var block = this.block; - - var declarators = block._letDeclarators || []; - var declar; - - // - for (var i = 0; i < declarators.length; i++) { - declar = declarators[i]; - extend(this.outsideLetReferences, t.getBindingIdentifiers(declar)); - } - - // - if (block.body) { - for (i = 0; i < block.body.length; i++) { - declar = block.body[i]; - if (isLet(declar, block)) { - declarators = declarators.concat(declar.declarations); - } +var loopLabelVisitor = { + enter(node, parent, scope, state) { + if (t.isLabeledStatement(node)) { + state.innerLabels.push(node.label.name); } } - - // - for (i = 0; i < declarators.length; i++) { - declar = declarators[i]; - var keys = t.getBindingIdentifiers(declar); - extend(this.letReferences, keys); - this.hasLetReferences = true; - } - - // no let references so we can just quit - if (!this.hasLetReferences) return; - - // set let references to plain var references - standardizeLets(declarators); - - var state = { - letReferences: this.letReferences, - closurify: false - }; - - // traverse through this block, stopping on functions and checking if they - // contain any local let references - this.scope.traverse(this.block, letReferenceBlockVisitor, state); - - return state.closurify; }; var loopNodeTo = function (node) { @@ -400,160 +217,346 @@ var loopVisitor = { } }; -var loopLabelVisitor = { - enter(node, parent, scope, state) { - if (t.isLabeledStatement(node)) { - state.innerLabels.push(node.label.name); - } - } -}; +class BlockScoping { -/** - * If we're inside of a loop then traverse it and check if it has one of - * the following node types `ReturnStatement`, `BreakStatement`, - * `ContinueStatement` and replace it with a return value that we can track - * later on. - * - * @returns {Object} - */ + /** + * Description + * + * @param {Boolean|Node} loopParent + * @param {Node} block + * @param {Node} parent + * @param {Scope} scope + * @param {File} file + */ -BlockScoping.prototype.checkLoop = function () { - var state = { - hasBreakContinue: false, - ignoreLabeless: false, - innerLabels: [], - hasReturn: false, - isLoop: !!this.loopParent, - map: {} - }; + constructor(loopParent, block, parent, scope, file) { + this.loopParent = loopParent; + this.parent = parent; + this.scope = scope; + this.block = block; + this.file = file; - this.scope.traverse(this.block, loopLabelVisitor, state); - this.scope.traverse(this.block, loopVisitor, state); - - return state; -}; - -var hoistVarDeclarationsVisitor = { - enter(node, parent, scope, self) { - if (t.isForStatement(node)) { - if (isVar(node.init, node)) { - node.init = t.sequenceExpression(self.pushDeclar(node.init)); - } - } else if (t.isFor(node)) { - if (isVar(node.left, node)) { - node.left = node.left.declarations[0].id; - } - } else if (isVar(node, parent)) { - return self.pushDeclar(node).map(t.expressionStatement); - } else if (t.isFunction(node)) { - return this.skip(); - } - } -}; - -/** - * Hoist all var declarations in this block to before it so they retain scope - * once we wrap everything in a closure. - */ - -BlockScoping.prototype.hoistVarDeclarations = function () { - traverse(this.block, hoistVarDeclarationsVisitor, this.scope, this); -}; - -/** - * Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with - * their declarations hoisted to before the closure wrapper. - * - * @param {Node} node VariableDeclaration - * @returns {Array} - */ - -BlockScoping.prototype.pushDeclar = function (node) { - this.body.push(t.variableDeclaration(node.kind, node.declarations.map(function (declar) { - return t.variableDeclarator(declar.id); - }))); - - var replace = []; - - for (var i = 0; i < node.declarations.length; i++) { - var declar = node.declarations[i]; - if (!declar.init) continue; - - var expr = t.assignmentExpression("=", declar.id, declar.init); - replace.push(t.inherits(expr, declar)); + this.outsideLetReferences = object(); + this.hasLetReferences = false; + this.letReferences = block._letReferences = object(); + this.body = []; } - return replace; -}; + /** + * Start the ball rolling. + */ -/** - * Push the closure to the body. - * - * @param {Node} ret Identifier - * @param {Node} call CallExpression - */ + run() { + var block = this.block; + if (block._letDone) return; + block._letDone = true; -BlockScoping.prototype.build = function (ret, call) { - var has = this.has; - if (has.hasReturn || has.hasBreakContinue) { - this.buildHas(ret, call); - } else { - this.body.push(t.expressionStatement(call)); - } -}; + var needsClosure = this.getLetReferences(); -/** - * Description - * - * @param {Node} ret Identifier - * @param {Node} call CallExpression - */ + // this is a block within a `Function/Program` so we can safely leave it be + if (t.isFunction(this.parent) || t.isProgram(this.block)) return; -BlockScoping.prototype.buildHas = function (ret, call) { - var body = this.body; + // we can skip everything + if (!this.hasLetReferences) return; - body.push(t.variableDeclaration("var", [ - t.variableDeclarator(ret, call) - ])); - - var loopParent = this.loopParent; - var retCheck; - var has = this.has; - var cases = []; - - if (has.hasReturn) { - // typeof ret === "object" - retCheck = util.template("let-scoping-return", { - RETURN: ret - }); - } - - if (has.hasBreakContinue) { - if (!loopParent) { - throw new Error("Has no loop parent but we're trying to reassign breaks " + - "and continues, something is going wrong here."); - } - - for (var key in has.map) { - cases.push(t.switchCase(t.literal(key), [has.map[key]])); - } - - if (has.hasReturn) { - cases.push(t.switchCase(null, [retCheck])); - } - - if (cases.length === 1) { - var single = cases[0]; - body.push(this.file.attachAuxiliaryComment(t.ifStatement( - t.binaryExpression("===", ret, single.test), - single.consequent[0] - ))); + if (needsClosure) { + this.wrapClosure(); } else { - body.push(this.file.attachAuxiliaryComment(t.switchStatement(ret, cases))); - } - } else { - if (has.hasReturn) { - body.push(this.file.attachAuxiliaryComment(retCheck)); + this.remap(); } } -}; + + /** + * Description + */ + + remap() { + var hasRemaps = false; + var letRefs = this.letReferences; + var scope = this.scope; + + // 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 = object(); + + 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.parentHasBinding(key) || scope.hasGlobal(key)) { + var uid = scope.generateUidIdentifier(ref.name).name; + ref.name = uid; + + hasRemaps = true; + remaps[key] = remaps[uid] = { + binding: ref, + uid: uid + }; + } + } + + if (!hasRemaps) return; + + // + + var loopParent = this.loopParent; + if (loopParent) { + traverseReplace(loopParent.right, loopParent, scope, remaps); + traverseReplace(loopParent.test, loopParent, scope, remaps); + traverseReplace(loopParent.update, loopParent, scope, remaps); + } + + scope.traverse(this.block, replaceVisitor, remaps); + } + + /** + * Description + */ + + wrapClosure() { + var block = this.block; + + var outsideRefs = this.outsideLetReferences; + + // remap loop heads with colliding variables + if (this.loopParent) { + for (var name in outsideRefs) { + var id = outsideRefs[name]; + + if (this.scope.hasGlobal(id.name)) { + delete outsideRefs[id.name]; + delete this.letReferences[id.name]; + + this.scope.rename(id.name); + + this.letReferences[id.name] = id; + outsideRefs[id.name] = id; + } + } + } + + // if we're inside of a for loop then we search to see if there are any + // `break`s, `continue`s, `return`s etc + this.has = this.checkLoop(); + + // hoist var references to retain scope + this.hoistVarDeclarations(); + + // turn outsideLetReferences into an array + var params = 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._aliasFunction = true; + + // 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 ret = this.scope.generateUidIdentifier("ret"); + + // handle generators + var hasYield = traverse.hasType(fn.body, this.scope, "YieldExpression", t.FUNCTION_TYPES); + if (hasYield) { + fn.generator = true; + call = t.yieldExpression(call, true); + } + + // handlers async functions + var hasAsync = traverse.hasType(fn.body, this.scope, "AwaitExpression", t.FUNCTION_TYPES); + if (hasAsync) { + fn.async = true; + call = t.awaitExpression(call, true); + } + + this.build(ret, call); + } + + /** + * Description + */ + + getLetReferences() { + var block = this.block; + + var declarators = block._letDeclarators || []; + var declar; + + // + for (var i = 0; i < declarators.length; i++) { + declar = declarators[i]; + extend(this.outsideLetReferences, t.getBindingIdentifiers(declar)); + } + + // + if (block.body) { + for (i = 0; i < block.body.length; i++) { + declar = block.body[i]; + if (isLet(declar, block)) { + declarators = declarators.concat(declar.declarations); + } + } + } + + // + for (i = 0; i < declarators.length; i++) { + declar = declarators[i]; + var keys = t.getBindingIdentifiers(declar); + extend(this.letReferences, keys); + this.hasLetReferences = true; + } + + // no let references so we can just quit + if (!this.hasLetReferences) return; + + // set let references to plain var references + standardizeLets(declarators); + + var state = { + letReferences: this.letReferences, + closurify: false + }; + + // traverse through this block, stopping on functions and checking if they + // contain any local let references + this.scope.traverse(this.block, letReferenceBlockVisitor, state); + + return state.closurify; + } + + /** + * If we're inside of a loop then traverse it and check if it has one of + * the following node types `ReturnStatement`, `BreakStatement`, + * `ContinueStatement` and replace it with a return value that we can track + * later on. + * + * @returns {Object} + */ + + checkLoop() { + var state = { + hasBreakContinue: false, + ignoreLabeless: false, + innerLabels: [], + hasReturn: false, + isLoop: !!this.loopParent, + map: {} + }; + + this.scope.traverse(this.block, loopLabelVisitor, state); + this.scope.traverse(this.block, loopVisitor, state); + + return state; + } + + /** + * Hoist all var declarations in this block to before it so they retain scope + * once we wrap everything in a closure. + */ + + hoistVarDeclarations() { + traverse(this.block, hoistVarDeclarationsVisitor, this.scope, this); + } + + /** + * Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with + * their declarations hoisted to before the closure wrapper. + * + * @param {Node} node VariableDeclaration + * @returns {Array} + */ + + pushDeclar(node) { + this.body.push(t.variableDeclaration(node.kind, node.declarations.map(function (declar) { + return t.variableDeclarator(declar.id); + }))); + + var replace = []; + + for (var i = 0; i < node.declarations.length; i++) { + var declar = node.declarations[i]; + if (!declar.init) continue; + + var expr = t.assignmentExpression("=", declar.id, declar.init); + replace.push(t.inherits(expr, declar)); + } + + return replace; + } + + /** + * Push the closure to the body. + * + * @param {Node} ret Identifier + * @param {Node} call CallExpression + */ + + build(ret, call) { + var has = this.has; + if (has.hasReturn || has.hasBreakContinue) { + this.buildHas(ret, call); + } else { + this.body.push(t.expressionStatement(call)); + } + } + + /** + * Description + * + * @param {Node} ret Identifier + * @param {Node} call CallExpression + */ + + buildHas(ret, call) { + var body = this.body; + + body.push(t.variableDeclaration("var", [ + t.variableDeclarator(ret, call) + ])); + + var loopParent = this.loopParent; + var retCheck; + var has = this.has; + var cases = []; + + if (has.hasReturn) { + // typeof ret === "object" + retCheck = util.template("let-scoping-return", { + RETURN: ret + }); + } + + if (has.hasBreakContinue) { + if (!loopParent) { + throw new Error("Has no loop parent but we're trying to reassign breaks " + + "and continues, something is going wrong here."); + } + + for (var key in has.map) { + cases.push(t.switchCase(t.literal(key), [has.map[key]])); + } + + if (has.hasReturn) { + cases.push(t.switchCase(null, [retCheck])); + } + + if (cases.length === 1) { + var single = cases[0]; + body.push(this.file.attachAuxiliaryComment(t.ifStatement( + t.binaryExpression("===", ret, single.test), + single.consequent[0] + ))); + } else { + body.push(this.file.attachAuxiliaryComment(t.switchStatement(ret, cases))); + } + } else { + if (has.hasReturn) { + body.push(this.file.attachAuxiliaryComment(retCheck)); + } + } + } +} diff --git a/src/babel/transformation/transformers/es6/classes.js b/src/babel/transformation/transformers/es6/classes.js index 6c13608a56..b0957f9277 100644 --- a/src/babel/transformation/transformers/es6/classes.js +++ b/src/babel/transformation/transformers/es6/classes.js @@ -27,277 +27,280 @@ exports.ClassExpression = function (node, parent, scope, file) { return new ClassTransformer(node, file, scope, false).run(); }; -/** - * Description - * - * @param {Node} node - * @param {File} file - * @param {Scope} scope - * @param {Boolean} isStatement - */ +class ClassTransformer { -function ClassTransformer(node, file, scope, isStatement) { - this.isStatement = isStatement; - this.scope = scope; - this.node = node; - this.file = file; + /** + * Description + * + * @param {Node} node + * @param {File} file + * @param {Scope} scope + * @param {Boolean} isStatement + */ - this.hasInstanceMutators = false; - this.hasStaticMutators = false; + constructor(node, file, scope, isStatement) { + this.isStatement = isStatement; + this.scope = scope; + this.node = node; + this.file = file; - this.instanceMutatorMap = {}; - this.staticMutatorMap = {}; - this.hasConstructor = false; - this.className = node.id || scope.generateUidIdentifier("class"); - this.superName = node.superClass || t.identifier("Function"); - this.hasSuper = !!node.superClass; - this.isLoose = file.isLoose("es6.classes"); -} + this.hasInstanceMutators = false; + this.hasStaticMutators = false; -/** - * Description - * - * @returns {Array} - */ - -ClassTransformer.prototype.run = function () { - var superName = this.superName; - var className = this.className; - var classBody = this.node.body.body; - var file = this.file; - - // - - var body = this.body = []; - - var constructorBody = t.blockStatement([ - t.expressionStatement(t.callExpression(file.addHelper("class-call-check"), [ - t.thisExpression(), - className - ])) - ]); - - var constructor; - if (this.node.id) { - constructor = t.functionDeclaration(className, [], constructorBody); - body.push(constructor); - } else { - var constructorName = null; - // when a class has no parent and there is only a constructor or no body - // then the constructor is not wrapped in a closure and needs to be named - var containsOnlyConstructor = classBody.length === 1 && classBody[0].key.name === "constructor"; - if (!this.hasSuper && (classBody.length === 0 || containsOnlyConstructor)) { - constructorName = className; - } - - constructor = t.functionExpression(constructorName, [], constructorBody); - body.push(t.variableDeclaration("var", [ - t.variableDeclarator(className, constructor) - ])); - } - this.constructor = constructor; - - var closureParams = []; - var closureArgs = []; - - // - - if (this.hasSuper) { - closureArgs.push(superName); - - if (!t.isIdentifier(superName)) { - superName = this.scope.generateUidBasedOnNode(superName, this.file); - } - - closureParams.push(superName); - - this.superName = superName; - body.push(t.expressionStatement(t.callExpression(file.addHelper("inherits"), [className, superName]))); + this.instanceMutatorMap = {}; + this.staticMutatorMap = {}; + this.hasConstructor = false; + this.className = node.id || scope.generateUidIdentifier("class"); + this.superName = node.superClass || t.identifier("Function"); + this.hasSuper = !!node.superClass; + this.isLoose = file.isLoose("es6.classes"); } - this.buildBody(); + /** + * Description + * + * @returns {Array} + */ - t.inheritsComments(body[0], this.node); + run() { + var superName = this.superName; + var className = this.className; + var classBody = this.node.body.body; + var file = this.file; - var init; + // - if (body.length === 1) { - // only a constructor so no need for a closure container - init = t.toExpression(constructor); - } else { - body.push(t.returnStatement(className)); - init = t.callExpression( - t.functionExpression(null, closureParams, t.blockStatement(body)), - closureArgs - ); - } + var body = this.body = []; - if (this.isStatement) { - return t.variableDeclaration("let", [ - t.variableDeclarator(className, init) + var constructorBody = t.blockStatement([ + t.expressionStatement(t.callExpression(file.addHelper("class-call-check"), [ + t.thisExpression(), + className + ])) ]); - } else { - return init; - } -}; -/** - * Description - */ - -ClassTransformer.prototype.buildBody = function () { - var constructor = this.constructor; - var className = this.className; - var superName = this.superName; - var classBody = this.node.body.body; - var body = this.body; - - for (var i = 0; i < classBody.length; i++) { - var node = classBody[i]; - if (t.isMethodDefinition(node)) { - var replaceSupers = new ReplaceSupers({ - methodNode: node, - className: this.className, - superName: this.superName, - isStatic: node.static, - isLoose: this.isLoose, - scope: this.scope, - file: this.file - }, true); - replaceSupers.replace(); - - if ((!node.computed && t.isIdentifier(node.key, { name: "constructor" })) || t.isLiteral(node.key, { value: "constructor" })) { - this.pushConstructor(node); - } else { - this.pushMethod(node); + var constructor; + if (this.node.id) { + constructor = t.functionDeclaration(className, [], constructorBody); + body.push(constructor); + } else { + var constructorName = null; + // when a class has no parent and there is only a constructor or no body + // then the constructor is not wrapped in a closure and needs to be named + var containsOnlyConstructor = classBody.length === 1 && classBody[0].key.name === "constructor"; + if (!this.hasSuper && (classBody.length === 0 || containsOnlyConstructor)) { + constructorName = className; } - } else if (t.isPrivateDeclaration(node)) { - this.closure = true; - body.unshift(node); - } else if (t.isClassProperty(node)) { - this.pushProperty(node); + + constructor = t.functionExpression(constructorName, [], constructorBody); + body.push(t.variableDeclaration("var", [ + t.variableDeclarator(className, constructor) + ])); + } + this.constructor = constructor; + + var closureParams = []; + var closureArgs = []; + + // + + if (this.hasSuper) { + closureArgs.push(superName); + + if (!t.isIdentifier(superName)) { + superName = this.scope.generateUidBasedOnNode(superName, this.file); + } + + closureParams.push(superName); + + this.superName = superName; + body.push(t.expressionStatement(t.callExpression(file.addHelper("inherits"), [className, superName]))); + } + + this.buildBody(); + + t.inheritsComments(body[0], this.node); + + var init; + + if (body.length === 1) { + // only a constructor so no need for a closure container + init = t.toExpression(constructor); + } else { + body.push(t.returnStatement(className)); + init = t.callExpression( + t.functionExpression(null, closureParams, t.blockStatement(body)), + closureArgs + ); + } + + if (this.isStatement) { + return t.variableDeclaration("let", [ + t.variableDeclarator(className, init) + ]); + } else { + return init; } } - // we have no constructor, we have a super, and the super doesn't appear to be falsy - if (!this.hasConstructor && this.hasSuper && !t.isFalsyExpression(superName)) { - var helperName = "class-super-constructor-call"; - if (this.isLoose) helperName += "-loose"; - constructor.body.body.push(util.template(helperName, { - CLASS_NAME: className, - SUPER_NAME: this.superName - }, true)); - } + /** + * Description + */ - var instanceProps; - var staticProps; + buildBody() { + var constructor = this.constructor; + var className = this.className; + var superName = this.superName; + var classBody = this.node.body.body; + var body = this.body; - if (this.hasInstanceMutators) { - instanceProps = defineMap.build(this.instanceMutatorMap); - } + for (var i = 0; i < classBody.length; i++) { + var node = classBody[i]; + if (t.isMethodDefinition(node)) { + var replaceSupers = new ReplaceSupers({ + methodNode: node, + className: this.className, + superName: this.superName, + isStatic: node.static, + isLoose: this.isLoose, + scope: this.scope, + file: this.file + }, true); + replaceSupers.replace(); - if (this.hasStaticMutators) { - staticProps = defineMap.build(this.staticMutatorMap); - } - - if (instanceProps || staticProps) { - staticProps ||= t.literal(null); - - var args = [className, staticProps]; - if (instanceProps) args.push(instanceProps); - - body.push(t.expressionStatement( - t.callExpression(this.file.addHelper("prototype-properties"), args) - )); - } -}; - -/** - * Push a method to its respective mutatorMap. - * - * @param {Node} node MethodDefinition - */ - -ClassTransformer.prototype.pushMethod = function (node) { - var methodName = node.key; - - var kind = node.kind; - - if (kind === "") { - nameMethod.property(node, this.file, this.scope); - - if (this.isLoose) { - // use assignments instead of define properties for loose classes - - var className = this.className; - if (!node.static) className = t.memberExpression(className, t.identifier("prototype")); - methodName = t.memberExpression(className, methodName, node.computed); - - var expr = t.expressionStatement(t.assignmentExpression("=", methodName, node.value)); - t.inheritsComments(expr, node); - this.body.push(expr); - return; + if ((!node.computed && t.isIdentifier(node.key, { name: "constructor" })) || t.isLiteral(node.key, { value: "constructor" })) { + this.pushConstructor(node); + } else { + this.pushMethod(node); + } + } else if (t.isPrivateDeclaration(node)) { + this.closure = true; + body.unshift(node); + } else if (t.isClassProperty(node)) { + this.pushProperty(node); + } } - kind = "value"; + // we have no constructor, we have a super, and the super doesn't appear to be falsy + if (!this.hasConstructor && this.hasSuper && !t.isFalsyExpression(superName)) { + var helperName = "class-super-constructor-call"; + if (this.isLoose) helperName += "-loose"; + constructor.body.body.push(util.template(helperName, { + CLASS_NAME: className, + SUPER_NAME: this.superName + }, true)); + } + + var instanceProps; + var staticProps; + + if (this.hasInstanceMutators) { + instanceProps = defineMap.build(this.instanceMutatorMap); + } + + if (this.hasStaticMutators) { + staticProps = defineMap.build(this.staticMutatorMap); + } + + if (instanceProps || staticProps) { + staticProps ||= t.literal(null); + + var args = [className, staticProps]; + if (instanceProps) args.push(instanceProps); + + body.push(t.expressionStatement( + t.callExpression(this.file.addHelper("prototype-properties"), args) + )); + } } - var mutatorMap = this.instanceMutatorMap; - if (node.static) { - this.hasStaticMutators = true; - mutatorMap = this.staticMutatorMap; - } else { - this.hasInstanceMutators = true; + /** + * Push a method to its respective mutatorMap. + * + * @param {Node} node MethodDefinition + */ + + pushMethod(node) { + var methodName = node.key; + + var kind = node.kind; + + if (kind === "") { + nameMethod.property(node, this.file, this.scope); + + if (this.isLoose) { + // use assignments instead of define properties for loose classes + + var className = this.className; + if (!node.static) className = t.memberExpression(className, t.identifier("prototype")); + methodName = t.memberExpression(className, methodName, node.computed); + + var expr = t.expressionStatement(t.assignmentExpression("=", methodName, node.value)); + t.inheritsComments(expr, node); + this.body.push(expr); + return; + } + + kind = "value"; + } + + var mutatorMap = this.instanceMutatorMap; + if (node.static) { + this.hasStaticMutators = true; + mutatorMap = this.staticMutatorMap; + } else { + this.hasInstanceMutators = true; + } + + defineMap.push(mutatorMap, methodName, kind, node.computed, node); + defineMap.push(mutatorMap, methodName, "enumerable", node.computed, false); } - defineMap.push(mutatorMap, methodName, kind, node.computed, node); - defineMap.push(mutatorMap, methodName, "enumerable", node.computed, false); -}; + /** + * Description + * + * @param {Node} node + */ -/** - * Description - * - * @param {Node} node - */ + pushProperty(node) { + if (!node.value) return; -ClassTransformer.prototype.pushProperty = function (node) { - if (!node.value) return; + var key; - var key; - - if (node.static) { - key = t.memberExpression(this.className, node.key); - this.body.push( - t.expressionStatement(t.assignmentExpression("=", key, node.value)) - ); - } else { - key = t.memberExpression(t.thisExpression(), node.key); - this.constructor.body.body.unshift( - t.expressionStatement(t.assignmentExpression("=", key, node.value)) - ); - } -}; - -/** - * Replace the constructor body of our class. - * - * @param {Node} method MethodDefinition - */ - -ClassTransformer.prototype.pushConstructor = function (method) { - if (method.kind) { - throw this.file.errorWithNode(method, messages.get("classesIllegalConstructorKind")); + if (node.static) { + key = t.memberExpression(this.className, node.key); + this.body.push( + t.expressionStatement(t.assignmentExpression("=", key, node.value)) + ); + } else { + key = t.memberExpression(t.thisExpression(), node.key); + this.constructor.body.body.unshift( + t.expressionStatement(t.assignmentExpression("=", key, node.value)) + ); + } } - var construct = this.constructor; - var fn = method.value; + /** + * Replace the constructor body of our class. + * + * @param {Node} method MethodDefinition + */ - this.hasConstructor = true; + pushConstructor(method) { + if (method.kind) { + throw this.file.errorWithNode(method, messages.get("classesIllegalConstructorKind")); + } - t.inherits(construct, fn); - t.inheritsComments(construct, method); + var construct = this.constructor; + var fn = method.value; - construct._ignoreUserWhitespace = true; - construct.params = fn.params; - construct.body.body = construct.body.body.concat(fn.body.body); -}; + this.hasConstructor = true; + + t.inherits(construct, fn); + t.inheritsComments(construct, method); + + construct._ignoreUserWhitespace = true; + construct.params = fn.params; + construct.body.body = construct.body.body.concat(fn.body.body); + } +} diff --git a/src/babel/transformation/transformers/es6/destructuring.js b/src/babel/transformation/transformers/es6/destructuring.js index 2743349afb..1e9a59481f 100644 --- a/src/babel/transformation/transformers/es6/destructuring.js +++ b/src/babel/transformation/transformers/es6/destructuring.js @@ -3,269 +3,6 @@ var t = require("../../../types"); exports.check = t.isPattern; -function DestructuringTransformer(opts) { - this.blockHoist = opts.blockHoist; - this.operator = opts.operator; - this.nodes = opts.nodes; - this.scope = opts.scope; - this.file = opts.file; - this.kind = opts.kind; -} - -DestructuringTransformer.prototype.buildVariableAssignment = function (id, init) { - var op = this.operator; - if (t.isMemberExpression(id)) op = "="; - - var node; - - if (op) { - node = t.expressionStatement(t.assignmentExpression(op, id, init)); - } else { - node = t.variableDeclaration(this.kind, [ - t.variableDeclarator(id, init) - ]); - } - - node._blockHoist = this.blockHoist; - - return node; -}; - -DestructuringTransformer.prototype.buildVariableDeclaration = function (id, init) { - var declar = t.variableDeclaration("var", [ - t.variableDeclarator(id, init) - ]); - declar._blockHoist = this.blockHoist; - return declar; -}; - -DestructuringTransformer.prototype.push = function (id, init) { - if (t.isObjectPattern(id)) { - this.pushObjectPattern(id, init); - } else if (t.isArrayPattern(id)) { - this.pushArrayPattern(id, init); - } else if (t.isAssignmentPattern(id)) { - this.pushAssignmentPattern(id, init); - } else { - this.nodes.push(this.buildVariableAssignment(id, init)); - } -}; - -DestructuringTransformer.prototype.get = function () { - -}; - -DestructuringTransformer.prototype.pushAssignmentPattern = function (pattern, valueRef) { - // we need to assign the current value of the assignment to avoid evaluating - // it more than once - - var tempValueRef = this.scope.generateUidBasedOnNode(valueRef); - - var declar = t.variableDeclaration("var", [ - t.variableDeclarator(tempValueRef, valueRef) - ]); - declar._blockHoist = this.blockHoist; - this.nodes.push(declar); - - // - - this.nodes.push(this.buildVariableAssignment( - pattern.left, - t.conditionalExpression( - t.binaryExpression("===", tempValueRef, t.identifier("undefined")), - pattern.right, - tempValueRef - ) - )); -}; - -DestructuringTransformer.prototype.pushObjectSpread = function (pattern, objRef, spreadProp, spreadPropIndex) { - // get all the keys that appear in this object before the current spread - - var keys = []; - - for (var i = 0; i < pattern.properties.length; i++) { - var prop = pattern.properties[i]; - - // we've exceeded the index of the spread property to all properties to the - // right need to be ignored - if (i >= spreadPropIndex) break; - - // ignore other spread properties - if (t.isSpreadProperty(prop)) continue; - - var key = prop.key; - if (t.isIdentifier(key)) key = t.literal(prop.key.name); - keys.push(key); - } - - keys = t.arrayExpression(keys); - - // - - var value = t.callExpression(this.file.addHelper("object-without-properties"), [objRef, keys]); - this.nodes.push(this.buildVariableAssignment(spreadProp.argument, value)); -}; - -DestructuringTransformer.prototype.pushObjectProperty = function (prop, propRef) { - if (t.isLiteral(prop.key)) prop.computed = true; - - var pattern = prop.value; - var objRef = t.memberExpression(propRef, prop.key, prop.computed); - - if (t.isPattern(pattern)) { - this.push(pattern, objRef); - } else { - this.nodes.push(this.buildVariableAssignment(pattern, objRef)); - } -}; - -DestructuringTransformer.prototype.pushObjectPattern = function (pattern, objRef) { - // https://github.com/babel/babel/issues/681 - - if (!pattern.properties.length) { - this.nodes.push(t.expressionStatement( - t.callExpression(this.file.addHelper("object-destructuring-empty"), [objRef]) - )); - } - - // if we have more than one properties in this pattern and the objectRef is a - // member expression then we need to assign it to a temporary variable so it's - // only evaluated once - - if (pattern.properties.length > 1 && t.isMemberExpression(objRef)) { - var temp = this.scope.generateUidBasedOnNode(objRef, this.file); - this.nodes.push(this.buildVariableDeclaration(temp, objRef)); - objRef = temp; - } - - // - - for (var i = 0; i < pattern.properties.length; i++) { - var prop = pattern.properties[i]; - if (t.isSpreadProperty(prop)) { - this.pushObjectSpread(pattern, objRef, prop, i); - } else { - this.pushObjectProperty(prop, objRef); - } - } -}; - -var hasRest = function (pattern) { - for (var i = 0; i < pattern.elements.length; i++) { - if (t.isRestElement(pattern.elements[i])) { - return true; - } - } - return false; -}; - -DestructuringTransformer.prototype.canUnpackArrayPattern = function (pattern, arr) { - // not an array so there's no way we can deal with this - if (!t.isArrayExpression(arr)) return false; - - // pattern has less elements than the array and doesn't have a rest so some - // elements wont be evaluated - if (pattern.elements.length > arr.elements.length) return; - if (pattern.elements.length < arr.elements.length && !hasRest(pattern)) return false; - - // deopt on holes - for (var i = 0; i < pattern.elements.length; i++) { - if (!pattern.elements[i]) return false; - } - - return true; -}; - -DestructuringTransformer.prototype.pushUnpackedArrayPattern = function (pattern, arr) { - for (var i = 0; i < pattern.elements.length; i++) { - var elem = pattern.elements[i]; - if (t.isRestElement(elem)) { - this.push(elem.argument, t.arrayExpression(arr.elements.slice(i))); - } else { - this.push(elem, arr.elements[i]); - } - } -}; - -DestructuringTransformer.prototype.pushArrayPattern = function (pattern, arrayRef) { - if (!pattern.elements) return; - - // optimise basic array destructuring of an array expression - // - // we can't do this to a pattern of unequal size to it's right hand - // array expression as then there will be values that wont be evaluated - // - // eg: var [a, b] = [1, 2]; - - if (this.canUnpackArrayPattern(pattern, arrayRef)) { - return this.pushUnpackedArrayPattern(pattern, arrayRef); - } - - // if we have a rest then we need all the elements so don't tell - // `scope.toArray` to only get a certain amount - - var count = !hasRest(pattern) && pattern.elements.length; - - // so we need to ensure that the `arrayRef` is an array, `scope.toArray` will - // return a locally bound identifier if it's been inferred to be an array, - // otherwise it'll be a call to a helper that will ensure it's one - - var toArray = this.scope.toArray(arrayRef, count); - - if (t.isIdentifier(toArray)) { - // we've been given an identifier so it must have been inferred to be an - // array - arrayRef = toArray; - } else { - arrayRef = this.scope.generateUidBasedOnNode(arrayRef); - this.nodes.push(this.buildVariableDeclaration(arrayRef, toArray)); - this.scope.assignTypeGeneric(arrayRef.name, "Array"); - } - - // - - for (var i = 0; i < pattern.elements.length; i++) { - var elem = pattern.elements[i]; - - // hole - if (!elem) continue; - - var elemRef; - - if (t.isRestElement(elem)) { - elemRef = this.scope.toArray(arrayRef); - - if (i > 0) { - elemRef = t.callExpression(t.memberExpression(elemRef, t.identifier("slice")), [t.literal(i)]); - } - - // set the element to the rest element argument since we've dealt with it - // being a rest already - elem = elem.argument; - } else { - elemRef = t.memberExpression(arrayRef, t.literal(i), true); - } - - this.push(elem, elemRef); - } -}; - -DestructuringTransformer.prototype.init = function (pattern, ref) { - // trying to destructure a value that we can't evaluate more than once so we - // need to save it to a variable - - if (!t.isArrayExpression(ref) && !t.isMemberExpression(ref) && !t.isIdentifier(ref)) { - var key = this.scope.generateUidBasedOnNode(ref); - this.nodes.push(this.buildVariableDeclaration(key, ref)); - ref = key; - } - - // - - this.push(pattern, ref); -}; - exports.ForInStatement = exports.ForOfStatement = function (node, parent, scope, file) { var left = node.left; @@ -481,3 +218,264 @@ exports.VariableDeclaration = function (node, parent, scope, file) { return nodes; }; + +var hasRest = function (pattern) { + for (var i = 0; i < pattern.elements.length; i++) { + if (t.isRestElement(pattern.elements[i])) { + return true; + } + } + return false; +}; + +class DestructuringTransformer { + constructor(opts) { + this.blockHoist = opts.blockHoist; + this.operator = opts.operator; + this.nodes = opts.nodes; + this.scope = opts.scope; + this.file = opts.file; + this.kind = opts.kind; + } + + buildVariableAssignment(id, init) { + var op = this.operator; + if (t.isMemberExpression(id)) op = "="; + + var node; + + if (op) { + node = t.expressionStatement(t.assignmentExpression(op, id, init)); + } else { + node = t.variableDeclaration(this.kind, [ + t.variableDeclarator(id, init) + ]); + } + + node._blockHoist = this.blockHoist; + + return node; + } + + buildVariableDeclaration(id, init) { + var declar = t.variableDeclaration("var", [ + t.variableDeclarator(id, init) + ]); + declar._blockHoist = this.blockHoist; + return declar; + } + + push(id, init) { + if (t.isObjectPattern(id)) { + this.pushObjectPattern(id, init); + } else if (t.isArrayPattern(id)) { + this.pushArrayPattern(id, init); + } else if (t.isAssignmentPattern(id)) { + this.pushAssignmentPattern(id, init); + } else { + this.nodes.push(this.buildVariableAssignment(id, init)); + } + } + + pushAssignmentPattern(pattern, valueRef) { + // we need to assign the current value of the assignment to avoid evaluating + // it more than once + + var tempValueRef = this.scope.generateUidBasedOnNode(valueRef); + + var declar = t.variableDeclaration("var", [ + t.variableDeclarator(tempValueRef, valueRef) + ]); + declar._blockHoist = this.blockHoist; + this.nodes.push(declar); + + // + + this.nodes.push(this.buildVariableAssignment( + pattern.left, + t.conditionalExpression( + t.binaryExpression("===", tempValueRef, t.identifier("undefined")), + pattern.right, + tempValueRef + ) + )); + } + + pushObjectSpread(pattern, objRef, spreadProp, spreadPropIndex) { + // get all the keys that appear in this object before the current spread + + var keys = []; + + for (var i = 0; i < pattern.properties.length; i++) { + var prop = pattern.properties[i]; + + // we've exceeded the index of the spread property to all properties to the + // right need to be ignored + if (i >= spreadPropIndex) break; + + // ignore other spread properties + if (t.isSpreadProperty(prop)) continue; + + var key = prop.key; + if (t.isIdentifier(key)) key = t.literal(prop.key.name); + keys.push(key); + } + + keys = t.arrayExpression(keys); + + // + + var value = t.callExpression(this.file.addHelper("object-without-properties"), [objRef, keys]); + this.nodes.push(this.buildVariableAssignment(spreadProp.argument, value)); + } + + pushObjectProperty(prop, propRef) { + if (t.isLiteral(prop.key)) prop.computed = true; + + var pattern = prop.value; + var objRef = t.memberExpression(propRef, prop.key, prop.computed); + + if (t.isPattern(pattern)) { + this.push(pattern, objRef); + } else { + this.nodes.push(this.buildVariableAssignment(pattern, objRef)); + } + } + + pushObjectPattern(pattern, objRef) { + // https://github.com/babel/babel/issues/681 + + if (!pattern.properties.length) { + this.nodes.push(t.expressionStatement( + t.callExpression(this.file.addHelper("object-destructuring-empty"), [objRef]) + )); + } + + // if we have more than one properties in this pattern and the objectRef is a + // member expression then we need to assign it to a temporary variable so it's + // only evaluated once + + if (pattern.properties.length > 1 && t.isMemberExpression(objRef)) { + var temp = this.scope.generateUidBasedOnNode(objRef, this.file); + this.nodes.push(this.buildVariableDeclaration(temp, objRef)); + objRef = temp; + } + + // + + for (var i = 0; i < pattern.properties.length; i++) { + var prop = pattern.properties[i]; + if (t.isSpreadProperty(prop)) { + this.pushObjectSpread(pattern, objRef, prop, i); + } else { + this.pushObjectProperty(prop, objRef); + } + } + } + + canUnpackArrayPattern(pattern, arr) { + // not an array so there's no way we can deal with this + if (!t.isArrayExpression(arr)) return false; + + // pattern has less elements than the array and doesn't have a rest so some + // elements wont be evaluated + if (pattern.elements.length > arr.elements.length) return; + if (pattern.elements.length < arr.elements.length && !hasRest(pattern)) return false; + + // deopt on holes + for (var i = 0; i < pattern.elements.length; i++) { + if (!pattern.elements[i]) return false; + } + + return true; + } + + pushUnpackedArrayPattern(pattern, arr) { + for (var i = 0; i < pattern.elements.length; i++) { + var elem = pattern.elements[i]; + if (t.isRestElement(elem)) { + this.push(elem.argument, t.arrayExpression(arr.elements.slice(i))); + } else { + this.push(elem, arr.elements[i]); + } + } + } + + pushArrayPattern(pattern, arrayRef) { + if (!pattern.elements) return; + + // optimise basic array destructuring of an array expression + // + // we can't do this to a pattern of unequal size to it's right hand + // array expression as then there will be values that wont be evaluated + // + // eg: var [a, b] = [1, 2]; + + if (this.canUnpackArrayPattern(pattern, arrayRef)) { + return this.pushUnpackedArrayPattern(pattern, arrayRef); + } + + // if we have a rest then we need all the elements so don't tell + // `scope.toArray` to only get a certain amount + + var count = !hasRest(pattern) && pattern.elements.length; + + // so we need to ensure that the `arrayRef` is an array, `scope.toArray` will + // return a locally bound identifier if it's been inferred to be an array, + // otherwise it'll be a call to a helper that will ensure it's one + + var toArray = this.scope.toArray(arrayRef, count); + + if (t.isIdentifier(toArray)) { + // we've been given an identifier so it must have been inferred to be an + // array + arrayRef = toArray; + } else { + arrayRef = this.scope.generateUidBasedOnNode(arrayRef); + this.nodes.push(this.buildVariableDeclaration(arrayRef, toArray)); + this.scope.assignTypeGeneric(arrayRef.name, "Array"); + } + + // + + for (var i = 0; i < pattern.elements.length; i++) { + var elem = pattern.elements[i]; + + // hole + if (!elem) continue; + + var elemRef; + + if (t.isRestElement(elem)) { + elemRef = this.scope.toArray(arrayRef); + + if (i > 0) { + elemRef = t.callExpression(t.memberExpression(elemRef, t.identifier("slice")), [t.literal(i)]); + } + + // set the element to the rest element argument since we've dealt with it + // being a rest already + elem = elem.argument; + } else { + elemRef = t.memberExpression(arrayRef, t.literal(i), true); + } + + this.push(elem, elemRef); + } + } + + init(pattern, ref) { + // trying to destructure a value that we can't evaluate more than once so we + // need to save it to a variable + + if (!t.isArrayExpression(ref) && !t.isMemberExpression(ref) && !t.isIdentifier(ref)) { + var key = this.scope.generateUidBasedOnNode(ref); + this.nodes.push(this.buildVariableDeclaration(key, ref)); + ref = key; + } + + // + + this.push(pattern, ref); + } +} diff --git a/src/babel/transformation/transformers/es6/tail-call.js b/src/babel/transformation/transformers/es6/tail-call.js index e2ff372e1e..a6d9b123a8 100644 --- a/src/babel/transformation/transformers/es6/tail-call.js +++ b/src/babel/transformation/transformers/es6/tail-call.js @@ -5,302 +5,15 @@ var util = require("../../../util"); var map = require("lodash/collection/map"); var t = require("../../../types"); +exports.Function = function (node, parent, scope, file) { + var tailCall = new TailCallTransformer(node, scope, file); + tailCall.run(); +}; + function returnBlock(expr) { return t.blockStatement([t.returnStatement(expr)]); } -function TailCallTransformer(node, scope, file) { - this.hasTailRecursion = false; - this.needsArguments = false; - this.setsArguments = false; - this.needsThis = false; - this.ownerId = node.id; - this.vars = []; - - this.scope = scope; - this.file = file; - this.node = node; -} - -TailCallTransformer.prototype.getArgumentsId = function () { - return this.argumentsId ||= this.scope.generateUidIdentifier("arguments"); -}; - -TailCallTransformer.prototype.getThisId = function () { - return this.thisId ||= this.scope.generateUidIdentifier("this"); -}; - -TailCallTransformer.prototype.getLeftId = function () { - return this.leftId ||= this.scope.generateUidIdentifier("left"); -}; - -TailCallTransformer.prototype.getFunctionId = function () { - return this.functionId ||= this.scope.generateUidIdentifier("function"); -}; - -TailCallTransformer.prototype.getAgainId = function () { - return this.againId ||= this.scope.generateUidIdentifier("again"); -}; - -TailCallTransformer.prototype.getParams = function () { - var params = this.params; - - if (!params) { - params = this.node.params; - this.paramDecls = []; - - for (var i = 0; i < params.length; i++) { - var param = params[i]; - if (!param._isDefaultPlaceholder) { - this.paramDecls.push(t.variableDeclarator( - param, - params[i] = this.scope.generateUidIdentifier("x") - )); - } - } - } - - return this.params = params; -}; - -TailCallTransformer.prototype.hasDeopt = function () { - // check if the ownerId has been reassigned, if it has then it's not safe to - // perform optimisations - var ownerIdInfo = this.scope.getBindingInfo(this.ownerId.name); - return ownerIdInfo && ownerIdInfo.reassigned; -}; - -TailCallTransformer.prototype.run = function () { - var scope = this.scope; - var node = this.node; - - // only tail recursion can be optimized as for now, so we can skip anonymous - // functions entirely - var ownerId = this.ownerId; - if (!ownerId) return; - - // traverse the function and look for tail recursion - scope.traverse(node, firstPass, this); - - if (!this.hasTailRecursion) return; - - if (this.hasDeopt()) { - this.file.logDeopt(node, messages.get("tailCallReassignmentDeopt")); - return; - } - - // - - scope.traverse(node, secondPass, this); - - if (!this.needsThis || !this.needsArguments) { - scope.traverse(node, thirdPass, this); - } - - var body = t.ensureBlock(node).body; - - if (this.vars.length > 0) { - var declarations = flatten(map(this.vars, function (decl) { - return decl.declarations; - }, this)); - var statement = reduceRight(declarations, function (expr, decl) { - return t.assignmentExpression("=", decl.id, expr); - }, t.identifier("undefined")); - body.unshift(t.expressionStatement(statement)); - } - - var paramDecls = this.paramDecls; - if (paramDecls.length > 0) { - body.unshift(t.variableDeclaration("var", paramDecls)); - } - - body.unshift(t.expressionStatement( - t.assignmentExpression("=", this.getAgainId(), t.literal(false))) - ); - - node.body = util.template("tail-call-body", { - AGAIN_ID: this.getAgainId(), - THIS_ID: this.thisId, - ARGUMENTS_ID: this.argumentsId, - FUNCTION_ID: this.getFunctionId(), - BLOCK: node.body - }); - - var topVars = []; - - if (this.needsThis) { - topVars.push(t.variableDeclarator(this.getThisId(), t.thisExpression())); - } - - if (this.needsArguments || this.setsArguments) { - var decl = t.variableDeclarator(this.getArgumentsId()); - if (this.needsArguments) { - decl.init = t.identifier("arguments"); - } - topVars.push(decl); - } - - var leftId = this.leftId; - if (leftId) { - topVars.push(t.variableDeclarator(leftId)); - } - - if (topVars.length > 0) { - node.body.body.unshift(t.variableDeclaration("var", topVars)); - } -}; - -TailCallTransformer.prototype.subTransform = function (node) { - if (!node) return; - - var handler = this["subTransform" + node.type]; - if (handler) return handler.call(this, node); -}; - -TailCallTransformer.prototype.subTransformConditionalExpression = function (node) { - var callConsequent = this.subTransform(node.consequent); - var callAlternate = this.subTransform(node.alternate); - if (!callConsequent && !callAlternate) { - return; - } - - // if ternary operator had tail recursion in value, convert to optimized if-statement - node.type = "IfStatement"; - node.consequent = callConsequent ? t.toBlock(callConsequent) : returnBlock(node.consequent); - - if (callAlternate) { - node.alternate = t.isIfStatement(callAlternate) ? callAlternate : t.toBlock(callAlternate); - } else { - node.alternate = returnBlock(node.alternate); - } - - return [node]; -}; - -TailCallTransformer.prototype.subTransformLogicalExpression = function (node) { - // only call in right-value of can be optimized - var callRight = this.subTransform(node.right); - if (!callRight) return; - - // cache left value as it might have side-effects - var leftId = this.getLeftId(); - var testExpr = t.assignmentExpression( - "=", - leftId, - node.left - ); - - if (node.operator === "&&") { - testExpr = t.unaryExpression("!", testExpr); - } - - return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight); -}; - -TailCallTransformer.prototype.subTransformSequenceExpression = function (node) { - var seq = node.expressions; - - // only last element can be optimized - var lastCall = this.subTransform(seq[seq.length - 1]); - if (!lastCall) { - return; - } - - // remove converted expression from sequence - // and convert to regular expression if needed - if (--seq.length === 1) { - node = seq[0]; - } - - return [t.expressionStatement(node)].concat(lastCall); -}; - -TailCallTransformer.prototype.subTransformCallExpression = function (node) { - var callee = node.callee, thisBinding, args; - - if (t.isMemberExpression(callee, { computed: false }) && t.isIdentifier(callee.property)) { - switch (callee.property.name) { - case "call": - args = t.arrayExpression(node.arguments.slice(1)); - break; - - case "apply": - args = node.arguments[1] || t.identifier("undefined"); - break; - - default: - return; - } - - thisBinding = node.arguments[0]; - callee = callee.object; - } - - // only tail recursion can be optimized as for now - if (!t.isIdentifier(callee) || !this.scope.bindingIdentifierEquals(callee.name, this.ownerId)) { - return; - } - - this.hasTailRecursion = true; - - if (this.hasDeopt()) return; - - var body = []; - - if (!t.isThisExpression(thisBinding)) { - body.push(t.expressionStatement(t.assignmentExpression( - "=", - this.getThisId(), - thisBinding || t.identifier("undefined") - ))); - } - - if (!args) { - args = t.arrayExpression(node.arguments); - } - - var argumentsId = this.getArgumentsId(); - var params = this.getParams(); - - body.push(t.expressionStatement(t.assignmentExpression( - "=", - argumentsId, - args - ))); - - var i, param; - - if (t.isArrayExpression(args)) { - var elems = args.elements; - for (i = 0; i < elems.length && i < params.length; i++) { - param = params[i]; - var elem = elems[i] || (elems[i] = t.identifier("undefined")); - if (!param._isDefaultPlaceholder) { - elems[i] = t.assignmentExpression("=", param, elem); - } - } - } else { - this.setsArguments = true; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (!param._isDefaultPlaceholder) { - body.push(t.expressionStatement(t.assignmentExpression( - "=", - param, - t.memberExpression(argumentsId, t.literal(i), true) - ))); - } - } - } - - body.push(t.expressionStatement( - t.assignmentExpression("=", this.getAgainId(), t.literal(true)) - )); - body.push(t.continueStatement(this.getFunctionId())); - - return body; -}; - // looks for and replaces tail recursion calls var firstPass = { enter(node, parent, scope, state) { @@ -371,7 +84,296 @@ var thirdPass = { } }; -exports.Function = function (node, parent, scope, file) { - var tailCall = new TailCallTransformer(node, scope, file); - tailCall.run(); -}; +class TailCallTransformer { + constructor(node, scope, file) { + this.hasTailRecursion = false; + this.needsArguments = false; + this.setsArguments = false; + this.needsThis = false; + this.ownerId = node.id; + this.vars = []; + + this.scope = scope; + this.file = file; + this.node = node; + } + + getArgumentsId() { + return this.argumentsId ||= this.scope.generateUidIdentifier("arguments"); + } + + getThisId() { + return this.thisId ||= this.scope.generateUidIdentifier("this"); + } + + getLeftId() { + return this.leftId ||= this.scope.generateUidIdentifier("left"); + } + + getFunctionId() { + return this.functionId ||= this.scope.generateUidIdentifier("function"); + } + + getAgainId() { + return this.againId ||= this.scope.generateUidIdentifier("again"); + } + + getParams() { + var params = this.params; + + if (!params) { + params = this.node.params; + this.paramDecls = []; + + for (var i = 0; i < params.length; i++) { + var param = params[i]; + if (!param._isDefaultPlaceholder) { + this.paramDecls.push(t.variableDeclarator( + param, + params[i] = this.scope.generateUidIdentifier("x") + )); + } + } + } + + return this.params = params; + } + + hasDeopt() { + // check if the ownerId has been reassigned, if it has then it's not safe to + // perform optimisations + var ownerIdInfo = this.scope.getBindingInfo(this.ownerId.name); + return ownerIdInfo && ownerIdInfo.reassigned; + } + + run() { + var scope = this.scope; + var node = this.node; + + // only tail recursion can be optimized as for now, so we can skip anonymous + // functions entirely + var ownerId = this.ownerId; + if (!ownerId) return; + + // traverse the function and look for tail recursion + scope.traverse(node, firstPass, this); + + if (!this.hasTailRecursion) return; + + if (this.hasDeopt()) { + this.file.logDeopt(node, messages.get("tailCallReassignmentDeopt")); + return; + } + + // + + scope.traverse(node, secondPass, this); + + if (!this.needsThis || !this.needsArguments) { + scope.traverse(node, thirdPass, this); + } + + var body = t.ensureBlock(node).body; + + if (this.vars.length > 0) { + var declarations = flatten(map(this.vars, function (decl) { + return decl.declarations; + }, this)); + var statement = reduceRight(declarations, function (expr, decl) { + return t.assignmentExpression("=", decl.id, expr); + }, t.identifier("undefined")); + body.unshift(t.expressionStatement(statement)); + } + + var paramDecls = this.paramDecls; + if (paramDecls.length > 0) { + body.unshift(t.variableDeclaration("var", paramDecls)); + } + + body.unshift(t.expressionStatement( + t.assignmentExpression("=", this.getAgainId(), t.literal(false))) + ); + + node.body = util.template("tail-call-body", { + AGAIN_ID: this.getAgainId(), + THIS_ID: this.thisId, + ARGUMENTS_ID: this.argumentsId, + FUNCTION_ID: this.getFunctionId(), + BLOCK: node.body + }); + + var topVars = []; + + if (this.needsThis) { + topVars.push(t.variableDeclarator(this.getThisId(), t.thisExpression())); + } + + if (this.needsArguments || this.setsArguments) { + var decl = t.variableDeclarator(this.getArgumentsId()); + if (this.needsArguments) { + decl.init = t.identifier("arguments"); + } + topVars.push(decl); + } + + var leftId = this.leftId; + if (leftId) { + topVars.push(t.variableDeclarator(leftId)); + } + + if (topVars.length > 0) { + node.body.body.unshift(t.variableDeclaration("var", topVars)); + } + } + + subTransform(node) { + if (!node) return; + + var handler = this["subTransform" + node.type]; + if (handler) return handler.call(this, node); + } + + subTransformConditionalExpression(node) { + var callConsequent = this.subTransform(node.consequent); + var callAlternate = this.subTransform(node.alternate); + if (!callConsequent && !callAlternate) { + return; + } + + // if ternary operator had tail recursion in value, convert to optimized if-statement + node.type = "IfStatement"; + node.consequent = callConsequent ? t.toBlock(callConsequent) : returnBlock(node.consequent); + + if (callAlternate) { + node.alternate = t.isIfStatement(callAlternate) ? callAlternate : t.toBlock(callAlternate); + } else { + node.alternate = returnBlock(node.alternate); + } + + return [node]; + } + + subTransformLogicalExpression(node) { + // only call in right-value of can be optimized + var callRight = this.subTransform(node.right); + if (!callRight) return; + + // cache left value as it might have side-effects + var leftId = this.getLeftId(); + var testExpr = t.assignmentExpression( + "=", + leftId, + node.left + ); + + if (node.operator === "&&") { + testExpr = t.unaryExpression("!", testExpr); + } + + return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight); + } + + subTransformSequenceExpression(node) { + var seq = node.expressions; + + // only last element can be optimized + var lastCall = this.subTransform(seq[seq.length - 1]); + if (!lastCall) { + return; + } + + // remove converted expression from sequence + // and convert to regular expression if needed + if (--seq.length === 1) { + node = seq[0]; + } + + return [t.expressionStatement(node)].concat(lastCall); + } + + subTransformCallExpression(node) { + var callee = node.callee, thisBinding, args; + + if (t.isMemberExpression(callee, { computed: false }) && t.isIdentifier(callee.property)) { + switch (callee.property.name) { + case "call": + args = t.arrayExpression(node.arguments.slice(1)); + break; + + case "apply": + args = node.arguments[1] || t.identifier("undefined"); + break; + + default: + return; + } + + thisBinding = node.arguments[0]; + callee = callee.object; + } + + // only tail recursion can be optimized as for now + if (!t.isIdentifier(callee) || !this.scope.bindingIdentifierEquals(callee.name, this.ownerId)) { + return; + } + + this.hasTailRecursion = true; + + if (this.hasDeopt()) return; + + var body = []; + + if (!t.isThisExpression(thisBinding)) { + body.push(t.expressionStatement(t.assignmentExpression( + "=", + this.getThisId(), + thisBinding || t.identifier("undefined") + ))); + } + + if (!args) { + args = t.arrayExpression(node.arguments); + } + + var argumentsId = this.getArgumentsId(); + var params = this.getParams(); + + body.push(t.expressionStatement(t.assignmentExpression( + "=", + argumentsId, + args + ))); + + var i, param; + + if (t.isArrayExpression(args)) { + var elems = args.elements; + for (i = 0; i < elems.length && i < params.length; i++) { + param = params[i]; + var elem = elems[i] || (elems[i] = t.identifier("undefined")); + if (!param._isDefaultPlaceholder) { + elems[i] = t.assignmentExpression("=", param, elem); + } + } + } else { + this.setsArguments = true; + for (i = 0; i < params.length; i++) { + param = params[i]; + if (!param._isDefaultPlaceholder) { + body.push(t.expressionStatement(t.assignmentExpression( + "=", + param, + t.memberExpression(argumentsId, t.literal(i), true) + ))); + } + } + } + + body.push(t.expressionStatement( + t.assignmentExpression("=", this.getAgainId(), t.literal(true)) + )); + body.push(t.continueStatement(this.getFunctionId())); + + return body; + } +}