Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial

  • Loading branch information...
commit 3fe88053da3c77d6b2f81434effa8623578f0f8a 0 parents
@indutny authored
2  .gitignore
@@ -0,0 +1,2 @@
+node_modules/
+npm-debug.log
1  README.md
@@ -0,0 +1 @@
+# Spoon
10 lib/spoon.js
@@ -0,0 +1,10 @@
+var spoon = exports;
+
+spoon.block = require('./spoon/block');
+spoon.instruction = require('./spoon/instruction');
+spoon.cfg = require('./spoon/cfg');
+spoon.renderer = require('./spoon/renderer');
+
+// Export API
+spoon.construct = require('./spoon/api').construct;
+spoon.render = require('./spoon/api').render;
16 lib/spoon/api.js
@@ -0,0 +1,16 @@
+var api = exports,
+ spoon = require('../spoon');
+
+api.construct = function construct(ast) {
+ var cfg = spoon.cfg.create();
+
+ cfg.translate(ast);
+
+ return cfg;
+};
+
+api.render = function render(cfg) {
+ var r = spoon.renderer.create(cfg);
+
+ return r.render();
+};
74 lib/spoon/block.js
@@ -0,0 +1,74 @@
+var block = exports,
+ spoon = require('../spoon');
+
+function Block(cfg) {
+ this.cfg = cfg;
+ this.id = cfg.blockId++;
+ this.successors = [];
+ this.predecessors = [];
+ this.instructions = [];
+ this.root = null;
+ this.loop = false;
+
+ this.ended = false;
+};
+block.Block = Block;
+block.create = function create(cfg) {
+ return new Block(cfg);
+};
+
+Block.prototype.toString = function toString() {
+ var buff = '[block ' + this.id + '' + (this.loop ? ' loop' : '') + ']\n';
+
+ buff += '# predecessors: ' + this.predecessors.map(function(b) {
+ return b.id
+ }).join(', ') + '\n';
+
+ this.instructions.forEach(function(instr) {
+ buff += instr.toString() + '\n';
+ });
+
+ buff += '# successors: ' + this.successors.map(function(b) {
+ return b.id
+ }).join(', ') + '\n';
+
+ return buff;
+};
+
+Block.prototype.add = function add(type, args) {
+ var instr = spoon.instruction.create(this, type, args || []);
+ if (this.ended) return instr;
+
+ this.instructions.push(instr);
+ return instr;
+};
+
+Block.prototype.end = function end() {
+ this.ended = true;
+};
+
+Block.prototype.addSuccessor = function addSuccessor(block) {
+ if (this.successors.length == 2) {
+ throw new Error('Block can\'t have more than 2 successors');
+ }
+ this.successors.push(block);
+ block.addPredecessor(this);
+};
+
+Block.prototype.addPredecessor = function addPredecessor(block) {
+ if (this.predecessors.length == 2) {
+ throw new Error('Block can\'t have more than 2 predecessors');
+ }
+ this.predecessors.push(block);
+};
+
+Block.prototype.goto = function goto(block) {
+ if (this.ended) return block;
+
+ this.add('goto');
+ this.addSuccessor(block);
+ this.end();
+
+ // For chaining
+ return block;
+};
348 lib/spoon/cfg.js
@@ -0,0 +1,348 @@
+var cfg = exports,
+ spoon = require('../spoon');
+
+function Cfg() {
+ this.instructionId = 0;
+ this.blockId = 0;
+
+ this.root = null;
+ this.blocks = [];
+ this.roots = [];
+ this.rootQueue = [];
+ this.current = null;
+
+ this.breakInfo = null;
+};
+cfg.Cfg = Cfg;
+cfg.create = function create() {
+ return new Cfg();
+};
+
+Cfg.prototype.toString = function toString() {
+ var buff = '--- CFG ---\n';
+
+ this.blocks.forEach(function(block) {
+ buff += block.toString() + '\n';
+ });
+
+ return buff;
+};
+
+Cfg.prototype.createBlock = function createBlock() {
+ var block = spoon.block.create(this);
+
+ this.blocks.push(block);
+
+ return block;
+};
+
+Cfg.prototype.setCurrentBlock = function setCurrentBlock(block) {
+ this.current = block;
+};
+
+Cfg.prototype.add = function add(type, args) {
+ return this.current.add(type, args);
+};
+
+Cfg.prototype.translate = function translate(ast) {
+ this.rootQueue.push({
+ instr: null,
+ ast: ast,
+ });
+
+ while (this.rootQueue.length > 0) {
+ var root = this.rootQueue.shift(),
+ block = this.createBlock();
+
+ if (!this.root) this.root = block;
+
+ this.roots.push(block);
+ this.setCurrentBlock(block);
+ if (root.instr) root.instr.addArg(block);
+ block.root = root;
+
+ this.visit(root.ast);
+ }
+};
+
+Cfg.prototype.visit = function visit(ast) {
+ var t = ast.type;
+
+ if (t === 'Program' || t === 'BlockStatement') {
+ return this.visitBlock(ast);
+ } else if (t === 'ExpressionStatement') {
+ return this.visitExpr(ast);
+ } else if (t === 'CallExpression') {
+ return this.visitCall(ast);
+ } else if (t === 'VariableDeclaration') {
+ return this.visitVar(ast);
+ } else if (t === 'AssignmentExpression') {
+ return this.visitAssign(ast);
+ } else if (t === 'BinaryExpression') {
+ return this.visitBinop(ast);
+ } else if (t === 'UnaryExpression') {
+ return this.visitUnop(ast);
+ } else if (t === 'UpdateExpression') {
+ return this.visitUnop(ast);
+ } else if (t === 'Literal') {
+ return this.visitLiteral(ast);
+ } else if (t === 'Identifier') {
+ return this.visitIdentifier(ast);
+ } else if (t === 'MemberExpression') {
+ return this.visitMember(ast);
+ } else if (t === 'IfStatement') {
+ return this.visitIf(ast);
+ } else if (t === 'FunctionExpression') {
+ return this.visitFunction(ast);
+ } else if (t === 'FunctionDeclaration') {
+ return this.visitFunction(ast);
+ } else if (t === 'ReturnStatement') {
+ return this.visitReturn(ast);
+ } else if (t === 'WhileStatement') {
+ return this.visitWhile(ast);
+ } else if (t === 'DoWhileStatement') {
+ return this.visitDoWhile(ast);
+ } else if (t === 'BreakStatement') {
+ return this.visitBreak(ast);
+ } else if (t === 'ContinueStatement') {
+ return this.visitContinue(ast);
+ } else {
+ throw new Error('Type: ' + t + ' is not supported yet!');
+ }
+};
+
+Cfg.prototype.visitBlock = function visitBlock(ast) {
+ // Visit each statement
+ ast.body.forEach(function(instr) {
+ this.visit(instr);
+ }, this);
+
+ return null;
+};
+
+Cfg.prototype.visitExpr = function visitExpr(ast) {
+ return this.visit(ast.expression);
+};
+
+Cfg.prototype.visitCall = function visitCall(ast) {
+ return this.add('call', [
+ this.visit(ast.callee)
+ ].concat(ast.arguments.map(function(arg) {
+ return this.visit(arg);
+ }, this)));
+};
+
+Cfg.prototype.visitVar = function visitVar(ast) {
+ // Add variables
+ this.add('var', ast.declarations.map(function(ast) {
+ return ast.id.name;
+ }, this));
+
+ // Put values into them
+ ast.declarations.forEach(function(ast) {
+ if (!ast.init) return;
+
+ this.visit({
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: ast.id,
+ right: ast.init
+ });
+ }, this);
+
+ return null;
+};
+
+Cfg.prototype.visitAssign = function visitAssign(ast) {
+ if (ast.left.type === 'Identifier') {
+ return this.add('set', [ast.left.name, this.visit(ast.right)]);
+ } else if (ast.left.type === 'MemberExpression') {
+ return this.add('setprop', [this.visit(ast.left.object),
+ this.visit(ast.left.property),
+ this.visit(ast.right)]);
+ } else {
+ throw new Error('Incorrect lhs of assignment');
+ }
+};
+
+Cfg.prototype.visitBinop = function visitBinop(ast) {
+ return this.add('binop', [ast.operator,
+ this.visit(ast.left),
+ this.visit(ast.right)]);
+};
+
+Cfg.prototype.visitUnop = function visitUnop(ast) {
+ return this.add('unop', [ast.operator,
+ ast.prefix,
+ this.visit(ast.argument)]);
+};
+
+Cfg.prototype.visitLiteral = function visitLiteral(ast) {
+ return this.add('literal', [ast.value]);
+};
+
+Cfg.prototype.visitIdentifier = function visitIdentifier(ast) {
+ return this.add('get', [ast.name]);
+};
+
+Cfg.prototype.visitMember = function visitMember(ast) {
+ if (!ast.computed) {
+ return this.add('getprop', [this.visit(ast.object),
+ this.visit({
+ type: 'Literal',
+ value: ast.property.name
+ })]);
+ } else {
+ return this.add('getprop', [this.visit(ast.object),
+ this.visit(ast.property)]);
+ }
+};
+
+Cfg.prototype.visitIf = function visitIf(ast) {
+ var tblock = this.createBlock(),
+ fblock = ast.alternate && this.createBlock(),
+ join = this.createBlock();
+
+ this.add('if', fblock ? [this.visit(ast.test), tblock, fblock] :
+ [this.visit(ast.test), tblock]);
+ this.current.addSuccessor(tblock);
+ this.current.addSuccessor(ast.alternate ? fblock : join);
+ this.current.end();
+
+ // True branch
+ this.setCurrentBlock(tblock);
+ this.visit(ast.consequent);
+ this.current.goto(join);
+
+ if (fblock) {
+ // False branch
+ this.setCurrentBlock(fblock);
+ this.visit(ast.alternate);
+ this.current.goto(join);
+ }
+
+ this.setCurrentBlock(join);
+
+ return null;
+};
+
+Cfg.prototype.visitFunction = function visitFunction(ast) {
+ var instr = this.add('fn');
+ instr.ast = ast;
+
+ this.rootQueue.push({
+ instr: instr,
+ ast: ast.body
+ });
+
+ return instr;
+};
+
+Cfg.prototype.visitReturn = function visitReturn(ast) {
+ this.add('return', [this.visit(ast.argument)]);
+ this.current.end();
+
+ return null;
+};
+
+Cfg.prototype.visitBreak = function visitBreak(ast) {
+ var block = this.createBlock();
+
+ this.add('break');
+ this.current.addSuccessor(block);
+ this.current.end();
+
+ this.breakInfo.breakBlocks.push(block);
+ return null;
+};
+
+Cfg.prototype.visitContinue = function visitContinue(ast) {
+ var block = this.createBlock();
+
+ this.add('continue');
+ this.current.addSuccessor(block);
+ this.current.end();
+
+ this.breakInfo.continueBlocks.push(block);
+ return null;
+};
+
+Cfg.prototype.enterLoop = function enterLoop(cb) {
+ var old = this.breakInfo,
+ pre = this.current,
+ start = this.createBlock(),
+ end = this.createBlock();
+
+ this.breakInfo = {
+ breakBlocks: [],
+ continueBlocks: []
+ };
+
+ start.loop = true;
+ this.setCurrentBlock(start);
+
+ var result = cb.call(this, end);
+
+ // Add continue blocks before loop
+ var lastCont = this.breakInfo.continueBlocks.reduce(function(p, b) {
+ b.loop = true;
+ return p.goto(b);
+ }, pre);
+ lastCont.addSuccessor(start);
+ lastCont.end();
+
+ // Add break blocks after end
+ var lastBrk = this.breakInfo.breakBlocks.reduce(function(p, b) {
+ return p.goto(b);
+ }, end);
+
+ // Add one last block that will have only one parent
+ this.setCurrentBlock(lastBrk.goto(this.createBlock()));
+
+ // Restore
+ this.breakInfo = old;
+
+ return null;
+};
+
+Cfg.prototype.visitWhile = function visitContinue(ast) {
+ return this.enterLoop(function(end) {
+ var start = this.current,
+ body = this.createBlock();
+
+ this.add('while', [this.visit(ast.test)]);
+ start.addSuccessor(body);
+ start.addSuccessor(end);
+ start.end();
+
+ this.setCurrentBlock(body);
+ this.visit(ast.body);
+
+ // Fill looping block
+ if (!this.current.ended) {
+ this.current.goto(start);
+ }
+ });
+};
+
+Cfg.prototype.visitDoWhile = function visitContinue(ast) {
+ return this.enterLoop(function(end) {
+ var start = this.current,
+ pre = this.createBlock();
+
+ this.add('do');
+ this.current.addSuccessor(pre);
+ this.current.end();
+ this.setCurrentBlock(pre);
+ this.visit(ast.body);
+
+ var cond = this.createBlock();
+ this.current.goto(cond);
+
+ this.setCurrentBlock(cond);
+ this.add('doend', [this.visit(ast.test)]);
+ cond.addSuccessor(start);
+ cond.addSuccessor(end);
+ cond.end();
+ });
+};
28 lib/spoon/instruction.js
@@ -0,0 +1,28 @@
+var instruction = exports,
+ spoon = require('../spoon');
+
+function Instruction(block, type, args) {
+ this.block = block;
+ this.cfg = block.cfg;
+ this.id = block.cfg.instructionId++;
+
+ this.type = type;
+ this.args = args;
+};
+instruction.Instruction = Instruction;
+instruction.create = function create(block, type, args) {
+ return new Instruction(block, type, args);
+};
+
+Instruction.prototype.toString = function toString() {
+ return 'i' + this.id + ' = ' + this.type + ' ' + this.args.map(function(arg) {
+ if (arg instanceof Instruction) return 'i' + arg.id;
+ if (arg instanceof spoon.block.Block) return 'b' + arg.id;
+
+ return arg;
+ }).join(', ');
+};
+
+Instruction.prototype.addArg = function addArg(arg) {
+ this.args.push(arg);
+};
242 lib/spoon/renderer.js
@@ -0,0 +1,242 @@
+var renderer = exports,
+ assert = require('assert'),
+ spoon = require('../spoon');
+
+function Renderer(cfg) {
+ this.ctx = null;
+ this.cfg = cfg;
+
+ this.queue = [];
+ this.slots = [];
+
+ this.blocks = {};
+ this.blockVisits = {};
+ this.instructions = {};
+};
+renderer.Renderer = Renderer;
+renderer.create = function create(cfg) {
+ return new Renderer(cfg);
+};
+
+Renderer.prototype.canVisit = function canVisit(block, update) {
+ var r;
+
+ if (update !== false) {
+ if (!this.blockVisits[block.id]) {
+ r = this.blockVisits[block.id] = 1;
+ } else {
+ r = ++this.blockVisits[block.id];
+ }
+ } else {
+ r = this.blockVisits[block.id] || 0;
+ }
+
+ return block.loop ?
+ r == (update === false ? 0 : 1)
+ :
+ r >= block.predecessors.length;
+};
+
+Renderer.prototype.render = function render() {
+ var result = ['toplevel', []];
+
+ this.queue.push(this.cfg.root);
+ this.slots.unshift(result[1]);
+
+ while (this.queue.length > 0) {
+ var current = this.queue.pop(),
+ slot = this.slots[0];
+
+ // Visit only if all parents were processed
+ if (!this.canVisit(current)) continue;
+
+ this.renderBlock(current).forEach(function(instr) {
+ slot.push(['stat', instr]);
+ });
+
+ var deadEnd = current.successors.length === 0;
+
+ var preJoin = false;
+ if (current.successors.length === 1) {
+ var succ = current.successors[0];
+ preJoin = succ.loop ? !this.canVisit(succ, false) :
+ succ.predecessors.length === 2;
+ }
+
+ // Move to another ast slot on dead-end or pre-join
+ if (deadEnd || preJoin) {
+ this.slots.shift();
+ }
+
+ // Enqueue blocks with priority to left one
+ current.successors.slice().reverse().forEach(function(block) {
+ this.queue.push(block);
+ }, this);
+ }
+
+ return result;
+};
+
+Renderer.prototype.renderBlock = function renderBlock(block) {
+ var ast = [];
+
+ // Visit instructions in reverse order to detect dependencies
+ block.instructions.slice().reverse().forEach(function(instr) {
+ // If instruction was already rendered - skip it
+ if (this.instructions[instr.id]) return;
+
+ var instr = this.renderInstruction(instr);
+ if (instr) ast.push(instr);
+ }, this);
+
+ ast.reverse();
+ this.blocks[block.id] = ast;
+
+ return ast;
+};
+
+Renderer.prototype.renderInstruction = function renderInstruction(instr) {
+ var args = instr.args.map(function(arg) {
+ if (arg instanceof spoon.instruction.Instruction) {
+ return this.renderInstruction(arg);
+ }
+ return arg;
+ }, this);
+
+ var t = instr.type,
+ fn;
+
+ if (t === 'literal') {
+ fn = this.renderLiteral;
+ } else if (t === 'get') {
+ fn = this.renderGet;
+ } else if (t === 'set') {
+ fn = this.renderSet;
+ } else if (t === 'var') {
+ fn = this.renderVar;
+ } else if (t === 'binop') {
+ fn = this.renderBinop;
+ } else if (t === 'unop') {
+ fn = this.renderUnop;
+ } else if (t === 'return') {
+ fn = this.renderReturn;
+ } else if (t === 'fn') {
+ fn = this.renderFn;
+ } else if (t === 'goto') {
+ fn = this.renderGoto;
+ } else if (t === 'call') {
+ fn = this.renderCall;
+ } else if (t === 'getprop') {
+ fn = this.renderGetprop;
+ } else if (t === 'if') {
+ fn = this.renderIf;
+ } else if (t === 'while') {
+ fn = this.renderWhile;
+ } else if (t === 'do') {
+ fn = this.renderDo;
+ } else if (t === 'doend') {
+ fn = this.renderDoEnd;
+ } else if (t === 'break') {
+ fn = this.renderBreak;
+ } else if (t === 'continue') {
+ fn = this.renderContinue;
+ } else {
+ throw new Error('Unexpected instruction: ' + t);
+ }
+ var ast = fn.call(this, args, instr);
+ this.instructions[instr.id] = ast;
+ return ast;
+};
+
+Renderer.prototype.renderLiteral = function renderLiteral(args) {
+ if (typeof args[0] === 'string') {
+ return ['string', args[0]];
+ } else {
+ return ['num', args[0]];
+ }
+};
+
+Renderer.prototype.renderGet = function renderGet(args) {
+ return ['name', args[0]];
+};
+
+Renderer.prototype.renderSet = function renderSet(args) {
+ return ['assign', true, ['name', args[0]], args[1]];
+};
+
+Renderer.prototype.renderVar = function renderVar(args) {
+ return ['var', args.map(function(name) {
+ return [name];
+ })];
+};
+
+Renderer.prototype.renderBinop = function renderBinop(args) {
+ return ['binary', args[0], args[1], args[2]];
+};
+
+Renderer.prototype.renderUnop = function renderUnop(args) {
+ return ['unary-' + (args[1] ? 'prefix' : 'postfix'), args[0], args[2]];
+};
+
+Renderer.prototype.renderReturn = function renderReturn(args) {
+ return ['return', args[0]];
+};
+
+Renderer.prototype.renderFn = function renderFn(args, instr) {
+ var prefix = instr.ast.type === 'FunctionExpression' ? 'function' : 'defun',
+ name = instr.ast.id && instr.ast.id.name,
+ slot = [];
+
+ var inputs = instr.ast.params.map(function(param) {
+ return param.name;
+ });
+
+ this.queue.unshift(args[0]);
+ this.slots.push(slot);
+ return [prefix, name, inputs, slot];
+};
+
+Renderer.prototype.renderGoto = function renderGoto() {
+ return null;
+};
+
+Renderer.prototype.renderCall = function renderCall(args) {
+ return ['call', args[0], args.slice(1)];
+};
+
+Renderer.prototype.renderGetprop = function renderGetprop(args) {
+ return ['sub', args[0], args[1]];
+};
+
+Renderer.prototype.renderIf = function renderIf(args) {
+ return ['if', args[0]].concat(args.slice(1).reverse().map(function() {
+ var slot = [];
+ this.slots.unshift(slot);
+ return ['block', slot];
+ }, this).reverse());
+};
+
+Renderer.prototype.renderWhile = function renderWhile(args) {
+ var slot = [];
+ this.slots.unshift(slot);
+ return ['while', args[0], ['block', slot]];
+};
+
+Renderer.prototype.renderDo = function renderDo(args) {
+ var slot = [];
+ this.slots.unshift(slot);
+ console.log(args);
+ return ['do', ['num', 0], ['block', slot]];
+};
+
+Renderer.prototype.renderDoEnd = function renderDoEnd(args) {
+ return null;
+};
+
+Renderer.prototype.renderBreak = function renderBreak(args) {
+ return ['break'];
+};
+
+Renderer.prototype.renderContinue = function renderContinue(args) {
+ return ['continue'];
+};
15 package.json
@@ -0,0 +1,15 @@
+{
+ "name": "spoon",
+ "version": "0.0.0",
+ "main": "lib/spoon",
+ "dependencies": {
+ "esprima": "~0.9.9",
+ "uglify-js": "~1.3.3"
+ },
+ "devDependencies": {
+ "mocha": "~1.4.2"
+ },
+ "scripts": {
+ "test": "mocha --reporter spec test/*-test.js"
+ }
+}
52 test/api-test.js
@@ -0,0 +1,52 @@
+var spoon = require('..'),
+ esprima = require('esprima'),
+ uglify = require('uglify-js');
+
+describe('Spoon', function() {
+ function apply(code) {
+ var ast = esprima.parse(code),
+ cfg = spoon.construct(ast);
+
+ console.log(cfg.toString());
+
+ var out = spoon.render(cfg);
+ console.log(require('util').inspect(out, false, 40));
+ console.log(uglify.uglify.gen_code(out, { beautify: true }));
+
+ return out;
+ }
+ describe('constructing CFG from AST', function() {
+ it('should work with sample code', function() {
+ apply('var x = 1 + 2 * 3;\n' +
+ 'if (x > 2) {\n' +
+ ' console.log("yay");\n' +
+ '} else {\n' +
+ ' log(function() { "yay" });\n' +
+ '}\n' +
+ 'function x(a,b) {\n' +
+ ' return a + b;\n' +
+ '}');
+ });
+
+ it('should work with while loop', function() {
+ apply('var i = 0;\n' +
+ 'while (i < 10) {\n' +
+ ' if (i == 9) {\n' +
+ ' break;\n' +
+ ' } else if (i > 10) {\n' +
+ ' continue;\n' +
+ ' }\n' +
+ ' i++;\n' +
+ '}\n' +
+ 'i');
+ });
+
+ it('should work with do while loop', function() {
+ apply('var i = 0;\n' +
+ 'do {\n' +
+ ' i++;\n' +
+ '} while (i < 10)\n' +
+ 'i');
+ });
+ });
+});

0 comments on commit 3fe8805

Please sign in to comment.
Something went wrong with that request. Please try again.