Merge pull request #378 from asarhaddon/test-macro-not-changing-function
[jackhill/mal.git] / ts / reader.ts
CommitLineData
6071876f 1import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNil, MalKeyword, MalSymbol, MalVector, MalHashMap } from "./types";
f406f88b 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);
29db6f4e 22 return readForm(reader);
f406f88b 23}
24
25function tokenizer(input: string): string[] {
4aa0ebdf 26 const regexp = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/g;
f406f88b 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
29db6f4e 45function readForm(reader: Reader): MalType {
f406f88b 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");
29db6f4e 68 const target = readForm(reader);
69 return new MalList([sym, readForm(reader), target]);
f406f88b 70 }
71 default:
72 return readAtom(reader);
73 }
74
75 function readSymbol(name: string) {
76 reader.next();
77 const sym = MalSymbol.get(name);
29db6f4e 78 const target = readForm(reader);
f406f88b 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 }
29db6f4e 108 list.push(readForm(reader));
f406f88b 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] === '"') {
4aa0ebdf
JM
126 if (token.slice(-1) !== '"') {
127 throw new Error("expected '\"', got EOF");
128 }
f406f88b 129 const v = token.slice(1, token.length - 1)
da9aef12 130 .replace(/\\(.)/g, (_, c: string) => c == 'n' ? '\n' : c)
f406f88b 131 return new MalString(v);
132 }
133 if (token[0] === ":") {
10f8aa84 134 return MalKeyword.get(token.substr(1));
f406f88b 135 }
136 switch (token) {
137 case "nil":
6071876f 138 return MalNil.instance;
f406f88b 139 case "true":
140 return new MalBoolean(true);
141 case "false":
142 return new MalBoolean(false);
143 }
144
145 return MalSymbol.get(token);
146}