Merge pull request #378 from asarhaddon/test-macro-not-changing-function
[jackhill/mal.git] / ts / types.ts
CommitLineData
79a10a6e 1import { Env } from "./env";
2
6071876f 3export type MalType = MalList | MalNumber | MalString | MalNil | MalBoolean | MalSymbol | MalKeyword | MalVector | MalHashMap | MalFunction | MalAtom;
f406f88b 4
5bb7479d 5export 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 19export 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 77export function isSeq(ast: MalType): ast is MalList | MalVector {
5bb7479d 78 return ast.type === Node.List || ast.type === Node.Vector;
92bf0530 79}
80
10f8aa84 81export function isAST(v: MalType): v is MalType {
82 return !!v.type;
83}
84
f406f88b 85export 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
99export 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
113export 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 127export 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
149export 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
163export 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
188export 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
213export 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
227export 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 329type MalF = (...args: (MalType | undefined)[]) => MalType;
330
83aaf848 331export 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
387export 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}