Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements for TCO #736

Merged
merged 2 commits into from Feb 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 1 addition & 13 deletions lib/6to5/transformation/templates/tail-call-body.js
@@ -1,15 +1,3 @@
{
var ARGUMENTS_ID = arguments,
THIS_ID = this,
SHOULD_CONTINUE_ID,
RESULT_ID;

var CALLEE_ID = FUNCTION;

do {
SHOULD_CONTINUE_ID = false;
RESULT_ID = CALLEE_ID.apply(THIS_ID, ARGUMENTS_ID);
} while(SHOULD_CONTINUE_ID);

return RESULT_ID;
FUNCTION_ID:while (true) BLOCK
}
Expand Up @@ -73,7 +73,9 @@ exports.Function = function (node, parent, scope, file) {
var left = param.left;
var right = param.right;

node.params[i] = scope.generateUidIdentifier("x");
var placeholder = scope.generateUidIdentifier("x");
placeholder._isDefaultPlaceholder = true;
node.params[i] = placeholder;

if (!state.iife) {
if (t.isIdentifier(right) && scope.hasOwnReference(right.name)) {
Expand Down
212 changes: 165 additions & 47 deletions lib/6to5/transformation/transformers/es6/tail-call.js
@@ -1,5 +1,6 @@
"use strict";

var _ = require("lodash");
var util = require("../../../util");
var t = require("../../../types");

Expand Down Expand Up @@ -27,7 +28,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
Expand Down Expand Up @@ -96,71 +97,137 @@ function transformExpression(node, scope, state) {

state.hasTailRecursion = true;

return [
t.expressionStatement(t.assignmentExpression(
"=",
state.getArgumentsId(),
args || t.arrayExpression(node.arguments)
)),
var body = [];

t.expressionStatement(t.assignmentExpression(
if (!t.isThisExpression(thisBinding)) {
body.push(t.expressionStatement(t.assignmentExpression(
"=",
state.getThisId(),
thisBinding || t.identifier("undefined")
)),
)));
}

t.returnStatement(t.assignmentExpression(
"=",
state.getShouldContinueId(),
t.literal(true)
))
];
if (!args) {
args = t.arrayExpression(node.arguments);
}

var argumentsId = state.getArgumentsId();
var params = state.getParams();

body.push(t.expressionStatement(t.assignmentExpression(
"=",
argumentsId,
args
)));

var i, param;

if (t.isArrayExpression(args)) {
var elems = args.elements;
for (i = 0; i < elems.length && i < params.length; i++) {
param = params[i];
var elem = elems[i] || (elems[i] = t.identifier("undefined"));
if (!param._isDefaultPlaceholder) {
elems[i] = t.assignmentExpression("=", param, elem);
}
}
} else {
state.setsArguments = true;
for (i = 0; i < params.length; i++) {
param = params[i];
if (!param._isDefaultPlaceholder) {
body.push(t.expressionStatement(t.assignmentExpression(
"=",
param,
t.memberExpression(argumentsId, t.literal(i), true)
)));
}
}
}

body.push(t.continueStatement(state.getFunctionId()));

return body;
}
})(node);
}

var functionChildrenVisitor = {
// Looks for and replaces tail recursion calls.
var firstPass = {
enter: function (node, parent, scope, state) {
if (t.isReturnStatement(node)) {
// prevent entrance by current visitor
this.skip();

// transform return argument into statement if
// it contains tail recursion
return transformExpression(node.argument, scope, state);
} else if (t.isFunction(node)) {
this.skip();
} else if (t.isTryStatement(parent)) {
if (node === parent.block) {
this.skip();
} else if (parent.finalizer && node !== parent.finalizer) {
this.skip();
}
} else if (t.isFunction(node)) {
this.skip();
} else if (t.isVariableDeclaration(node)) {
this.skip();
state.vars.push(node);
}
}
};

var functionVisitor = {
// Hoists up function declarations, replaces `this` and `arguments` and
// marks them as needed.
var secondPass = {
enter: function (node, parent, scope, state) {
// traverse all child nodes of this function and find `arguments` and `this`
scope.traverse(node, functionChildrenVisitor, state);
if (t.isThisExpression(node)) {
state.needsThis = true;
return state.getThisId();
} else if (t.isReferencedIdentifier(node, parent, { name: "arguments" })) {
state.needsArguments = true;
return state.getArgumentsId();
} else if (t.isFunction(node)) {
this.skip();
if (t.isFunctionDeclaration(node)) {
node = t.variableDeclaration("var", [
t.variableDeclarator(node.id, t.toExpression(node))
]);
node._blockHoist = 2;
return node;
}
}
}
};

return this.skip();
// Optimizes recursion by removing `this` and `arguments`
// if they are not used.
var thirdPass = {
enter: function (node, parent, scope, state) {
if (!t.isExpressionStatement(node)) return;
var expr = node.expression;
if (!t.isAssignmentExpression(expr)) return;
if (!state.needsThis && expr.left === state.getThisId()) {
this.remove();
} else if (!state.needsArguments && expr.left === state.getArgumentsId() && t.isArrayExpression(expr.right)) {
return _.map(expr.right.elements, function (elem) {
return t.expressionStatement(elem);
});
}
}
};

exports.FunctionDeclaration =
exports.FunctionExpression = function (node, parent, scope) {
exports.Function = 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;
var argumentsId, thisId, leftId, functionId, params, paramDecls;

var state = {
hasTailRecursion: false,
needsThis: false,
needsArguments: false,
setsArguments: false,
ownerId: ownerId,
vars: [],

getArgumentsId: function () {
return argumentsId = argumentsId || scope.generateUidIdentifier("arguments");
Expand All @@ -170,37 +237,88 @@ exports.FunctionExpression = function (node, parent, scope) {
return thisId = thisId || scope.generateUidIdentifier("this");
},

getShouldContinueId: function () {
return shouldContinueId = shouldContinueId || scope.generateUidIdentifier("shouldContinue");
},

getLeftId: function () {
return leftId = leftId || scope.generateUidIdentifier("left");
},

getFunctionId: function () {
return functionId = functionId || scope.generateUidIdentifier("function");
},

getParams: function () {
if (!params) {
params = node.params;
paramDecls = [];
for (var i = 0; i < params.length; i++) {
var param = params[i];
if (!param._isDefaultPlaceholder) {
paramDecls.push(t.variableDeclarator(
param,
params[i] = scope.generateUidIdentifier("x")
));
}
}
}
return params;
}
};

// traverse the function and look for tail recursion
scope.traverse(node, functionVisitor, state);
scope.traverse(node, firstPass, state);

if (!state.hasTailRecursion) return;

var block = t.ensureBlock(node);
scope.traverse(node, secondPass, state);

if (leftId) {
block.body.unshift(t.variableDeclaration("var", [
t.variableDeclarator(leftId)
]));
if (!state.needsThis || !state.needsArguments) {
scope.traverse(node, thirdPass, state);
}

var body = t.ensureBlock(node).body;

if (state.vars.length > 0) {
body.unshift(t.expressionStatement(
_(state.vars)
.map(function (decl) {
return decl.declarations;
})
.flatten()
.reduceRight(function (expr, decl) {
return t.assignmentExpression("=", decl.id, expr);
}, t.identifier("undefined"))
));
}

var resultId = scope.generateUidIdentifier("result");
state.getShouldContinueId();
if (paramDecls.length > 0) {
body.unshift(t.variableDeclaration("var", paramDecls));
}

node.body = util.template("tail-call-body", {
SHOULD_CONTINUE_ID: shouldContinueId,
ARGUMENTS_ID: argumentsId,
RESULT_ID: resultId,
CALLEE_ID: scope.generateUidIdentifier("callee"),
FUNCTION: t.functionExpression(null, node.params, block),
THIS_ID: thisId,
THIS_ID: thisId,
ARGUMENTS_ID: argumentsId,
FUNCTION_ID: state.getFunctionId(),
BLOCK: node.body
});

var topVars = [];

if (state.needsThis) {
topVars.push(t.variableDeclarator(state.getThisId(), t.thisExpression()));
}

if (state.needsArguments || state.setsArguments) {
var decl = t.variableDeclarator(state.getArgumentsId());
if (state.needsArguments) {
decl.init = t.identifier("arguments");
}
topVars.push(decl);
}

if (leftId) {
topVars.push(t.variableDeclarator(leftId));
}

if (topVars.length > 0) {
node.body.body.unshift(t.variableDeclaration("var", topVars));
}
};
2 changes: 2 additions & 0 deletions lib/6to5/transformation/transformers/index.js
Expand Up @@ -63,6 +63,8 @@ module.exports = {
// needs to be after `es6.blockScoping` due to needing `letReferences` set on blocks
"es6.blockScopingTDZ": require("./es6/block-scoping-tdz"),

// needs to be after `es6.parameters.*` and `es6.blockScoping` due to needing pure
// identifiers in parameters and variable declarators
"es6.tailCall": require("./es6/tail-call"),

regenerator: require("./other/regenerator"),
Expand Down
2 changes: 1 addition & 1 deletion lib/6to5/traversal/scope.js
Expand Up @@ -408,7 +408,7 @@ Scope.prototype.addBindingToFunctionScope = function (node, kind) {
extend(scope.bindings, ids);
extend(scope.references, ids);

if (kind) extend(scope.bindingKinds[kind], ids)
if (kind) extend(scope.bindingKinds[kind], ids);
};

/**
Expand Down
3 changes: 3 additions & 0 deletions lib/6to5/util.js
Expand Up @@ -84,6 +84,9 @@ exports.sourceMapToComment = function (map) {

var templateVisitor = {
enter: function (node, parent, scope, nodes) {
if (t.isExpressionStatement(node)) {
node = node.expression;
}
if (t.isIdentifier(node) && has(nodes, node.name)) {
return nodes[node.name];
}
Expand Down
33 changes: 12 additions & 21 deletions test/fixtures/transformation/es6-tail-call/call-apply/expected.js
@@ -1,29 +1,20 @@
"use strict";

(function f(n) {
var _arguments = arguments,
_this = this,
_shouldContinue,
_result;
var _callee = function (n) {
(function f(_x) {
var _this = this,
_arguments = arguments;
_function: while (true) {
var n = _x;
if (n <= 0) {
console.log(this, arguments);
console.log(_this, _arguments);
return "foo";
}
if (Math.random() > 0.5) {
_arguments = [n - 1];
_this = this;
return _shouldContinue = true;
_arguments = [_x = n - 1];
continue _function;
} else {
_arguments = [n - 1];
_this = this;
return _shouldContinue = true;
_arguments = [_x = n - 1];
continue _function;
}
};

do {
_shouldContinue = false;
_result = _callee.apply(_this, _arguments);
} while (_shouldContinue);
return _result;
})(1000000) === "foo";
}
})(1000000) === "foo";