-
Notifications
You must be signed in to change notification settings - Fork 77
/
Copy pathtiny.js
146 lines (110 loc) · 3.51 KB
/
tiny.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
/*
# Lexer
The lexer is responsible for turning the input string into
a list of tokens. Usually a token looks the following way:
```javascript
{
"type": Symbol("Operator"),
"value: "-"
}
```
In our case we're keeping everything simplified and store
only the token's value. We can infer the type based on
regular expressions defined below.
In short, `lex` will turn the following expression:
```
mul 3 sub 2 sum 1 3 4
```
To the following array:
```
["mul", "3", "sub", "2", "sum", "1", "3", "4"]
```
*/
const lex = str => str.split(' ').map(s => s.trim()).filter(s => s.length);
/*
# Parser
The parser is responsible for turning the list of tokens
into an AST or Abstract Syntax Tree. In the example below
we use recursive descent parsing to produce the AST
from the input token array.
Visually, the parsing is a process which turns the array:
```javascript
const tokens = ["sub", "2", "sum", "1", "3", "4"];
```
to the following tree:
```
sub
/ \
2 sum
/|\
1 3 4
```
The parser uses the following grammar to parse the input token array:
```
num := 0-9+
op := sum | sub | div | mul
expr := num | op expr+
```
This translated to plain English, means:
- `num` can be any sequence of the numbers between 0 and 9.
- `op` can be any of `sum`, `sub`, `div`, `mul`.
- `expr` can be either a number (i.e. `num`) or an operation followed by one or more `expr`s.
Notice that `expr` has a recursive declaration.
*/
const Op = Symbol('op');
const Num = Symbol('num');
const parse = tokens => {
let c = 0;
const peek = () => tokens[c];
const consume = () => tokens[c++];
const parseNum = () => ({ val: parseInt(consume()), type: Num });
const parseOp = () => {
const node = { val: consume(), type: Op, expr: [] };
while (peek()) node.expr.push(parseExpr());
return node;
};
const parseExpr = () => /\d/.test(peek()) ? parseNum() : parseOp();
return parseExpr();
};
/*
# Evaluator
Finally, this is our evaluator. In it we simply visit each node
from the tree with pre-order traversal and either:
- Return the corresponding value, in case the node is of type number.
- Perform the corresponding arithmetic operation, in case of an operation node.
*/
const evaluate = ast => {
const opAcMap = {
sum: args => args.reduce((a, b) => a + b, 0),
sub: args => args.reduce((a, b) => a - b),
div: args => args.reduce((a, b) => a / b),
mul: args => args.reduce((a, b) => a * b, 1)
};
if (ast.type === Num) return ast.val;
return opAcMap[ast.val](ast.expr.map(evaluate));
};
/*
# Code generator
Alternatively, instead of interpreting the AST, we can translate
it to another language. Here's how we can do that with JavaScript.
*/
const compile = ast => {
const opMap = { sum: '+', mul: '*', sub: '-', div: '/' };
const compileNum = ast => ast.val;
const compileOp = ast => `(${ast.expr.map(compile).join(' ' + opMap[ast.val] + ' ')})`;
const compile = ast => ast.type === Num ? compileNum(ast) : compileOp(ast);
return compile(ast);
};
const program = 'mul 3 sub 2 sum 1 3 4';
/*
# Interpreter
In order to interpret the input stream we feed the parser with the input
from the lexer and the evaluator with the output of the parser.
*/
console.log(evaluate(parse(lex(program))));
/*
# Compiler
In order to compile the expression to JavaScript, the only change we need to make
is to update the outermost `evaluate` invocation to `compile`.
*/
console.log(compile(parse(lex(program))));