diff --git a/lib/6to5/transformation/templates/tail-call-body.js b/lib/6to5/transformation/templates/tail-call-body.js new file mode 100644 index 0000000000..c2366a8cb6 --- /dev/null +++ b/lib/6to5/transformation/templates/tail-call-body.js @@ -0,0 +1,13 @@ +{ + var ARGUMENTS_ID = arguments, + THIS_ID = this, + SHOULD_CONTINUE_ID, + RESULT_ID; + + do { + SHOULD_CONTINUE_ID = false; + RESULT_ID = FUNCTION.apply(THIS_ID, ARGUMENTS_ID); + } while(SHOULD_CONTINUE_ID); + + return RESULT_ID; +} diff --git a/lib/6to5/transformation/transformers/es6/.tail-call.js b/lib/6to5/transformation/transformers/es6/.tail-call.js new file mode 100644 index 0000000000..cd942211e1 --- /dev/null +++ b/lib/6to5/transformation/transformers/es6/.tail-call.js @@ -0,0 +1,116 @@ +"use strict"; + +var t = require("../../../types"); + +function transformExpression(node, scope, state) { + if (!node) return; + + return (function subTransform(node) { + switch (node.type) { + case "ConditionalExpression": + // any value of ternary operator can be final one + subTransform(node.consequent); + subTransform(node.alternate); + break; + + case "LogicalExpression": + // only right expression can be final and so optimized + subTransform(node.right); + break; + + case "SequenceExpression": + // only last element of sequence can be optimized + var seq = node.expressions; + subTransform(seq[seq.length - 1]); + break; + + case "CallExpression": + var callee = node.callee, thisBinding; + var args = [callee]; + + // bind `this` to object in member expressions + if (t.isMemberExpression(callee)) { + var object = state.wrapSideEffect(callee.object); + callee.object = object.expr; + thisBinding = object.ref; + } + + if (node.arguments.length > 0 || thisBinding) { + args.push(t.arrayExpression(node.arguments)); + } + + if (thisBinding) { + args.push(thisBinding); + } + + node.callee = state.getHelperRef(); + node.arguments = args; + break; + } + })(node); +} + +var functionChildrenVisitor = { + enter: function (node, parent, scope, state) { + if (t.isReturnStatement(node)) { + // prevent entrance by current visitor + this.skip(); + // transform return argument into statement if + // it contains tail recursion + transformExpression(node.argument, scope, state); + } else if (t.isFunction(node)) { + // inner function's bodies are irrelevant + this.skip(); + } else if (t.isTryStatement(parent)) { + if (node === parent.block) { + // `try`-blocks can't be optimized + this.skip(); + } else if (parent.finalizer && node !== parent.finalizer) { + // `catch` clause followed by `finally` can't be optimized + this.skip(); + } + } + } +}; + +var functionVisitor = { + enter: function (node, parent, scope, state) { + // traverse all child nodes of this function and find `arguments` and `this` + scope.traverse(node, functionChildrenVisitor, state); + + return this.skip(); + } +}; + +exports.FunctionDeclaration = +exports.FunctionExpression = function (node, parent, scope, file) { + var tempId, helperRef; + + var state = { + ownerId: node.id, + + getHelperRef: function () { + return helperRef = helperRef || file.addHelper("tail-call"); + }, + + wrapSideEffect: function (node) { + if (t.isIdentifier(node) || t.isLiteral(node)) { + return {expr: node, ref: node}; + } + tempId = tempId || scope.generateUidIdentifier("temp"); + return { + expr: t.assignmentExpression("=", tempId, node), + ref: tempId + }; + } + }; + + // traverse the function and look for tail recursion + scope.traverse(node, functionVisitor, state); + + if (tempId) { + t.ensureBlock(node).body.unshift(t.variableDeclaration("var", [ + t.variableDeclarator(tempId) + ])); + } +}; diff --git a/lib/6to5/transformation/transformers/es6/tail-call.js b/lib/6to5/transformation/transformers/es6/tail-call.js index 0f38fb697e..b8dfba2418 100644 --- a/lib/6to5/transformation/transformers/es6/tail-call.js +++ b/lib/6to5/transformation/transformers/es6/tail-call.js @@ -1,52 +1,118 @@ "use strict"; -/* +var util = require("../../../util"); var t = require("../../../types"); +function returnBlock(expr) { + return t.blockStatement([t.returnStatement(expr)]); +} + function transformExpression(node, scope, state) { if (!node) return; return (function subTransform(node) { switch (node.type) { case "ConditionalExpression": - // any value of ternary operator can be final one - subTransform(node.consequent); - subTransform(node.alternate); - break; + 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 right expression can be final and so optimized - subTransform(node.right); - break; + // 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": - // only last element of sequence can be optimized var seq = node.expressions; - subTransform(seq[seq.length - 1]); - break; + + // 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, thisBinding; - var args = [callee]; + var callee = node.callee, prop, thisBinding, args; - // bind `this` to object in member expressions - if (t.isMemberExpression(callee)) { - var object = state.wrapSideEffect(callee.object); - callee.object = object.expr; - thisBinding = object.ref; + 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; } - if (node.arguments.length > 0 || thisBinding) { - args.push(t.arrayExpression(node.arguments)); + // only tail recursion can be optimized as for now + if (!t.isIdentifier(callee) || !scope.bindingEquals(callee.name, state.ownerId)) { + return; } - if (thisBinding) { - args.push(thisBinding); - } + state.hasTailRecursion = true; - node.callee = state.getHelperRef(); - node.arguments = args; - break; + return [ + t.expressionStatement(t.assignmentExpression( + "=", + state.getArgumentsId(), + args || t.arrayExpression(node.arguments) + )), + + t.expressionStatement(t.assignmentExpression( + "=", + state.getThisId(), + thisBinding || t.identifier("undefined") + )), + + t.returnStatement(t.assignmentExpression( + "=", + state.getShouldContinueId(), + t.literal(true) + )) + ]; } })(node); } @@ -58,17 +124,19 @@ var functionChildrenVisitor = { this.skip(); // transform return argument into statement if // it contains tail recursion - transformExpression(node.argument, scope, state); + return transformExpression(node.argument, scope, state); } else if (t.isFunction(node)) { - // inner function's bodies are irrelevant - this.skip(); + return this.skip(); } else if (t.isTryStatement(parent)) { if (node === parent.block) { - // `try`-blocks can't be optimized - this.skip(); - } else if (parent.finalizer && node !== parent.finalizer) { - // `catch` clause followed by `finally` can't be optimized - this.skip(); + return this.skip(); + } else if (node === parent.finalizer) { + return; + } else { + if (parent.finalizer) { + this.skip(); + } + return; } } } @@ -84,35 +152,56 @@ var functionVisitor = { }; exports.FunctionDeclaration = -exports.FunctionExpression = function (node, parent, scope, file) { - var tempId, helperRef; +exports.FunctionExpression = 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, shouldContinueId, leftId; var state = { - ownerId: node.id, + hasTailRecursion: false, + ownerId: ownerId, - getHelperRef: function () { - return helperRef = helperRef || file.addHelper("tail-call"); + getArgumentsId: function () { + return argumentsId = argumentsId || scope.generateUidIdentifier("arguments"); }, - wrapSideEffect: function (node) { - if (t.isIdentifier(node) || t.isLiteral(node)) { - return {expr: node, ref: node}; - } - tempId = tempId || scope.generateUidIdentifier("temp"); - return { - expr: t.assignmentExpression("=", tempId, node), - ref: tempId - }; + getThisId: function () { + return thisId = thisId || scope.generateUidIdentifier("this"); + }, + + getShouldContinueId: function () { + return shouldContinueId = shouldContinueId || scope.generateUidIdentifier("shouldContinue"); + }, + + getLeftId: function () { + return leftId = leftId || scope.generateUidIdentifier("left"); } }; // traverse the function and look for tail recursion scope.traverse(node, functionVisitor, state); - if (tempId) { - t.ensureBlock(node).body.unshift(t.variableDeclaration("var", [ - t.variableDeclarator(tempId) + if (!state.hasTailRecursion) return; + + var block = t.ensureBlock(node); + + if (leftId) { + block.body.unshift(t.variableDeclaration("var", [ + t.variableDeclarator(leftId) ])); } + + var resultId = scope.generateUidIdentifier("result"); + state.getShouldContinueId(); + + node.body = util.template("tail-call-body", { + SHOULD_CONTINUE_ID: shouldContinueId, + ARGUMENTS_ID: argumentsId, + RESULT_ID: resultId, + FUNCTION: t.functionExpression(null, node.params, block), + THIS_ID: thisId, + }); }; -*/ diff --git a/test/fixtures/transformation/es6-tail-call/call-apply/actual.js b/test/fixtures/transformation/es6-tail-call/call-apply/actual.js new file mode 100755 index 0000000000..2bb42c6c81 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/call-apply/actual.js @@ -0,0 +1,7 @@ +(function f(n) { + if (n <= 0) { + console.log(this, arguments); + return "foo"; + } + return Math.random() > 0.5 ? f.call(this, n - 1) : f.apply(this, [n - 1]); +})(1e6) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/call-apply/expected.js b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js new file mode 100755 index 0000000000..56732fe861 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js @@ -0,0 +1,27 @@ +"use strict"; + +(function f(n) { + var _arguments = arguments, + _this = this, + _shouldContinue, + _result; + do { + _shouldContinue = false; + _result = (function (n) { + if (n <= 0) { + console.log(this, arguments); + return "foo"; + } + if (Math.random() > 0.5) { + _arguments = [n - 1]; + _this = this; + return _shouldContinue = true; + } else { + _arguments = [n - 1]; + _this = this; + return _shouldContinue = true; + } + }).apply(_this, _arguments); + } while (_shouldContinue); + return _result; +})(1000000) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/expressions/actual.js b/test/fixtures/transformation/es6-tail-call/expressions/actual.js new file mode 100755 index 0000000000..824fdf80d5 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/expressions/actual.js @@ -0,0 +1,3 @@ +(function f(n) { + return n <= 0 ? "foo" : (doSmth(), getTrueValue() && (getFalseValue() || f(n - 1))); +})(1e6, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/expressions/expected.js b/test/fixtures/transformation/es6-tail-call/expressions/expected.js new file mode 100755 index 0000000000..b00fec1a5b --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/expressions/expected.js @@ -0,0 +1,30 @@ +"use strict"; + +(function f(n) { + var _arguments = arguments, + _this = this, + _shouldContinue, + _result; + do { + _shouldContinue = false; + _result = (function (n) { + var _left; + if (n <= 0) { + return "foo"; + } else { + doSmth(); + + if (!(_left = getTrueValue())) { + return _left; + } + if (_left = getFalseValue()) { + return _left; + } + _arguments = [n - 1]; + _this = undefined; + return _shouldContinue = true; + } + }).apply(_this, _arguments); + } while (_shouldContinue); + return _result; +})(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/recursion/actual.js b/test/fixtures/transformation/es6-tail-call/recursion/actual.js new file mode 100755 index 0000000000..67de2e6943 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/recursion/actual.js @@ -0,0 +1,8 @@ +(function f(n = getDefaultValue(), /* should be undefined after first pass */ m) { + if (n <= 0) { + return "foo"; + } + // Should be clean (undefined) on each pass + var local; + return f(n - 1); +})(1e6, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/recursion/expected.js b/test/fixtures/transformation/es6-tail-call/recursion/expected.js new file mode 100755 index 0000000000..1f5e8fcaf5 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/recursion/expected.js @@ -0,0 +1,23 @@ +"use strict"; + +(function f(_x, /* should be undefined after first pass */m) { + var _arguments = arguments, + _this = this, + _shouldContinue, + _result; + do { + _shouldContinue = false; + _result = (function (_x, m) { + var n = arguments[0] === undefined ? getDefaultValue() : arguments[0]; + if (n <= 0) { + return "foo"; + } + // Should be clean (undefined) on each pass + var local; + _arguments = [n - 1]; + _this = undefined; + return _shouldContinue = true; + }).apply(_this, _arguments); + } while (_shouldContinue); + return _result; +})(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/try-catch/actual.js b/test/fixtures/transformation/es6-tail-call/try-catch/actual.js new file mode 100755 index 0000000000..0a6be74326 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/try-catch/actual.js @@ -0,0 +1,39 @@ +(function f(n) { + if (n <= 0) { + return "foo"; + } + try { + return f(n - 1); + } catch (e) {} +})(1e6) === "foo"; + +(function f(n) { + if (n <= 0) { + return "foo"; + } + try { + throw new Error(); + } catch (e) { + return f(n - 1); + } +})(1e6) === "foo"; + +(function f(n) { + if (n <= 0) { + return "foo"; + } + try { + throw new Error(); + } catch (e) { + return f(n - 1); + } finally {} +})(1e6) === "foo"; + +(function f(n) { + if (n <= 0) { + return "foo"; + } + try {} finally { + return f(n - 1); + } +})(1e6) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/try-catch/expected.js b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js new file mode 100755 index 0000000000..cc8434fd01 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js @@ -0,0 +1,65 @@ +"use strict"; + +(function f(n) { + if (n <= 0) { + return "foo"; + } + try { + return f(n - 1); + } catch (e) {} +})(1000000) === "foo"; + +(function f(n) { + var _arguments = arguments, + _this = this, + _shouldContinue, + _result; + do { + _shouldContinue = false; + _result = (function (n) { + if (n <= 0) { + return "foo"; + } + try { + throw new Error(); + } catch (e) { + _arguments = [n - 1]; + _this = undefined; + return _shouldContinue = true; + } + }).apply(_this, _arguments); + } while (_shouldContinue); + return _result; +})(1000000) === "foo"; + +(function f(n) { + if (n <= 0) { + return "foo"; + } + try { + throw new Error(); + } catch (e) { + return f(n - 1); + } finally {} +})(1000000) === "foo"; + +(function f(n) { + var _arguments = arguments, + _this = this, + _shouldContinue, + _result; + do { + _shouldContinue = false; + _result = (function (n) { + if (n <= 0) { + return "foo"; + } + try {} finally { + _arguments = [n - 1]; + _this = undefined; + return _shouldContinue = true; + } + }).apply(_this, _arguments); + } while (_shouldContinue); + return _result; +})(1000000) === "foo";