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