diff --git a/lib/6to5/transformation/transformers/es6/tail-call.js b/lib/6to5/transformation/transformers/es6/tail-call.js index a15f8ca3a0..52c8baad2f 100644 --- a/lib/6to5/transformation/transformers/es6/tail-call.js +++ b/lib/6to5/transformation/transformers/es6/tail-call.js @@ -1,163 +1,287 @@ "use strict"; -var _ = require("lodash"); var util = require("../../../util"); var t = require("../../../types"); +var _ = require("lodash"); function returnBlock(expr) { return t.blockStatement([t.returnStatement(expr)]); } -function transformExpression(node, scope, state) { - if (!node) return; +function TailCall(node, scope, file) { + this.hasTailRecursion = false; + this.needsArguments = false; + this.setsArguments = false; + this.needsThis = false; + this.ownerId = node.id; + this.vars = []; - return (function subTransform(node) { - switch (node.type) { - case "ConditionalExpression": - var callConsequent = subTransform(node.consequent); - var callAlternate = 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]; - - case "LogicalExpression": - // only call in right-value of can be optimized - var callRight = subTransform(node.right); - if (!callRight) { - return; - } - - // cache left value as it might have side-effects - var leftId = state.getLeftId(); - var testExpr = t.assignmentExpression( - "=", - leftId, - node.left - ); - - if (node.operator === "&&") { - testExpr = t.unaryExpression("!", testExpr); - } - - return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight); - - case "SequenceExpression": - var seq = node.expressions; - - // only last element can be optimized - var lastCall = 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); - - case "CallExpression": - var callee = node.callee, prop, thisBinding, args; - - if (t.isMemberExpression(callee, { computed: false }) && - t.isIdentifier(prop = callee.property)) { - switch (prop.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) || !scope.bindingEquals(callee.name, state.ownerId)) { - return; - } - - state.hasTailRecursion = true; - - var body = []; - - if (!t.isThisExpression(thisBinding)) { - body.push(t.expressionStatement(t.assignmentExpression( - "=", - state.getThisId(), - thisBinding || t.identifier("undefined") - ))); - } - - if (!args) { - args = t.arrayExpression(node.arguments); - } - - var argumentsId = state.getArgumentsId(); - var params = state.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 { - state.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.continueStatement(state.getFunctionId())); - - return body; - } - })(node); + this.scope = scope; + this.file = file; + this.node = node; } -// Looks for and replaces tail recursion calls. +TailCall.prototype.getArgumentsId = function () { + return this.argumentsId = this.argumentsId || this.scope.generateUidIdentifier("arguments"); +}; + +TailCall.prototype.getThisId = function () { + return this.thisId = this.thisId || this.scope.generateUidIdentifier("this"); +}; + +TailCall.prototype.getLeftId = function () { + return this.leftId = this.leftId || this.scope.generateUidIdentifier("left"); +}; + +TailCall.prototype.getFunctionId = function () { + return this.functionId = this.functionId || this.scope.generateUidIdentifier("function"); +}; + +TailCall.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; +}; + +TailCall.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; + + // + + 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) { + body.unshift(t.expressionStatement( + _(this.vars) + .map(function (decl) { + return decl.declarations; + }) + .flatten() + .reduceRight(function (expr, decl) { + return t.assignmentExpression("=", decl.id, expr); + }, t.identifier("undefined")) + )); + } + + var paramDecls = this.paramDecls; + if (paramDecls.length > 0) { + body.unshift(t.variableDeclaration("var", paramDecls)); + } + + node.body = util.template("tail-call-body", { + 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)); + } +}; + +TailCall.prototype.subTransform = function (node) { + var handler = this["subTransform" + node.type]; + if (handler) return handler.call(this, node); +}; + +TailCall.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]; +}; + +TailCall.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); +}; + +TailCall.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); +}; + +TailCall.prototype.subTransformCallExpression = function (node) { + var callee = node.callee, prop, thisBinding, args; + + if (t.isMemberExpression(callee, { computed: false }) && + t.isIdentifier(prop = callee.property)) { + switch (prop.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.bindingEquals(callee.name, this.ownerId)) { + return; + } + + this.hasTailRecursion = true; + + 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.continueStatement(this.getFunctionId())); + + return body; +}; + +// looks for and replaces tail recursion calls var firstPass = { enter: function (node, parent, scope, state) { if (t.isReturnStatement(node)) { this.skip(); - return transformExpression(node.argument, scope, state); + return state.subTransform(node.argument); } else if (t.isTryStatement(parent)) { if (node === parent.block) { this.skip(); @@ -173,8 +297,8 @@ var firstPass = { } }; -// Hoists up function declarations, replaces `this` and `arguments` and -// marks them as needed. +// hoists up function declarations, replaces `this` and `arguments` and marks +// them as needed var secondPass = { enter: function (node, parent, scope, state) { if (t.isThisExpression(node)) { @@ -196,8 +320,7 @@ var secondPass = { } }; -// Optimizes recursion by removing `this` and `arguments` -// if they are not used. +// optimizes recursion by removing `this` and `arguments` if they aren't used var thirdPass = { enter: function (node, parent, scope, state) { if (!t.isExpressionStatement(node)) return; @@ -213,112 +336,7 @@ var thirdPass = { } }; -exports.Function = function (node, parent, scope) { - // only tail recursion can be optimized as for now, - // so we can skip anonymous functions entirely - var ownerId = node.id; - if (!ownerId) return; - - var argumentsId, thisId, leftId, functionId, params, paramDecls; - - var state = { - hasTailRecursion: false, - needsThis: false, - needsArguments: false, - setsArguments: false, - ownerId: ownerId, - vars: [], - - getArgumentsId: function () { - return argumentsId = argumentsId || scope.generateUidIdentifier("arguments"); - }, - - getThisId: function () { - return thisId = thisId || scope.generateUidIdentifier("this"); - }, - - getLeftId: function () { - return leftId = leftId || scope.generateUidIdentifier("left"); - }, - - getFunctionId: function () { - return functionId = functionId || scope.generateUidIdentifier("function"); - }, - - getParams: function () { - if (!params) { - params = node.params; - paramDecls = []; - for (var i = 0; i < params.length; i++) { - var param = params[i]; - if (!param._isDefaultPlaceholder) { - paramDecls.push(t.variableDeclarator( - param, - params[i] = scope.generateUidIdentifier("x") - )); - } - } - } - return params; - } - }; - - // traverse the function and look for tail recursion - scope.traverse(node, firstPass, state); - - if (!state.hasTailRecursion) return; - - scope.traverse(node, secondPass, state); - - if (!state.needsThis || !state.needsArguments) { - scope.traverse(node, thirdPass, state); - } - - var body = t.ensureBlock(node).body; - - if (state.vars.length > 0) { - body.unshift(t.expressionStatement( - _(state.vars) - .map(function (decl) { - return decl.declarations; - }) - .flatten() - .reduceRight(function (expr, decl) { - return t.assignmentExpression("=", decl.id, expr); - }, t.identifier("undefined")) - )); - } - - if (paramDecls.length > 0) { - body.unshift(t.variableDeclaration("var", paramDecls)); - } - - node.body = util.template("tail-call-body", { - THIS_ID: thisId, - ARGUMENTS_ID: argumentsId, - FUNCTION_ID: state.getFunctionId(), - BLOCK: node.body - }); - - var topVars = []; - - if (state.needsThis) { - topVars.push(t.variableDeclarator(state.getThisId(), t.thisExpression())); - } - - if (state.needsArguments || state.setsArguments) { - var decl = t.variableDeclarator(state.getArgumentsId()); - if (state.needsArguments) { - decl.init = t.identifier("arguments"); - } - topVars.push(decl); - } - - if (leftId) { - topVars.push(t.variableDeclarator(leftId)); - } - - if (topVars.length > 0) { - node.body.body.unshift(t.variableDeclaration("var", topVars)); - } +exports.Function = function (node, parent, scope, file) { + var tailCall = new TailCall(node, scope, file); + tailCall.run(); };