UNPKG

lispyscript

Version:

A JavaScript with Lispy Syntax and Macros

589 lines (526 loc) 17.1 kB
/* * LispyScript - Javascript using tree syntax! This is the compiler written in javascipt * */ var version = "0.3.3", 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 = [], cache = {}, fs if (typeof window === "undefined") { fs = require('fs') } 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 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(token) token = "" } } tree._line = lineno tree._filename = filename while (pos < length) { var c = code.charAt(pos) pos++ if (c == "\n") { lineno++ 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 == "(") { tree.push(parser()) continue } if (c == ")") { isListComplete = true handleToken() break } if (isWhitespace.test(c)) { handleToken() 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 = "", l = exprs.length, indentstr = " ".repeat(indent) exprs.forEach(function(expr, i, exprs) { var exprName, tmp = "", r = "" if (Array.isArray(expr)) { exprName = expr[0] if (exprName === "include") ret += handleExpression(expr) else tmp = handleExpression(expr) } else { tmp = expr } if (i === l - 1 && indent) { if (!noReturn.test(exprName)) r = "return " } if (tmp.length > 0) { var endline = noSemiColon ? "\n" : ";\n" noSemiColon = false ret += indentstr + r + tmp + endline } }) indent -= indentSize return ret } var handleExpression = function(expr) { if (!expr) { return "" } var command = expr[0] 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) === ".") { return "(" + (Array.isArray(expr[1]) ? handleExpression(expr[1]) : expr[1]) + ")" + command } } handleSubExpressions(expr) var fName = expr[0] if (!fName) { handleError(1, expr._line) } if (isFunction.test(fName)) { fName = "(" + fName + ")" } return fName + "(" + expr.slice(1).join(",") + ")" } 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], template = macros[command]["template"], code = macros[command]["code"], replacements = {} for (var i = 0; i < template.length; i++) { if (template[i] == "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]] = 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] if (isHomoiconicExpr.test(expr_name)) { var replarray = replacements["~" + source[1]] 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 (typeof source[i] == "object") { var replcode = replaceCode(source[i]) if (typeof replcode !== "undefined") { ret.push(replcode) } } else { var token = source[i], tokenbak = token, isATSign = false if (token.indexOf("@") >= 0) { isATSign = true tokenbak = token.replace("@", "") } if (replacements[tokenbak]) { var repl = replacements[tokenbak] if (isATSign || tokenbak == "~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 handleOperator = function(arr) { if (arr.length != 3) handleError(0, arr._line) handleSubExpressions(arr) if (arr[0] == "=") arr[0] = "===" if (arr[0] == "!=") arr[0] = "!==" return "(" + arr[1] + " " + arr[0] + " " + arr[2] + ")" } keywords["var"] = function(arr) { if (arr.length < 3) handleError(0, arr._line, arr._filename) if (arr.length > 3) { indent += indentSize } handleSubExpressions(arr) var ret = "var " for (var i = 1; i < arr.length; i = i + 2) { if (i > 1) { ret += ",\n" + " ".repeat(indent) } if (!validName.test(arr[i])) handleError(9, arr._line, arr._filename) ret += 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) return "new " + handleExpression(arr.slice(1)) } keywords["throw"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) return "(function(){throw " + (Array.isArray(arr[1]) ? handleExpression(arr[1]) : arr[1]) + ";})()" } 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 arr[1] + " = " + (Array.isArray(arr[2]) ? handleExpression(arr[2]) : arr[2]) } keywords["function"] = function(arr) { if (arr.length < 2) handleError(0, arr._line, arr._filename) if (!Array.isArray(arr[1])) handleError(0, arr._line) return "function(" + arr[1].join(",") + ") {\n" + handleExpressions(arr.slice(2)) + " ".repeat(indent) + "}" } keywords["try"] = function(arr) { if (arr.length < 3) handleError(0, arr._line, arr._filename) var c = arr.pop(), ind = " ".repeat(indent) return "(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 + "})()" } keywords["if"] = function(arr) { if (arr.length < 3 || arr.length > 4) handleError(0, arr._line, arr._filename) indent += indentSize handleSubExpressions(arr) var ret = "(" + 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 arr[2] + "[" + arr[1] + "]" } keywords["str"] = function(arr) { if (arr.length < 2) handleError(0, arr._line, arr._filename) handleSubExpressions(arr) return "[" + arr.slice(1).join(",") + "].join('')" } keywords["array"] = function(arr) { if (arr.length == 1) { return "[]" } indent += indentSize handleSubExpressions(arr) var ret = "[\n" + " ".repeat(indent) for (var i = 1; i < arr.length; ++i) { if (i > 1) { ret += ",\n" + " ".repeat(indent) } ret += arr[i] } indent -= indentSize return ret + "\n" + " ".repeat(indent) + "]" } keywords["object"] = function(arr) { if (arr.length == 1) { return "{}" } indent += indentSize handleSubExpressions(arr) var ret = "{\n" + " ".repeat(indent) for (var i = 1; i < arr.length; i = i + 2) { if (i > 1) { ret += ",\n" + " ".repeat(indent) } ret += arr[i] + ': ' + arr[i + 1] } indent -= indentSize return ret + "\n" + " ".repeat(indent) + "}" } 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] if (typeof filename === "string") filename = filename.replace(/["']/g, "") try { filename = fs.realpathSync(filename) } catch (err) { try { filename = fs.realpathSync(__dirname + "/../includes/" + filename) } catch (err) { handleError(11, arr._line, arr._filename) } } var ret = includeFile(filename) indent += indentSize return ret } keywords["javascript"] = function(arr) { if (arr.length != 2) handleError(0, arr._line, arr._filename) noSemiColon = true return arr[1].replace(/"/g, '') } keywords["macro"] = function(arr) { if (arr.length != 4) handleError(0, arr._line, arr._filename) macros[arr[1]] = {template: arr[2], code: arr[3]} return "" } keywords["+"] = handleOperator keywords["-"] = handleOperator keywords["*"] = handleOperator keywords["/"] = handleOperator keywords["%"] = handleOperator keywords["="] = handleOperator keywords["!="] = handleOperator keywords[">"] = handleOperator keywords[">="] = handleOperator keywords["<"] = handleOperator keywords["<="] = handleOperator keywords["||"] = handleOperator keywords["&&"] = handleOperator 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) { var tree = parse(code, filename) return banner + handleExpressions(tree) } exports.version = version exports._compile = compile