79a10a6e |
1 | import { Env } from "./env"; |
2 | |
6071876f |
3 | export type MalType = MalList | MalNumber | MalString | MalNil | MalBoolean | MalSymbol | MalKeyword | MalVector | MalHashMap | MalFunction | MalAtom; |
f406f88b |
4 | |
5bb7479d |
5 | export const enum Node { |
6 | List = 1, |
7 | Number, |
8 | String, |
6071876f |
9 | Nil, |
5bb7479d |
10 | Boolean, |
11 | Symbol, |
12 | Keyword, |
13 | Vector, |
14 | HashMap, |
15 | Function, |
16 | Atom, |
17 | } |
18 | |
dfe70453 |
19 | export function equals(a: MalType, b: MalType, strict?: boolean): boolean { |
92bf0530 |
20 | if (strict && a.type !== b.type) { |
dfe70453 |
21 | return false; |
dfe70453 |
22 | } |
23 | |
6071876f |
24 | if (a.type === Node.Nil && b.type === Node.Nil) { |
dfe70453 |
25 | return true; |
26 | } |
92bf0530 |
27 | if (isSeq(a) && isSeq(b)) { |
dfe70453 |
28 | return listEquals(a.list, b.list); |
29 | } |
5bb7479d |
30 | if (a.type === Node.HashMap && b.type === Node.HashMap) { |
10f8aa84 |
31 | if (a.keywordMap.size !== b.keywordMap.size) { |
32 | return false; |
33 | } |
34 | if (Object.keys(a.stringMap).length !== Object.keys(b.stringMap).length) { |
35 | return false; |
36 | } |
37 | for (const [aK, aV] of a.entries()) { |
5bb7479d |
38 | if (aK.type !== Node.String && aK.type !== Node.Keyword) { |
10f8aa84 |
39 | throw new Error(`unexpected symbol: ${aK.type}, expected: string or keyword`); |
40 | } |
41 | const bV = b.get(aK); |
6071876f |
42 | if (aV.type === Node.Nil && bV.type === Node.Nil) { |
10f8aa84 |
43 | continue; |
44 | } |
45 | if (!equals(aV, bV)) { |
46 | return false; |
47 | } |
48 | } |
49 | |
50 | return true; |
51 | } |
dfe70453 |
52 | if ( |
5bb7479d |
53 | (a.type === Node.Number && b.type === Node.Number) |
54 | || (a.type === Node.String && b.type === Node.String) |
55 | || (a.type === Node.Boolean && b.type === Node.Boolean) |
56 | || (a.type === Node.Symbol && b.type === Node.Symbol) |
57 | || (a.type === Node.Keyword && b.type === Node.Keyword) |
dfe70453 |
58 | ) { |
59 | return a.v === b.v; |
60 | } |
61 | |
62 | return false; |
63 | |
64 | function listEquals(a: MalType[], b: MalType[]): boolean { |
65 | if (a.length !== b.length) { |
66 | return false; |
67 | } |
68 | for (let i = 0; i < a.length; i++) { |
69 | if (!equals(a[i], b[i], strict)) { |
70 | return false; |
71 | } |
72 | } |
73 | return true; |
74 | } |
75 | } |
76 | |
92bf0530 |
77 | export function isSeq(ast: MalType): ast is MalList | MalVector { |
5bb7479d |
78 | return ast.type === Node.List || ast.type === Node.Vector; |
92bf0530 |
79 | } |
80 | |
10f8aa84 |
81 | export function isAST(v: MalType): v is MalType { |
82 | return !!v.type; |
83 | } |
84 | |
f406f88b |
85 | export class MalList { |
5bb7479d |
86 | type: Node.List = Node.List; |
bbddf168 |
87 | meta?: MalType; |
f406f88b |
88 | |
89 | constructor(public list: MalType[]) { |
90 | } |
bbddf168 |
91 | |
92 | withMeta(meta: MalType) { |
93 | const v = new MalList(this.list); |
94 | v.meta = meta; |
95 | return v; |
96 | } |
f406f88b |
97 | } |
98 | |
99 | export class MalNumber { |
5bb7479d |
100 | type: Node.Number = Node.Number; |
bbddf168 |
101 | meta?: MalType; |
102 | |
f406f88b |
103 | constructor(public v: number) { |
104 | } |
bbddf168 |
105 | |
106 | withMeta(meta: MalType) { |
107 | const v = new MalNumber(this.v); |
108 | v.meta = meta; |
109 | return v; |
110 | } |
f406f88b |
111 | } |
112 | |
113 | export class MalString { |
5bb7479d |
114 | type: Node.String = Node.String; |
bbddf168 |
115 | meta?: MalType; |
116 | |
f406f88b |
117 | constructor(public v: string) { |
118 | } |
bbddf168 |
119 | |
120 | withMeta(meta: MalType) { |
121 | const v = new MalString(this.v); |
122 | v.meta = meta; |
123 | return v; |
124 | } |
f406f88b |
125 | } |
126 | |
6071876f |
127 | export class MalNil { |
dfe70453 |
128 | |
6071876f |
129 | private static _instance?: MalNil; |
bbddf168 |
130 | |
6071876f |
131 | static get instance(): MalNil { |
132 | if (this._instance) { |
133 | return this._instance; |
134 | } |
135 | this._instance = new MalNil(); |
136 | return this._instance; |
137 | } |
138 | |
139 | type: Node.Nil = Node.Nil; |
bbddf168 |
140 | meta?: MalType; |
dfe70453 |
141 | |
142 | private constructor() { } |
bbddf168 |
143 | |
6071876f |
144 | withMeta(_meta: MalType): MalNil { |
bbddf168 |
145 | throw new Error(`not supported`); |
146 | } |
f406f88b |
147 | } |
148 | |
149 | export class MalBoolean { |
5bb7479d |
150 | type: Node.Boolean = Node.Boolean; |
bbddf168 |
151 | meta?: MalType; |
152 | |
f406f88b |
153 | constructor(public v: boolean) { |
154 | } |
bbddf168 |
155 | |
156 | withMeta(meta: MalType) { |
157 | const v = new MalBoolean(this.v); |
158 | v.meta = meta; |
159 | return v; |
160 | } |
f406f88b |
161 | } |
162 | |
163 | export class MalSymbol { |
164 | static map = new Map<symbol, MalSymbol>(); |
165 | |
166 | static get(name: string): MalSymbol { |
167 | const sym = Symbol.for(name); |
168 | let token = this.map.get(sym); |
169 | if (token) { |
170 | return token; |
171 | } |
172 | token = new MalSymbol(name); |
173 | this.map.set(sym, token); |
174 | return token; |
175 | } |
176 | |
5bb7479d |
177 | type: Node.Symbol = Node.Symbol; |
bbddf168 |
178 | meta?: MalType; |
f406f88b |
179 | |
180 | private constructor(public v: string) { |
181 | } |
bbddf168 |
182 | |
183 | withMeta(_meta: MalType): MalSymbol { |
184 | throw new Error(`not supported`); |
185 | } |
f406f88b |
186 | } |
187 | |
188 | export class MalKeyword { |
10f8aa84 |
189 | static map = new Map<symbol, MalKeyword>(); |
190 | |
191 | static get(name: string): MalKeyword { |
192 | const sym = Symbol.for(name); |
193 | let token = this.map.get(sym); |
194 | if (token) { |
195 | return token; |
196 | } |
197 | token = new MalKeyword(name); |
198 | this.map.set(sym, token); |
199 | return token; |
200 | } |
201 | |
5bb7479d |
202 | type: Node.Keyword = Node.Keyword; |
bbddf168 |
203 | meta?: MalType; |
10f8aa84 |
204 | |
205 | private constructor(public v: string) { |
f406f88b |
206 | } |
bbddf168 |
207 | |
208 | withMeta(_meta: MalType): MalKeyword { |
209 | throw new Error(`not supported`); |
210 | } |
f406f88b |
211 | } |
212 | |
213 | export class MalVector { |
5bb7479d |
214 | type: Node.Vector = Node.Vector; |
bbddf168 |
215 | meta?: MalType; |
216 | |
f406f88b |
217 | constructor(public list: MalType[]) { |
218 | } |
bbddf168 |
219 | |
220 | withMeta(meta: MalType) { |
221 | const v = new MalVector(this.list); |
222 | v.meta = meta; |
223 | return v; |
224 | } |
f406f88b |
225 | } |
226 | |
227 | export class MalHashMap { |
5bb7479d |
228 | type: Node.HashMap = Node.HashMap; |
10f8aa84 |
229 | stringMap: { [key: string]: MalType } = {}; |
230 | keywordMap = new Map<MalType, MalType>(); |
bbddf168 |
231 | meta?: MalType; |
10f8aa84 |
232 | |
f406f88b |
233 | constructor(list: MalType[]) { |
234 | while (list.length !== 0) { |
76e06b96 |
235 | const key = list.shift()!; |
f406f88b |
236 | const value = list.shift(); |
237 | if (value == null) { |
238 | throw new Error("unexpected hash length"); |
239 | } |
5bb7479d |
240 | if (key.type === Node.Keyword) { |
10f8aa84 |
241 | this.keywordMap.set(key, value); |
5bb7479d |
242 | } else if (key.type === Node.String) { |
10f8aa84 |
243 | this.stringMap[key.v] = value; |
244 | } else { |
245 | throw new Error(`unexpected key symbol: ${key.type}, expected: keyword or string`); |
246 | } |
247 | } |
248 | } |
249 | |
bbddf168 |
250 | withMeta(meta: MalType) { |
251 | const v = this.assoc([]); |
252 | v.meta = meta; |
253 | return v; |
254 | } |
255 | |
10f8aa84 |
256 | has(key: MalKeyword | MalString) { |
5bb7479d |
257 | if (key.type === Node.Keyword) { |
10f8aa84 |
258 | return !!this.keywordMap.get(key); |
259 | } |
260 | return !!this.stringMap[key.v]; |
261 | } |
262 | |
263 | get(key: MalKeyword | MalString) { |
5bb7479d |
264 | if (key.type === Node.Keyword) { |
6071876f |
265 | return this.keywordMap.get(key) || MalNil.instance; |
10f8aa84 |
266 | } |
6071876f |
267 | return this.stringMap[key.v] || MalNil.instance; |
10f8aa84 |
268 | } |
269 | |
270 | entries(): [MalType, MalType][] { |
271 | const list: [MalType, MalType][] = []; |
272 | |
eb7a2bbd |
273 | this.keywordMap.forEach((v, k) => { |
10f8aa84 |
274 | list.push([k, v]); |
eb7a2bbd |
275 | }); |
10f8aa84 |
276 | Object.keys(this.stringMap).forEach(v => list.push([new MalString(v), this.stringMap[v]])); |
277 | |
278 | return list; |
279 | } |
280 | |
281 | keys(): MalType[] { |
282 | const list: MalType[] = []; |
eb7a2bbd |
283 | this.keywordMap.forEach((_v, k) => { |
284 | list.push(k); |
285 | }); |
10f8aa84 |
286 | Object.keys(this.stringMap).forEach(v => list.push(new MalString(v))); |
287 | return list; |
288 | } |
289 | |
290 | vals(): MalType[] { |
291 | const list: MalType[] = []; |
eb7a2bbd |
292 | this.keywordMap.forEach(v => { |
10f8aa84 |
293 | list.push(v); |
eb7a2bbd |
294 | }); |
10f8aa84 |
295 | Object.keys(this.stringMap).forEach(v => list.push(this.stringMap[v])); |
296 | return list; |
297 | } |
298 | |
299 | assoc(args: MalType[]): MalHashMap { |
300 | const list: MalType[] = []; |
301 | this.keywordMap.forEach((value, key) => { |
302 | list.push(key); |
303 | list.push(value); |
304 | }); |
305 | Object.keys(this.stringMap).forEach(keyStr => { |
306 | list.push(new MalString(keyStr)); |
307 | list.push(this.stringMap[keyStr]); |
308 | }); |
309 | |
310 | return new MalHashMap(list.concat(args)); |
311 | } |
312 | |
313 | dissoc(args: MalType[]): MalHashMap { |
314 | const newHashMap = this.assoc([]); |
315 | |
316 | args.forEach(arg => { |
5bb7479d |
317 | if (arg.type === Node.String) { |
10f8aa84 |
318 | delete newHashMap.stringMap[arg.v]; |
5bb7479d |
319 | } else if (arg.type === Node.Keyword) { |
10f8aa84 |
320 | newHashMap.keywordMap.delete(arg); |
321 | } else { |
322 | throw new Error(`unexpected symbol: ${arg.type}, expected: keyword or string`); |
323 | } |
324 | }); |
325 | return newHashMap; |
f406f88b |
326 | } |
327 | } |
83aaf848 |
328 | |
79a10a6e |
329 | type MalF = (...args: (MalType | undefined)[]) => MalType; |
330 | |
83aaf848 |
331 | export class MalFunction { |
9c92462f |
332 | static fromLisp(evalMal: (ast: MalType, env: Env) => MalType, env: Env, params: MalSymbol[], bodyAst: MalType): MalFunction { |
79a10a6e |
333 | const f = new MalFunction(); |
9c92462f |
334 | f.func = (...args) => evalMal(bodyAst, new Env(env, params, checkUndefined(args))); |
79a10a6e |
335 | f.env = env; |
336 | f.params = params; |
337 | f.ast = bodyAst; |
e21a85a3 |
338 | f.isMacro = false; |
79a10a6e |
339 | |
340 | return f; |
341 | |
555f7fc7 |
342 | function checkUndefined(args: (MalType | undefined)[]): MalType[] { |
79a10a6e |
343 | return args.map(arg => { |
344 | if (!arg) { |
345 | throw new Error(`undefined argument`); |
346 | } |
79a10a6e |
347 | return arg; |
348 | }); |
349 | } |
350 | } |
351 | |
352 | static fromBootstrap(func: MalF): MalFunction { |
353 | const f = new MalFunction(); |
354 | f.func = func; |
e21a85a3 |
355 | f.isMacro = false; |
356 | |
79a10a6e |
357 | return f; |
358 | } |
359 | |
5bb7479d |
360 | type: Node.Function = Node.Function; |
79a10a6e |
361 | func: MalF; |
362 | ast: MalType; |
363 | env: Env; |
364 | params: MalSymbol[]; |
e21a85a3 |
365 | isMacro: boolean; |
bbddf168 |
366 | meta?: MalType; |
79a10a6e |
367 | |
368 | private constructor() { } |
369 | |
bbddf168 |
370 | withMeta(meta: MalType) { |
371 | const f = new MalFunction(); |
372 | f.func = this.func; |
373 | f.ast = this.ast; |
374 | f.env = this.env; |
375 | f.params = this.params; |
376 | f.isMacro = this.isMacro; |
377 | f.meta = meta; |
378 | |
379 | return f; |
380 | } |
381 | |
79a10a6e |
382 | newEnv(args: MalType[]) { |
383 | return new Env(this.env, this.params, args); |
83aaf848 |
384 | } |
385 | } |
555f7fc7 |
386 | |
387 | export class MalAtom { |
5bb7479d |
388 | type: Node.Atom = Node.Atom; |
bbddf168 |
389 | meta?: MalType; |
555f7fc7 |
390 | |
391 | constructor(public v: MalType) { |
392 | } |
bbddf168 |
393 | |
394 | withMeta(meta: MalType) { |
395 | const v = new MalAtom(this.v); |
396 | v.meta = meta; |
397 | return v; |
398 | } |
555f7fc7 |
399 | } |