Calculator
Tools:
- Pratt Parser
- Document Object Model (DOM)
Project:
, I resolved to teach myself how to program. This calculator — which I built with plain HTML, CSS, and JavaScript — was the culmination of careful study. , I rewrote this application to better reflect my improving skillset.
Both calculators are built on top of expression parsers. My first parser was rudimentary — easily misled by unexpected character combinations and misplaced whitespace. Anticipating malformed user input is complex, and I had shifted much of this complexity over to a tangle of regular expressions, which were cryptic and hard to debug. These too would fail.
My new calculator's core is a top-down operator precedence parser modeled on ideas pioneered by the computer scientist Vaughan R. Pratt. A full-featured parser, it handles infix and prefix notation, left and right associativity, nested and mismatched parenthetical groupings, multiple levels of precedence and all types of whitespace. Errors are made explicit through robust error handling and a formatted user interface.
A version of this calculator application, complete with extensive inline documentation, can be found on Github.
Notes:
Top-down operator precedence parsing, as imagined by Vaughan Pratt, combines lexical semantics with functions. Each lexeme is assigned a function — its semantic code. To parse a string of lexemes is to execute the semantic code of each lexeme in turn from left to right.
There are two types of semantic code:
- nud: a lexeme function without a left expression — null denotation.
- led: a lexeme function with a left expression — left denotation.
The engine of Pratt's technique,
parse_expression
drives the parser, calling the
semantic code of each lexeme in turn from left to right. For
every level of precedence — dictated by position and binding
power — there is a call to parse_expression
either
through the nud or led parser
of the associated lexeme. The resolution of
parse_expression
is to return either an evaluated
expression or an error token.
function parse_expression(rbp) {
const token = state.next();
const [nud, ok] = table.get_parser(token.type, "nud");
if (!ok) {
token.message += constants.NOT_PREFIX;
return [null, token];
}
let [x, error] = nud(token);
if (error !== null) {
return [null, error];
}
while (rbp < table.get_binding(state.peek())) {
const token = state.next();
const [led, ok] = table.get_parser(token.type, "led");
if (!ok) {
token.message += constants.NOT_INFIX;
return [null, token];
}
[x, error] = led(x, token);
if (error !== null) {
return [null, error];
}
}
return [x, null];
}