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