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