Merge pull request #323 from jig/master
[jackhill/mal.git] / ts / reader.ts
1 import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNil, MalKeyword, MalSymbol, MalVector, MalHashMap } from "./types";
2
3 class 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
19 export function readStr(input: string): MalType {
20 const tokens = tokenizer(input);
21 const reader = new Reader(tokens);
22 return readForm(reader);
23 }
24
25 function 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
45 function readForm(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 = readForm(reader);
69 return new MalList([sym, readForm(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 = readForm(reader);
79 return new MalList([sym, target]);
80 }
81 }
82
83 function readList(reader: Reader): MalType {
84 return readParen(reader, MalList, "(", ")");
85 }
86
87 function readVector(reader: Reader): MalType {
88 return readParen(reader, MalVector, "[", "]");
89 }
90
91 function readHashMap(reader: Reader): MalType {
92 return readParen(reader, MalHashMap, "{", "}");
93 }
94
95 function 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(readForm(reader));
109 }
110 reader.next(); // drop close paren
111
112 return new ctor(list);
113 }
114
115 function 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, (_, c: string) => c == 'n' ? '\n' : c)
128 return new MalString(v);
129 }
130 if (token[0] === ":") {
131 return MalKeyword.get(token.substr(1));
132 }
133 switch (token) {
134 case "nil":
135 return MalNil.instance;
136 case "true":
137 return new MalBoolean(true);
138 case "false":
139 return new MalBoolean(false);
140 }
141
142 return MalSymbol.get(token);
143 }