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:
Ingvar Stepanyan 2015-02-07 22:20:07 +02:00
parent c0af67eca1
commit 4c318166e1
24 changed files with 181 additions and 213 deletions

View File

@ -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

View File

@ -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 = [

View File

@ -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;
}

View 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;
};
})()

View File

@ -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,
});
}; };

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -1,3 +1,4 @@
{ {
"loose": ["es6.classes"] "loose": ["es6.classes"],
"blacklist": ["es6.tailCall"]
} }

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -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";

View 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);
}

View 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]);
}
}

View File

@ -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";

View File

@ -0,0 +1,4 @@
{
"runtime": true,
"blacklist": []
}

View File

@ -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";

View File

@ -0,0 +1,7 @@
function f() {
return getObj().method();
}
function g() {
return getFalseValue() || getValue();
}

View 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);
}

View File

@ -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";

View File

@ -1,3 +1,4 @@
{ {
"experimental": true "experimental": true,
"blacklist": ["es6.tailCall"]
} }

View File

@ -0,0 +1,3 @@
{
"blacklist": ["es6.tailCall"]
}

View File

@ -0,0 +1,3 @@
{
"blacklist": ["useStrict", "es6.tailCall"]
}