Last active
December 6, 2016 07:37
-
-
Save bellbind/924d804eb021e8052ce8acbc2f8c5531 to your computer and use it in GitHub Desktop.
[es6][nodejs]lisp REPL interpreter with Promise
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// lisp interpreter | |
// for nodejs-v5: node --es_staging lisp.js | |
"use strict"; | |
function tokenize(code) { | |
const comment = /;.*$/gm; | |
const token = /\(|\)|'|,@?|`|"(?:[^"]|\\")*"|[^\s"'`,\(\)]+/g; | |
return code.replace(comment, "").match(token) || []; | |
} | |
function parse(tokens, depth, tree) { | |
depth = depth || 0, tree = tree || []; | |
if (tokens.length === 0) { | |
if (depth === 0) return Promise.resolve([tree, []]); | |
return Promise.reject("not finished"); | |
} | |
const quote = (quote) => (ret) => [ | |
tree.concat([[quote, ret[0][0]]].concat(ret[0].slice(1))), ret[1]]; | |
const token = tokens[0], rest = tokens.slice(1); | |
switch (token) { | |
case "(": | |
return parse(rest, depth + 1, []).then( | |
ret => parse(ret[1], depth, tree.concat([ret[0]]))); | |
case ")": | |
if (depth > 0) return Promise.resolve([tree, rest]); | |
return Promise.reject("too match ')'"); | |
case "'": return parse(rest, depth, []).then(quote("quote")); | |
case "`": return parse(rest, depth, []).then(quote("quasiquote")); | |
case ",": return parse(rest, depth, []).then(quote("unquote")); | |
case ",@": return parse(rest, depth, []).then(quote("'splice")); | |
} | |
return parse(rest, depth, tree.concat(token)); | |
} | |
function closure(env, params, body) { | |
return function () { | |
const bound = matchparams({}, params, Array.from(arguments)); | |
const childenv = { | |
macros: Object.assign({}, env.macros), | |
vars: Object.assign({}, env.vars, bound) | |
}; | |
return evaluate(childenv, body); | |
}; | |
} | |
function matchparams(vars, params, args) { | |
if (params.length === 0) return vars; | |
const param = params[0], rest = params.slice(1); | |
if (param === "&rest") return matchparams(vars, rest, [args]); | |
const bound = Object.assign(vars, {[param]: args[0]}); | |
return matchparams(bound, rest, args.slice(1)); | |
} | |
function preprocessor(env, params, body) { | |
return (list) => closure(env, params, body)(...list); | |
} | |
function ref(env, sym) { | |
if (Object.keys(env.vars).indexOf(sym) >= 0) return env.vars[sym]; | |
try { | |
return JSON.parse(sym); // symbol as value: e.g. number, string, bool | |
} catch (_) { | |
return sym; // symbol as string | |
} | |
} | |
function quasiquote(list) { | |
if (!Array.isArray(list)) return ["quote", list]; | |
if (list[0] === "unquote") return list[1]; | |
return ["'splist"].concat(list.map(e => quasiquote(e))); | |
} | |
function progn(env, list) { | |
return evaluate(env, list[0]).then( | |
v => list.length === 1 ? v : progn(env, list.slice(1))); | |
} | |
function evaluate(env, list) { | |
if (!Array.isArray(list)) return Promise.resolve(ref(env, list)); | |
if (list.length === 0) return Promise.resolve([]); // as nil | |
const macro = env.macros[list[0]]; | |
if (macro) return macro(list.slice(1)).then(l => evaluate(env, l)); | |
switch (list[0]) { // special forms | |
case "quote": return Promise.resolve(list[1]); | |
case "quasiquote": return evaluate(env, quasiquote(list[1])); | |
case "progn": return progn(env, list.slice(1)); | |
case "if": return evaluate(env, list[1]).then( | |
cond => evaluate(env, list[cond ? 2 : 3])); | |
case "lambda": return Promise.resolve(closure(env, list[1], list[2])); | |
case "defmacro": | |
env.macros[list[1]] = preprocessor(env, list[2], list[3]); | |
return Promise.resolve([]); | |
case "defun": | |
env.vars[list[1]] = closure(env, list[2], list[3]); | |
return Promise.resolve(env.vars[list[1]]); | |
} | |
return Promise.all(list.map(e => evaluate(env, e))).then(values => { | |
const func = values[0], args = values.slice(1); | |
if (typeof func === "function") return func.apply(env, args); | |
return func; // non closure object as the value | |
}); | |
} | |
const builtins = {}; | |
builtins.macros = {}; | |
builtins.vars = { | |
get nil() {return [];}, | |
["nil?"](o) {return Array.isArray(o) && o.length === 0;}, | |
cons(a, b) {return [a].concat(b);}, | |
car(l) {return l[0];}, | |
cdr(l) {return l.slice(1);}, | |
list() {return Array.from(arguments);}, | |
["'splice"]() {return ["''splice", Array.from(arguments)];}, | |
["'splist"]() { | |
return [].concat(...Array.from( | |
arguments, | |
e => Array.isArray(e) && e[0] === "''splice" ? e.slice(1) : [e])); | |
}, | |
apply(func, args) {return evaluate(this, [func].concat(args));}, | |
read() { | |
return new Promise((f, r) => function loop(code, p) { | |
process.stdout.write(p); | |
process.stdin.setEncoding("utf8"); | |
process.stdin.on("readable", function read() { | |
const buf = process.stdin.read(); | |
if (buf === null) return; | |
process.stdin.removeListener("readable", read); | |
parse(tokenize(code + buf)).then(tree => { | |
tree[0].length > 0 ? f(tree[0][0]) : loop("", "> "); | |
}).catch(err => { | |
if (err === "not finished") loop(code + buf, "+ "); | |
else { | |
console.log(`Parse Error: ${err}`); | |
loop("", "> "); | |
} | |
}); | |
}); | |
}("", "> ")); | |
}, | |
print(v) { | |
const s = `${v}`; | |
console.log(s); | |
return s.length; | |
}, | |
exit() {return Promise.reject("quit");}, | |
["+"](a, ...bs) {return bs.reduce((s, b) => s + b, a);}, | |
["-"](a, ...bs) {return bs.reduce((s, b) => s - b, a);}, | |
["*"](a, ...bs) {return bs.reduce((s, b) => s * b, a);}, | |
["/"](a, ...bs) {return bs.reduce((s, b) => s / b, a);}, | |
["%"](a, b) {return a % b;}, | |
["="](a, b) {return a === b;}, | |
["!="](a, b) {return a !== b;}, | |
[">"](a, b) {return a > b;}, | |
[">="](a, b) {return a >= b;}, | |
["<"](a, b) {return a < b;}, | |
["<="](a, b) {return a <= b;}, | |
["string-to-number"](a, b) {return b ? parseInt(a, b) : parseFloat(a);}, | |
["number-to-string"](a, b) {return b ? a.toString(b) : a.toString();}, | |
eval(l) {return evaluate(this, l);} | |
}; | |
function run(code) { | |
return parse(tokenize(code)).then(tree => progn(builtins, tree[0])); | |
} | |
if (process.argv.length < 3) { | |
const repl = ` | |
(defmacro loop (body) \`(progn (eval ,body) (loop ,body))) | |
(loop (print (eval (read))))`; | |
run(repl).catch(err => console.error(err.stack ? err.stack : err)); | |
} else { | |
new Promise((f, r) => require("fs").readFile( | |
process.argv[2], "utf8", (err, data) => err ? r(err) : f(data) | |
)).then(run).catch(err => console.error(err.stack ? err.stack : err)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment