-
Notifications
You must be signed in to change notification settings - Fork 0
/
four-function-calc.ts
305 lines (271 loc) · 8.03 KB
/
four-function-calc.ts
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
import {
Positioned,
RopeIter,
RopeLeaf,
compilePattern,
lexerFromString,
makeParseletBuilder,
matchToken,
parserFromLexer,
position,
token,
} from "../dist";
// NOTE: See this article to understand the design of this parser more in-depth:
// https://engineering.desmos.com/articles/pratt-parser/
// token type
type TokenSuccess<T extends string> = {
type: "Success";
match: T;
};
// number literal
type NumberNode = {
type: "Number";
number: number;
};
// binary operation between two expressions
type BinaryOpNode = {
type: "BinaryOp";
left: PositionedNode;
right: PositionedNode;
op: "+" | "-" | "*" | "/";
};
// all expressions
type ExpressionNode = NumberNode | BinaryOpNode;
// node that is created if the parser encounters an error during parsing
type ErrorNode = {
type: "Error";
reason: string;
};
// both types of parse state that will influence the parser
type InitParseState = {
bindingPower: number;
};
type ConsequentParseState = InitParseState & {
left: PositionedNode;
};
// expressions or error nodes
type Node = ExpressionNode | ErrorNode;
// type to represent a node with position and skipToken information attached
// (i.e. where it is in the input string and what tokens around it were skipped)
export type PositionedNode = Node &
Positioned<{ type: "Success"; match: string }>;
// This type is never instantiated directly. Instead, it acts as a "bundle"
// of generics to supply to various functions so you don't have to supply
// all of them individually.
type ParserTypes = {
MyOutputType: ExpressionNode;
State: InitParseState;
Error: ErrorNode;
ErrorMessage: string;
SkipToken: { type: "Success"; match: string };
};
// Helper function for creating tokens from a list of possible alternative chars
// while retaining type safety for the specific chars matched
function charToken<T extends string>(alts: readonly T[]) {
return token<TokenSuccess<T>, string>((lexer) => {
const tkn = lexer.next(1);
if ((alts as readonly string[]).includes(tkn)) {
return {
type: "Success",
match: tkn as T,
};
} else {
throw lexer.err(
`Expected one of ${alts.map((a) => `'${a}'`).join(", ")}`
);
}
});
}
const op = charToken(["+", "-", "*", "/"]);
const openParen = charToken(["("]);
const closeParen = charToken([")"]);
const whitespace = charToken([" ", "\n"]);
// Tokens can also be made with a custom regex-like language
// Builtin JS regex can't be used because it can't do a partial match.
// this lamnguage is compiled to a bytecode format
const num = matchToken(
compilePattern("[0-9]+"),
(str) => {
return {
type: "Success",
match: str,
};
},
"Expected a number."
);
// map of binding powers (operator precedence)
const bindingPowers = {
"+": 1,
"-": 1,
"*": 2,
"/": 2,
};
// hash function for initialParseState
const hashIPS = (state: InitParseState) => {
const hash = state.bindingPower;
return hash;
};
// "is equal?" function for initialParseState
const eqIPS = (a: InitParseState, b: InitParseState) => {
return a.bindingPower === b.bindingPower;
};
// Create a "parselet builder", a function for easily making parselets.
// This mainly exists to avoid having to supply tons of generics.
// Note that the "parselet" function has three parameters.
// The first is a handler that will actually build the parseNode
// The second is a hash function for the parse state. This is necessary for
// incremental parsing. Its only real purpose is to allow hashing objects
// by value, so it doesn't need to be any good.
// The third is an equality function for the parse state, which is also
// needed for caching.
const parselet = makeParseletBuilder<ParserTypes>();
// Matches operators and other such things that "combine" parsenodes together.
const consequentExpressionParselet = parselet<
ConsequentParseState,
BinaryOpNode
>(
(p) => {
// get the next token, expecting it to be an operator
const first = p.lex(op);
// get its precedence
const nextBindingPower = bindingPowers[first.match];
// operator precedence of next binary op is too low,
// so exit early
if (nextBindingPower <= p.state.bindingPower) p.err("");
// it's a valid binary operator
return {
type: "BinaryOp",
op: first.match,
left: p.state.left,
right: p.parse(expressionParselet, {
bindingPower: nextBindingPower,
}),
};
},
(state) => {
return state.bindingPower * 100000000 + state.left[position].id * 100000;
},
(a, b) => {
return a.bindingPower === b.bindingPower && a.left === b.left;
}
);
// Parse initial expressions (numbers or parenthesized expressions)
const initExpressionParselet = parselet<InitParseState, ExpressionNode>(
(p) => {
// lexFirstMatch tries to lex all the listed tokens in order
// The first match is chosen.
const first = p.lexFirstMatch(
[openParen, num],
"Expected '(' or a number."
);
// parenthesized
if (first.match === "(") {
const result = p.parse(expressionParselet, {
bindingPower: 0,
}) as ExpressionNode | ErrorNode;
p.lex(closeParen);
return result;
// non-parenthesized
} else {
return {
type: "Number",
number: Number(first.match),
};
}
},
hashIPS,
eqIPS
);
// This function actually parses a complete expression.
const expressionParselet = parselet<InitParseState, ExpressionNode>(
(p) => {
let left = p.parse(initExpressionParselet, p.state);
// eslint-disable-next-line no-constant-condition
while (true) {
const snapshot = p.getParserSnapshot();
const nextParseNode = p.parse(consequentExpressionParselet, {
bindingPower: p.state.bindingPower,
left,
});
if (nextParseNode.type === "Error") {
p.setParserSnapshot(snapshot);
break;
}
left = nextParseNode;
}
return left;
},
hashIPS,
eqIPS
);
// Makes a four-function calculator parser from the ground up.
export function ffcParser(src: RopeIter) {
// Make the lexer
const lexer = lexerFromString(src);
// make the parser
return parserFromLexer(
lexer,
{ bindingPower: 0 }, // Initial parse state
expressionParselet, // Initial parselet
[whitespace], // List of tokens to be skipped
{
// Converts an error message to a full error node
makeErrorMessage(msg) {
return { type: "Error", reason: msg } satisfies ErrorNode;
},
// Converts a lexer error to a full error node
makeLexerError(pos) {
return {
type: "Error",
reason: `Lexer error at position ${pos}`,
} satisfies ErrorNode;
},
// Converts an arbitrary error with an unknown type (since you can throw anything)
// into an error node.
makeUnhandledError(err) {
return {
type: "Error",
reason: `Unhandled internal error: ${JSON.stringify(err)} `,
} satisfies ErrorNode;
},
// Detects if a node is an error node.
isErr(err): err is ErrorNode {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (err as any).type === "Error";
},
}
);
}
// Parse a string into a four-function calculator syntax tree
export function parseFFC(src: string) {
// Create the parser
const parser = ffcParser(new RopeLeaf(src).iter(0));
// Run the parser. The callback here is just used for cache invalidation.
const parserOutput = parser.exec(() => true);
return parserOutput;
}
// Actually evaluate the syntax tree as a mathematical expression.
export function evalFFC(tree: PositionedNode): number {
switch (tree.type) {
case "Number":
return tree.number;
case "BinaryOp":
{
const l = evalFFC(tree.left);
const r = evalFFC(tree.right);
switch (tree.op) {
case "+":
return l + r;
case "-":
return l - r;
case "*":
return l * r;
case "/":
return l / r;
}
}
break;
case "Error":
return NaN;
}
}