lib/instrument.js
"use strict";
var astgen = require("./astgen.js");
var esprima = require("esprima");
var estraverse = require("estraverse");
var escodegen = require("escodegen");
var jsstana = new (require("jsstana"))();
var _ = require("lodash");
jsstana.addMatcher("var!", function () {
var that = this;
var declarationsM = Array.prototype.slice.call(arguments).map(that.matcher, that);
return function (node) {
if (node.type !== "VariableDeclaration") { return undefined; }
if (node.declarations.length !== declarationsM.length) { return undefined; }
var matches = [];
for (var i = 0; i < declarationsM.length; i++) {
var m = declarationsM[i](node.declarations[i]);
if (m === undefined) { return undefined; }
matches.push(m);
}
return that.combineMatches.apply(that, matches);
};
});
jsstana.addMatcher("fn-expr", function () {
return function (node) {
return node !== null && node.type === "FunctionExpression" ? {} : undefined;
};
});
var ESPRIMA_OPTIONS = {
comment: true,
range: true,
tokens: true,
};
var ESCODEGEN_OPTIONS = {
format: {
indent: {
style: " ",
},
quotes: "double",
},
};
function astgentypify(signature, node) {
return astgen.call(
astgen.property(
astgen.identifier("global"),
astgen.identifier("__typify")
),
[astgen.literal(signature), node]
);
}
// Find comments with ::
function findSignature(node) {
if (!node.leadingComments) {
return;
}
var m;
for (var i = 0; i < node.leadingComments.length; i++) {
m = node.leadingComments[i].value.match(/::\s*(.*)/);
if (m) {
break;
}
}
if (!m) {
return;
}
return m[1];
}
function instrumentFunctionDeclaration(node) {
// console.log(node.leadingComments);
var signature = findSignature(node);
if (!signature) {
// console.log(escodegen.generate(node, ESCODEGEN_OPTIONS));
return;
}
signature = node.id.name + " :: " + signature;
var g = astgen.property(node.id, astgen.identifier("__typify__"));
var newnode = astgen.fndecl(node.id, [], astgen.block([
astgen.expr(astgen.assign("=", g, astgen.logical("||", g,
astgentypify(
signature,
astgen.fnexpr(node.params, node.body)
)
))),
astgen.returnstmt(astgen.call(
astgen.property(g, astgen.identifier("apply")),
[astgen.identifier("this"), astgen.identifier("arguments")]
)),
]));
// console.log(escodegen.generate(newnode));
return newnode;
}
function instrumentFunctionExpressionVar(node, id, fnnode) {
var signature = findSignature(node);
if (!signature) {
return;
}
var fnid = (fnnode.id && fnnode.id.name) || id;
signature = fnid + " :: " + signature;
var newnode = _.cloneDeep(node);
newnode.declarations[0].init = astgentypify(signature, fnnode);
return newnode;
}
function instrumentFunctionExpressionReturn(node, fnnode) {
var signature = findSignature(node);
if (!signature) {
return;
}
var fnid = (fnnode.id && fnnode.id.name);
signature = (fnid ? fnid + " :: " : "") + signature;
var newnode = _.cloneDeep(node);
newnode.argument = astgentypify(signature, fnnode);
return newnode;
}
function trim(str) {
return str.replace(/^\s*/, "").replace(/\s*$/, "");
}
function instrument(stats, code, file) {
// console.log("Instrumenting file:", file);
var syntax = esprima.parse(code, ESPRIMA_OPTIONS);
// attach comments
estraverse.attachComments(syntax, syntax.comments, syntax.tokens);
// Find all blocks
var blocks = [];
estraverse.traverse(syntax, {
enter: function (node) {
if (node.type === "BlockStatement" || node.type === "Program") {
blocks.push(node);
}
},
});
syntax.comments.forEach(function (comment) {
if (comment.value.match(/^\s*typify:/)) {
var m;
var newnode;
m = comment.value.match(/^\s*typify:\s*type\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+=\s*(.*)$/);
if (m) {
newnode = astgen.exprstmt(astgen.call(
astgen.property(
astgen.property(
astgen.identifier("global"),
astgen.identifier("__typify")
),
astgen.identifier("alias")
),
[astgen.literal(m[1]), astgen.literal(m[2])]
));
}
m = comment.value.match(/^\s*typify:\s*instance\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*$/);
if (m) {
newnode = astgen.exprstmt(astgen.call(
astgen.property(
astgen.property(
astgen.identifier("global"),
astgen.identifier("__typify")
),
astgen.identifier("instance")
),
[astgen.literal(m[1]), astgen.identifier(m[1])]
));
}
m = comment.value.match(/^\s*typify:\s*adt\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\n*((?:\s*(?:[a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*[^\n]+\n)+)\s*/);
if (m) {
var name = m[1];
var parts = _.extend.apply(undefined,
m[2].split("\n").map(trim).map(function (part) {
if (part === "") {
return {};
}
var subm = part.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
var ret = {};
ret[subm[1]] = subm[2];
return ret;
})
);
newnode = astgen.exprstmt(astgen.call(
astgen.property(
astgen.property(
astgen.identifier("global"),
astgen.identifier("__typify")
),
astgen.identifier("adt")
),
[astgen.literal(name), astgen.literal(parts)]
));
}
if (newnode) {
// console.info("typify: " + escodegen.generate(newnode, ESCODEGEN_OPTIONS));
newnode.range = comment.range;
var block = syntax;
blocks.forEach(function (possibleBlock) {
if (possibleBlock.range[0] <= comment.range[0] && possibleBlock.range[1] >= comment.range[1]) {
if (possibleBlock.range[0] >= block.range[0] && possibleBlock.range[1] <= block.range[1]) {
block = possibleBlock;
}
}
});
var i = 0;
while (i < block.body.length && block.body[i].range[0] < comment.range[0]) {
i++;
}
block.body.splice(i, 0, newnode);
}
}
});
function count(type, value) {
stats[type].total += 1;
if (value) {
stats[type].count += 1;
}
return value;
}
// traverse
estraverse.replace(syntax, {
enter: function (node) {
if (node.type === "FunctionDeclaration") {
return count("functionDeclaration", instrumentFunctionDeclaration(node));
}
var m = jsstana.match("(var! (var ?ident (?f fn-expr)))", node);
if (m) {
return count("varFunctionExpression", instrumentFunctionExpressionVar(node, m.ident, m.f));
}
m = jsstana.match("(return (?f fn-expr))", node);
if (m) {
return count("returnFunctionExpression", instrumentFunctionExpressionReturn(node, m.f));
}
},
});
var result = escodegen.generate(syntax, _.extend({}, ESCODEGEN_OPTIONS, {
sourceMap: true,
sourceMapWithCode: true,
}));
return result.code;
}
function StatEntry() {
this.total = 0;
this.count = 0;
}
function Stats() {
this.functionDeclaration = new StatEntry();
this.varFunctionExpression = new StatEntry();
this.returnFunctionExpression = new StatEntry();
}
instrument.Stats = Stats;
module.exports = instrument;