Added complete TCO (tail call optimization).
Works across functions and generates simpler and faster code than #701. Works even across files when used in conjunction with `runtime` option. Closes #256.
This commit is contained in:
parent
c0af67eca1
commit
4c318166e1
@ -6,6 +6,6 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.js]
|
[*.{js,json}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|||||||
@ -56,7 +56,8 @@ File.helpers = [
|
|||||||
"class-call-check",
|
"class-call-check",
|
||||||
"object-destructuring-empty",
|
"object-destructuring-empty",
|
||||||
"temporal-undefined",
|
"temporal-undefined",
|
||||||
"temporal-assert-defined"
|
"temporal-assert-defined",
|
||||||
|
"tail-call"
|
||||||
];
|
];
|
||||||
|
|
||||||
File.validOptions = [
|
File.validOptions = [
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
21
lib/6to5/transformation/templates/tail-call.js
Normal file
21
lib/6to5/transformation/templates/tail-call.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
})()
|
||||||
@ -1,6 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var util = require("../../../util");
|
|
||||||
var t = require("../../../types");
|
var t = require("../../../types");
|
||||||
|
|
||||||
function returnBlock(expr) {
|
function returnBlock(expr) {
|
||||||
@ -27,7 +26,7 @@ function transformExpression(node, scope, state) {
|
|||||||
} else {
|
} else {
|
||||||
node.alternate = returnBlock(node.alternate);
|
node.alternate = returnBlock(node.alternate);
|
||||||
}
|
}
|
||||||
return node;
|
return [node];
|
||||||
|
|
||||||
case "LogicalExpression":
|
case "LogicalExpression":
|
||||||
// only call in right-value of can be optimized
|
// only call in right-value of can be optimized
|
||||||
@ -36,17 +35,11 @@ function transformExpression(node, scope, state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache left value as it might have side-effects
|
var test = state.wrapSideEffect(node.left);
|
||||||
var leftId = state.getLeftId();
|
|
||||||
var testExpr = t.assignmentExpression(
|
|
||||||
"=",
|
|
||||||
leftId,
|
|
||||||
node.left
|
|
||||||
);
|
|
||||||
if (node.operator === "&&") {
|
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":
|
case "SequenceExpression":
|
||||||
var seq = node.expressions;
|
var seq = node.expressions;
|
||||||
@ -66,53 +59,28 @@ function transformExpression(node, scope, state) {
|
|||||||
return [t.expressionStatement(node)].concat(lastCall);
|
return [t.expressionStatement(node)].concat(lastCall);
|
||||||
|
|
||||||
case "CallExpression":
|
case "CallExpression":
|
||||||
var callee = node.callee, prop, thisBinding, args;
|
var callee = node.callee, thisBinding;
|
||||||
|
var args = [callee];
|
||||||
|
|
||||||
if (t.isMemberExpression(callee, { computed: false }) &&
|
// bind `this` to object in member expressions
|
||||||
t.isIdentifier(prop = callee.property)) {
|
if (t.isMemberExpression(callee)) {
|
||||||
switch (prop.name) {
|
var object = state.wrapSideEffect(callee.object);
|
||||||
case "call":
|
callee.object = object.expr;
|
||||||
args = t.arrayExpression(node.arguments.slice(1));
|
thisBinding = object.ref;
|
||||||
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 (node.arguments.length > 0 || thisBinding) {
|
||||||
if (!t.isIdentifier(callee) || !scope.bindingEquals(callee.name, state.ownerId)) {
|
args.push(t.arrayExpression(node.arguments));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.hasTailRecursion = true;
|
if (thisBinding) {
|
||||||
|
args.push(thisBinding);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [t.returnStatement(t.callExpression(
|
||||||
t.expressionStatement(t.assignmentExpression(
|
state.getHelperRef(),
|
||||||
"=",
|
args
|
||||||
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);
|
})(node);
|
||||||
}
|
}
|
||||||
@ -152,56 +120,34 @@ var functionVisitor = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.FunctionDeclaration =
|
exports.FunctionDeclaration =
|
||||||
exports.FunctionExpression = function (node, parent, scope) {
|
exports.FunctionExpression = function (node, parent, scope, file) {
|
||||||
// only tail recursion can be optimized as for now,
|
var tempId, helperRef;
|
||||||
// so we can skip anonymous functions entirely
|
|
||||||
var ownerId = node.id;
|
|
||||||
if (!ownerId) return;
|
|
||||||
|
|
||||||
var argumentsId, thisId, shouldContinueId, leftId;
|
|
||||||
|
|
||||||
var state = {
|
var state = {
|
||||||
hasTailRecursion: false,
|
ownerId: node.id,
|
||||||
ownerId: ownerId,
|
|
||||||
|
|
||||||
getArgumentsId: function () {
|
getHelperRef: function () {
|
||||||
return argumentsId = argumentsId || scope.generateUidIdentifier("arguments");
|
return helperRef = helperRef || file.addHelper("tail-call");
|
||||||
},
|
},
|
||||||
|
|
||||||
getThisId: function () {
|
wrapSideEffect: function (node) {
|
||||||
return thisId = thisId || scope.generateUidIdentifier("this");
|
if (t.isIdentifier(node) || t.isLiteral(node)) {
|
||||||
},
|
return {expr: node, ref: node};
|
||||||
|
}
|
||||||
getShouldContinueId: function () {
|
tempId = tempId || scope.generateUidIdentifier("temp");
|
||||||
return shouldContinueId = shouldContinueId || scope.generateUidIdentifier("shouldContinue");
|
return {
|
||||||
},
|
expr: t.assignmentExpression("=", tempId, node),
|
||||||
|
ref: tempId
|
||||||
getLeftId: function () {
|
};
|
||||||
return leftId = leftId || scope.generateUidIdentifier("left");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// traverse the function and look for tail recursion
|
// traverse the function and look for tail recursion
|
||||||
scope.traverse(node, functionVisitor, state);
|
scope.traverse(node, functionVisitor, state);
|
||||||
|
|
||||||
if (!state.hasTailRecursion) return;
|
if (tempId) {
|
||||||
|
t.ensureBlock(node).body.unshift(t.variableDeclaration("var", [
|
||||||
var block = t.ensureBlock(node);
|
t.variableDeclarator(tempId)
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
3
test/fixtures/transformation/es6-arrow-functions/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-arrow-functions/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"loose": ["es6.classes"]
|
"loose": ["es6.classes"],
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
}
|
}
|
||||||
|
|||||||
3
test/fixtures/transformation/es6-classes/calling-super-properties/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-classes/calling-super-properties/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
3
test/fixtures/transformation/es6-destructuring/spread/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-destructuring/spread/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
3
test/fixtures/transformation/es6-modules-system/hoist-function-exports/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-modules-system/hoist-function-exports/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
3
test/fixtures/transformation/es6-properties.shorthand/method-self-reference/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-properties.shorthand/method-self-reference/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
3
test/fixtures/transformation/es6-spread/options.json
vendored
Normal file
3
test/fixtures/transformation/es6-spread/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
@ -1,27 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function f(n) {
|
(function f(n) {
|
||||||
var _arguments = arguments,
|
if (n <= 0) {
|
||||||
_this = this,
|
console.log(this, arguments);
|
||||||
_shouldContinue,
|
return "foo";
|
||||||
_result;
|
}
|
||||||
do {
|
if (Math.random() > 0.5) {
|
||||||
_shouldContinue = false;
|
return to5Runtime.tailCall(f.call, [this, n - 1], f);
|
||||||
_result = (function (n) {
|
} else {
|
||||||
if (n <= 0) {
|
return to5Runtime.tailCall(f.apply, [this, [n - 1]], f);
|
||||||
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";
|
})(1000000) === "foo";
|
||||||
|
|||||||
7
test/fixtures/transformation/es6-tail-call/cross-function/actual.js
vendored
Normal file
7
test/fixtures/transformation/es6-tail-call/cross-function/actual.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
function f(n) {
|
||||||
|
return n <= 0 ? "foo" : g(n - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function g(n) {
|
||||||
|
return n <= 0 ? "goo" : f(n - 1);
|
||||||
|
}
|
||||||
17
test/fixtures/transformation/es6-tail-call/cross-function/expected.js
vendored
Normal file
17
test/fixtures/transformation/es6-tail-call/cross-function/expected.js
vendored
Normal file
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,30 +1,18 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function f(n) {
|
(function f(n) {
|
||||||
var _arguments = arguments,
|
var _temp;
|
||||||
_this = this,
|
if (n <= 0) {
|
||||||
_shouldContinue,
|
return "foo";
|
||||||
_result;
|
} else {
|
||||||
do {
|
doSmth();
|
||||||
_shouldContinue = false;
|
|
||||||
_result = (function (n) {
|
|
||||||
var _left;
|
|
||||||
if (n <= 0) {
|
|
||||||
return "foo";
|
|
||||||
} else {
|
|
||||||
doSmth();
|
|
||||||
|
|
||||||
if (!(_left = getTrueValue())) {
|
if (!(_temp = getTrueValue())) {
|
||||||
return _left;
|
return _temp;
|
||||||
}
|
}
|
||||||
if (_left = getFalseValue()) {
|
if (_temp = getFalseValue()) {
|
||||||
return _left;
|
return _temp;
|
||||||
}
|
}
|
||||||
_arguments = [n - 1];
|
return to5Runtime.tailCall(f, [n - 1]);
|
||||||
_this = undefined;
|
}
|
||||||
return _shouldContinue = true;
|
|
||||||
}
|
|
||||||
}).apply(_this, _arguments);
|
|
||||||
} while (_shouldContinue);
|
|
||||||
return _result;
|
|
||||||
})(1000000, true) === "foo";
|
})(1000000, true) === "foo";
|
||||||
|
|||||||
4
test/fixtures/transformation/es6-tail-call/options.json
vendored
Normal file
4
test/fixtures/transformation/es6-tail-call/options.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"runtime": true,
|
||||||
|
"blacklist": []
|
||||||
|
}
|
||||||
@ -1,23 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function f(_x, /* should be undefined after first pass */m) {
|
(function f(_x, /* should be undefined after first pass */m) {
|
||||||
var _arguments = arguments,
|
var n = arguments[0] === undefined ? getDefaultValue() : arguments[0];
|
||||||
_this = this,
|
if (n <= 0) {
|
||||||
_shouldContinue,
|
return "foo";
|
||||||
_result;
|
}
|
||||||
do {
|
// Should be clean (undefined) on each pass
|
||||||
_shouldContinue = false;
|
var local;
|
||||||
_result = (function (_x, m) {
|
return to5Runtime.tailCall(f, [n - 1]);
|
||||||
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";
|
})(1000000, true) === "foo";
|
||||||
|
|||||||
7
test/fixtures/transformation/es6-tail-call/side-effect/actual.js
vendored
Normal file
7
test/fixtures/transformation/es6-tail-call/side-effect/actual.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
function f() {
|
||||||
|
return getObj().method();
|
||||||
|
}
|
||||||
|
|
||||||
|
function g() {
|
||||||
|
return getFalseValue() || getValue();
|
||||||
|
}
|
||||||
14
test/fixtures/transformation/es6-tail-call/side-effect/expected.js
vendored
Normal file
14
test/fixtures/transformation/es6-tail-call/side-effect/expected.js
vendored
Normal file
@ -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);
|
||||||
|
}
|
||||||
@ -10,26 +10,14 @@
|
|||||||
})(1000000) === "foo";
|
})(1000000) === "foo";
|
||||||
|
|
||||||
(function f(n) {
|
(function f(n) {
|
||||||
var _arguments = arguments,
|
if (n <= 0) {
|
||||||
_this = this,
|
return "foo";
|
||||||
_shouldContinue,
|
}
|
||||||
_result;
|
try {
|
||||||
do {
|
throw new Error();
|
||||||
_shouldContinue = false;
|
} catch (e) {
|
||||||
_result = (function (n) {
|
return to5Runtime.tailCall(f, [n - 1]);
|
||||||
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";
|
})(1000000) === "foo";
|
||||||
|
|
||||||
(function f(n) {
|
(function f(n) {
|
||||||
@ -44,22 +32,10 @@
|
|||||||
})(1000000) === "foo";
|
})(1000000) === "foo";
|
||||||
|
|
||||||
(function f(n) {
|
(function f(n) {
|
||||||
var _arguments = arguments,
|
if (n <= 0) {
|
||||||
_this = this,
|
return "foo";
|
||||||
_shouldContinue,
|
}
|
||||||
_result;
|
try {} finally {
|
||||||
do {
|
return to5Runtime.tailCall(f, [n - 1]);
|
||||||
_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";
|
})(1000000) === "foo";
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"experimental": true
|
"experimental": true,
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
}
|
}
|
||||||
|
|||||||
3
test/fixtures/transformation/playground/method-binding/options.json
vendored
Normal file
3
test/fixtures/transformation/playground/method-binding/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["es6.tailCall"]
|
||||||
|
}
|
||||||
3
test/fixtures/transformation/react/arrow-functions/options.json
vendored
Normal file
3
test/fixtures/transformation/react/arrow-functions/options.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"blacklist": ["useStrict", "es6.tailCall"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user