-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
409 lines (380 loc) · 13.5 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/*
Given a series of tokens,
// (+ 2 3) -> well-formed individual statement
// + 2 3 -> list of tokens
// list 1 2 "sid" (list 1 2) -> list of tokens
process it from left to right and return a array of analyzed tokens
// supported tokens
// 1. number
// 2. string
// 3. symbol --> anything that is not number of string or a bracket
// whenever a ( is encountered, tokenize the list of tokens inside the bracket in a separate object
*/
function tokenizer(seriesOfTokens) {
if (seriesOfTokens.length === 0) {
return [];
}
// The first token in the list is a well-formed individual statement
if (seriesOfTokens[0] === "(") {
// find the closing bracket
var openBrackets = 0;
var closingBracketIndex = -1;
for (var i = 0; i < seriesOfTokens.length; i++) {
if (seriesOfTokens[i] === "(") {
openBrackets++;
} else if (seriesOfTokens[i] === ")") {
openBrackets--;
if (openBrackets === 0) {
closingBracketIndex = i;
break;
}
}
}
// list of tokens inside this bracket
var tokensOfthisBracket = seriesOfTokens
.substring(1, closingBracketIndex)
.trim();
// rest of the tokens;
// remember seriesOfTokens might not be a well-formed individual statement.
// so there can be more tokens left to process.
// example if the original statement sent to tokenizer is (list 1 2) 3 4
// then we are going to tokenize (list 1 2) and 3 4 separately
var restOfTheTokens = seriesOfTokens
.substring(closingBracketIndex + 1)
.trim();
return [
{
children: [...tokenizer(tokensOfthisBracket)],
},
...tokenizer(restOfTheTokens),
];
}
// The first element in the list is the tokens is a string.
// strip the string and tokenize the rest of the tokens
if (seriesOfTokens[0] === '"') {
return [
{
type: "string",
stringContent: seriesOfTokens.substring(
1,
seriesOfTokens.indexOf('"', 1)
),
},
...tokenizer(
seriesOfTokens
.substring(seriesOfTokens.indexOf('"', 1) + 1)
.trim()
),
];
}
// Not a string. Not a (). What else could it be? analyze first word
var firstSpaceIndex = seriesOfTokens.indexOf(" ");
if (firstSpaceIndex === -1) {
firstSpaceIndex = seriesOfTokens.length;
}
var firstWord = seriesOfTokens.substring(0, firstSpaceIndex);
var restOfTheTokens = seriesOfTokens.substring(firstSpaceIndex + 1).trim();
if (!isNaN(firstWord)) {
// it is a number
return [
{
type: "number",
number: Number(firstWord),
},
...tokenizer(restOfTheTokens),
];
}
// it is a symbol. return it as it is. tokenizer is not smart enough to analyze what it is.
// it just says it is something to analyze
return [
{
type: "symbol",
symbol: firstWord,
},
...tokenizer(restOfTheTokens),
];
}
/*
Given lisp code,
1. Parse it into well-formed individual statements
2. Use tokenizer to tokenize each statement
*/
function parser(lisp) {
// let's remove comments
// comment starts with ; and can appear in any line.
lisp = lisp
.split("\n")
.map((x) => x.split(";")[0])
.join("\n");
// let's convert lisp into well-formed individual statements
// (+ 2 3) -> well-formed individual statement
// (list 2 3) -> another well-formed individual statement
// simple brute-force algorithm to parse the lisp code character by character and splitting based on
// open and closing brackets
var statements = [];
var openBrackets = 0;
var currentStatement = "";
for (var i = 0; i < lisp.length; i++) {
var token = lisp[i];
currentStatement += token;
if (token === "(") {
openBrackets++;
}
if (token === ")") {
openBrackets--;
if (openBrackets === 0) {
statements.push(currentStatement);
currentStatement = "";
}
}
}
if (openBrackets !== 0) {
throw new Error("Unbalanced brackets");
}
if (currentStatement.length > 0) {
statements.push(currentStatement);
}
// now we have individual well formed statements to work on
// use tokenizer to tokenize each statement
var tokenizedStatements = statements
.filter((x) => x)
.map((x) => x.trim())
.filter((x) => x)
.map((statement) => {
var tokens = tokenizer(statement);
var tokenizedStatement = {
children: tokens,
}; // OR tokens[0]
// interpreter works on whole formed individual statements that are represented by objects
// tokenizer returns a list of tokens which themselves are objects
// so we can either send tokens[0] as the statement to interpreter
// OR
// form a wrapped object with children as tokens and send it to interpreter
// we are doing the latter because it is fun
return {
statement,
tokenizedStatement,
};
});
return tokenizedStatements;
}
/*
Given a tokenized statement, interpret/ evaluate it and return the result
env is the environment in which the statement is interpreted
*/
function interpret(statement, env) {
var type = statement.type;
// If we are dealing with atomic step
// i.e, 1 2 3 "sid" list square etc.
// return it's value from environment
if (type === "number") {
return Number(statement.number);
} else if (type === "string") {
return statement.stringContent;
} else if (type === "symbol") {
return env[statement.symbol];
}
// If it is not atomic, then it must have children
var children = statement.children;
// First children gives us the type of the statement to interpret
var firstItem = children[0];
var rest = children.slice(1);
var type = firstItem.type;
if (type === "symbol") {
// If it is a symbol, then it is a special form or a function call
var symbol = firstItem.symbol;
// "rest" then becomes the arguments to the special form or function call
// Special forms that we support
if (symbol === "list") {
return [...rest.map((x) => interpret(x, env))];
} else if (symbol === "first") {
return [...interpret(rest[0], env)][0];
} else if (symbol === "rest") {
return [...interpret(rest[0], env)].slice(1);
} else if (symbol === "last") {
return [...interpret(rest[0], env)].slice(-1)[0];
} else if (symbol === "cons") {
return [interpret(rest[0], env), ...interpret(rest[1], env)];
} else if (symbol === "append") {
return rest
.map((x) => interpret(x, env))
.reduce((a, b) => a.concat(b), []);
} else if (symbol === "length") {
return [...interpret(rest[0], env)].length;
} else if (symbol === "+") {
return [...rest.map((x) => interpret(x, env))].reduce(
(a, b) => a + b,
0
);
} else if (symbol === "-") {
const nums = [...rest.map((x) => interpret(x, env))];
if (nums.length === 0) {
// (-) -> 0
return 0;
} else if (nums.length === 1) {
// (- 2) => -2
return -1 * nums[0];
}
// (- a b c) => a - b - c
return nums[0] - nums.slice(1).reduce((a, b) => a + b, 0);
} else if (symbol === "*") {
return [...rest.map((x) => interpret(x, env))].reduce(
(a, b) => a * b,
1
);
} else if (symbol === "/") {
return [...rest.map((x) => interpret(x, env))].reduce(
(a, b) => a / b,
0
);
} else if (symbol === "%") {
return [...rest.map((x) => interpret(x, env))].reduce(
(a, b) => a % b,
0
);
} else if (symbol === ">") {
return interpret(rest[0], env) > interpret(rest[1], env);
} else if (symbol === "<") {
return interpret(rest[0], env) < interpret(rest[1], env);
} else if (symbol === ">=") {
return interpret(rest[0], env) >= interpret(rest[1], env);
} else if (symbol === "<=") {
return interpret(rest[0], env) <= interpret(rest[1], env);
} else if (symbol === "eql") {
return interpret(rest[0], env) === interpret(rest[1], env);
} else if (symbol === "or") {
return rest.map((x) => interpret(x, env)).some((x) => x);
} else if (symbol === "and") {
return rest.map((x) => interpret(x, env)).every((x) => x);
} else if (symbol === "let") {
const firstRest = rest[0];
const restRest = rest.slice(1);
const newEnv = { ...env };
for (var i = 0; i < firstRest.children.length; i++) {
const child = firstRest.children[i];
const symbol = child.children[0].symbol;
const value = interpret(child.children[1], env);
newEnv[symbol] = value;
}
return interpret(restRest[restRest.length - 1], newEnv);
} else if (symbol === "set") {
const firstRest = rest[0];
const restRest = rest.slice(1);
const symbol = firstRest.symbol;
const value = interpret(restRest[0], env);
env[symbol] = value;
return value;
} else if (symbol === "define") {
const firstRest = rest[0];
const restRest = rest.slice(1);
const fnName = firstRest.children[0].symbol;
const fnArgs = firstRest.children.slice(1).map((x) => x.symbol);
const fnBody = restRest[0];
env[fnName] = {
fnArgs,
fnBody,
};
return fnName;
} else if (symbol === "if") {
const firstRest = rest[0];
const secondRest = rest[1];
const thirdRest = rest[2];
const condition = interpret(firstRest, env);
if (condition) {
return interpret(secondRest, env);
} else if (thirdRest) {
return interpret(thirdRest, env);
} else {
return false;
}
} else {
// check if this is a function call
if (env[symbol] && env[symbol].fnArgs) {
const fnArgs = env[symbol].fnArgs;
const fnBody = env[symbol].fnBody;
const newEnv = { ...env };
for (var i = 0; i < fnArgs.length; i++) {
newEnv[fnArgs[i]] = interpret(rest[i], newEnv);
}
return interpret(fnBody, newEnv);
} else {
return env[symbol];
}
}
} else {
return interpret(firstItem, env);
}
}
// Given a list of parsed statements, interpret them and return the result
// env is the environment in which the statements are interpreted
// for debugging purposes, we can pass debug=true to get the environment at each step
function interpreter(parsedStatements, env, debug) {
return parsedStatements.map((s) => {
const eval = interpret(s.tokenizedStatement, env);
return {
...s,
eval,
...(debug
? {
env: JSON.parse(JSON.stringify(env)),
}
: {}),
};
});
}
var lisp = `
(+ 2 3)
(list 2 3)
(list 1 (+ 2 3) 4 "sid")
(first (list 1 2 3))
(rest (list 1 2 3))
(cons 2 (list 1 2 3))
(length (list 1 2))
(let ((x 10) (y 20))
(+ x y))
(define (square x) (* x x))
(square 2)
(set x (list 1 2 3))
(set x 40)
(square x)
(define (sum-of-squares x y)
(+ (square x) (square y)))
(sum-of-squares 3 4)
(> 2 4)
(if (> 2 4) "two is > 4" "lesser")
(define (! x) (if (< x 1) 1 (* x (! (- x 1)))))
(! 5)
(define (fibo x) (if (or (eql x 0) (eql x 1)) 1 (+ (fibo (- x 1)) (fibo (- x 2)))))
(fibo 10)
(define
(reverse lst)
(if (eql (length lst) 0)
(list)
(append (reverse (rest lst)) (list (first lst)))
)
)
(reverse (list 1 2 3 4 5))
`;
var env = {};
const parsedOutput = parser(lisp);
console.log(parsedOutput);
const interpretedOutput = interpreter(parser(lisp), env, (debug = true));
for (const x of interpretedOutput) {
console.log(`${x.statement} => ${x.eval}`, x.env);
}
/*
practice problems (doesn't require new syntax)):
- Find kth element of a list
- Find if a list is a palindrome
- Find index of an element in list
- Find minimum of an element in list
- Find if a list is sorted
- Find if a list has an element in it
- Intersection of lists
practice problems (add loop syntax):
- Find if a number is a prime
practice ux problems:
- Given a proper list statement, pretty print it with proper indentation (raise a PR to the repo)
practice library problems:
- Add forms to enable lisp-js to access DOM (example: (document.getElementById "id"))
*/