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
|
||||
insert_final_newline = true
|
||||
|
||||
[*.js]
|
||||
[*.{js,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@ -56,7 +56,8 @@ File.helpers = [
|
||||
"class-call-check",
|
||||
"object-destructuring-empty",
|
||||
"temporal-undefined",
|
||||
"temporal-assert-defined"
|
||||
"temporal-assert-defined",
|
||||
"tail-call"
|
||||
];
|
||||
|
||||
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";
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
(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";
|
||||
|
||||
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";
|
||||
|
||||
(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";
|
||||
|
||||
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";
|
||||
|
||||
(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";
|
||||
|
||||
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";
|
||||
|
||||
(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";
|
||||
|
||||
@ -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