Crafting a parser, part 6: ambiguity, lookahead, and the lexer hack
Real grammars have corners where one token of lookahead isn't enough — or where the same token sequence has two legal readings. This post collects the classic ambiguities and the honest tricks parsers use to resolve them.
The dangling else
if a if b x else y — whose else is it? The grammar is genuinely ambiguous; the fix is
a rule, not more parsing: bind else to the nearest if. In recursive descent you get
this for free by greedily consuming else in the inner call.
Most ambiguities are resolved by a documented preference, not a cleverer grammar:
nearest-else, longest-match, max-munch. Write the rule down — it is part of the language
definition.
The less-than problem
In a < b, is < less-than, or the start of generics like Vec<T>? C++ famously needs
type information during parsing to decide — the original lexer hack. Modern languages
dodge it with syntax (Rust's turbofish) or with bounded lookahead that tries to scan a
type and backtracks.
12345678
Bounded backtracking
Pure LL(1) forbids backtracking; real parsers allow a little, behind a checkpoint and rewind. The discipline: backtrack only across a bounded, local region, never the whole input, or you reinvent exponential-time parsing.
Before reaching for backtracking, ask whether the language designer can remove the ambiguity. The turbofish exists precisely so the parser never has to guess.
Takeaways
- Most ambiguity is resolved by a written rule (nearest-else, max-munch).
- The less-than problem is real; syntax or bounded lookahead beats type-directed parsing.
- Allow backtracking only locally, behind explicit checkpoints.