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