Commit | Line | Data |
---|---|---|
3934e3f8 HT |
1 | import 'dart:io'; |
2 | ||
3 | import 'core.dart'; | |
4 | import 'env.dart'; | |
5 | import 'printer.dart' as printer; | |
6 | import 'reader.dart' as reader; | |
7 | import 'types.dart'; | |
8 | ||
9 | final Env replEnv = new Env(); | |
10 | ||
11 | void setupEnv(List<String> argv) { | |
12 | ns.forEach((sym, fun) => replEnv.set(sym, fun)); | |
13 | ||
14 | replEnv.set(new MalSymbol('eval'), | |
15 | new MalBuiltin((List<MalType> args) => EVAL(args.single, replEnv))); | |
16 | ||
17 | replEnv.set(new MalSymbol('*ARGV*'), | |
18 | new MalList(argv.map((s) => new MalString(s)).toList())); | |
19 | ||
20 | replEnv.set(new MalSymbol('*host-language*'), new MalString('dart')); | |
21 | ||
22 | rep('(def! not (fn* (a) (if a false true)))'); | |
23 | rep("(def! load-file " | |
e6d41de4 | 24 | " (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))"); |
3934e3f8 HT |
25 | rep("(defmacro! cond " |
26 | " (fn* (& xs) (if (> (count xs) 0) " | |
27 | " (list 'if (first xs) " | |
28 | " (if (> (count xs) 1) " | |
29 | " (nth xs 1) " | |
30 | " (throw \"odd number of forms to cond\")) " | |
31 | " (cons 'cond (rest (rest xs)))))))"); | |
3934e3f8 HT |
32 | } |
33 | ||
34 | /// Returns `true` if [ast] is a macro call. | |
35 | /// | |
36 | /// This checks that [ast] is a list whose first element is a symbol that refers | |
37 | /// to a function in the current [env] that is a macro. | |
38 | bool isMacroCall(MalType ast, Env env) { | |
39 | if (ast is MalList) { | |
40 | if (ast.isNotEmpty && ast.first is MalSymbol) { | |
41 | try { | |
42 | var value = env.get(ast.first); | |
43 | if (value is MalCallable) { | |
44 | return value.isMacro; | |
45 | } | |
46 | } on NotFoundException { | |
47 | return false; | |
48 | } | |
49 | } | |
50 | } | |
51 | return false; | |
52 | } | |
53 | ||
54 | MalType macroexpand(MalType ast, Env env) { | |
55 | while (isMacroCall(ast, env)) { | |
56 | var macroSymbol = (ast as MalList).first; | |
57 | var macro = env.get(macroSymbol) as MalCallable; | |
58 | ast = macro((ast as MalList).sublist(1)); | |
59 | } | |
60 | return ast; | |
61 | } | |
62 | ||
fbfe6784 NB |
63 | bool starts_with(MalType ast, String sym) { |
64 | return ast is MalList && ast.length == 2 && ast.first == new MalSymbol(sym); | |
65 | } | |
66 | ||
67 | MalType qq_loop(List<MalType> xs) { | |
68 | var acc = new MalList([]); | |
69 | for (var i=xs.length-1; 0<=i; i-=1) { | |
70 | if (starts_with(xs[i], "splice-unquote")) { | |
71 | acc = new MalList([new MalSymbol("concat"), (xs[i] as MalList)[1], acc]); | |
72 | } else { | |
73 | acc = new MalList([new MalSymbol("cons"), quasiquote(xs[i]), acc]); | |
74 | } | |
3934e3f8 | 75 | } |
fbfe6784 NB |
76 | return acc; |
77 | } | |
3934e3f8 | 78 | |
fbfe6784 NB |
79 | MalType quasiquote(MalType ast) { |
80 | if (starts_with(ast, "unquote")) { | |
81 | return (ast as MalList).elements[1]; | |
82 | } else if (ast is MalList) { | |
83 | return qq_loop(ast.elements); | |
84 | } else if (ast is MalVector) { | |
85 | return new MalList([new MalSymbol("vec"), qq_loop(ast.elements)]); | |
86 | } else if (ast is MalSymbol || ast is MalHashMap) { | |
3934e3f8 HT |
87 | return new MalList([new MalSymbol("quote"), ast]); |
88 | } else { | |
fbfe6784 | 89 | return ast; |
3934e3f8 HT |
90 | } |
91 | } | |
92 | ||
93 | MalType READ(String x) => reader.read_str(x); | |
94 | ||
95 | MalType eval_ast(MalType ast, Env env) { | |
96 | if (ast is MalSymbol) { | |
97 | return env.get(ast); | |
98 | } else if (ast is MalList) { | |
99 | return new MalList(ast.elements.map((x) => EVAL(x, env)).toList()); | |
100 | } else if (ast is MalVector) { | |
101 | return new MalVector(ast.elements.map((x) => EVAL(x, env)).toList()); | |
102 | } else if (ast is MalHashMap) { | |
103 | var newMap = new Map<MalType, MalType>.from(ast.value); | |
104 | for (var key in newMap.keys) { | |
105 | newMap[key] = EVAL(newMap[key], env); | |
106 | } | |
107 | return new MalHashMap(newMap); | |
108 | } else { | |
109 | return ast; | |
110 | } | |
111 | } | |
112 | ||
113 | MalType EVAL(MalType ast, Env env) { | |
114 | while (true) { | |
115 | if (ast is! MalList) { | |
116 | return eval_ast(ast, env); | |
117 | } else { | |
118 | if ((ast as MalList).elements.isEmpty) { | |
119 | return ast; | |
120 | } else { | |
121 | ast = macroexpand(ast, env); | |
122 | if (ast is! MalList) return eval_ast(ast, env); | |
123 | if ((ast as MalList).isEmpty) return ast; | |
124 | ||
125 | var list = ast as MalList; | |
126 | ||
127 | if (list.elements.first is MalSymbol) { | |
128 | var symbol = list.elements.first as MalSymbol; | |
129 | var args = list.elements.sublist(1); | |
130 | if (symbol.value == "def!") { | |
131 | MalSymbol key = args.first; | |
132 | MalType value = EVAL(args[1], env); | |
133 | env.set(key, value); | |
134 | return value; | |
135 | } else if (symbol.value == "defmacro!") { | |
136 | MalSymbol key = args.first; | |
137 | MalClosure macro = EVAL(args[1], env) as MalClosure; | |
138 | macro.isMacro = true; | |
139 | env.set(key, macro); | |
140 | return macro; | |
141 | } else if (symbol.value == "let*") { | |
142 | // TODO(het): If elements.length is not even, give helpful error | |
143 | Iterable<List<MalType>> pairs(List<MalType> elements) sync* { | |
144 | for (var i = 0; i < elements.length; i += 2) { | |
145 | yield [elements[i], elements[i + 1]]; | |
146 | } | |
147 | } | |
148 | ||
149 | var newEnv = new Env(env); | |
150 | MalIterable bindings = args.first; | |
151 | for (var pair in pairs(bindings.elements)) { | |
152 | MalSymbol key = pair[0]; | |
153 | MalType value = EVAL(pair[1], newEnv); | |
154 | newEnv.set(key, value); | |
155 | } | |
156 | ast = args[1]; | |
157 | env = newEnv; | |
158 | continue; | |
159 | } else if (symbol.value == "do") { | |
160 | eval_ast(new MalList(args.sublist(0, args.length - 1)), env); | |
161 | ast = args.last; | |
162 | continue; | |
163 | } else if (symbol.value == "if") { | |
164 | var condition = EVAL(args[0], env); | |
165 | if (condition is MalNil || | |
166 | condition is MalBool && condition.value == false) { | |
167 | // False side of branch | |
168 | if (args.length < 3) { | |
169 | return new MalNil(); | |
170 | } | |
171 | ast = args[2]; | |
172 | continue; | |
173 | } else { | |
174 | // True side of branch | |
175 | ast = args[1]; | |
176 | continue; | |
177 | } | |
178 | } else if (symbol.value == "fn*") { | |
179 | var params = (args[0] as MalIterable) | |
180 | .elements | |
181 | .map((e) => e as MalSymbol) | |
182 | .toList(); | |
183 | return new MalClosure( | |
184 | params, | |
185 | args[1], | |
186 | env, | |
187 | (List<MalType> funcArgs) => | |
188 | EVAL(args[1], new Env(env, params, funcArgs))); | |
189 | } else if (symbol.value == "quote") { | |
190 | return args.single; | |
fbfe6784 NB |
191 | } else if (symbol.value == "quasiquoteexpand") { |
192 | return quasiquote(args.first); | |
3934e3f8 HT |
193 | } else if (symbol.value == "quasiquote") { |
194 | ast = quasiquote(args.first); | |
195 | continue; | |
196 | } else if (symbol.value == 'macroexpand') { | |
77fd710c | 197 | return macroexpand(args.first, env); |
3934e3f8 HT |
198 | } else if (symbol.value == 'try*') { |
199 | var body = args.first; | |
dd7a4f55 JM |
200 | if (args.length < 2) { |
201 | ast = EVAL(body, env); | |
202 | continue; | |
203 | } | |
3934e3f8 HT |
204 | var catchClause = args[1] as MalList; |
205 | try { | |
206 | ast = EVAL(body, env); | |
207 | } catch (e) { | |
208 | assert((catchClause.first as MalSymbol).value == 'catch*'); | |
209 | var exceptionSymbol = catchClause[1] as MalSymbol; | |
210 | var catchBody = catchClause[2]; | |
211 | MalType exceptionValue; | |
212 | if (e is MalException) { | |
213 | exceptionValue = e.value; | |
6c4cc8ad JM |
214 | } else if (e is reader.ParseException) { |
215 | exceptionValue = new MalString(e.message); | |
3934e3f8 HT |
216 | } else { |
217 | exceptionValue = new MalString(e.toString()); | |
218 | } | |
219 | var newEnv = new Env(env, [exceptionSymbol], [exceptionValue]); | |
220 | ast = EVAL(catchBody, newEnv); | |
221 | } | |
222 | continue; | |
223 | } | |
224 | } | |
225 | var newAst = eval_ast(ast, env) as MalList; | |
226 | var f = newAst.elements.first; | |
227 | var args = newAst.elements.sublist(1); | |
228 | if (f is MalBuiltin) { | |
229 | return f.call(args); | |
230 | } else if (f is MalClosure) { | |
231 | ast = f.ast; | |
232 | env = new Env(f.env, f.params, args); | |
233 | continue; | |
234 | } else { | |
235 | throw 'bad!'; | |
236 | } | |
237 | } | |
238 | } | |
239 | } | |
240 | } | |
241 | ||
242 | String PRINT(MalType x) => printer.pr_str(x); | |
243 | ||
244 | String rep(String x) { | |
dd7a4f55 | 245 | return PRINT(EVAL(READ(x), replEnv)); |
3934e3f8 HT |
246 | } |
247 | ||
248 | const prompt = 'user> '; | |
249 | main(List<String> args) { | |
250 | setupEnv(args.isEmpty ? const <String>[] : args.sublist(1)); | |
251 | if (args.isNotEmpty) { | |
252 | rep("(load-file \"${args.first}\")"); | |
253 | return; | |
254 | } | |
255 | rep("(println (str \"Mal [\" *host-language* \"]\"))"); | |
256 | while (true) { | |
257 | stdout.write(prompt); | |
258 | var input = stdin.readLineSync(); | |
259 | if (input == null) return; | |
260 | var output; | |
261 | try { | |
262 | output = rep(input); | |
dd7a4f55 JM |
263 | } on reader.ParseException catch (e) { |
264 | stdout.writeln("Error: '${e.message}'"); | |
265 | continue; | |
266 | } on NotFoundException catch (e) { | |
267 | stdout.writeln("Error: '${e.value}' not found"); | |
268 | continue; | |
269 | } on MalException catch (e) { | |
270 | stdout.writeln("Error: ${printer.pr_str(e.value)}"); | |
271 | continue; | |
3934e3f8 HT |
272 | } on reader.NoInputException { |
273 | continue; | |
274 | } | |
275 | stdout.writeln(output); | |
276 | } | |
277 | } |