Recursive Descent and Predictive Parsing
Learn how to implement a top-down parser by hand using recursive descent, and how predictive parsing eliminates backtracking by using lookahead — making parsing efficient and deterministic.
Recursive Descent Parsing
Recursive descent is the most intuitive way to write a top-down parser. The idea is simple:
Write one parsing function for each non-terminal in the grammar.
Each function reads the input and either successfully recognises the non-terminal or fails. Functions call each other recursively, mirroring the structure of the grammar — hence "recursive descent."
From Grammar to Functions
Grammar:
Corresponding parsing functions:
The function match(t) checks that the current input token is t, then advances to the next token. If the token does not match, it reports an error.
Why "Recursive"?
The functions call each other recursively to mirror the grammar's recursive structure. For example, if ( E ) S has S nested inside itself — the call to parseS() inside parseS() captures this.
Recursive Descent with Backtracking
When a grammar has multiple productions for the same non-terminal that could all match, a recursive descent parser must try each one in order and backtrack if one fails.
Example — Backtracking Recursive Descent
Grammar:
Input: c a d
Backtracking requires the parser to remember where it was so it can restore the input position on failure.
The Problem with Backtracking
Backtracking works but is slow. For grammars with many alternatives and deep nesting, the parser may try exponentially many combinations before finding the right one (or giving up with an error).
This is why predictive parsers — which never backtrack — are preferred.
Predictive Recursive Descent (No Backtracking)
A predictive recursive descent parser is like a regular recursive descent parser, but it never backtracks. It always knows which production to use by looking at the current input token (lookahead).
This is possible when the grammar is:
- Free of left recursion
- Left-factored (no two productions for the same non-terminal share a prefix)
Example — Predictive Recursive Descent
Grammar (already left-factored, no left recursion):
Parsing functions:
Tracing the Parser on "id + id * id"
No backtracking. Every function call makes exactly the right choice on the first try.
Building an AST During Parsing
A recursive descent parser can be easily extended to build an AST (Abstract Syntax Tree) while it parses. Each parsing function returns a tree node.
Example — Returning AST Nodes
AST for "a + b * c":
The AST correctly groups multiplication before addition because the grammar enforces it through the T and F levels.
Predictive vs. Recursive Descent — Key Differences
Both are top-down parsing strategies. Here is what sets them apart:
| Feature | Recursive Descent (with backtracking) | Predictive Recursive Descent |
|---|---|---|
| Grammar class | Any CFG (if written carefully) | LL(1) grammars only |
| Backtracking | Yes — may try multiple alternatives | No — always picks the right alternative |
| Lookahead | May need arbitrary lookahead | Exactly one token |
| Speed | Potentially exponential | Linear O(n) |
| Implementation | Simple to write | Requires left recursion elimination + left factoring |
| Error recovery | Easy to customise | Slightly harder |
When to Use Each
-
Recursive descent with backtracking: For quick parser prototypes, small languages, or when the grammar is awkward to transform. Not suitable for performance-critical parsers.
-
Predictive recursive descent: For production parsers written by hand. Most compiler front-ends (like GCC, Clang, and Go's parser) use this approach. Fast, readable, and easy to extend with good error messages.