+import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNull, 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 readFrom(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 readFrom(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 = readFrom(reader);
+ return new MalList([sym, readFrom(reader), target]);
+ }
+ default:
+ return readAtom(reader);
+ }
+
+ function readSymbol(name: string) {
+ reader.next();
+ const sym = MalSymbol.get(name);
+ const target = readFrom(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(readFrom(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, '"')
+ .replace(/\\n/g, "\n")
+ .replace(/\\\\/g, "\\");
+ return new MalString(v);
+ }
+ if (token[0] === ":") {
+ return new MalKeyword(token.substr(1));
+ }
+ switch (token) {
+ case "nil":
+ return new MalNull();
+ case "true":
+ return new MalBoolean(true);
+ case "false":
+ return new MalBoolean(false);
+ }
+
+ return MalSymbol.get(token);
+}
+export type MalType = MalList | MalNumber | MalString | MalNull | MalBoolean | MalSymbol | MalKeyword | MalVector | MalHashMap;
+
+export class MalList {
+ type: "list" = "list";
+
+ constructor(public list: MalType[]) {
+ }
+}
+
+export class MalNumber {
+ type: "number" = "number";
+ constructor(public v: number) {
+ }
+}
+
+export class MalString {
+ type: "string" = "string";
+ constructor(public v: string) {
+ }
+}
+
+export class MalNull {
+ type: "null" = "null";
+}
+
+export class MalBoolean {
+ type: "boolean" = "boolean";
+ constructor(public v: boolean) {
+ }
+}
+
+export class MalSymbol {
+ static map = new Map<symbol, MalSymbol>();
+
+ static get(name: string): MalSymbol {
+ const sym = Symbol.for(name);
+ let token = this.map.get(sym);
+ if (token) {
+ return token;
+ }
+ token = new MalSymbol(name);
+ this.map.set(sym, token);
+ return token;
+ }
+
+ type: "symbol" = "symbol";
+
+ private constructor(public v: string) {
+ }
+}
+
+export class MalKeyword {
+ type: "keyword" = "keyword";
+ constructor(public v: string) {
+ this.v = String.fromCodePoint(0x29E) + this.v;
+ }
+}
+
+export class MalVector {
+ type: "vector" = "vector";
+ constructor(public list: MalType[]) {
+ }
+}
+
+export class MalHashMap {
+ type: "hash-map" = "hash-map";
+ map = new Map<MalType, MalType>();
+ constructor(list: MalType[]) {
+ while (list.length !== 0) {
+ const key = list.shift() !;
+ const value = list.shift();
+ if (value == null) {
+ throw new Error("unexpected hash length");
+ }
+ this.map.set(key, value);
+ }
+ }
+}