refactor tail call transformer into a class - @RReverser

This commit is contained in:
Sebastian McKenzie 2015-02-10 22:11:22 +11:00
parent 87af83f1cb
commit f1bca0013e

View File

@ -1,163 +1,287 @@
"use strict"; "use strict";
var _ = require("lodash");
var util = require("../../../util"); var util = require("../../../util");
var t = require("../../../types"); var t = require("../../../types");
var _ = require("lodash");
function returnBlock(expr) { function returnBlock(expr) {
return t.blockStatement([t.returnStatement(expr)]); return t.blockStatement([t.returnStatement(expr)]);
} }
function transformExpression(node, scope, state) { function TailCall(node, scope, file) {
if (!node) return; this.hasTailRecursion = false;
this.needsArguments = false;
this.setsArguments = false;
this.needsThis = false;
this.ownerId = node.id;
this.vars = [];
return (function subTransform(node) { this.scope = scope;
switch (node.type) { this.file = file;
case "ConditionalExpression": this.node = node;
var callConsequent = subTransform(node.consequent);
var callAlternate = subTransform(node.alternate);
if (!callConsequent && !callAlternate) {
return;
}
// if ternary operator had tail recursion in value, convert to optimized if-statement
node.type = "IfStatement";
node.consequent = callConsequent ? t.toBlock(callConsequent) : returnBlock(node.consequent);
if (callAlternate) {
node.alternate = t.isIfStatement(callAlternate) ? callAlternate : t.toBlock(callAlternate);
} else {
node.alternate = returnBlock(node.alternate);
}
return [node];
case "LogicalExpression":
// only call in right-value of can be optimized
var callRight = subTransform(node.right);
if (!callRight) {
return;
}
// cache left value as it might have side-effects
var leftId = state.getLeftId();
var testExpr = t.assignmentExpression(
"=",
leftId,
node.left
);
if (node.operator === "&&") {
testExpr = t.unaryExpression("!", testExpr);
}
return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight);
case "SequenceExpression":
var seq = node.expressions;
// only last element can be optimized
var lastCall = subTransform(seq[seq.length - 1]);
if (!lastCall) {
return;
}
// remove converted expression from sequence
// and convert to regular expression if needed
if (--seq.length === 1) {
node = seq[0];
}
return [t.expressionStatement(node)].concat(lastCall);
case "CallExpression":
var callee = node.callee, prop, thisBinding, args;
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;
}
// only tail recursion can be optimized as for now
if (!t.isIdentifier(callee) || !scope.bindingEquals(callee.name, state.ownerId)) {
return;
}
state.hasTailRecursion = true;
var body = [];
if (!t.isThisExpression(thisBinding)) {
body.push(t.expressionStatement(t.assignmentExpression(
"=",
state.getThisId(),
thisBinding || t.identifier("undefined")
)));
}
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);
} }
// Looks for and replaces tail recursion calls. TailCall.prototype.getArgumentsId = function () {
return this.argumentsId = this.argumentsId || this.scope.generateUidIdentifier("arguments");
};
TailCall.prototype.getThisId = function () {
return this.thisId = this.thisId || this.scope.generateUidIdentifier("this");
};
TailCall.prototype.getLeftId = function () {
return this.leftId = this.leftId || this.scope.generateUidIdentifier("left");
};
TailCall.prototype.getFunctionId = function () {
return this.functionId = this.functionId || this.scope.generateUidIdentifier("function");
};
TailCall.prototype.getParams = function () {
var params = this.params;
if (!params) {
params = this.node.params;
this.paramDecls = [];
for (var i = 0; i < params.length; i++) {
var param = params[i];
if (!param._isDefaultPlaceholder) {
this.paramDecls.push(t.variableDeclarator(
param,
params[i] = this.scope.generateUidIdentifier("x")
));
}
}
}
return this.params = params;
};
TailCall.prototype.run = function () {
var scope = this.scope;
var node = this.node;
// only tail recursion can be optimized as for now,
// so we can skip anonymous functions entirely
var ownerId = this.ownerId;
if (!ownerId) return;
// traverse the function and look for tail recursion
scope.traverse(node, firstPass, this);
if (!this.hasTailRecursion) return;
//
scope.traverse(node, secondPass, this);
if (!this.needsThis || !this.needsArguments) {
scope.traverse(node, thirdPass, this);
}
var body = t.ensureBlock(node).body;
if (this.vars.length > 0) {
body.unshift(t.expressionStatement(
_(this.vars)
.map(function (decl) {
return decl.declarations;
})
.flatten()
.reduceRight(function (expr, decl) {
return t.assignmentExpression("=", decl.id, expr);
}, t.identifier("undefined"))
));
}
var paramDecls = this.paramDecls;
if (paramDecls.length > 0) {
body.unshift(t.variableDeclaration("var", paramDecls));
}
node.body = util.template("tail-call-body", {
THIS_ID: this.thisId,
ARGUMENTS_ID: this.argumentsId,
FUNCTION_ID: this.getFunctionId(),
BLOCK: node.body
});
var topVars = [];
if (this.needsThis) {
topVars.push(t.variableDeclarator(this.getThisId(), t.thisExpression()));
}
if (this.needsArguments || this.setsArguments) {
var decl = t.variableDeclarator(this.getArgumentsId());
if (this.needsArguments) {
decl.init = t.identifier("arguments");
}
topVars.push(decl);
}
var leftId = this.leftId;
if (leftId) {
topVars.push(t.variableDeclarator(leftId));
}
if (topVars.length > 0) {
node.body.body.unshift(t.variableDeclaration("var", topVars));
}
};
TailCall.prototype.subTransform = function (node) {
var handler = this["subTransform" + node.type];
if (handler) return handler.call(this, node);
};
TailCall.prototype.subTransformConditionalExpression = function (node) {
var callConsequent = this.subTransform(node.consequent);
var callAlternate = this.subTransform(node.alternate);
if (!callConsequent && !callAlternate) {
return;
}
// if ternary operator had tail recursion in value, convert to optimized if-statement
node.type = "IfStatement";
node.consequent = callConsequent ? t.toBlock(callConsequent) : returnBlock(node.consequent);
if (callAlternate) {
node.alternate = t.isIfStatement(callAlternate) ? callAlternate : t.toBlock(callAlternate);
} else {
node.alternate = returnBlock(node.alternate);
}
return [node];
};
TailCall.prototype.subTransformLogicalExpression = function (node) {
// only call in right-value of can be optimized
var callRight = this.subTransform(node.right);
if (!callRight) return;
// cache left value as it might have side-effects
var leftId = this.getLeftId();
var testExpr = t.assignmentExpression(
"=",
leftId,
node.left
);
if (node.operator === "&&") {
testExpr = t.unaryExpression("!", testExpr);
}
return [t.ifStatement(testExpr, returnBlock(leftId))].concat(callRight);
};
TailCall.prototype.subTransformSequenceExpression = function (node) {
var seq = node.expressions;
// only last element can be optimized
var lastCall = this.subTransform(seq[seq.length - 1]);
if (!lastCall) {
return;
}
// remove converted expression from sequence
// and convert to regular expression if needed
if (--seq.length === 1) {
node = seq[0];
}
return [t.expressionStatement(node)].concat(lastCall);
};
TailCall.prototype.subTransformCallExpression = function (node) {
var callee = node.callee, prop, thisBinding, args;
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;
}
// only tail recursion can be optimized as for now
if (!t.isIdentifier(callee) || !this.scope.bindingEquals(callee.name, this.ownerId)) {
return;
}
this.hasTailRecursion = true;
var body = [];
if (!t.isThisExpression(thisBinding)) {
body.push(t.expressionStatement(t.assignmentExpression(
"=",
this.getThisId(),
thisBinding || t.identifier("undefined")
)));
}
if (!args) {
args = t.arrayExpression(node.arguments);
}
var argumentsId = this.getArgumentsId();
var params = this.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 {
this.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(this.getFunctionId()));
return body;
};
// looks for and replaces tail recursion calls
var firstPass = { var firstPass = {
enter: function (node, parent, scope, state) { enter: function (node, parent, scope, state) {
if (t.isReturnStatement(node)) { if (t.isReturnStatement(node)) {
this.skip(); this.skip();
return transformExpression(node.argument, scope, state); return state.subTransform(node.argument);
} else if (t.isTryStatement(parent)) { } else if (t.isTryStatement(parent)) {
if (node === parent.block) { if (node === parent.block) {
this.skip(); this.skip();
@ -173,8 +297,8 @@ var firstPass = {
} }
}; };
// Hoists up function declarations, replaces `this` and `arguments` and // hoists up function declarations, replaces `this` and `arguments` and marks
// marks them as needed. // them as needed
var secondPass = { var secondPass = {
enter: function (node, parent, scope, state) { enter: function (node, parent, scope, state) {
if (t.isThisExpression(node)) { if (t.isThisExpression(node)) {
@ -196,8 +320,7 @@ var secondPass = {
} }
}; };
// Optimizes recursion by removing `this` and `arguments` // optimizes recursion by removing `this` and `arguments` if they aren't used
// if they are not used.
var thirdPass = { var thirdPass = {
enter: function (node, parent, scope, state) { enter: function (node, parent, scope, state) {
if (!t.isExpressionStatement(node)) return; if (!t.isExpressionStatement(node)) return;
@ -213,112 +336,7 @@ var thirdPass = {
} }
}; };
exports.Function = function (node, parent, scope) { exports.Function = function (node, parent, scope, file) {
// only tail recursion can be optimized as for now, var tailCall = new TailCall(node, scope, file);
// so we can skip anonymous functions entirely tailCall.run();
var ownerId = node.id;
if (!ownerId) return;
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");
},
getThisId: function () {
return thisId = thisId || scope.generateUidIdentifier("this");
},
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, firstPass, state);
if (!state.hasTailRecursion) return;
scope.traverse(node, secondPass, state);
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"))
));
}
if (paramDecls.length > 0) {
body.unshift(t.variableDeclaration("var", paramDecls));
}
node.body = util.template("tail-call-body", {
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));
}
}; };