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.
410 lines
11 KiB
410 lines
11 KiB
3 years ago
|
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();
|
||
|
}
|
||
|
}
|
||
|
|