Pratt Parser
Pratt Parser
Many of the presentations of the Pratt parser essentially fake a lexer with one-
token peek lookahead from a lexer without built-in lookahead. This is done by first
calling next to get a token which acts as a peek. Then, when getting another token,
this peek token is saved as the current token and next is called to get the next peek
token. When the Pratt parser algorithm is written assuming that the lexer provides
both a next and a peek function it requires slightly less code, and, more importantly,
is easier to follow.
In this writeup we tend to refer to the parsing of expressions, but it could also be a
full program being parsed. Every expression is assumed to be made up
of subexpressions. Subexpressions are defined by the grammar of the language
being parsed, including operator precedences for infix operators. Subexpressions
can be made up of sub-subexpressions, and so forth. A Pratt parser recursively
parses these expressions, subexpressions, etc., top-down.
An expression tree is a tree for an expression such that each function or operator
corresponds to an interior node of the tree, and the arguments/parameters of the
function are the ordered child nodes. The leaves of an expression tree are the literal
tokens, i.e., the tokens which act as single-node subtrees in the final expression
tree. The other tokens appear in the tree as interior nodes (e.g., tokens for infix
operators like *). The syntactical construct corresponding to a literal token will be
called a token literal.
A derivation tree or parse tree, on the other hand, corresponds to a grammar. The
internal nodes all correspond to production rules for nonterminal symbols in a
grammar. In a derivation tree every non-ignored token returned by the lexer ends up
as a leaf node in the tree.
A Pratt parser can produce any of the above kinds of trees, depending on how the
handler functions are defined, but naturally produces expression trees. In common
usage the parsing of text tends to refer to any kind of formal decomposition into a
predefined structure, particularly into a tree structure. This may include parsing text
to a derivation tree, an expression tree, or an AST. It may also include the use of
various ad hoc methods for breaking the text down into components. Informally,
expression trees are often referred to as parse trees.
The parser’s parse function is assumed to return a syntax tree for the expression it
parses. In general, many parsers do not return a syntax tree but instead evaluate,
interpret, or otherwise process the expressions as they go along. We assume that
any such evaluation or interpretation is applied at a later stage, based on the
returned syntax tree. Pratt parsers in general can easily interpret or evaluate
languages on-the-fly, but for simplicity the builtin methods of the Typped package
always form a syntax tree by default. (Note that if type-checking is disabled then
arbitrary Pratt Parsers can still be defined using Typped.)
+
2
*
5
8
Here an indented column under a token represent its children/arguments. Note that
the leaves of the tree are always token literals corresponding to literal tokens such
as 2 and 5, which form their own subtrees.
In producing the parse tree above it has been assumed that the usual operator
precedence rules in mathematics hold: * has higher precedence than +. In most
computer languages this is implemented by assigning a fixed precedence value to
each operator, and the Pratt parser works the same way.
Every kind of token has a fixed, non-changing precedence value associated with it.
This is called its token precedence. The default token precedence value is zero,
which is also the minimum possible token precedence value. Infix
operators must have a token precedence > 0, as we will see. When it is clear in the
context the token precedence will simply be called the precedence.
Note
In Pratt’s terminology a token’s precedence is called its left binding power or lbp.
2.4. Subexpressions
By definition, every subtree in an expression tree represents a subexpression. The
token precedence values determine the resulting tree structure of subexpressions for
infix operators. In the simple example expression 2 + 5 * 8 the top-level
expression is represented by the full tree, with root at the operator +. Each token
literal also defines a (trivial) subexpression. The subtree rooted at operator * defines
a non-trivial subexpression which corresponds to the string 5 * 8 in the full
expression.
It was mentioned earler that in Pratt parsing each token can have one or
more handler functions defined for it. The handler function for when the token is the
first token in a subexpression is called the head handler function. The handler
function for when the token is not the first token in a subexpression is called the tail
handler function.
Note
In Pratt’s terminology the head handler function is called the null denotation or nud.
The tail handler function is called the left denotation or led. The left denotation is
passed the previously-evaluated left part as an argument, while the null denotation
receives no such argument. Pratt’s terminology can seem confusing since the left
denotation is actually called for tokens in the rightmost part of a subexpression (the
returned value becomes the new, evaluated left part).
return processed_left
The first thing that recursive_parse does is get a token from the lexer as the
current token. This token will always be the head token of the subexpression, i.e.,
the first token of the subexpression (the full expression is also considered a
subexpression). By definition recursive_parse is only called when that condition
holds.
The next thing that recursive_parse does is call the head handler function for that
head token. It must have a head handler defined for it or else an exception is raised.
The head handler for a token is a function that defines the meaning or denotation of
the token when it is the first token in a subexpression. It returns a partial parse tree.
The result is stored as processed_left, which holds the processed leftmost part of
the current subexpression (currently just the result of the head handler evaluation on
the first token).
The recursive_parse function now needs to evaluate the rest of its current
subexpression, calling the tail handler in a while loop for each remaining token in the
tail of the subexpression. The results each time will be combined with the
current processed_left to produce the new processed_left, which will
eventually be returned at the end as the final result. The only tricky part is
how recursive_parse determines when it has reached the end of its
subexpression and should return its result. This is where precedences come into
play.
Each call of recursive_parse is passed both a lexer and a numerical value called
the subexpression precedence. The subexpression precedence is just a number
that gives the precedence of the subexpression that this call of recursive_parse is
processing. This subexpression precedence value does not change within a
particular invocation of recursive_parse. The subexpression precedence is
compared to the fixed token precedence for individual tokens.
Note
In particular, the while loop continues consuming tokens and calling their tail handler
functions until the subexpression precedence subexp_prec is less than the
precedence of the upcoming token, given by lex.peek().prec(). You can think of
the loop ending when the power of the subexpression to bind to the right and get
another token (the subexpression’s precedence) is not strong enough to overcome
the power of the next token to bind to the left (the next token’s token precedence
value). The subexpression ends when that occurs. The while loop is exited
and processed_left is returned as the resulting subtree for the subexpression.
The initial call of recursive_parse from parse always starts with a subexpression
precedence of 0 for the full expression. Literal tokens and the end token always have
a token precedence of 0, and those are the only tokens with that precedence. So the
full expression always ends when the next token is the end token or the next token is
a literal token, and the latter is an error condition.
Generally, any token with only a head handler definition has a token precedence of 0
and any token with a tail handler definition has a precedence greater than 0. This
can be seen in the while loop of recursive_parse: Since tail handlers are only
called inside the while loop the precedence of a token with a tail must be greater
than 0, or else it will always fail the test and thus can never be called. A token with
only a head handler that does pass the test will not have a tail handler to call.
This table summarizes the correspondence between Pratt’s terminology and the
terminology that is used in this documentation and in the code:
The head handler will be made into a method of the subclass for the kind of token it
is associated with. So the arguments are self and a lexer instance lex:
All other head and tail handlers are also made into methods for the subtoken that
they are associated with (but see the note below).
Generally, head and tail handlers do two things while constructing the result value to
return: 1) they call recursive_parse to evaluate sub-subexpressions of their
subexpression, and 2) they possibly peek at and/or consume additional tokens from
the lexer. For example, this is the definition of the tail handler for the + operator:
The tail handler for the * operator is identical to the definition for + except that it is
associated with the subclass representing the token *. We will assume that the
precedence defined for + is 3, and that the precedence for * is 4.
return processed_left
The steps the Pratt parser takes in parsing this expression are described in the box
below.
The handler functions are as defined earlier. The parsing proceeds as follows:
First, the parse function is called, passed a lexer instance lex and the
expression text to be parsed. The parse function just initializes the
lexer with the text and then calls the recursive_parse on the full
expression to do the real work. The full expression is always
associated with a subexpression precedence of zero, so
the subexp_prec argument to recursive_parse is 0 on this initial
call.
o The recursive_parse function at the top level first
consumes a token from the lexer, which is the token for 2.
It then and calls the head handler associated with it.
The head handler for the token 2 returns the
token for 2 itself as the corresponding node
in the subtree, since literal tokens are their
own subtrees (leaves) of the final
expression tree.
o Back in the top level
of recursive_parse the processed_left variable is
set to the returned node, which is the token 2.
o The while loop in recursive_parse is now run to
handle the tail of the expression. It peeks ahead and sees
that the + operator has a higher token precedence than
the current subexpression precedence of 0, so the loop
executes. The loop code first consumes another token
from the lexer, which is the + token. It then calls the tail
handler associated with the + token, passing it the
current processed_left (which currently points to the
node 2) as the left argument.
The tail handler for + sets the left child of
the token/node for + to be the passed-in
subtree left (which is currently the
node 2). This sets the left operand for +. To
get the right operand the tail handler
for + then
calls recursive_parse recursively,
passing in the value of 3 (which is the
precedence value we assumed for
the + operator) as the subexpression
precedence argument subexp_prec. Note
how the operator’s precedence is passed to
the recursive_parse routine as the
subexpression precedence in the recursive
call; to get right-association instead of left-
association the operator precedence minus
one should instead be passed in.
This recursive call
of recursive_parse consum
es another token, the token
for 5, and calls the head
handler for that token.
The head
handler returns
the node for 5 as
the subtree,
since it is a
literal token.
The returned node/subtree
for 5 is set as the initial value
for processed_left at this
level of recursion.
The while loop now peeks
ahead and sees that the token
precedence of 4 for
the * operator is greater than
its own subexpression
precedence (subexp_prec at
this level equals 3), so the
loop executes. Inside the loop
the next token, *, is consumed
from the lexer. The tail handler
for that token is called, passed
the processed_left value at
this level of recursion as
its left argument (which
currently points to the node 5).
The tail handler
for * sets that
passed-in left
value to be the
left child of
the * node, so
the left
child/operand
of * is set to the
node for 5. It
then
calls recursive
_parse to get
the right
child/operand.
The * token’s
precedence
value of 4 is
passed
to recursive_p
arse as the
subexpression
precedence
argument subex
p_prec.
Thi
s
call
of
re
cu
rs
iv
e_
pa
rs
e fi
rst
co
ns
um
es
the
tok
en
8 fr
om
the
lex
er
an
d
call
s
the
he
ad
ha
ndl
er
for
it.
Th
e
he
ad
ha
ndl
er
for
8r
etu
rns
the
no
de
itse
lf.
Th
ep
ro
ce
ss
ed
_l
ef
tv
ari
abl
e
at
this
lev
el
of
rec
urs
ion
is
no
w
set
to
the
ret
urn
ed
no
de
8.
Th
e
whi
le
loo
p
pe
eks
ah
ea
d
an
d
se
es
the
en
d-
tok
en,
whi
ch
alw
ays
ha
s a
pre
ce
de
nc
e
of
0.
Sin
ce
tha
t is
les
s
tha
n
the
cur
ren
t
su
be
xpr
ess
ion
pre
ce
de
nc
e
of
4,
the
whi
le
loo
p
do
es
not
ex
ec
ute
.
Th
e
tok
en
8 is
ret
urn
ed.
The tail handler
for * now sets
the
node/token 8 as
the right child of
the * node. It
then returns
the * node.
The while loop at this level
of recursive_parse once
again peeks ahead but, upon
seeing the end-token, does
not execute. So the loop is
exited and the subtree
for * (which now has two
children, 5 and 8) is returned.
The tail handler for + now sets the returned
subtree (the subtree for *, with its children
already set) as the right subtree for
the + token/node. The + token is returned
as the root of the subtree.
o Back at the top level of recursive_parse the while loop
looks ahead and sees the end-token, so it does not
execute. The subtree for + is returned to
the parse routine.
The parse routine returns the result returned by
the recursive_parse call as its value. So it returns the node for +,
now with children representing the expression tree shown earlier, as
the final expression tree of token nodes.
Note that when recursive_parse is called recursively in the tail of an infix operator
it is called with a subexp_prec argument equal to the current node’s precedence.
That gives left-to-right precedence evaluation (left associative) for infix operators with
equal precedence values. To get right-to-left evaluation (right
associative), recursive_parse should instead be passed the current
precedence minus one as the value for subexp_prec. Interested readers can
consider the evaluation of 2 ^ 5 ^ 8 (similar to the box above) in the case where
for ^ is defined as left associative.
2.8. Summary
In this section we introduced some basic parsing terminology, including heads and
tails of subexpressions. The Pratt parser was then defined as a top-down, mutually-
recursive parsing algorithm. The routines parse and recursive_parse were
defined and discussed. Finally, head and tail handler functions were discussed and
an example parse was described in detail.
The Typped parser package generalizes this basic Pratt parser in a few ways. These
generalizations are discussed in later sections. A generalization allowing multiple,
dispatched head and tail handler functions for tokens, based on preconditions, is
described in the next section. Another generalization
modifies recursive_parse slightly to allow implicit juxtaposition operators between
tokens. Type-definition and type-checking routines are also added. Types are
checked inside head and tail handlers by calling a
function process_and_check_node on the subtrees before they are returned.
Operator overloading is also allowed, and is resolved during these checks.
2.9. References
Vaughan R. Pratt, “Top down operator precedence,” 1973. The original article, at the
ACM site (paywall).
Douglas Crockford, “Top Down Operator Precedence,” Feb. 21, 2007. Uses
JavaScript.
Bob Nystrom, “Pratt Parsers: Expression Parsing Made Easy,” Mar. 19, 2011. Uses
Java.