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
insert_final_newline = true
[*.js]
[*.{js,json}]
indent_style = space
indent_size = 2

View File

@ -56,7 +56,8 @@ File.helpers = [
"class-call-check",
"object-destructuring-empty",
"temporal-undefined",
"temporal-assert-defined"
"temporal-assert-defined",
"tail-call"
];
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";
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;
// bind `this` to object in member expressions
if (t.isMemberExpression(callee)) {
var object = state.wrapSideEffect(callee.object);
callee.object = object.expr;
thisBinding = object.ref;
}
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;
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,
});
};

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";
(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;
return to5Runtime.tailCall(f.call, [this, n - 1], f);
} else {
_arguments = [n - 1];
_this = this;
return _shouldContinue = true;
return to5Runtime.tailCall(f.apply, [this, [n - 1]], f);
}
}).apply(_this, _arguments);
} while (_shouldContinue);
return _result;
})(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";
(function f(n) {
var _arguments = arguments,
_this = this,
_shouldContinue,
_result;
do {
_shouldContinue = false;
_result = (function (n) {
var _left;
var _temp;
if (n <= 0) {
return "foo";
} else {
doSmth();
if (!(_left = getTrueValue())) {
return _left;
if (!(_temp = getTrueValue())) {
return _temp;
}
if (_left = getFalseValue()) {
return _left;
if (_temp = getFalseValue()) {
return _temp;
}
_arguments = [n - 1];
_this = undefined;
return _shouldContinue = true;
return to5Runtime.tailCall(f, [n - 1]);
}
}).apply(_this, _arguments);
} while (_shouldContinue);
return _result;
})(1000000, true) === "foo";

View File

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

View File

@ -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;
return to5Runtime.tailCall(f, [n - 1]);
})(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";
(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;
return to5Runtime.tailCall(f, [n - 1]);
}
}).apply(_this, _arguments);
} while (_shouldContinue);
return _result;
})(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;
return to5Runtime.tailCall(f, [n - 1]);
}
}).apply(_this, _arguments);
} while (_shouldContinue);
return _result;
})(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"]
}