diff --git a/.editorconfig b/.editorconfig index 3f922a1077..98d20ea2e6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.js] +[*.{js,json}] indent_style = space indent_size = 2 diff --git a/lib/6to5/transformation/file.js b/lib/6to5/transformation/file.js index 6c3a6c62a3..6dfdfe2602 100644 --- a/lib/6to5/transformation/file.js +++ b/lib/6to5/transformation/file.js @@ -56,7 +56,8 @@ File.helpers = [ "class-call-check", "object-destructuring-empty", "temporal-undefined", - "temporal-assert-defined" + "temporal-assert-defined", + "tail-call" ]; File.validOptions = [ diff --git a/lib/6to5/transformation/templates/tail-call-body.js b/lib/6to5/transformation/templates/tail-call-body.js deleted file mode 100644 index c2366a8cb6..0000000000 --- a/lib/6to5/transformation/templates/tail-call-body.js +++ /dev/null @@ -1,13 +0,0 @@ -{ - 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/templates/tail-call.js b/lib/6to5/transformation/templates/tail-call.js new file mode 100644 index 0000000000..e606340503 --- /dev/null +++ b/lib/6to5/transformation/templates/tail-call.js @@ -0,0 +1,21 @@ +(function () { + function Tail(func, args, context) { + this.func = func; + this.args = args; + this.context = context; + } + + var isRunning = false; + + return function (func, args, context) { + var result = new Tail(func, args, context); + if (!isRunning) { + isRunning = true; + do { + result = result.func.apply(result.context, result.args); + } while (result instanceof Tail); + isRunning = false; + } + return result; + }; +})() diff --git a/lib/6to5/transformation/transformers/es6/tail-call.js b/lib/6to5/transformation/transformers/es6/tail-call.js index b8dfba2418..b5ff8e71b2 100644 --- a/lib/6to5/transformation/transformers/es6/tail-call.js +++ b/lib/6to5/transformation/transformers/es6/tail-call.js @@ -1,6 +1,5 @@ "use strict"; -var util = require("../../../util"); var t = require("../../../types"); function returnBlock(expr) { @@ -27,7 +26,7 @@ function transformExpression(node, scope, state) { } else { node.alternate = returnBlock(node.alternate); } - return node; + return [node]; case "LogicalExpression": // only call in right-value of can be optimized @@ -36,17 +35,11 @@ function transformExpression(node, scope, state) { return; } - // cache left value as it might have side-effects - var leftId = state.getLeftId(); - var testExpr = t.assignmentExpression( - "=", - leftId, - node.left - ); + var test = state.wrapSideEffect(node.left); if (node.operator === "&&") { - testExpr = t.unaryExpression("!", testExpr); + test.expr = t.unaryExpression("!", test.expr); } - return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight); + return [t.ifStatement(test.expr, returnBlock(test.ref))].concat(callRight); case "SequenceExpression": var seq = node.expressions; @@ -66,53 +59,28 @@ function transformExpression(node, scope, state) { return [t.expressionStatement(node)].concat(lastCall); case "CallExpression": - var callee = node.callee, prop, thisBinding, args; + var callee = node.callee, thisBinding; + var args = [callee]; - 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; + // bind `this` to object in member expressions + if (t.isMemberExpression(callee)) { + var object = state.wrapSideEffect(callee.object); + callee.object = object.expr; + thisBinding = object.ref; } - // only tail recursion can be optimized as for now - if (!t.isIdentifier(callee) || !scope.bindingEquals(callee.name, state.ownerId)) { - return; + if (node.arguments.length > 0 || thisBinding) { + args.push(t.arrayExpression(node.arguments)); } - state.hasTailRecursion = true; + if (thisBinding) { + args.push(thisBinding); + } - 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) - )) - ]; + return [t.returnStatement(t.callExpression( + state.getHelperRef(), + args + ))]; } })(node); } @@ -152,56 +120,34 @@ var functionVisitor = { }; exports.FunctionDeclaration = -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; +exports.FunctionExpression = function (node, parent, scope, file) { + var tempId, helperRef; var state = { - hasTailRecursion: false, - ownerId: ownerId, + ownerId: node.id, - getArgumentsId: function () { - return argumentsId = argumentsId || scope.generateUidIdentifier("arguments"); + getHelperRef: function () { + return helperRef = helperRef || file.addHelper("tail-call"); }, - getThisId: function () { - return thisId = thisId || scope.generateUidIdentifier("this"); - }, - - getShouldContinueId: function () { - return shouldContinueId = shouldContinueId || scope.generateUidIdentifier("shouldContinue"); - }, - - getLeftId: function () { - return leftId = leftId || scope.generateUidIdentifier("left"); + 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 (!state.hasTailRecursion) return; - - var block = t.ensureBlock(node); - - if (leftId) { - block.body.unshift(t.variableDeclaration("var", [ - t.variableDeclarator(leftId) + if (tempId) { + t.ensureBlock(node).body.unshift(t.variableDeclaration("var", [ + t.variableDeclarator(tempId) ])); } - - 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-arrow-functions/options.json b/test/fixtures/transformation/es6-arrow-functions/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/es6-arrow-functions/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/es6-classes-loose/options.json b/test/fixtures/transformation/es6-classes-loose/options.json index d4a7c9a51e..d9b047c54c 100644 --- a/test/fixtures/transformation/es6-classes-loose/options.json +++ b/test/fixtures/transformation/es6-classes-loose/options.json @@ -1,3 +1,4 @@ { - "loose": ["es6.classes"] + "loose": ["es6.classes"], + "blacklist": ["es6.tailCall"] } diff --git a/test/fixtures/transformation/es6-classes/calling-super-properties/options.json b/test/fixtures/transformation/es6-classes/calling-super-properties/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/es6-classes/calling-super-properties/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/es6-destructuring/spread/options.json b/test/fixtures/transformation/es6-destructuring/spread/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/es6-destructuring/spread/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/es6-modules-system/hoist-function-exports/options.json b/test/fixtures/transformation/es6-modules-system/hoist-function-exports/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/es6-modules-system/hoist-function-exports/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/es6-properties.shorthand/method-self-reference/options.json b/test/fixtures/transformation/es6-properties.shorthand/method-self-reference/options.json new file mode 100644 index 0000000000..e9accc85db --- /dev/null +++ b/test/fixtures/transformation/es6-properties.shorthand/method-self-reference/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} \ No newline at end of file diff --git a/test/fixtures/transformation/es6-spread/options.json b/test/fixtures/transformation/es6-spread/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/es6-spread/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/es6-tail-call/call-apply/expected.js b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js index 56732fe861..9224827414 100644 --- a/test/fixtures/transformation/es6-tail-call/call-apply/expected.js +++ b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js @@ -1,27 +1,13 @@ "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; + if (n <= 0) { + console.log(this, arguments); + return "foo"; + } + if (Math.random() > 0.5) { + return to5Runtime.tailCall(f.call, [this, n - 1], f); + } else { + return to5Runtime.tailCall(f.apply, [this, [n - 1]], f); + } })(1000000) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/cross-function/actual.js b/test/fixtures/transformation/es6-tail-call/cross-function/actual.js new file mode 100644 index 0000000000..289cfe3893 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/cross-function/actual.js @@ -0,0 +1,7 @@ +function f(n) { + return n <= 0 ? "foo" : g(n - 1); +} + +function g(n) { + return n <= 0 ? "goo" : f(n - 1); +} diff --git a/test/fixtures/transformation/es6-tail-call/cross-function/expected.js b/test/fixtures/transformation/es6-tail-call/cross-function/expected.js new file mode 100644 index 0000000000..6c4743fba4 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/cross-function/expected.js @@ -0,0 +1,17 @@ +"use strict"; + +function f(n) { + if (n <= 0) { + return "foo"; + } else { + return to5Runtime.tailCall(g, [n - 1]); + } +} + +function g(n) { + if (n <= 0) { + return "goo"; + } else { + return to5Runtime.tailCall(f, [n - 1]); + } +} diff --git a/test/fixtures/transformation/es6-tail-call/expressions/expected.js b/test/fixtures/transformation/es6-tail-call/expressions/expected.js index b00fec1a5b..4d778083a9 100644 --- a/test/fixtures/transformation/es6-tail-call/expressions/expected.js +++ b/test/fixtures/transformation/es6-tail-call/expressions/expected.js @@ -1,30 +1,18 @@ "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(); + var _temp; + 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; + if (!(_temp = getTrueValue())) { + return _temp; + } + if (_temp = getFalseValue()) { + return _temp; + } + return to5Runtime.tailCall(f, [n - 1]); + } })(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/options.json b/test/fixtures/transformation/es6-tail-call/options.json new file mode 100644 index 0000000000..4c0c25eacc --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/options.json @@ -0,0 +1,4 @@ +{ + "runtime": true, + "blacklist": [] +} diff --git a/test/fixtures/transformation/es6-tail-call/recursion/expected.js b/test/fixtures/transformation/es6-tail-call/recursion/expected.js index 1f5e8fcaf5..1920e61d56 100644 --- a/test/fixtures/transformation/es6-tail-call/recursion/expected.js +++ b/test/fixtures/transformation/es6-tail-call/recursion/expected.js @@ -1,23 +1,11 @@ "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; + var n = arguments[0] === undefined ? getDefaultValue() : arguments[0]; + if (n <= 0) { + return "foo"; + } + // Should be clean (undefined) on each pass + var local; + return to5Runtime.tailCall(f, [n - 1]); })(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/side-effect/actual.js b/test/fixtures/transformation/es6-tail-call/side-effect/actual.js new file mode 100644 index 0000000000..249d520be4 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/side-effect/actual.js @@ -0,0 +1,7 @@ +function f() { + return getObj().method(); +} + +function g() { + return getFalseValue() || getValue(); +} diff --git a/test/fixtures/transformation/es6-tail-call/side-effect/expected.js b/test/fixtures/transformation/es6-tail-call/side-effect/expected.js new file mode 100644 index 0000000000..bd18748164 --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/side-effect/expected.js @@ -0,0 +1,14 @@ +"use strict"; + +function f() { + var _temp; + return to5Runtime.tailCall((_temp = getObj()).method, [], _temp); +} + +function g() { + var _temp; + if (_temp = getFalseValue()) { + return _temp; + } + return to5Runtime.tailCall(getValue); +} diff --git a/test/fixtures/transformation/es6-tail-call/try-catch/expected.js b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js index cc8434fd01..1406f3adb3 100644 --- a/test/fixtures/transformation/es6-tail-call/try-catch/expected.js +++ b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js @@ -10,26 +10,14 @@ })(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; + if (n <= 0) { + return "foo"; + } + try { + throw new Error(); + } catch (e) { + return to5Runtime.tailCall(f, [n - 1]); + } })(1000000) === "foo"; (function f(n) { @@ -44,22 +32,10 @@ })(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; + if (n <= 0) { + return "foo"; + } + try {} finally { + return to5Runtime.tailCall(f, [n - 1]); + } })(1000000) === "foo"; diff --git a/test/fixtures/transformation/es7-comprehensions/options.json b/test/fixtures/transformation/es7-comprehensions/options.json index 252f473a73..eaf1e3ba7b 100644 --- a/test/fixtures/transformation/es7-comprehensions/options.json +++ b/test/fixtures/transformation/es7-comprehensions/options.json @@ -1,3 +1,4 @@ { - "experimental": true + "experimental": true, + "blacklist": ["es6.tailCall"] } diff --git a/test/fixtures/transformation/playground/method-binding/options.json b/test/fixtures/transformation/playground/method-binding/options.json new file mode 100644 index 0000000000..b8b90ecb11 --- /dev/null +++ b/test/fixtures/transformation/playground/method-binding/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["es6.tailCall"] +} diff --git a/test/fixtures/transformation/react/arrow-functions/options.json b/test/fixtures/transformation/react/arrow-functions/options.json new file mode 100644 index 0000000000..d27c59a46a --- /dev/null +++ b/test/fixtures/transformation/react/arrow-functions/options.json @@ -0,0 +1,3 @@ +{ + "blacklist": ["useStrict", "es6.tailCall"] +}