lispyscript
Version:
A JavaScript with Lispy Syntax and Macros
773 lines (634 loc) • 20.7 kB
JavaScript
/*
*
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();
}