In computer science, an operator-precedence parser is a bottom-up parser that interprets an operator-precedence grammar. For example, most calculators use operator-precedence parsers to convert from the human-readable infix notation relying on order of operations to a format that is optimized for evaluation such as Reverse Polish notation (RPN).

Edsger Dijkstra's shunting yard algorithm is commonly used to implement operator-precedence parsers.

Relationship to other parsers

An operator-precedence parser is a simple shift-reduce parser that is capable of parsing a subset of LR(1) grammars. More precisely, the operator-precedence parser can parse all LR(1) grammars where two consecutive nonterminals and epsilon never appear in the right-hand side of any rule.

Operator-precedence parsers are not used often in practice; however they do have some properties that make them useful within a larger design. First, they are simple enough to write by hand, which is not generally the case with more sophisticated right shift-reduce parsers. Second, they can be written to consult an operator table at run time, which makes them suitable for languages that can add to or change their operators while parsing. (An example is Haskell, which allows user-defined infix operators with custom associativity and precedence; consequently, an operator-precedence parser must be run on the program after parsing of all referenced modules.)

Raku sandwiches an operator-precedence parser between two recursive descent parsers in order to achieve a balance of speed and dynamism. GCC's C and C++ parsers, which are hand-coded recursive descent parsers, are both sped up by an operator-precedence parser that can quickly examine arithmetic expressions. Operator-precedence parsers are also embedded within compiler-compiler-generated parsers to noticeably speed up the recursive descent approach to expression parsing.

Precedence climbing method

The precedence climbing method is a compact, efficient, and flexible algorithm for parsing expressions that was first described by Martin Richards and Colin Whitby-Strevens.

An infix-notation expression grammar in EBNF format will usually look like this:

<syntaxhighlight lang="abnf">

expression ::= equality-expression

equality-expression ::= additive-expression ( ( '==' | '!=' ) additive-expression ) *

additive-expression ::= multiplicative-expression ( ( '+' | '-' ) multiplicative-expression ) *

multiplicative-expression ::= primary ( ( '*' | '/' ) primary ) *

primary ::= '(' expression ')' | NUMBER | VARIABLE | '-' primary

</syntaxhighlight>

With many levels of precedence, implementing this grammar with a predictive recursive-descent parser can become inefficient. Parsing a number, for example, can require five function calls: one for each non-terminal in the grammar until reaching primary.

An operator-precedence parser can do the same more efficiently. based on recursive descent. Though it predates precedence climbing, it can be viewed as a generalization of precedence climbing.

Pratt designed the parser originally to implement the CGOL programming language, and it was treated in much more depth in a Masters Thesis under his supervision.

Tutorials and implementations:

  • Douglas Crockford based the JavaScript parser in JSLint on Pratt parsing.
  • Comparison between Python implementations of precedence climbing and Pratt parsing: "Pratt Parsing and Precedence Climbing Are the Same Algorithm" (2016) by Andy Chu
  • Tutorial using Rust: "Simple but Powerful Pratt Parsing" (2020) by Aleksey Kladov
  • Tutorial using Rust: "The Pratt Parsing Technique" (2024) by William Rågstad
  • Tutorial using Python: "Simple Top-Down Parsing in Python" (2008) by Fredrik Lundh
  • Tutorial using Java: "Pratt Parsers: Expression Parsing Made Easy" (2011) by Bob Nystrom, author of Crafting Interpreters
  • Implementation in C#: "Gratt: A Generic Vaughn Pratt's top-down operator precedence parser for .NET Standard" (a generic version inspired by the Java implementation presented by Bob Nystrom in "Pratt Parsers: Expression Parsing Made Easy")

Alternative methods

There are other ways to apply operator precedence rules. One is to build a tree of the original expression and then apply tree rewrite rules to it.

Such trees do not necessarily need to be implemented using data structures conventionally used for trees. Instead, tokens can be stored in flat structures, such as tables, by simultaneously building a priority list which states what elements to process in which order.

Full parenthesization

Another approach is to first fully parenthesize the expression, inserting a number of parentheses around each operator, such that they lead to the correct precedence even when parsed with a linear, left-to-right parser. This algorithm was used in the early FORTRAN I compiler:

<blockquote>

The Fortran I compiler would expand each operator with a sequence of parentheses. In a simplified form of the algorithm, it would

  • replace <code>+</code> and <code>–</code> with <code>))+((</code> and <code>))-((</code>, respectively;
  • replace <code>*</code> and <code>/</code> with <code>)*(</code> and <code>)/(</code>, respectively;
  • add <code>((</code> at the beginning of each expression and after each left parenthesis in the original expression; and
  • add <code>))</code> at the end of the expression and before each right parenthesis in the original expression.

Although not obvious, the algorithm was correct, and, in the words of Knuth, “The resulting formula is properly parenthesized, believe it or not.”

</blockquote>

Example code of a simple C application that handles parenthesisation of basic math operators (<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>^</code>, <code>(</code> and <code>)</code><!-- parentheses -->):

<syntaxhighlight lang="c">

  1. include <stdio.h>
  2. include <string.h>

// The command-line argument boundary is our lexer.

int main(int argc, char *argv[]) {

int i;

printf("((((");

for (i=1; i!=argc; i++) {

// strlen(argv[i]) == 2

if (argv[i] && !argv[i][1]) {

switch (*argv[i]) {

case '(': printf("(((("); continue;

case ')': printf("))))"); continue;

case '^': printf(")^("); continue;

case '*': printf("))*(("); continue;

case '/': printf("))/(("); continue;

case '+':

// unary check: either first or had an operator expecting secondary argument

if (i == 1 || strchr("(^*/+-", *argv[i-1]))

printf("+");

else

printf(")))+(((");

continue;

case '-':

if (i == 1 || strchr("(^*/+-", *argv[i-1]))

printf("-");

else

printf(")))-(((");

continue;

}

}

printf("%s", argv[i]);

}

printf("))))\n");

return 0;

}

</syntaxhighlight>

First, you need to compile your program. Assuming your program is written in C and the source code is in a file named program.c, you would use the following command:

<pre>gcc program.c -o program</pre>

The above command tells gcc to compile program.c and create an executable named program.

Command to run the program with parameters, For example; a * b + c ^ d / e

<pre>./program a '*' b + c '^' d / e</pre>

it produces

<pre>((((a))*((b)))+(((c)^(d))/((e))))</pre>

as output on the console.

A limitation to this strategy is that unary operators must all have higher precedence than infix operators. The "negative" operator in the above code has a higher precedence than exponentiation. Running the program with this input

<pre>- a ^ 2</pre>

produces this output

<pre>((((-a)^(2))))</pre>

which is probably not what is intended.

References

  • Example C++ code by Keith Clarke for parsing infix expressions using the precedence climbing method
  • Parser for expression with infix notation using precedence lists