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