You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
11 KiB
409 lines
11 KiB
import { Reader, ReaderMacros } from './reader.js'; |
|
import { DataTypes, Cons, Function, Nil, True, SpecialForm, Macro, Number, String, Symbol } from './datatypes.js'; |
|
import { Printer } from './printer.js'; |
|
import { Evaluator } from './evaluator.js'; |
|
import { Env } from './env.js'; |
|
|
|
|
|
|
|
|
|
export class Stream { |
|
onwrite = null; |
|
|
|
constructor(data = '') { |
|
this.data = Array.from(data); |
|
} |
|
|
|
readChar() { |
|
return this.data.shift(); |
|
} |
|
|
|
peekChar() { |
|
return this.data[0]; |
|
} |
|
|
|
pushData(data) { |
|
this.data.push(...Array.from(data)); |
|
if (data && this.onwrite) { |
|
this.onwrite(data); |
|
} |
|
} |
|
|
|
readData() { |
|
return this.data.splice(0).join(""); |
|
} |
|
} |
|
|
|
|
|
|
|
export class Repl { |
|
stdin = new Stream(); |
|
stdout = new Stream(); |
|
env = new Env( |
|
null, |
|
{ |
|
'STANDARD-INPUT-STREAM': this.stdin, |
|
'STANDARD-OUTPUT-STREAM': this.stdout, |
|
'NIL': new Nil(), |
|
'T': new True(), |
|
'+': new Function(async operands => new Number(await Cons.reduce(operands, (rslt, i) => rslt + i.value, 0))), |
|
'-': new Function(async operands => new Number( |
|
operands.cdr.type === DataTypes.Nil |
|
? -operands.car.value |
|
: await Cons.reduce(operands.cdr, (rslt, i) => rslt - i.value, operands.car.value))), |
|
'/': new Function(async operands => new Number(await operands.reduce((rslt, i) => rslt / i.value, 1))), |
|
'*': new Function(async operands => new Number(await operands.reduce((rslt, i) => rslt * i.value, 1))), |
|
'%': new Function(operands => new Number(operands.car.value % operands.cdr.car.value)), |
|
'NOT': new Function(operands => operands.car.type === DataTypes.Nil ? new True() : new Nil()), |
|
'=': new Function(operands => this.listCompare(operands, (a, b) => a === b)), |
|
'<': new Function(operands => this.listCompare(operands, (a, b) => a < b)), |
|
'<=': new Function(operands => this.listCompare(operands, (a, b) => a <= b)), |
|
'>': new Function(operands => this.listCompare(operands, (a, b) => a > b)), |
|
'>=': new Function(operands => this.listCompare(operands, (a, b) => a >= b)), |
|
'PRINT': new Function(operands => { |
|
Printer.print(operands.car, this.env); |
|
return operands.car; |
|
}), |
|
'PRINC': new Function(operands => { |
|
Printer.princ(operands.car, this.env); |
|
return operands.car; |
|
}), |
|
'CONS': new Function(operands => new Cons(operands.car, operands.cdr.car)), |
|
'LIST': new Function(operands => operands), |
|
'CAR': new Function(operands => { |
|
if (operands.car.type === DataTypes.Nil) { |
|
return new Nil(); |
|
} else if (operands.car.type === DataTypes.Cons) { |
|
return operands.car.car; |
|
} |
|
return new String('Can not car on not cons'); |
|
}), |
|
'CDR': new Function(operands => { |
|
if (operands.car.type === DataTypes.Nil) { |
|
return new Nil(); |
|
} else if (operands.car.type === DataTypes.Cons) { |
|
return operands.car.cdr |
|
} |
|
return new String('Can not cdr on not cons'); |
|
}), |
|
'LET': new SpecialForm(async (sexp, env, macros) => { |
|
env = await Cons.reduce( |
|
sexp.car, |
|
async (prev, o) => { |
|
let env = new Env(prev, {[o.car.value]: new Nil()}); |
|
Env.set(env, o.car.value, await Evaluator.eval(o.cdr.car, env, macros)); |
|
return env; |
|
}, |
|
env); |
|
return await Cons.reduce(sexp.cdr, |
|
async (p, o) => await Evaluator.eval(o, env, macros), |
|
null)}), |
|
'SET': new SpecialForm(async (sexp, env, macros) => { |
|
const value = await Evaluator.eval(sexp.cdr.car, env, macros); |
|
Env.set(env, sexp.car.value, value); |
|
return value |
|
}), |
|
'DEF': new SpecialForm(async (sexp, env, macros) => { |
|
Env.set(this.env, sexp.car.value, await Evaluator.eval(sexp.cdr.car, env, macros)); |
|
return sexp.car |
|
}), |
|
'FN': new SpecialForm((sexp, env, macros) => { |
|
const outer = env; |
|
return new Function(async (operands) => { |
|
env = new Env(env, |
|
sexp.car.type === DataTypes.Cons |
|
? await Cons.reduce( |
|
Cons.zip(sexp.car, operands), |
|
(prev, o) => ({...prev, [o.car.value]: o.cdr}), |
|
{}) |
|
: {}); |
|
return await Cons.reduce(sexp.cdr, |
|
async (_, o) => await Evaluator.eval(o, env, macros), |
|
null); |
|
}, sexp, outer); |
|
}), |
|
'AND': new SpecialForm(async (sexp, env, macros) => { |
|
let last = new Nil(); |
|
const f = async o => { |
|
const cur = await Evaluator.eval(o.car, env, macros); |
|
if (cur.type === DataTypes.Nil) { |
|
return last; |
|
} |
|
last = cur; |
|
if (o.cdr.type === DataTypes.Nil) { |
|
return cur; |
|
} |
|
|
|
return await f(o.cdr); |
|
}; |
|
|
|
return await f(sexp); |
|
}), |
|
'OR': new SpecialForm(async (sexp, env, macros) => { |
|
let o = sexp; |
|
const f = async o => { |
|
const cur = await Evaluator.eval(o.car, env, macros); |
|
if (!(cur === DataTypes.Nil)) { |
|
return cur; |
|
} |
|
|
|
if (o.cdr === DataTypes.Nil) { |
|
return new Nil(); |
|
} |
|
|
|
return await f(o.cdr); |
|
}; |
|
|
|
return await f(sexp); |
|
}), |
|
'IF': new SpecialForm(async (sexp, env, macros) => { |
|
const cur = await Evaluator.eval(sexp.car, env, macros); |
|
return !(cur.type === DataTypes.Nil) |
|
? await Evaluator.eval(sexp.cdr.car, env, macros) |
|
: await Evaluator.eval(sexp.cdr.cdr.car, env, macros); |
|
}), |
|
'QUASIQUOTE': new SpecialForm((sexp, env, macros) => { |
|
return this.unquote(sexp.car, env, macros); |
|
}), |
|
'QUOTE': new SpecialForm((sexp, env, macros) => { |
|
return sexp.car; |
|
}), |
|
'EVAL': new SpecialForm(async (sexp, env, macros) => { |
|
return await Evaluator.eval( |
|
await Evaluator.eval(sexp.car, env, macros), |
|
env, |
|
macros); |
|
}), |
|
'EXPAND': new SpecialForm((sexp, env, macros) => { |
|
return Evaluator.expand(sexp.car, macros); |
|
}), |
|
'SAVE': new Function(() => { |
|
const converter = (key, val) => { |
|
if ( |
|
typeof val === 'function' || |
|
val && val.constructor === RegExp) |
|
{ |
|
return String(val); |
|
} else { |
|
return val; |
|
} |
|
}; |
|
window.localStorage.setItem( |
|
'image', |
|
JSON.stringify( |
|
repl_env.data, |
|
converter, |
|
2) |
|
); |
|
return new Nil(); |
|
}), |
|
'LOAD': new Function(() => { |
|
const image = window.localStorage.getItem('image') |
|
const data = JSON.parse(image); |
|
const keys = Object.keys(data); |
|
for (let key of keys) { |
|
const atom = data[key]; |
|
repl_env.set(key, eval(atom)); |
|
} |
|
return new Nil(); |
|
}), |
|
'GET-TIME': new Function(() => { |
|
return new Number(Date.now()); |
|
}), |
|
'WAIT': new Function(operands => { |
|
return new Promise(resolve => setTimeout(_ => resolve(new Nil()), operands.car.value)) |
|
}), |
|
'DO': new Function(operands => Cons.last(operands)), |
|
}); |
|
|
|
macros = new Env( |
|
null, |
|
{ |
|
'MACRO': new SpecialForm((sexp, env) => this.macro(sexp)), |
|
'DEFN': new Macro( |
|
operands => new Cons(new Symbol('DEF'), |
|
new Cons(operands.car, |
|
new Cons(new Cons(new Symbol('FN'), |
|
operands.cdr))))), |
|
'DEFMACRO': new SpecialForm((sexp, env) => { |
|
Env.set(this.macros, |
|
sexp.car.value, |
|
this.macro(sexp.cdr)); |
|
return new Cons(new Symbol("QUOTE"), new Cons(sexp.car)); |
|
}), |
|
} |
|
); |
|
|
|
async unquote(sexp, env, macros) { |
|
if (sexp.type === DataTypes.Cons) { |
|
if (sexp.car.type === DataTypes.Symbol && |
|
sexp.car.value == 'UNQUOTE') { |
|
return await Evaluator.eval(sexp.cdr.car, env, macros); |
|
} else if (sexp.car.type === DataTypes.Cons && |
|
sexp.car.car.type === DataTypes.Symbol && |
|
sexp.car.car.value == 'UNQUOTE-SPLICING') { |
|
return await Evaluator.eval(sexp.car.cdr.car, env, macros); |
|
} |
|
return new Cons( |
|
await this.unquote(sexp.car, env, macros), |
|
await this.unquote(sexp.cdr, env, macros)); |
|
} |
|
|
|
return sexp; |
|
} |
|
|
|
listCompare(list, comparer) { |
|
let compare = (last, list) => { |
|
const result = comparer(last, list.car.value); |
|
if (!result || list.cdr.type === DataTypes.Nil) { |
|
return result; |
|
} |
|
|
|
return compare(list.car.value, list.cdr); |
|
} |
|
|
|
return compare(list.car.value, list.cdr) ? new True() : new Nil(); |
|
} |
|
|
|
// Return a new Promise always. |
|
|
|
|
|
|
|
macro(sexp) { |
|
return new Macro(async (operands) => { |
|
let args = sexp.car; |
|
let body = sexp.cdr; |
|
let env = new Env(this.env, |
|
args.type === DataTypes.Cons |
|
? await Cons.reduce(Cons.zip(args, operands), |
|
(prev, o) => ({...prev, |
|
[o.car.value]: o.cdr}), |
|
{}) |
|
: {}); |
|
|
|
return await Cons.reduce( |
|
body, |
|
async (_, o) => await Evaluator.eval(o, env, this.macros), |
|
null); |
|
}); |
|
} |
|
|
|
|
|
|
|
// READER |
|
|
|
constructor(loadFromFile, loadTest, read) { |
|
this.stdin.onwrite = () => this.readEvalPrint(); |
|
|
|
Env.set(this.env, 'READ', new Function(async () => { |
|
const rep = this.stdin.onwrite; |
|
return new Promise(resolve => { |
|
read(); |
|
this.stdin.onwrite = (a) => { |
|
console.log(a); |
|
this.stdin.onwrite = rep; |
|
resolve(Reader.read(this.readerMacros, this.stdin)); |
|
}; |
|
}); |
|
})); |
|
|
|
Env.set(this.env, 'LOAD-FROM-FILE', new Function(async () => { |
|
this.stdin.pushData(await loadFromFile()); |
|
return new Nil(); |
|
})); |
|
|
|
Env.set(this.env, 'LOAD-TEST', new Function(async () => { |
|
this.stdin.pushData(await loadTest()); |
|
return new Nil(); |
|
})); |
|
|
|
this.readerMacros = new ReaderMacros(); |
|
// List read macro |
|
ReaderMacros.setMacroCharacter(this.readerMacros, '(', (c, stream) => { |
|
let cons; |
|
let list; |
|
let obj; |
|
ReaderMacros.setMacroCharacter(this.readerMacros, ')', (c, stream) => { |
|
ReaderMacros.popMacroCharacter(this.readerMacros, c); |
|
return null; |
|
}); |
|
while ((obj = Reader.read(this.readerMacros, stream))) { |
|
// list.push(obj); |
|
if (!cons) { |
|
cons = new Cons(obj); |
|
list = cons; |
|
} else { |
|
cons = cons.cdr = new Cons(obj); |
|
} |
|
} |
|
if (list && list.car) { |
|
return list; |
|
} |
|
|
|
return new Nil(); |
|
}); |
|
|
|
// String read macro |
|
ReaderMacros.setMacroCharacter(this.readerMacros, '"', (c, stream) => { |
|
let char = null, |
|
lastc = null, |
|
data = ''; |
|
while(char = stream.readChar()) { |
|
if (char === '"' && lastc !== '\\') { |
|
return new String(data); |
|
} |
|
|
|
data += char; |
|
lastc = char; |
|
} |
|
|
|
return null; |
|
}); |
|
|
|
// Quote read macro |
|
ReaderMacros.setMacroCharacter(this.readerMacros, '\'', (c, stream) => { |
|
return new Cons(new Symbol('QUOTE'), |
|
new Cons(Reader.read(this.readerMacros, stream))); |
|
}); |
|
|
|
// Quasiquote read macro |
|
ReaderMacros.setMacroCharacter(this.readerMacros, '`', (c, stream) => { |
|
return new Cons(new Symbol('QUASIQUOTE'), |
|
new Cons(Reader.read(this.readerMacros, stream))); |
|
}); |
|
|
|
// unquote read macro |
|
ReaderMacros.setMacroCharacter(this.readerMacros, ',', (c, stream) => { |
|
return new Cons(new Symbol(stream.peekChar() == '@' |
|
? stream.readChar() && 'UNQUOTE-SPLICING' |
|
: 'UNQUOTE'), |
|
new Cons(Reader.read(this.readerMacros, stream))); |
|
}); |
|
} |
|
|
|
async readEvalPrint() { |
|
if (!this.stdin.peekChar()) { |
|
return; |
|
} |
|
|
|
const sexp = Reader.read(this.readerMacros, this.stdin); |
|
if (sexp) { |
|
let evaluated; |
|
try { |
|
evaluated = await Evaluator.eval( |
|
sexp, |
|
this.env, |
|
this.macros); |
|
} catch (e) { |
|
console.error(e); |
|
evaluated = new String(e); |
|
} |
|
Printer.print(new String(""), this.env); |
|
Printer.prin1(evaluated, this.env); |
|
Printer.print(new String(""), this.env); |
|
} |
|
|
|
await this.readEvalPrint(); |
|
} |
|
} |
|
|
|
|