TypeScript: step 4
[jackhill/mal.git] / ts / reader.ts
CommitLineData
f406f88b 1import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNull, MalKeyword, MalSymbol, MalVector, MalHashMap } from "./types";
2
3class Reader {
4 position = 0;
5
6 constructor(private tokens: string[]) { }
7
8 next(): string {
9 const ret = this.peek();
10 this.position += 1;
11 return ret;
12 }
13
14 peek(): string {
15 return this.tokens[this.position];
16 }
17}
18
19export function readStr(input: string): MalType {
20 const tokens = tokenizer(input);
21 const reader = new Reader(tokens);
22 return readFrom(reader);
23}
24
25function tokenizer(input: string): string[] {
26 const regexp = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g;
27 const tokens: string[] = [];
28 while (true) {
29 const matches = regexp.exec(input);
30 if (!matches) {
31 break;
32 }
33 const match = matches[1];
34 if (match === "") {
35 break;
36 }
37 if (match[0] !== ";") {
38 tokens.push(match);
39 }
40 }
41
42 return tokens;
43}
44
45function readFrom(reader: Reader): MalType {
46 const token = reader.peek();
47 switch (token) {
48 case "(":
49 return readList(reader);
50 case "[":
51 return readVector(reader);
52 case "{":
53 return readHashMap(reader);
54 case "'":
55 return readSymbol("quote");
56 case "`":
57 return readSymbol("quasiquote");
58 case "~":
59 return readSymbol("unquote");
60 case "~@":
61 return readSymbol("splice-unquote");
62 case "@":
63 return readSymbol("deref");
64 case "^":
65 {
66 reader.next();
67 const sym = MalSymbol.get("with-meta");
68 const target = readFrom(reader);
69 return new MalList([sym, readFrom(reader), target]);
70 }
71 default:
72 return readAtom(reader);
73 }
74
75 function readSymbol(name: string) {
76 reader.next();
77 const sym = MalSymbol.get(name);
78 const target = readFrom(reader);
79 return new MalList([sym, target]);
80 }
81}
82
83function readList(reader: Reader): MalType {
84 return readParen(reader, MalList, "(", ")");
85}
86
87function readVector(reader: Reader): MalType {
88 return readParen(reader, MalVector, "[", "]");
89}
90
91function readHashMap(reader: Reader): MalType {
92 return readParen(reader, MalHashMap, "{", "}");
93}
94
95function readParen(reader: Reader, ctor: { new (list: MalType[]): MalType; }, open: string, close: string): MalType {
96 const token = reader.next(); // drop open paren
97 if (token !== open) {
98 throw new Error(`unexpected token ${token}, expected ${open}`);
99 }
100 const list: MalType[] = [];
101 while (true) {
102 const next = reader.peek();
103 if (next === close) {
104 break;
105 } else if (!next) {
106 throw new Error("unexpected EOF");
107 }
108 list.push(readFrom(reader));
109 }
110 reader.next(); // drop close paren
111
112 return new ctor(list);
113}
114
115function readAtom(reader: Reader): MalType {
116 const token = reader.next();
117 if (token.match(/^-?[0-9]+$/)) {
118 const v = parseInt(token, 10);
119 return new MalNumber(v);
120 }
121 if (token.match(/^-?[0-9]\.[0-9]+$/)) {
122 const v = parseFloat(token);
123 return new MalNumber(v);
124 }
125 if (token[0] === '"') {
126 const v = token.slice(1, token.length - 1)
127 .replace(/\\"/g, '"')
128 .replace(/\\n/g, "\n")
129 .replace(/\\\\/g, "\\");
130 return new MalString(v);
131 }
132 if (token[0] === ":") {
133 return new MalKeyword(token.substr(1));
134 }
135 switch (token) {
136 case "nil":
dfe70453 137 return MalNull.instance;
f406f88b 138 case "true":
139 return new MalBoolean(true);
140 case "false":
141 return new MalBoolean(false);
142 }
143
144 return MalSymbol.get(token);
145}