//All kinds of conversion passes over the source code var jsp = require("acorn"); var walk = require("acorn/util/walk.js"); var rnonIdentMember = /[.\-_$a-zA-Z0-9]/g; var global = new Function("return this")(); function equals( a, b ) { if( a.type === b.type ) { if( a.type === "MemberExpression" ) { return equals( a.object, b.object ) && equals( a.property, b.property ); } else if( a.type === "Identifier" ) { return a.name === b.name; } else if( a.type === "ThisExpression" ) { return true; } else { console.log("equals", a, b); unhandled(); } } return false; } function getReceiver( expr ) { if( expr.type === "MemberExpression" ) { return expr.object; } return null; } function nodeToString( expr ) { if( expr == null || typeof expr !== "object" ) { if( expr === void 0 ) { return "void 0"; } else if( typeof expr === "string" ) { return '"' + safeToEmbedString(expr) + '"'; } return ("" + expr); } if( expr.type === "Identifier" ) { return expr.name; } else if( expr.type === "MemberExpression" ) { if( expr.computed ) return nodeToString( expr.object ) + "[" + nodeToString( expr.property ) + "]"; else return nodeToString( expr.object ) + "." + nodeToString( expr.property ); } else if( expr.type === "UnaryExpression" ) { if( expr.operator === "~" || expr.operator === "-" || expr.operator === "+" || expr.operator === "-" ) { return expr.operator + nodeToString( expr.argument ); } return "(" + expr.operator + " " + nodeToString( expr.argument ) + ")"; } else if( expr.type === "Literal" ) { return expr.raw; } else if( expr.type === "BinaryExpression" || expr.type === "LogicalExpression" ) { return "("+nodeToString(expr.left) + " " + expr.operator + " " + nodeToString(expr.right) + ")"; } else if( expr.type === "ThisExpression" ) { return "this"; } else if( expr.type === "ObjectExpression") { var props = expr.properties; var ret = []; for( var i = 0, len = props.length; i < len; ++i ) { var prop = props[i]; ret.push( nodeToString(prop.key) + ": " + nodeToString(prop.value)); } return "({"+ret.join(",\n")+"})"; } else if( expr.type === "NewExpression" ) { return "new " + nodeToString(expr.callee) + "(" + nodeToString(expr.arguments) +")"; } //assuming it is arguments else if( Array.isArray( expr ) ) { var tmp = []; for( var i = 0, len = expr.length; i < len; ++i ) { tmp.push( nodeToString(expr[i]) ); } return tmp.join(", "); } else if( expr.type === "FunctionExpression" ) { var params = []; for( var i = 0, len = expr.params.length; i < len; ++i ) { params.push( nodeToString(expr.params[i]) ); } } else if( expr.type === "BlockStatement" ) { var tmp = []; for( var i = 0, len = expr.body.length; i < len; ++i ) { tmp.push( nodeToString(expr.body[i]) ); } return tmp.join(";\n"); } else if( expr.type === "CallExpression" ) { var args = []; for( var i = 0, len = expr.arguments.length; i < len; ++i ) { args.push( nodeToString(expr.arguments[i]) ); } return nodeToString( expr.callee ) + "("+args.join(",")+")"; } else { console.log( "nodeToString", expr ); unhandled() } } function DynamicCall( receiver, fnDereference, arg, start, end ) { this.receiver = receiver; this.fnDereference = fnDereference; this.arg = arg; this.start = start; this.end = end; } DynamicCall.prototype.toString = function() { return nodeToString(this.fnDereference) + ".call(" + nodeToString(this.receiver) + ", " + nodeToString(this.arg) + ")"; }; function DirectCall( receiver, fnName, arg, start, end ) { this.receiver = receiver; this.fnName = fnName; this.arg = arg; this.start = start; this.end = end; } DirectCall.prototype.toString = function() { return nodeToString(this.receiver) + "." + nodeToString(this.fnName) + "(" + nodeToString(this.arg) + ")" }; function ConstantReplacement( value, start, end ) { this.value = value; this.start = start; this.end = end; } ConstantReplacement.prototype.toString = function() { return nodeToString(this.value); }; function Empty(start, end) { this.start = start; this.end = end; } Empty.prototype.toString = function() { return ""; }; function Assertion( expr, exprStr, start, end ) { this.expr = expr; this.exprStr = exprStr; this.start = start; this.end = end; } Assertion.prototype.toString = function() { return 'ASSERT('+nodeToString(this.expr)+',\n '+this.exprStr+')'; }; function BitFieldRead(mask, start, end, fieldExpr) { if (mask === 0) throw new Error("mask cannot be zero"); this.mask = mask; this.start = start; this.end = end; this.fieldExpr = fieldExpr; } BitFieldRead.prototype.getShiftCount = function() { var b = 1; var shiftCount = 0; while ((this.mask & b) === 0) { b <<= 1; shiftCount++; } return shiftCount; }; BitFieldRead.prototype.toString = function() { var fieldExpr = this.fieldExpr ? nodeToString(this.fieldExpr) : "bitField"; var mask = this.mask; var shiftCount = this.getShiftCount(); return shiftCount === 0 ? "(" + fieldExpr + " & " + mask + ")" : "((" + fieldExpr + " & " + mask + ") >>> " + shiftCount + ")"; }; function BitFieldCheck(value, inverted, start, end, fieldExpr) { this.value = value; this.inverted = inverted; this.start = start; this.end = end; this.fieldExpr = fieldExpr; } BitFieldCheck.prototype.toString = function() { var fieldExpr = this.fieldExpr ? nodeToString(this.fieldExpr) : "bitField"; var equality = this.inverted ? "===" : "!=="; return "((" + fieldExpr + " & " + this.value + ") " + equality + " 0)"; }; function InlineSlice(varExpr, collectionExpression, startExpression, endExpression, start, end, isBrowser) { this.varExpr = varExpr; this.collectionExpression = collectionExpression; this.startExpression = startExpression; this.endExpression = endExpression; this.start = start; this.end = end; this.isBrowser = isBrowser; } InlineSlice.prototype.hasSimpleStartExpression = function InlineSlice$hasSimpleStartExpression() { return this.startExpression.type === "Identifier" || this.startExpression.type === "Literal"; }; InlineSlice.prototype.hasSimpleEndExpression = function InlineSlice$hasSimpleEndExpression() { return this.endExpression.type === "Identifier" || this.endExpression.type === "Literal"; }; InlineSlice.prototype.hasSimpleCollection = function InlineSlice$hasSimpleCollection() { return this.collectionExpression.type === "Identifier"; }; InlineSlice.prototype.toString = function InlineSlice$toString() { var init = this.hasSimpleCollection() ? "" : "var $_collection = " + nodeToString(this.collectionExpression) + ";"; var collectionExpression = this.hasSimpleCollection() ? nodeToString(this.collectionExpression) : "$_collection"; init += "var $_len = " + collectionExpression + ".length;"; var varExpr = nodeToString(this.varExpr); //No offset arguments at all if( this.startExpression === firstElement ) { if (this.isBrowser) { return "var " + varExpr + " = [].slice.call("+collectionExpression+");"; } else { return init + "var " + varExpr + " = new Array($_len); " + "for(var $_i = 0; $_i < $_len; ++$_i) {" + varExpr + "[$_i] = " + collectionExpression + "[$_i];" + "}"; } } else { if( !this.hasSimpleStartExpression() ) { init += "var $_start = " + nodeToString(this.startExpression) + ";"; } var startExpression = this.hasSimpleStartExpression() ? nodeToString(this.startExpression) : "$_start"; //Start offset argument given if( this.endExpression === lastElement ) { if (this.isBrowser) { return "var " + varExpr + " = [].slice.call("+collectionExpression+", "+startExpression+");"; } else { return init + "var " + varExpr + " = new Array(Math.max($_len - " + startExpression + ", 0)); " + "for(var $_i = " + startExpression + "; $_i < $_len; ++$_i) {" + varExpr + "[$_i - "+startExpression+"] = " + collectionExpression + "[$_i];" + "}"; } } //Start and end offset argument given else { if( !this.hasSimpleEndExpression() ) { init += "var $_end = " + nodeToString(this.endExpression) + ";"; } var endExpression = this.hasSimpleEndExpression() ? nodeToString(this.endExpression) : "$_end"; if (this.isBrowser) { return "var " + varExpr + " = [].slice.call("+collectionExpression+", "+startExpression+", "+endExpression+");"; } else { return init + "var " + varExpr + " = new Array(Math.max(" + endExpression + " - " + startExpression + ", 0)); " + "for(var $_i = " + startExpression + "; $_i < " + endExpression + "; ++$_i) {" + varExpr + "[$_i - "+startExpression+"] = " + collectionExpression + "[$_i];" + "}"; } } } }; var opts = { ecmaVersion: 5, strictSemicolons: false, allowTrailingCommas: true, forbidReserved: false, locations: false, onComment: null, ranges: false, program: null, sourceFile: null }; var rlineterm = /[\r\n\u2028\u2029]/; var rhorizontalws = /[ \t]/; var convertSrc = function( src, results ) { if( results.length ) { results.sort(function(a, b){ var ret = a.start - b.start; if( ret === 0 ) { ret = a.end - b.end; } return ret; }); for( var i = 1; i < results.length; ++i ) { var item = results[i]; if( item.start === results[i-1].start && item.end === results[i-1].end ) { results.splice(i++, 1); } } var ret = ""; var start = 0; for( var i = 0, len = results.length; i < len; ++i ) { var item = results[i]; ret += src.substring( start, item.start ); ret += item.toString(); start = item.end; } ret += src.substring( start ); return ret; } return src; }; var rescape = /[\r\n\u2028\u2029"]/g; var replacer = function( ch ) { return "\\u" + (("0000") + (ch.charCodeAt(0).toString(16))).slice(-4); }; function safeToEmbedString( str ) { return str.replace( rescape, replacer ); } function parse( src, opts, fileName) { if( !fileName ) { fileName = opts; opts = void 0; } try { return jsp.parse(src, opts); } catch(e) { e.message = e.message + " " + fileName; e.scriptSrc = src; throw e; } } var inlinedFunctions = Object.create(null); var lastElement = jsp.parse("___input.length").body[0].expression; var firstElement = jsp.parse("0").body[0].expression; inlinedFunctions.INLINE_SLICE = function( node, isBrowser ) { var statement = node; node = node.expression; var args = node.arguments; if( !(2 <= args.length && args.length <= 4 ) ) { throw new Error("INLINE_SLICE must have exactly 2, 3 or 4 arguments"); } var varExpression = args[0]; var collectionExpression = args[1]; var startExpression = args.length < 3 ? firstElement : args[2]; var endExpression = args.length < 4 ? lastElement : args[3]; return new InlineSlice(varExpression, collectionExpression, startExpression, endExpression, statement.start, statement.end, isBrowser); }; inlinedFunctions.BIT_FIELD_READ = function(node) { var statement = node; var args = node.expression.arguments; if (args.length !== 1 && args.length !== 2) { throw new Error("BIT_FIELD must have 1 or 2 arguments"); } var arg = args[0]; if (arg.type !== "Identifier") { throw new Error("BIT_FIELD argument must be an identifier"); } var name = arg.name; var constant = constants[name]; if (constant === undefined) { throw new Error(name + " is not a constant"); } var value = constant.value; return new BitFieldRead(value, statement.start, statement.end, args[1]); }; inlinedFunctions.BIT_FIELD_CHECK = function(node) { var statement = node; var args = node.expression.arguments; if (args.length !== 1 && args.length !== 2) { throw new Error("BIT_FIELD must have 1 or 2 arguments"); } var arg = args[0]; if (arg.type !== "Identifier") { throw new Error("BIT_FIELD argument must be an identifier"); } var name = arg.name; var constant = constants[name]; if (constant === undefined) { throw new Error(name + " is not a constant"); } var value = constant.value; var inverted = false; if (name.slice(-4) === "_NEG") { inverted = true; } return new BitFieldCheck(value, inverted, statement.start, statement.end, args[1]); }; inlinedFunctions.USE = function(node) { return new Empty(node.start, node.end); }; var constants = {}; var ignore = []; Error.stackTraceLimit = 10000; var astPasses = module.exports = { inlineExpansion: function( src, fileName, isBrowser ) { var ast = parse(src, fileName); var results = []; var expr = []; function doInline(node) { if( node.expression.type !== 'CallExpression' ) { return; } var name = node.expression.callee.name; if(typeof inlinedFunctions[ name ] === "function" && expr.indexOf(node.expression) === -1) { expr.push(node.expression); try { results.push( inlinedFunctions[ name ]( node, isBrowser ) ); } catch(e) { e.fileName = fileName; throw e; } } } walk.simple(ast, { ExpressionStatement: doInline, CallExpression: function(node) { node.expression = node; doInline(node); } }); var ret = convertSrc( src, results ); return ret; }, //Parse constants in from constants.js readConstants: function( src, fileName ) { var ast = parse(src, fileName); walk.simple(ast, { ExpressionStatement: function( node ) { if( node.expression.type !== 'CallExpression' ) { return; } var start = node.start; var end = node.end; node = node.expression; var callee = node.callee; if( callee.name === "CONSTANT" && callee.type === "Identifier" ) { if( node.arguments.length !== 2 ) { throw new Error( "Exactly 2 arguments must be passed to CONSTANT\n" + src.substring(start, end) ); } if( node.arguments[0].type !== "Identifier" ) { throw new Error( "Can only define identifier as a constant\n" + src.substring(start, end) ); } var args = node.arguments; var name = args[0]; var nameStr = name.name; var expr = args[1]; var e = eval; constants[nameStr] = { identifier: name, value: e(nodeToString(expr)) }; walk.simple( expr, { Identifier: function( node ) { ignore.push(node); } }); global[nameStr] = constants[nameStr].value; } } }); }, //Expand constants in normal source files expandConstants: function( src, fileName ) { var results = []; var identifiers = []; var ast = parse(src, fileName); walk.simple(ast, { Identifier: function( node ) { identifiers.push( node ); } }); for( var i = 0, len = identifiers.length; i < len; ++i ) { var id = identifiers[i]; if( ignore.indexOf(id) > -1 ) { continue; } var constant = constants[id.name]; if( constant === void 0 ) { continue; } if( constant.identifier === id ) { continue; } results.push( new ConstantReplacement( constant.value, id.start, id.end ) ); } return convertSrc( src, results ); }, removeComments: function( src, fileName ) { var results = []; var rnoremove = /^[*\s\/]*(?:@preserve|jshint|global)/; opts.onComment = function( block, text, start, end ) { if( rnoremove.test(text) ) { return; } var e = end + 1; var s = start - 1; while(rhorizontalws.test(src.charAt(s--))); while(rlineterm.test(src.charAt(e++))); results.push( new Empty( s + 2, e - 1 ) ); }; var ast = parse(src, opts, fileName); return convertSrc( src, results ); }, expandAsserts: function( src, fileName ) { var ast = parse( src, fileName ); var results = []; walk.simple(ast, { CallExpression: function( node ) { var start = node.start; var end = node.end; var callee = node.callee; if( callee.type === "Identifier" && callee.name === "ASSERT" ) { if( node.arguments.length !== 1 ) { results.push({ start: start, end: end, toString: function() { return src.substring(start, end); } }); return; } var expr = node.arguments[0]; var str = src.substring(expr.start, expr.end); str = '"' + safeToEmbedString(str) + '"' var assertion = new Assertion( expr, str, start, end ); results.push( assertion ); } } }); return convertSrc( src, results ); }, removeAsserts: function( src, fileName ) { var ast = parse( src, fileName ); var results = []; walk.simple(ast, { ExpressionStatement: function( node ) { if( node.expression.type !== 'CallExpression' ) { return; } var start = node.start; var end = node.end; node = node.expression; var callee = node.callee; if( callee.type === "Identifier" && callee.name === "ASSERT" ) { var e = end + 1; var s = start - 1; while(rhorizontalws.test(src.charAt(s--))); while(rlineterm.test(src.charAt(e++))); results.push( new Empty( s + 2, e - 1) ); } }, VariableDeclaration: function(node) { var start = node.start; var end = node.end; if (node.kind === 'var' && node.declarations.length === 1) { var decl = node.declarations[0]; if (decl.id.type === "Identifier" && decl.id.name === "ASSERT") { var e = end + 1; var s = start - 1; while(rhorizontalws.test(src.charAt(s--))); while(rlineterm.test(src.charAt(e++))); results.push( new Empty( s + 2, e - 1) ); } } } }); return convertSrc( src, results ); }, asyncConvert: function( src, objName, fnProp, fileName ) { var ast = parse( src, fileName ); var results = []; walk.simple(ast, { CallExpression: function( node ) { var start = node.start; var end = node.end; if( node.callee.type === "MemberExpression" && node.callee.object.name === objName && node.callee.property.name === fnProp && node.arguments.length === 3 ) { var args = node.arguments; var fnDereference = args[0]; var dynamicReceiver = args[1]; var arg = args[2]; var receiver = getReceiver(fnDereference); if( receiver == null || !equals(receiver, dynamicReceiver) ) { //Have to use fnDereference.call(dynamicReceiver, arg); results.push( new DynamicCall( dynamicReceiver, fnDereference, arg, start, end ) ); } else { var fnName = fnDereference.property; results.push( new DirectCall( receiver, fnName, arg, start, end ) ); //Can use receiver.fnName( arg ); } } } }); return convertSrc( src, results ); } };