1
1
mirror of https://github.com/kanaka/mal.git synced 2024-09-21 10:37:58 +03:00
mal/ts/reader.ts

144 lines
3.8 KiB
TypeScript

import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNil, MalKeyword, MalSymbol, MalVector, MalHashMap } from "./types";
class Reader {
position = 0;
constructor(private tokens: string[]) { }
next(): string {
const ret = this.peek();
this.position += 1;
return ret;
}
peek(): string {
return this.tokens[this.position];
}
}
export function readStr(input: string): MalType {
const tokens = tokenizer(input);
const reader = new Reader(tokens);
return readForm(reader);
}
function tokenizer(input: string): string[] {
const regexp = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g;
const tokens: string[] = [];
while (true) {
const matches = regexp.exec(input);
if (!matches) {
break;
}
const match = matches[1];
if (match === "") {
break;
}
if (match[0] !== ";") {
tokens.push(match);
}
}
return tokens;
}
function readForm(reader: Reader): MalType {
const token = reader.peek();
switch (token) {
case "(":
return readList(reader);
case "[":
return readVector(reader);
case "{":
return readHashMap(reader);
case "'":
return readSymbol("quote");
case "`":
return readSymbol("quasiquote");
case "~":
return readSymbol("unquote");
case "~@":
return readSymbol("splice-unquote");
case "@":
return readSymbol("deref");
case "^":
{
reader.next();
const sym = MalSymbol.get("with-meta");
const target = readForm(reader);
return new MalList([sym, readForm(reader), target]);
}
default:
return readAtom(reader);
}
function readSymbol(name: string) {
reader.next();
const sym = MalSymbol.get(name);
const target = readForm(reader);
return new MalList([sym, target]);
}
}
function readList(reader: Reader): MalType {
return readParen(reader, MalList, "(", ")");
}
function readVector(reader: Reader): MalType {
return readParen(reader, MalVector, "[", "]");
}
function readHashMap(reader: Reader): MalType {
return readParen(reader, MalHashMap, "{", "}");
}
function readParen(reader: Reader, ctor: { new (list: MalType[]): MalType; }, open: string, close: string): MalType {
const token = reader.next(); // drop open paren
if (token !== open) {
throw new Error(`unexpected token ${token}, expected ${open}`);
}
const list: MalType[] = [];
while (true) {
const next = reader.peek();
if (next === close) {
break;
} else if (!next) {
throw new Error("unexpected EOF");
}
list.push(readForm(reader));
}
reader.next(); // drop close paren
return new ctor(list);
}
function readAtom(reader: Reader): MalType {
const token = reader.next();
if (token.match(/^-?[0-9]+$/)) {
const v = parseInt(token, 10);
return new MalNumber(v);
}
if (token.match(/^-?[0-9]\.[0-9]+$/)) {
const v = parseFloat(token);
return new MalNumber(v);
}
if (token[0] === '"') {
const v = token.slice(1, token.length - 1)
.replace(/\\(.)/g, (_, c: string) => c == 'n' ? '\n' : c)
return new MalString(v);
}
if (token[0] === ":") {
return MalKeyword.get(token.substr(1));
}
switch (token) {
case "nil":
return MalNil.instance;
case "true":
return new MalBoolean(true);
case "false":
return new MalBoolean(false);
}
return MalSymbol.get(token);
}