Home Reference Source

src/ASTPreprocessor.js

'use strict';
let esper = require('./index.js');
let compiler;

function invokeCB(o, name) {
	if ( !(name in o ) ) return;
	var args = Array.prototype.slice.call(arguments, 2);
	o[name].apply(o, args);
}

function detectStrict(body) {
	if ( !body || body.length < 1 ) return;
	let first = body[0];
	if ( first.type === 'ExpressionStatement' ) {
		let exp = first.expression;
		if ( exp.type === 'Literal' && exp.value === 'use strict' ) {
			return true;
		}
	}
}


class ASTNode {
	constructor(o) {
		this.visits = 0;
		this.dispatch = false;
		if ( typeof o === 'object' ) {
			for ( var k in o ) this[k] = o[k];
		}
	}

	addHiddenProperty(name, value) {
		Object.defineProperty(this, name, {
			value: value,
			configurable: true
		});
	}

	source() {
		if ( !this._source ) return;
		if ( !this.range ) return;
		return this._source.substring(this.range[0], this.range[1]);
	}

	toString() {
		let extra = Object.keys(this).map((k) => {
			let v = this[k];
			if ( v === null || typeof v === 'function' ) return;
			if ( k == 'range' || k == 'loc' || k == 'nodeID') return;
			if ( v instanceof ASTNode ) return `${k}: [ASTNode: ${v.type}]`;
			if ( Array.isArray(v) ) return '[...]';
			else return `${k}: ${JSON.stringify(v)}`;
		}).filter((v) => !!v).join(', ');
		return `[ASTNode: ${this.type} ${extra}]`;
	}

}

class ASTPreprocessor {

	static clone(ast, extra) {
		return JSON.parse(JSON.stringify(ast), function(n, o) {
			if ( o === null ) return null;
			if ( typeof o !== 'object' ) return o;
			if ( Array.isArray(o) ) {
				return o;
			} else if ( o.type ) {
				let z = new ASTNode(o);
				if ( !o.range && typeof o.start != 'undefined' && typeof o.end != 'undefined' ) {
					z.range = [o.start, o.end];
				}
				if ( extra && extra.source ) z.addHiddenProperty('_source', extra.source);
				return z;
			} else if ( n === "start" || n === "end" || n === "loc" || n == "extra" ) {
				return o;
			} else {
				return o;
				//throw new TypeError("Tried to process ASTNode with no type:" + n);
			}
		});
	}

	static process(ast, extra) {
		if ( typeof ast !== 'object' ) throw new TypeError('Provided AST is invalid (type is ' + typeof ast + ')');
		let nast = ASTPreprocessor.clone(ast, extra);

		var options = extra || {};
		var cbs = new EsperASTInstructions(ast, options);
		new ASTPreprocessor(nast, extra).start(cbs);
		return nast;
	}

	static *walker(ast, cbs, parent) {
		var me = (a) => ASTPreprocessor.walker(a, cbs, ast);
		if ( ! ast instanceof ASTNode ) throw new TypeError("Walked a non ASTNode");
		if ( parent ) ast.addHiddenProperty('parent', parent);
		invokeCB(cbs, 'enter', ast);
		invokeCB(cbs, 'enter' + ast.type, ast);
		switch ( ast.type ) {
			case 'Program':
				for ( let e of ast.body ) yield * me(e);
				break;
			case 'BlockStatement':
				for ( let e of ast.body ) yield * me(e);
				break;
			case 'NewExpression':
			case 'CallExpression':
				for ( let e of ast.arguments ) yield * me(e);
				yield * me(ast.callee);
				break;
			case 'WhileStatement':
			case 'DoWhileStatement':
				if ( ast.test ) yield * me(ast.test);
				yield * me(ast.body);
				break;
			case 'VariableDeclaration':
				for ( let e of ast.declarations ) yield * me(e);
				break;
			case 'VariableDeclarator':
				invokeCB(cbs, 'decl', ast);
				if ( ast.init ) yield * me(ast.init);
				break;
			case 'FunctionDeclaration':
				invokeCB(cbs, 'decl', ast);
				invokeCB(cbs, 'enterFunction', ast);
				yield * me(ast.body);
				invokeCB(cbs, 'exitFunction', ast);
				break;
			case 'ClassBody':
				for ( let e of ast.body ) yield * me(e);
				break;
			case 'ArrowFunctionExpression':
			case 'FunctionExpression':
			case 'ClassMethod':
				invokeCB(cbs, 'enterFunction', ast);
				yield * me(ast.body);
				invokeCB(cbs, 'exitFunction', ast);
				break;
			case 'Identifier':
				break;
			case 'ArrayExpression':
				if ( ast.elements ) {
					for ( let e of ast.elements ) {
						if ( e ) yield * me(e);
					}
				}
				break;
			case 'ObjectExpression':
				if ( ast.properties ) {
					for ( let e of ast.properties ) {
						if ( e ) yield * me(e);
					}
				}
				break;
			case 'Property':
				yield * me(ast.key);
				yield * me(ast.value);
				break;
			default:
				for (var p in ast) {
					let n = ast[p];
					if ( p === 'parent' ) continue;
					if ( p === 'loc' ) continue;
					if ( p === 'range' ) continue;
					if ( p === 'type' ) continue;
					if ( p === 'nodeID' ) continue;
					if ( p === 'parentFunction' ) continue;
					if ( p === 'funcs' ) continue;
					if ( n === null ) continue;
					if ( typeof n.type !== 'string' ) {
						continue;
					}
					yield * me(n);
				}
		}


		invokeCB(cbs, 'exit' + ast.type, ast);
		invokeCB(cbs, 'exit', ast);
	}


	constructor(ast) {
		this.ast = ast;
	}

	start(cbs) {
		var gen = ASTPreprocessor.walker(this.ast, cbs);
		for ( var x of gen ) {

		}
	}


}
ASTPreprocessor.ASTNode = ASTNode;

class EsperASTInstructions {
	constructor(ast, options) {

		if ( !compiler && esper.plugins['jit']) {
			compiler = new (esper.plugins['jit'].Compiler)();
		}

		this.ast = ast;
		this.options = options;
		this.counter = 0;
		this.depth = 0;

		let globalScope = Object.create(null);
		let globalVars = Object.create(null);
		let globalFuncs = Object.create(null);

		if ( options.locals ) {
			for ( let o of options.locals ) globalScope[o] = true;
		}

		this.scopeStack = [globalScope];
		this.varStack = [globalVars];
		this.funcStack = [globalFuncs];
	}

	log() {
		let str = Array.prototype.join.call(arguments, ', ');
		let indent = new Array(this.depth).join('  ');
		//console.log(indent + str);
	}

	enter(a) {
		++this.depth;
		if ( this.options.markNonUser ) {
			a.nonUserCode = true;
		}
		a.nodeID = this.counter++;
		this.log('Entering', a.type);
	}

	enterIdentifier(a) {
		let fn = this.funcStack[0];
		let parent = a.parent;
		if ( parent.type == "MemberExpression" && !parent.computed && parent.property == a ) {
			return;
		}
		if ( parent.type == "Property" && parent.key == a) {
			return;
		}
		fn.refs[a.name] = true;
	}

	decl(a) {
		if ( a.parent.type == 'VariableDeclaration' ) {
			if ( a.parent.kind != 'var' ) {
				let stack = this.scopeStack[0];
				stack[a.id.name] = a;
				return
			}
		}
		if ( a.type == 'FunctionDeclaration' ) return;
		let stack = this.varStack[0];
		stack[a.id.name] = a;
	}

	enterProgram(a) {
		let scope = Object.create(this.scopeStack[0]);

		a.addHiddenProperty('refs', Object.create(null));
		a.addHiddenProperty('vars', Object.create(null));
		a.addHiddenProperty('funcs', Object.create(null));
		a.addHiddenProperty('ss', scope);

		this.funcStack.unshift(a);
		this.scopeStack.unshift(scope);
		this.varStack.unshift(a.vars);

		this.mangleBody(a);

		let strict = detectStrict(a.body);
		if ( strict !== undefined ) a.strict = strict;
	}

	enterThisExpression(a) {
		a.srcName = 'this';
	}

	enterLabeledStatement(a) {
		a.body.label = a.label.name;
	}

	exitArrayExpression(a) {
		a.srcName = '[' + a.elements.map((e) => e ? e.srcName : '').join() + ']';
	}

	mangleBody(a) {
		function prehoist(s) {
			if ( s.type === 'VariableDeclaration' && s.kind == 'var' ) {
				for ( var decl of s.declarations ) {
					a.vars[decl.id.name] = decl;
					a.ss[decl.id.name] = decl;
				}

			} else if ( s.type === 'FunctionDeclaration' ) {
				a.vars[s.id.name] = s;
				a.ss[s.id.name] = s;
			}
		}

		if ( a.body.type === 'BlockStatement' ) {
			for ( let stmt of a.body.body ) prehoist(stmt);
		} else if ( Array.isArray(a.body) ) {
			for ( let stmt of a.body ) prehoist(stmt);
		} else {
			prehoist(a.body);
		}

	}

	enterClassExpression(a) {
		let scope = Object.create(this.scopeStack[0]);
		this.scopeStack.unshift(scope);
		scope[a.id.name] = a;
		for ( let x of a.body.body ) {
			if ( !x ) continue;
			if ( x.key ) scope[x.key.name] = x;
			if ( x.id ) scope[x.id.name] = x;
		}
	}


	enterFunction(a) {
		this.funcStack.unshift(a);
		let scope = Object.create(this.scopeStack[0]);
		this.scopeStack.unshift(scope);

		a.addHiddenProperty('refs', Object.create(null));
		a.addHiddenProperty('vars', Object.create(null));
		a.addHiddenProperty('funcs', Object.create(null));
		a.addHiddenProperty('ss', scope);

		if ( this.options.nonUserCode ) {
			a.addHiddenProperty('nonUserCode', true);
		}

		for ( let o of a.params ) {
			if ( o.type == 'Identifier' ) {
				scope[o.name] = a;
				a.vars[o.name] = a;
			} else if ( o.type == 'RestElement' ) {
				scope[o.argument.name] = a;
				a.vars[o.argument.name] = a;
			}
		}

		this.mangleBody(a);

		let strict = detectStrict(a.body.body);
		if ( strict !== undefined ) a.strict = strict;

		this.varStack.unshift(a.vars);
	}

	enterFunctionDeclaration(a) {
		let parent = this.funcStack[0];
		//a.parentFunction = parent.nodeID;
		a.srcName = 'function ' + a.id.name + ' {';
		parent.funcs[a.id.name] = a;
	}

	exitIdentifier(a) {
		a.srcName = a.name;
	}

	exitLiteral(a) {
		if ( a.regex ) {
			a.srcName = '/' + a.regex.pattern + '/' + a.regex.flags;
		} else if ( typeof a.value === 'string' ) {
			a.srcName = a.raw;
		} else if ( typeof a.value === 'undefined' ) {
			a.srcName = 'undefiend';
		} else {
			a.srcName = a.raw;
		}
	}


	exitBinaryExpression(a) {
		a.srcName = a.left.srcName + ' ' + a.operator + ' ' + a.right.srcName;
	}

	exitMemberExpression(a) {
		let left = a.object.srcName || '??';
		let right = a.property.srcName || '(intermediate value)';
		if (!a.computed) a.srcName = left + '.' + right;
		else a.srcName = a.srcName = left + '[' + right + ']';
	}

	exitCallExpression(a) {
		a.srcName = a.callee.srcName + '(...)';
	}


	exitFunction(a) {
		var vars = this.varStack.shift();
		var scope = this.scopeStack.shift();
		var free = {};
		var upvars = {};
		var locals = {}
		for ( var r in a.refs ) {

			if ( r == 'arguments' ) continue;
			if (Object.hasOwnProperty.call(vars, r) || Object.hasOwnProperty.call(scope, r) ) {
				locals[r] = true;
			} else if ( r in this.varStack[0] ) {
				upvars[r] = true;
			} else if ( r in this.scopeStack[0] ) {
				upvars[r] = true;
			} else {
				free[r] = true;
			}
		}
		a.upvars = upvars;
		a.freevars = free;
		
		this.funcStack.shift();
		delete a.refs;

		let target = this.funcStack[0];
		if ( target && target.refs ) {
			for ( let v in upvars ) target.refs[v] = true;
			for ( let v in free ) target.refs[v] = true;
		}

		if ( compiler && this.options.compile === 'pre' && compiler.canCompile(a.body) ) {
			a.body.dispatch = compiler.compileNode(a.body);
		}
		//this.log("VARS:", Object.getOwnPropertyNames(a.vars).join(', '));
	}

	exitClassExpression(a) {
		this.scopeStack.shift();
	}

	exitProgram(a) {
		this.scopeStack.shift();
		var vars = this.varStack.shift();
		//this.log("VARS:", Object.getOwnPropertyNames(a.vars).join(', '));
	}

	exit(a) {
		this.log('Exiting', a.type);
		--this.depth;
	}

}
ASTPreprocessor.ASTNode = ASTNode;
ASTPreprocessor.EsperASTInstructions = EsperASTInstructions;

module.exports = ASTPreprocessor;