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