f406f88b |
1 | import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNull, 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 readFrom(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 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 | |
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(readFrom(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, '"') |
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 | } |