Commit | Line | Data |
---|---|---|
e21a85a3 | 1 | import { readline } from "./node_readline"; |
2 | ||
dd7a4f55 | 3 | import { Node, MalType, MalString, MalNil, MalList, MalVector, MalHashMap, MalSymbol, MalFunction, isAST, isSeq } from "./types"; |
e21a85a3 | 4 | import { Env } from "./env"; |
5 | import * as core from "./core"; | |
6 | import { readStr } from "./reader"; | |
7 | import { prStr } from "./printer"; | |
8 | ||
9c92462f | 9 | // READ |
e21a85a3 | 10 | function read(str: string): MalType { |
11 | return readStr(str); | |
12 | } | |
13 | ||
fbfe6784 NB |
14 | function starts_with(lst: MalType[], sym: string): boolean { |
15 | if (lst.length == 2) { | |
16 | let a0 = lst[0] | |
17 | switch (a0.type) { | |
18 | case Node.Symbol: | |
19 | return a0.v === sym; | |
e21a85a3 | 20 | } |
21 | } | |
fbfe6784 NB |
22 | return false; |
23 | } | |
e21a85a3 | 24 | |
fbfe6784 NB |
25 | function qq_loop(elt: MalType, acc: MalList): MalList { |
26 | if (elt.type == Node.List && starts_with(elt.list, "splice-unquote")) { | |
27 | return new MalList([MalSymbol.get("concat"), elt.list[1], acc]); | |
28 | } else { | |
29 | return new MalList([MalSymbol.get("cons"), quasiquote(elt), acc]); | |
30 | } | |
31 | } | |
e21a85a3 | 32 | |
fbfe6784 NB |
33 | function qq_foldr(xs : MalType[]): MalList { |
34 | let acc = new MalList([]) | |
35 | for (let i=xs.length-1; 0<=i; i-=1) { | |
36 | acc = qq_loop(xs[i], acc) | |
37 | } | |
38 | return acc; | |
39 | } | |
e21a85a3 | 40 | |
fbfe6784 NB |
41 | function quasiquote(ast: MalType): MalType { |
42 | switch (ast.type) { | |
43 | case Node.Symbol: | |
44 | return new MalList([MalSymbol.get("quote"), ast]); | |
45 | case Node.HashMap: | |
46 | return new MalList([MalSymbol.get("quote"), ast]); | |
47 | case Node.List: | |
48 | if (starts_with(ast.list, "unquote")) { | |
49 | return ast.list[1]; | |
50 | } else { | |
51 | return qq_foldr(ast.list); | |
52 | } | |
53 | case Node.Vector: | |
54 | return new MalList([MalSymbol.get("vec"), qq_foldr(ast.list)]); | |
55 | default: | |
56 | return ast; | |
e21a85a3 | 57 | } |
58 | } | |
59 | ||
9c92462f | 60 | function isMacro(ast: MalType, env: Env): boolean { |
92bf0530 | 61 | if (!isSeq(ast)) { |
e21a85a3 | 62 | return false; |
63 | } | |
64 | const s = ast.list[0]; | |
5bb7479d | 65 | if (s.type !== Node.Symbol) { |
e21a85a3 | 66 | return false; |
67 | } | |
68 | const foundEnv = env.find(s); | |
69 | if (!foundEnv) { | |
70 | return false; | |
71 | } | |
72 | ||
73 | const f = foundEnv.get(s); | |
5bb7479d | 74 | if (f.type !== Node.Function) { |
e21a85a3 | 75 | return false; |
76 | } | |
77 | ||
78 | return f.isMacro; | |
79 | } | |
80 | ||
81 | function macroexpand(ast: MalType, env: Env): MalType { | |
9c92462f | 82 | while (isMacro(ast, env)) { |
92bf0530 | 83 | if (!isSeq(ast)) { |
e21a85a3 | 84 | throw new Error(`unexpected token type: ${ast.type}, expected: list or vector`); |
85 | } | |
86 | const s = ast.list[0]; | |
5bb7479d | 87 | if (s.type !== Node.Symbol) { |
e21a85a3 | 88 | throw new Error(`unexpected token type: ${s.type}, expected: symbol`); |
89 | } | |
90 | const f = env.get(s); | |
5bb7479d | 91 | if (f.type !== Node.Function) { |
e21a85a3 | 92 | throw new Error(`unexpected token type: ${f.type}, expected: function`); |
93 | } | |
94 | ast = f.func(...ast.list.slice(1)); | |
95 | } | |
96 | ||
97 | return ast; | |
98 | } | |
99 | ||
100 | function evalAST(ast: MalType, env: Env): MalType { | |
101 | switch (ast.type) { | |
5bb7479d | 102 | case Node.Symbol: |
e21a85a3 | 103 | const f = env.get(ast); |
104 | if (!f) { | |
105 | throw new Error(`unknown symbol: ${ast.v}`); | |
106 | } | |
107 | return f; | |
5bb7479d | 108 | case Node.List: |
9c92462f | 109 | return new MalList(ast.list.map(ast => evalMal(ast, env))); |
5bb7479d | 110 | case Node.Vector: |
9c92462f | 111 | return new MalVector(ast.list.map(ast => evalMal(ast, env))); |
5bb7479d | 112 | case Node.HashMap: |
e21a85a3 | 113 | const list: MalType[] = []; |
10f8aa84 | 114 | for (const [key, value] of ast.entries()) { |
e21a85a3 | 115 | list.push(key); |
9c92462f | 116 | list.push(evalMal(value, env)); |
e21a85a3 | 117 | } |
118 | return new MalHashMap(list); | |
119 | default: | |
120 | return ast; | |
121 | } | |
122 | } | |
123 | ||
9c92462f | 124 | // EVAL |
125 | function evalMal(ast: MalType, env: Env): MalType { | |
e21a85a3 | 126 | loop: while (true) { |
5bb7479d | 127 | if (ast.type !== Node.List) { |
e21a85a3 | 128 | return evalAST(ast, env); |
129 | } | |
90ca8485 JM |
130 | if (ast.list.length === 0) { |
131 | return ast; | |
132 | } | |
e21a85a3 | 133 | |
134 | ast = macroexpand(ast, env); | |
5bb7479d | 135 | if (!isSeq(ast)) { |
e21a85a3 | 136 | return evalAST(ast, env); |
137 | } | |
138 | ||
139 | if (ast.list.length === 0) { | |
140 | return ast; | |
141 | } | |
142 | const first = ast.list[0]; | |
143 | switch (first.type) { | |
5bb7479d | 144 | case Node.Symbol: |
e21a85a3 | 145 | switch (first.v) { |
146 | case "def!": { | |
147 | const [, key, value] = ast.list; | |
5bb7479d | 148 | if (key.type !== Node.Symbol) { |
e21a85a3 | 149 | throw new Error(`unexpected token type: ${key.type}, expected: symbol`); |
150 | } | |
151 | if (!value) { | |
152 | throw new Error(`unexpected syntax`); | |
153 | } | |
677a1c9d | 154 | return env.set(key, evalMal(value, env)); |
e21a85a3 | 155 | } |
156 | case "let*": { | |
157 | env = new Env(env); | |
158 | const pairs = ast.list[1]; | |
92bf0530 | 159 | if (!isSeq(pairs)) { |
e21a85a3 | 160 | throw new Error(`unexpected token type: ${pairs.type}, expected: list or vector`); |
161 | } | |
162 | for (let i = 0; i < pairs.list.length; i += 2) { | |
163 | const key = pairs.list[i]; | |
164 | const value = pairs.list[i + 1]; | |
5bb7479d | 165 | if (key.type !== Node.Symbol) { |
e21a85a3 | 166 | throw new Error(`unexpected token type: ${key.type}, expected: symbol`); |
167 | } | |
168 | if (!key || !value) { | |
169 | throw new Error(`unexpected syntax`); | |
170 | } | |
171 | ||
9c92462f | 172 | env.set(key, evalMal(value, env)); |
e21a85a3 | 173 | } |
174 | ast = ast.list[2]; | |
175 | continue loop; | |
176 | } | |
177 | case "quote": { | |
178 | return ast.list[1]; | |
179 | } | |
fbfe6784 NB |
180 | case "quasiquoteexpand": { |
181 | return quasiquote(ast.list[1]); | |
182 | } | |
e21a85a3 | 183 | case "quasiquote": { |
184 | ast = quasiquote(ast.list[1]); | |
185 | continue loop; | |
186 | } | |
187 | case "defmacro!": { | |
188 | const [, key, value] = ast.list; | |
5bb7479d | 189 | if (key.type !== Node.Symbol) { |
e21a85a3 | 190 | throw new Error(`unexpected token type: ${key.type}, expected: symbol`); |
191 | } | |
192 | if (!value) { | |
193 | throw new Error(`unexpected syntax`); | |
194 | } | |
9c92462f | 195 | const f = evalMal(value, env); |
5bb7479d | 196 | if (f.type !== Node.Function) { |
e21a85a3 | 197 | throw new Error(`unexpected token type: ${f.type}, expected: function`); |
198 | } | |
3827366c | 199 | return env.set(key, f.toMacro()); |
e21a85a3 | 200 | } |
201 | case "macroexpand": { | |
202 | return macroexpand(ast.list[1], env); | |
203 | } | |
204 | case "do": { | |
bbddf168 | 205 | const list = ast.list.slice(1, -1); |
206 | evalAST(new MalList(list), env); | |
207 | ast = ast.list[ast.list.length - 1]; | |
e21a85a3 | 208 | continue loop; |
209 | } | |
210 | case "if": { | |
211 | const [, cond, thenExpr, elseExrp] = ast.list; | |
9c92462f | 212 | const ret = evalMal(cond, env); |
e21a85a3 | 213 | let b = true; |
5bb7479d | 214 | if (ret.type === Node.Boolean && !ret.v) { |
e21a85a3 | 215 | b = false; |
6071876f | 216 | } else if (ret.type === Node.Nil) { |
e21a85a3 | 217 | b = false; |
218 | } | |
219 | if (b) { | |
220 | ast = thenExpr; | |
221 | } else if (elseExrp) { | |
222 | ast = elseExrp; | |
223 | } else { | |
6071876f | 224 | ast = MalNil.instance; |
e21a85a3 | 225 | } |
226 | continue loop; | |
227 | } | |
228 | case "fn*": { | |
229 | const [, params, bodyAst] = ast.list; | |
92bf0530 | 230 | if (!isSeq(params)) { |
e21a85a3 | 231 | throw new Error(`unexpected return type: ${params.type}, expected: list or vector`); |
232 | } | |
233 | const symbols = params.list.map(param => { | |
5bb7479d | 234 | if (param.type !== Node.Symbol) { |
e21a85a3 | 235 | throw new Error(`unexpected return type: ${param.type}, expected: symbol`); |
236 | } | |
237 | return param; | |
238 | }); | |
9c92462f | 239 | return MalFunction.fromLisp(evalMal, env, symbols, bodyAst); |
e21a85a3 | 240 | } |
241 | } | |
242 | } | |
243 | const result = evalAST(ast, env); | |
92bf0530 | 244 | if (!isSeq(result)) { |
e21a85a3 | 245 | throw new Error(`unexpected return type: ${result.type}, expected: list or vector`); |
246 | } | |
247 | const [f, ...args] = result.list; | |
5bb7479d | 248 | if (f.type !== Node.Function) { |
e21a85a3 | 249 | throw new Error(`unexpected token: ${f.type}, expected: function`); |
250 | } | |
251 | if (f.ast) { | |
252 | ast = f.ast; | |
253 | env = f.newEnv(args); | |
254 | continue loop; | |
255 | } | |
256 | ||
257 | return f.func(...args); | |
258 | } | |
259 | } | |
260 | ||
9c92462f | 261 | |
e21a85a3 | 262 | function print(exp: MalType): string { |
263 | return prStr(exp); | |
264 | } | |
265 | ||
266 | const replEnv = new Env(); | |
9c92462f | 267 | function rep(str: string): string { |
268 | return print(evalMal(read(str), replEnv)); | |
269 | } | |
270 | ||
271 | // core.EXT: defined using Racket | |
eb7a2bbd | 272 | core.ns.forEach((value, key) => { |
e21a85a3 | 273 | replEnv.set(key, value); |
eb7a2bbd | 274 | }); |
e21a85a3 | 275 | replEnv.set(MalSymbol.get("eval"), MalFunction.fromBootstrap(ast => { |
276 | if (!ast) { | |
277 | throw new Error(`undefined argument`); | |
278 | } | |
9c92462f | 279 | return evalMal(ast, replEnv); |
e21a85a3 | 280 | })); |
e21a85a3 | 281 | replEnv.set(MalSymbol.get("*ARGV*"), new MalList([])); |
282 | ||
283 | // core.mal: defined using the language itself | |
284 | rep("(def! not (fn* (a) (if a false true)))"); | |
e6d41de4 | 285 | rep(`(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))`); |
e21a85a3 | 286 | rep(`(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))`); |
e21a85a3 | 287 | |
288 | if (typeof process !== "undefined" && 2 < process.argv.length) { | |
289 | replEnv.set(MalSymbol.get("*ARGV*"), new MalList(process.argv.slice(3).map(s => new MalString(s)))); | |
290 | rep(`(load-file "${process.argv[2]}")`); | |
291 | process.exit(0); | |
292 | } | |
293 | ||
e21a85a3 | 294 | while (true) { |
295 | const line = readline("user> "); | |
296 | if (line == null) { | |
297 | break; | |
298 | } | |
299 | if (line === "") { | |
300 | continue; | |
301 | } | |
302 | try { | |
303 | console.log(rep(line)); | |
304 | } catch (e) { | |
dd7a4f55 JM |
305 | if (isAST(e)) { |
306 | console.error("Error:", prStr(e)); | |
307 | } else { | |
308 | const err: Error = e; | |
309 | console.error("Error:", err.message); | |
310 | } | |
e21a85a3 | 311 | } |
312 | } |