UNPKG

lispyscript

Version:

A JavaScript with Lispy Syntax and Macros

773 lines (634 loc) 20.7 kB
/* * LispyScript - Javascript using tree syntax! This is the compiler written in javascipt * */ var version = "1.0.0", banner = "// Generated by LispyScript v" + version + "\n", isWhitespace = /\s/, isFunction = /^function\b/, validName = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/, noReturn = /^var\b|^set\b|^throw\b/, isHomoiconicExpr = /^#args-if\b|^#args-shift\b|^#args-second\b/, noSemiColon = false, indentSize = 4, indent = -indentSize, keywords = {}, macros = {}, errors = [], include_dirs = [__dirname + "/../includes", "includes"], fs, path, SourceNode = require('source-map').SourceNode if (typeof window === "undefined") { fs = require('fs') path = require('path') } if (!String.prototype.repeat) { String.prototype.repeat = function(num) { return new Array(num + 1).join(this) } } var parse = function(code, filename) { code = "(" + code + ")" var length = code.length, pos = 1, lineno = 1, colno = 1, token_begin_colno = 1 var parser = function() { var tree = [], token = "", isString = false, isSingleString = false, isJSArray = 0, isJSObject = 0, isListComplete = false, isComment = false, isRegex = false, isEscape = false, handleToken = function() { if (token) { tree.push(new SourceNode(lineno, token_begin_colno - 1, filename, token, token)) token = "" } } tree._line = lineno tree._filename = filename while (pos < length) { var c = code.charAt(pos) pos++ colno++ if (c == "\n") { lineno++ colno = 1 if (isComment) { isComment = false } } if (isComment) { continue } if (isEscape) { isEscape = false token += c continue } // strings if (c == '"') { isString = !isString token += c continue } if (isString) { if (c === "\n") { token += "\\n" } else { if (c === "\\") { isEscape = true } token += c } continue } if (c == "'") { isSingleString = !isSingleString token += c continue } if (isSingleString) { token += c continue } // data types if (c == '[') { isJSArray++ token += c continue } if (c == ']') { if (isJSArray === 0) { handleError(4, tree._line, tree._filename) } isJSArray-- token += c continue } if (isJSArray) { token += c continue } if (c == '{') { isJSObject++ token += c continue } if (c == '}') { if (isJSObject === 0) { handleError(6, tree._line, tree._filename) } isJSObject-- token += c continue } if (isJSObject) { token += c continue } if (c == ";") { isComment = true continue } // regex // regex in function position with first char " " is a prob. Use \s instead. if (c === "/" && !(tree.length === 0 && token.length === 0 && isWhitespace.test(code.charAt(pos)))) { isRegex = !isRegex token += c continue } if (isRegex) { if (c === "\\") { isEscape = true } token += c continue } if (c == "(") { handleToken() // catch e.g. "blah(" token_begin_colno = colno tree.push(parser()) continue } if (c == ")") { isListComplete = true handleToken() token_begin_colno = colno break } if (isWhitespace.test(c)) { if (c == '\n') lineno-- handleToken() if (c == '\n') lineno++ token_begin_colno = colno continue } token += c } if (isString) handleError(3, tree._line, tree._filename) if (isRegex) handleError(14, tree._line, tree._filename) if (isSingleString) handleError(3, tree._line, tree._filename) if (isJSArray > 0) handleError(5, tree._line, tree._filename) if (isJSObject > 0) handleError(7, tree._line, tree._filename) if (!isListComplete) handleError(8, tree._line, tree._filename) return tree } var ret = parser() if (pos < length) { handleError(10) } return ret } var handleExpressions = function(exprs) { indent += indentSize var ret = new SourceNode(), l = exprs.length, indentstr = " ".repeat(indent) exprs.forEach(function(expr, i, exprs) { var exprName, tmp = null, r = "" if (Array.isArray(expr)) { exprName = expr[0].name if (exprName === "include") ret.add(handleExpression(expr)) else tmp = handleExpression(expr) } else { tmp = expr } if (i === l - 1 && indent) { if (!noReturn.test(exprName)) r = "return " } if (tmp) { var endline = noSemiColon ? "\n" : ";\n" noSemiColon = false ret.add([indentstr + r, tmp, endline]) } }) indent -= indentSize return ret } var handleExpression = function(expr) { if (!expr || !expr[0]) { return null } var command = expr[0].name if (macros[command]) { expr = macroExpand(expr) if (Array.isArray(expr)) { return handleExpression(expr) } else { return expr } } if (typeof command === "string") { if (keywords[command]) { return keywords[command](expr) } if (command.charAt(0) === ".") { var ret = new SourceNode() ret.add(Array.isArray(expr[1]) ? handleExpression(expr[1]) : expr[1]) ret.prepend("(") ret.add([")", expr[0]]) return ret } } handleSubExpressions(expr) var fName = expr[0] if (!fName) { handleError(1, expr._line) } if (isFunction.test(fName)) fName = new SourceNode(null, null, null, ['(', fName, ')']) exprNode = new SourceNode (null, null, null, expr.slice(1)).join(",") return new SourceNode (null, null, null, [fName, "(", exprNode, ")"]) } var handleSubExpressions = function(expr) { expr.forEach(function(value, i, t) { if (Array.isArray(value)) t[i] = handleExpression(value) }) } var macroExpand = function(tree) { var command = tree[0].name, template = macros[command]["template"], code = macros[command]["code"], replacements = {} for (var i = 0; i < template.length; i++) { if (template[i].name == "rest...") { replacements["~rest..."] = tree.slice(i + 1) } else { if (tree.length === i + 1) { // we are here if any macro arg is not set handleError(12, tree._line, tree._filename, command) } replacements["~" + template[i].name] = tree[i + 1] } } var replaceCode = function(source) { var ret = [] ret._line = tree._line ret._filename = tree._filename // Handle homoiconic expressions in macro var expr_name = source[0] ? source[0].name : "" if (isHomoiconicExpr.test(expr_name)) { var replarray = replacements["~" + source[1].name] if (expr_name === "#args-shift") { if (!Array.isArray(replarray)) { handleError(13, tree._line, tree._filename, command) } var argshift = replarray.shift() if (typeof argshift === "undefined") { handleError(12, tree._line, tree._filename, command) } return argshift } if (expr_name === "#args-second") { if (!Array.isArray(replarray)) { handleError(13, tree._line, tree._filename, command) } var argsecond = replarray.splice(1, 1)[0] if (typeof argsecond === "undefined") { handleError(12, tree._line, tree._filename, command) } return argsecond } if (expr_name === "#args-if") { if (!Array.isArray(replarray)) { handleError(13, tree._line, tree._filename, command) } if (replarray.length) { return replaceCode(source[2]) } else if (source[3]) { return replaceCode(source[3]) } else { return } } } for (var i = 0; i < source.length; i++) { if (Array.isArray(source[i])) { var replcode = replaceCode(source[i]) if (typeof replcode !== "undefined") { ret.push(replcode) } } else { var token = source[i], tokenbak = token, isATSign = false if (token.name.indexOf("@") >= 0) { isATSign = true tokenbak = new SourceNode(token.line, token.column, token.source, token.name.replace("@", ""), token.name.replace("@", "")) } if (replacements[tokenbak.name]) { var repl = replacements[tokenbak.name] if (isATSign || tokenbak.name == "~rest...") { for (var j = 0; j < repl.length; j++) { ret.push(repl[j]) } } else { ret.push(repl) } } else { ret.push(token) } } } return ret } return replaceCode(code) } var handleCompOperator = function(arr) { if (arr.length < 3) handleError(0, arr._line) handleSubExpressions(arr) if (arr[0] == "=") arr[0] = "===" if (arr[0] == "!=") arr[0] = "!==" var op = arr.shift() var ret = new SourceNode() for (i = 0; i < arr.length - 1; i++) ret.add (new SourceNode (null, null, null, [arr[i], " ", op, " ", arr[i + 1]])) ret.join (' && ') ret.prepend('(') ret.add(')') return ret } var handleArithOperator = function(arr) { if (arr.length < 3) handleError(0, arr._line) handleSubExpressions(arr) var op = new SourceNode() op.add([" ", arr.shift(), " "]) var ret = new SourceNode() ret.add(arr) ret.join (op) ret.prepend("(") ret.add(")") return ret } var handleLogicalOperator = handleArithOperator keywords["var"] = function(arr) { if (arr.length < 3) handleError(0, arr._line, arr._filename) if (arr.length > 3) { indent += indentSize } handleSubExpressions(arr) var ret = new SourceNode () ret.add("var ") for (var i = 1; i < arr.length; i = i + 2) { if (i > 1) { ret.add(",\n" + " ".repeat(indent)) } if (!validName.test(arr[i])) handleError(9, arr._line, arr._filename) ret.add([arr[i], ' = ', arr[i + 1]]) } if (arr.length > 3) { indent -= indentSize } return ret } keywords["new"] = function(arr) { if (arr.length < 2) handleError(0, arr._line, arr._filename) var ret = new SourceNode() ret.add(handleExpression(arr.slice(1))) ret.prepend ("new ") return ret } keywords["throw"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) var ret = new SourceNode() ret.add(Array.isArray(arr[1]) ? handleExpression(arr[1]) : arr[1]) ret.prepend("(function(){throw ") ret.add(";})()") return ret } keywords["set"] = function(arr) { if (arr.length < 3 || arr.length > 4) handleError(0, arr._line, arr._filename) if (arr.length == 4) { arr[1] = (Array.isArray(arr[2]) ? handleExpression(arr[2]) : arr[2]) + "[" + arr[1] + "]" arr[2] = arr[3] } return new SourceNode(null, null, null, [arr[1], " = ", (Array.isArray(arr[2]) ? handleExpression(arr[2]) : arr[2])]) } keywords["function"] = function(arr) { var ret var fName, fArgs, fBody if (arr.length < 2) handleError(0, arr._line, arr._filename) if(Array.isArray(arr[1])) { // an anonymous function fArgs = arr[1] fBody = arr.slice(2) } else if(!Array.isArray(arr[1]) && Array.isArray(arr[2])) { // a named function fName = arr[1] fArgs = arr[2] fBody = arr.slice(3) } else handleError(0, arr._line) ret = new SourceNode(null, null, null, fArgs) ret.join(",") ret.prepend("function" + (fName ? " " + fName.name : "") + "(") ret.add([") {\n",handleExpressions(fBody), " ".repeat(indent), "}"]) if(fName) noSemiColon = true return ret } keywords["try"] = function(arr) { if (arr.length < 3) handleError(0, arr._line, arr._filename) var c = arr.pop(), ind = " ".repeat(indent), ret = new SourceNode() ret.add(["(function() {\n" + ind + "try {\n", handleExpressions(arr.slice(1)), "\n" + ind + "} catch (e) {\n" + ind + "return (", (Array.isArray(c) ? handleExpression(c) : c), ")(e);\n" + ind + "}\n" + ind + "})()"]) return ret } keywords["if"] = function(arr) { if (arr.length < 3 || arr.length > 4) handleError(0, arr._line, arr._filename) indent += indentSize handleSubExpressions(arr) var ret = new SourceNode() ret.add(["(", arr[1], " ?\n" + " ".repeat(indent), arr[2], " :\n" + " ".repeat(indent), (arr[3] || "undefined"), ")"]) indent -= indentSize return ret } keywords["get"] = function(arr) { if (arr.length != 3) handleError(0, arr._line, arr._filename) handleSubExpressions(arr) return new SourceNode(null, null, null, [arr[2], "[", arr[1], "]"]) } keywords["str"] = function(arr) { if (arr.length < 2) handleError(0, arr._line, arr._filename) handleSubExpressions(arr) var ret = new SourceNode() ret.add(arr.slice(1)) ret.join (",") ret.prepend("[") ret.add("].join('')") return ret } keywords["array"] = function(arr) { var ret = new SourceNode() if (arr.length == 1) { ret.add("[]") return ret } indent += indentSize handleSubExpressions(arr) ret.add("[\n" + " ".repeat(indent)) for (var i = 1; i < arr.length; ++i) { if (i > 1) { ret.add(",\n" + " ".repeat(indent)) } ret.add(arr[i]) } indent -= indentSize ret.add("\n" + " ".repeat(indent) + "]") return ret } keywords["object"] = function(arr) { var ret = new SourceNode() if (arr.length == 1) { ret.add("{}") return ret } indent += indentSize handleSubExpressions(arr) ret.add("{\n" + " ".repeat(indent)) for (var i = 1; i < arr.length; i = i + 2) { if (i > 1) { ret.add(",\n" + " ".repeat(indent)) } ret.add([arr[i], ': ', arr[i + 1]]) } indent -= indentSize ret.add("\n" + " ".repeat(indent) + "}") return ret } var includeFile = (function () { var included = [] return function(filename) { if (included.indexOf(filename) !== -1) return "" included.push(filename) var code = fs.readFileSync(filename) var tree = parse(code, filename) return handleExpressions(tree) } })() keywords["include"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) indent -= indentSize var filename = arr[1].name if (typeof filename === "string") filename = filename.replace(/["']/g, "") var found = false; include_dirs.concat([path.dirname(arr._filename)]) .forEach(function(prefix) { if (found) { return; } try { filename = fs.realpathSync(prefix + '/' +filename) found = true; } catch (err) { } }); if (!found) { handleError(11, arr._line, arr._filename) } var ret = new SourceNode() ret = includeFile(filename) indent += indentSize return ret } keywords["javascript"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) noSemiColon = true arr[1].replaceRight(/"/g, '') return arr[1] } keywords["macro"] = function(arr) { if (arr.length != 4) handleError(0, arr._line, arr._filename) macros[arr[1].name] = {template: arr[2], code: arr[3]} return "" } keywords["+"] = handleArithOperator keywords["-"] = handleArithOperator keywords["*"] = handleArithOperator keywords["/"] = handleArithOperator keywords["%"] = handleArithOperator keywords["="] = handleCompOperator keywords["!="] = handleCompOperator keywords[">"] = handleCompOperator keywords[">="] = handleCompOperator keywords["<"] = handleCompOperator keywords["<="] = handleCompOperator keywords["||"] = handleLogicalOperator keywords["&&"] = handleLogicalOperator keywords["!"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) handleSubExpressions(arr) return "(!" + arr[1] + ")" } var handleError = function(no, line, filename, extra) { throw new Error(errors[no] + ((extra) ? " - " + extra : "") + ((line) ? "\nLine no " + line : "") + ((filename) ? "\nFile " + filename : "")) } errors[0] = "Syntax Error" errors[1] = "Empty statement" errors[2] = "Invalid characters in function name" errors[3] = "End of File encountered, unterminated string" errors[4] = "Closing square bracket, without an opening square bracket" errors[5] = "End of File encountered, unterminated array" errors[6] = "Closing curly brace, without an opening curly brace" errors[7] = "End of File encountered, unterminated javascript object '}'" errors[8] = "End of File encountered, unterminated parenthesis" errors[9] = "Invalid character in var name" errors[10] = "Extra chars at end of file. Maybe an extra ')'." errors[11] = "Cannot Open include File" errors[12] = "Invalid no of arguments to " errors[13] = "Invalid Argument type to " errors[14] = "End of File encountered, unterminated regular expression" var _compile = function(code, filename, withSourceMap, a_include_dirs) { indent = -indentSize if (a_include_dirs) include_dirs = a_include_dirs var tree = parse(code, filename) var outputNode = handleExpressions(tree) outputNode.prepend(banner) if (withSourceMap) { var outputFilename = path.basename(filename, '.ls') + '.js' var sourceMapFile = outputFilename + '.map' var output = outputNode.toStringWithSourceMap({ file: outputFilename }); fs.writeFileSync(sourceMapFile, output.map) return output.code + "\n//# sourceMappingURL=" + path.relative(path.dirname(filename), sourceMapFile); } else return outputNode.toString() } exports.version = version exports._compile = _compile exports.parseWithSourceMap = function(code, filename) { var tree = parse(code, filename) var outputNode = handleExpressions(tree) outputNode.prepend(banner) return outputNode.toStringWithSourceMap(); }