Created
April 14, 2018 06:23
-
-
Save adelciotto/47f3619a8eeed8f683b577294749b533 to your computer and use it in GitHub Desktop.
Tiny and simple lisp implementation in node
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
class Env { | |
constructor(p, e = {}) { | |
this.p = p; | |
this.e = e; | |
} | |
get(name) { | |
let val = this.e[name]; | |
if (typeof val !== 'undefined') | |
return val; | |
if (this.p) | |
return this.p.get(name); | |
throw `Identifier ${name} is not defined`; | |
} | |
set(name, val) { | |
let exists = !!this.e[name]; | |
if (!exists) | |
this.e[name] = val; | |
else | |
throw `Identifier ${name} is already defined`; | |
} | |
} | |
function Func(params, body, _env) { | |
return (x) => { | |
let env = new Env(_env, params.reduce((acc, p, i) => { | |
acc[p.v] = x[i]; | |
return acc; | |
}, {})); | |
return eval(body, env); | |
}; | |
} | |
let proc = process, | |
con = console, | |
cmpop = (x, f) => x.slice(1).every(f), | |
globals = { | |
'+': (x) => x.reduce((acc, val) => acc + val), | |
'-': (x) => x.reduce((acc, val) => acc - val), | |
'/': (x) => x.reduce((acc, val) => { if (val === 0) throw "Cannot divide by 0"; return acc / val; }), | |
'*': (x) => x.reduce((acc, val) => acc * val), | |
'**': (x) => x.reduce((acc, val) => Math.pow(acc, val)), | |
'>': (x) => cmpop(x, (v) => x[0] > v), | |
'>=': (x) => cmpop(x, (v) => x[0] >= v), | |
'<': (x) => cmpop(x, (v) => x[0] < v), | |
'<=': (x) => cmpop(x, (v) => x[0] <= v), | |
'==': (x) => cmpop(x, (v) => x[0] === v), | |
'car': (x) => x[0], | |
'cdr': (x) => x.slice(1), | |
'cons': (x) => [x[0]].concat(x.slice(1)) | |
}, | |
genv = new Env(null, globals), | |
rl = require('readline').createInterface({ | |
input: proc.stdin, | |
output: proc.stdout, | |
prompt: '\u039b> ' | |
}); | |
function lex(source) { | |
return source.replace(/\(|\)/g, ' $& ').match(/".*?"|\S+/g); | |
} | |
function parse(tokens) { | |
if (tokens.length === 0) throw 'Unexpected EOF'; | |
let token = tokens.shift(); | |
switch (token) { | |
case '(': | |
let list = []; | |
while (tokens[0] !== ')') list.push(parse(tokens)); | |
tokens.shift(); | |
return { t: 'expression', args: list }; | |
case ')': | |
throw 'Unmatched delimiter )'; | |
default: | |
if (token[0] === '"') return { t: 'string', v: token.substr(1, token.length-2) }; | |
let numberVal = Number(token); | |
return isNaN(numberVal) ? { t: 'identifier', v: token } : { t: 'number', v: numberVal }; | |
} | |
} | |
function eval(exp, env) { | |
switch (exp.t) { | |
case 'expression': | |
let args = exp.args, | |
v = args[0].v; | |
if (v === 'if') { | |
return eval(args[1], env) ? eval(args[2], env) : eval(args[3], env); | |
} else if (v === 'define') { | |
env.set(args[1].v, eval(args[2], env)); | |
} else if (v === 'lambda') { | |
return Func(args[1].args, args[2], env); | |
} else if (v === 'quote') { | |
return args[1]; | |
} | |
else { | |
let f = eval(args[0], env), | |
vals = args.slice(1).map((e) => eval(e, env)); | |
return f(vals); | |
} | |
case 'string': | |
case 'number': | |
return exp.v; | |
case 'identifier': | |
return env.get(exp.v); | |
default: | |
return exp.v; | |
} | |
} | |
rl.prompt(); | |
rl.on('line', (l) => { | |
if (l) { | |
try { | |
let r = eval(parse(lex(l.trim())), genv); | |
if (typeof r !== 'undefined') con.log(r); | |
} catch (e) { | |
con.error(`Error: ${e}`); | |
} | |
} | |
rl.prompt(); | |
}).on('close', () => proc.exit(0)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment