Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active December 6, 2016 07:37
Show Gist options
  • Save bellbind/924d804eb021e8052ce8acbc2f8c5531 to your computer and use it in GitHub Desktop.
Save bellbind/924d804eb021e8052ce8acbc2f8c5531 to your computer and use it in GitHub Desktop.
[es6][nodejs]lisp REPL interpreter with Promise
// 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