Change quasiquote algorithm
[jackhill/mal.git] / impls / dart / stepA_mal.dart
CommitLineData
3934e3f8
HT
1import 'dart:io';
2
3import 'core.dart';
4import 'env.dart';
5import 'printer.dart' as printer;
6import 'reader.dart' as reader;
7import 'types.dart';
8
9final Env replEnv = new Env();
10
11void 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.
38bool 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
54MalType 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
63bool starts_with(MalType ast, String sym) {
64 return ast is MalList && ast.length == 2 && ast.first == new MalSymbol(sym);
65}
66
67MalType 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
79MalType 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
93MalType READ(String x) => reader.read_str(x);
94
95MalType 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
113MalType 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
242String PRINT(MalType x) => printer.pr_str(x);
243
244String rep(String x) {
dd7a4f55 245 return PRINT(EVAL(READ(x), replEnv));
3934e3f8
HT
246}
247
248const prompt = 'user> ';
249main(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}