Parsingimplementation

LL vs LR: Why Production Compilers Chose the Weaker One

LR accepts a strictly larger class of grammars. Clang, rustc, Roslyn, V8 and Go all hand-write recursive descent anyway. The reason is not ignorance or inertia — it is error messages, incremental reparse, and what an IDE needs.

The question

If LR is more powerful, why does almost every compiler I use hand-write a recursive-descent parser?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The same input and the same tree, reached from opposite ends. A top-down parser holds a stack of *expectations* — what it still intends to find — so at any moment it can name the construct it is inside. A bottom-up parser holds a stack of *findings* — what it has already recognised — so at any moment it knows what is possible and not what is intended. Both produce a syntax tree; the difference in what each *knows during the parse* is what decides everything downstream.

What this phase may assume or do

The formal containment is between grammars, and it runs one way: every LL(k) grammar is an LR(k) grammar, and there are LR(1) grammars — every left-recursive one, for a start — that are LL(k) for no k. At the language level the gap is real but narrower: the LR(1) languages are exactly the deterministic context-free languages, and there are deterministic languages with no LL(k) grammar at all. What that comparison is entitled to conclude is only which *grammars* a technique accepts. It is not entitled to conclude which technique to use, because acceptance is one requirement among six and the other five point the other way.

Key points

  • Every LL(k) grammar is an LR(k) grammar; the containment is strict, and LR(1) covers exactly the deterministic context-free languages.
  • Grammar power is one criterion out of six, and it is the only one on which LR wins.
  • Clang, rustc, Roslyn, V8, Go and modern GCC all hand-write recursive descent; GCC and Go migrated to it from generated LR and nobody migrated back.
  • Error messages: a top-down parser knows which construct it is inside because that is on the call stack; an LR state is a set of hypotheses and can only enumerate legal tokens.
  • IDE requirements — error tolerance and incremental reparse on every keystroke — are what settled the architecture, and they favour code over tables.
  • Every real language has constructs that are not decidable from the grammar alone; in hand-written code each is an if, in a generator each is machinery.
  • Languages designed after the LR era were deliberately kept near-LL so a hand-written frontend would be possible — the technique shaped the languages.
  • tree-sitter is the honest counter-example: a generated GLR parser that is excellent in editors, because forking buys tolerance without knowing which hypothesis is real.

The comparison as usually stated

On grammar power the answer is not close and not disputed. LL commits to a production before reading the evidence for it, so left recursion is fatal and common prefixes need factoring. LR commits only once an entire right-hand side is on the stack, so left recursion is natural and the accepted grammar class strictly contains LL's.

If grammar power were the deciding criterion, every compiler would be LR, and for about fifteen years — roughly 1975 to 1990 — most were. Yacc shipped with Unix, the theory was elegant, and generating a parser from a grammar file was obviously better engineering than writing one by hand. The subsequent migration *away* from generated LR, by essentially every major compiler team, is the thing this lesson exists to explain.

The two families across the dimensions that actually get weighedtypical
DimensionLL / recursive descentLR / generated
DirectionTop-down: start symbol toward tokensBottom-up: tokens toward start symbol
DerivationLeftmost, constructed forwardRightmost, discovered in reverse
Decision pointBefore the production is matchedAfter the whole right-hand side is on the stack
Grammar powerLL(k) — strictly smallerLR(k) ⊃ LL(k); LR(1) = the deterministic CFLs
Left recursionFatal; must be rewritten to iterationPreferred; keeps the stack shallow
ImplementationOrdinary functions you write and debugGenerated tables plus a build step
Error messagesArbitrary; the parser knows the construct"Expected one of…" derived from the state's legal tokens
Error recoveryPer-construct; sync sets chosen by handerror productions; recovery pops to the nearest one
Incremental reparsePractical — reparse the enclosing constructHard — parser state is a table position, not a construct
Context sensitivityAn ordinary if in the parserA lexer hack or a semantic predicate bolted on
Grammar as artifactNo machine-checkable grammar existsThe grammar file is the spec and is checked for ambiguity
Typical use todayProduction compilers, IDE frontendsQuery languages, DSLs, standards-tracking grammars, tooling

What the production compilers actually do

implementationThis is a snapshot of current primary implementations, and every item is version-specific. GCC 3.3 was Bison-generated where GCC 3.4 was not; Go 1.5 was yacc-generated where Go 1.6 was not; CPython replaced its LL(1) parser with a PEG parser in 3.9 (PEP 617), which is a third family and moved *toward* a generator, not away. Cite the version or do not cite it.

The list is short and consistent. Clang hand-writes recursive descent, with precedence climbing over a precedence table for binary expressions. rustc's rustc_parse is hand-written descent with an AssocOp precedence table. Roslyn (C# and VB) hand-writes descent, explicitly designed for incremental, error-tolerant reparse. V8 hand-writes descent, with a separate pre-parser that skips function bodies until they are called. Go's cmd/compile/internal/syntax is hand-written descent. GCC hand-writes descent for C and C++ — it did not always, which is the most informative data point of the set.

GCC's C++ frontend was Bison-generated until GCC 3.4 in 2004; the C frontend followed in 4.1. Go's parser was yacc-generated until Go 1.6. Both migrations went in the same direction, and both were justified in the same terms: better diagnostics and easier handling of constructs the grammar could not express cleanly. Nobody migrated the other way.

The counter-examples are real and they are informative too. Ruby's reference implementation still uses a Bison grammar, and Ruby's syntax is famously hard to reimplement. PHP uses Bison. Bash uses Yacc. PostgreSQL parses SQL with Bison, which is exactly the case the technique suits: a large grammar tracking a standard, where conformance matters more than message quality and the input is usually machine-generated anyway.

Reason one: the parser has to know what you were writing

A recursive-descent parser that fails is, at the moment of failure, several frames deep inside parseArgumentList called from parseCallExpression called from parseStatement. It can say: *"Expected ) after function arguments. Found { instead."* It can point at the opening ( as a secondary span. It can note that the argument list began on line 12. Every one of those facts is on the call stack.

An LR parser that fails is in state 143. State 143 is a set of items — a set of *hypotheses* — and the parser was tracking all of them simultaneously. It can enumerate the tokens the state permits, which is honest and reads as noise: syntax error, unexpected '{', expecting ')' or ',' or ';' or '->' or '::'. It cannot say "you were in an argument list", because it was in four things at once and had not chosen.

This is not a quality-of-implementation gap that a sufficiently determined team could close. It is a consequence of when each family commits. Committing early is what leaves you inside a named construct, and the same property that costs LL its grammar power is what buys it the message. [[diagnostic-quality]] develops what a good message actually contains; the point here is which architecture can produce one.

The same error, from a generated parser and a hand-written one
1# LALR, message derived from the state's legal-token set:
2foo.c:12:24: syntax error, unexpected '{', expecting ')' or ',' or ';'
3
4
5# Hand-written recursive descent, message written where the failure happened:
6error: expected `)` to close this function call
7 --> foo.c:12:24
8 |
912 | printf("%d %d", a, b {
10 | - ^ expected `)` here
11 | |
12 | the argument list opened here
13 |
14help: if you meant to start a block, close the call first

Both messages are accurate. The second is possible only because the parser knew, at the instant of failure, that it was inside an argument list that had opened at a specific column — which is a fact on its call stack and nowhere in an LR state.

Reason two: the IDE reparses on every keystroke

The requirement that reshaped frontend architecture is not compilation, it is editing. A language server must produce a usable tree for a file that is mid-keystroke and therefore syntactically broken, must do it in single-digit milliseconds, and must do it again on the next character.

That demands two things. Error tolerance: after an error, produce a tree with error nodes and keep going, because completion inside a function whose closing brace is not typed yet is the single most common IDE interaction there is. Incrementality: reparse the edited construct and reuse the rest, because reparsing ten thousand lines per keypress is not a budget anyone has.

Recursive descent gives both without heroics. The construct boundary is a function, so an edit inside a function body reparses that function; recovery is per-construct code you can tune; error nodes are a return value. A deterministic LR parser gives neither easily: its state is a position in a table with no correspondence to a source construct, so "reparse this function" has no natural expression, and after an error it has popped its stack to an error production and thrown away the context an IDE wanted.

The one bottom-up family that does serve editors is GLR, and it serves them by keeping every hypothesis alive rather than by knowing which is real — which is exactly what tree-sitter does, and it is why tree-sitter is a generated bottom-up parser that is nonetheless excellent in an editor. That is the honest exception to the argument, and it costs memory and the ambiguity-resolution question in exchange.

Reason three: real languages are not quite context-free

Every mainstream language has at least one construct the grammar cannot decide alone. In C and C++, T * x; is a declaration or a multiplication depending on whether T names a type — the parser must consult a symbol table. In C++, a < b > (c) may be a comparison chain or a template instantiation. In Rust, < after a path may open generic arguments, which is why the language has the turbofish ::<>. In JavaScript, / is division or the start of a regex depending on the preceding token.

In a hand-written parser each of these is an if. Ugly, localised, commented, testable. In a generated parser each is a semantic predicate, a lexer feedback channel, or a grammar contortion — machinery bolted onto a tool whose entire design premise was that the grammar decides.

It is worth being precise about the direction of causation. Languages designed after the LR era — Java, Go, Rust — were deliberately kept close to LL-parseable so that a hand-written frontend and good tooling would be possible. The parsing technique influenced the language design, not only the reverse.

When to use which

The decision is not "which is better" but "which of these six properties do I need". If the answer includes user-facing error messages or an editor integration, hand-write it. If the answer is conformance to a grammar somebody else revises, generate it.

Concretely: a programming language whose users will see its errors — hand-written recursive descent plus [[pratt-parsing]] for expressions. A query or configuration language embedded in a product, where the grammar is large and churns — a generator. A tool that must parse a language it does not own, tolerantly and incrementally — tree-sitter, whose GLR foundation is the right answer to that specific problem. A one-off data format — whichever you can finish today.

And one thing that is true regardless: understand the grammar concepts independently of the tool. FIRST sets, FOLLOW sets, ambiguity, precedence and associativity are properties of the language, and you reason about them whether you write an if or a %left — see [[parser-generators]].

Choosing, by what you need rather than by what is stronger
If you need…ChooseBecause
Error messages users will readHand-written descentThe construct in progress is on the call stack; an LR state cannot name it.
IDE / language-server frontendHand-written descent, or tree-sitterError tolerance and incremental reparse; GLR buys tolerance by keeping hypotheses alive.
A grammar that is the specificationLR generatorThe file is machine-checked for ambiguity and regenerated on each revision.
A large grammar that churns weeklyLR generatorA conflict report catches what a hand-edited parser would silently get wrong.
Left-recursive grammar you do not ownLR generatorNo rewrite needed; the transform to LL changes tree shape and is error-prone.
Context-sensitive constructsHand-written descentAn if beats a semantic predicate bolted onto a table-driven parser.
Proof the grammar is unambiguousLR generatorA conflict-free build is a mechanical proof; testing a hand-written parser is not.

How it works

The steps, in the order the compiler takes them.

  • Ask first whether the grammar is yours. If it is not and it is left-recursive, the LL route begins with a rewrite that changes tree shape.
  • Ask whether a human will read the parser's error messages. If yes, the parser must know the construct in progress, which requires the top-down shape.
  • Ask whether anything will reparse a broken file repeatedly — an editor, a linter running on save, a formatter. If yes, error tolerance and incrementality dominate, and a deterministic LR parser is the wrong shape.
  • Ask whether the grammar is itself a deliverable that must be checked for ambiguity. If yes, a generator gives a mechanical guarantee that hand-writing cannot.
  • Ask how often the grammar changes and who changes it. High churn by people who are not compiler engineers favours a declarative grammar file.
  • If both sets of requirements apply — a real language with a real IDE and a formal grammar — expect to maintain both: a hand-written frontend and a reference grammar kept in agreement by differential testing.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A team picks LR for the grammar power, ships, and then spends two years unable to improve the compiler's error messages because the architecture cannot express them.
  • A team picks hand-written descent, and the parser and the published language grammar drift until a program the specification accepts is rejected with no owner for the discrepancy.
  • An LL grammar is obtained by left-factoring an LR one, the tree shape changes, and an optional clause silently attaches to the wrong construct with no test failing.
  • A language server is retrofitted onto a generated LR frontend, and completion inside an unclosed block never works because there is no tree to complete against.
  • A context-sensitive construct is added to a language with a generated parser; the fix is a lexer feedback channel, and thereafter the lexer and parser cannot be tested independently.
  • A conflict count in a generated grammar is tolerated for years and a new construct's ambiguity is resolved by the generator's default, so the shipped language differs from the documented one in a way nobody chose.

When it helps

  • Choosing an architecture at the start of a language project, when the choice is nearly free and later is nearly impossible.
  • Explaining to a team why "LR is more powerful" is a true statement that does not answer the question being asked.
  • Evaluating a parsing library or generator: the six dimensions here are the checklist, and most tools are strong on two of them.
  • Reading an unfamiliar compiler: knowing which family it is in predicts the quality of its diagnostics and whether an IDE can share its frontend.

When it hurts

  • Treating the comparison as a ranking. Neither family dominates; they trade grammar acceptance against knowledge-during-parse, and which matters depends entirely on who reads the output.
  • Applying the compiler-industry conclusion to a data format. If the input is machine-generated and the errors are read by a log aggregator, every argument in this lesson evaporates and a generator is simply cheaper.

What it costs

Every one of these is paid by something.

  • Hand-written descent buys construct-aware diagnostics, per-construct recovery, incremental reparse and cheap context sensitivity — and pays with a grammar that no tool checks, a language whose syntax must be kept near-LL by design, and a rewrite of several functions every time precedence changes.
  • Generated LR buys a machine-checked, regenerable grammar, native left recursion and a mechanical ambiguity proof — and pays with diagnostics derived from state tables, recovery that discards whole constructs, no practical incrementality, and debugging that happens against generated code and a state number.
  • Maintaining both — a hand-written frontend plus a reference grammar — buys the guarantee and the messages, and pays double implementation plus a differential-testing harness to keep them honest. Several standards-bearing languages do exactly this, and it is genuinely expensive.
  • Designing a language to stay near-LL buys a hand-writable frontend and good tooling, and pays with syntax you cannot have: no construct whose meaning depends on an unbounded prefix, which rules out some notations that read very well to humans.

What else you could do

What a different compiler or language does instead, and when that is better.

  • GLR — Bison's %glr-parser, tree-sitter, Elkhound — keeps every conflicting hypothesis alive, giving generated parsing an error tolerance that deterministic LR lacks, at the cost of memory and of deciding what to do with multiple parses.
  • ANTLR's ALL(*) keeps the top-down shape and buys effectively unbounded lookahead by simulating over the real input, which recovers much of LR's grammar power while preserving construct-aware error handling — see [[parser-generators]].
  • PEG with packrat memoisation — CPython since 3.9 — resolves ambiguity by ordered choice, which eliminates conflict reports by making conflicts unrepresentable. Whether that is a fix or a hazard depends on whether you wanted to be told.
  • Hand-written descent for statements and declarations with a Pratt loop for expressions is not a compromise between the two families, it is the design that won: it takes the top-down shape and removes its one genuine ergonomic weakness.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Read two failures side by side: feed the same broken C file to gcc and to clang, and to a Bison-generated parser such as PostgreSQL's psql for a broken query. The difference in message shape is the architecture, not the effort.
  • Check what a compiler uses: look for a .y or .g4 file in its source tree. Its absence in clang/lib/Parse, rustc_parse and cmd/compile/internal/syntax is the evidence for the claim.
  • Read GCC's own rationale: the C++ recursive-descent parser landed in GCC 3.4 (2004) and the changelogs and mailing-list discussion state diagnostics and template handling as the reasons.
  • tree-sitter parse a file with a deliberate syntax error and observe that it still returns a full tree with an ERROR node — then try the same input through a Bison parser and observe that you get a message and nothing else.
  • Compare CPython 3.8 and 3.9+ on a syntax error involving a construct the old LL(1) grammar could not express well; PEP 617's motivation section documents the specific cases.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "LR is more powerful, therefore LR is better." More powerful over *grammars*. The compilers you use chose the weaker family for reasons that have nothing to do with which grammars it accepts.
  • "Hand-written parsers are a legacy of the era before good tools." The direction of migration is the other way: GCC and Go moved from generated to hand-written, in 2004 and 2015 respectively, on modern toolchains.
  • "LL(k) ⊂ LR(k) means LL parsers can parse fewer languages." It means fewer *grammars*. The language gap exists but is much narrower, and for a language you are designing you can simply choose a grammar in the smaller class.
  • "Generated parsers are faster because tables are faster than function calls." Hand-written descent is usually at least as fast in practice: no table indirection, predictable branches, and cross-function inlining.
  • "You cannot get good error messages from a generated parser." tree-sitter and ANTLR both do better than the Yacc baseline. The structural handicap is real; it is not an absolute ceiling.

Misconceptions

The claim, and what is actually true.

Parser generators fell out of favour because they were slow or buggy.
They are fast and mature. They fell out of favour in compilers because the output — messages and recovery — is structurally limited by when the family commits, and because IDE requirements arrived that the architecture cannot serve.
Choosing recursive descent means giving up on grammar rigour.
It means the rigour is not automatic. Teams that care maintain a reference grammar alongside the hand-written parser and test them against each other — which is a real cost, honestly paid, rather than an abandonment.
The comparison is settled and LL won.
It is settled for compiler frontends. For query languages, configuration formats, standards-tracking grammars and tools parsing languages they do not own, generated bottom-up parsing is still the better answer, and tree-sitter has made GLR the default choice for editor tooling.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

LR parsers accept more grammars than LL parsers. Almost every compiler you use is nonetheless a hand-written LL-style parser. The reason is that an LL parser always knows what it is in the middle of parsing, so it can say "expected a closing parenthesis for the argument list that started on line 12", while an LR parser can only list which tokens would have been legal. Once compilers had to serve editors as well as builds, that difference decided the architecture.

practical

Pick by requirements, not by power. Human-facing errors or an editor integration means hand-written recursive descent with a Pratt loop for expressions. A grammar that is a deliverable, changes on someone else's schedule, or must be proved unambiguous means a generator. Parsing a language you do not own, tolerantly, means tree-sitter. And whichever you pick, keep a grammar written down somewhere — hand-written parsers drift from their specification silently, and the only defence is differential testing against a reference.

advanced

The deepest way to state the difference is as an information-timing argument. Committing early leaves you inside a named construct with full context and costs grammar power; committing late maximises evidence and costs context. Every downstream property is downstream of that one trade: message quality, recovery granularity, incrementality, and whether context-sensitive hacks are localised or structural. GLR is interesting precisely because it refuses the trade — it commits late *and* keeps every hypothesis, buying error tolerance with memory rather than with knowledge, which is why a generated bottom-up parser ended up being the standard editor tool while deterministic LR left the compiler frontend entirely. The lesson generalises past parsing: whenever a design chooses between deciding early with less evidence and deciding late with less context, expect the tooling requirements — not the formal power — to settle it.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

implementationEvery implementation claim here is version-specific. GCC generated its C++ parser with Bison through 3.3 and hand-writes it from 3.4 (2004); its C parser switched in 4.1. Go used a yacc grammar through 1.5 and hand-written descent from 1.6. CPython used an LL(1) parser through 3.8 and a PEG parser from 3.9. Ruby, PHP, Bash and PostgreSQL still use Bison or Yacc today. A claim about "what compilers do" without a version is not a claim.
specThe containment LL(k) ⊂ LR(k) is a theorem about grammars, proved by Knuth's original 1965 LR construction together with the standard LL characterisation. The language-level statement is different and weaker: the LR(1) languages are exactly the deterministic context-free languages, and there exist deterministic languages with no LL(k) grammar for any k. Conflating the grammar result with the language result is the most common error in this comparison.
typicalThat hand-written descent is at least as fast as generated table-driven parsing reflects common measurements on mainstream compilers, where instruction locality and branch predictability favour straight-line code. It is not a guarantee: a compact table can win on a grammar with very many alternatives per nonterminal, and neither result transfers without measuring your own grammar.

If you were asked this in an interview

  • LR accepts a strictly larger class of grammars. Name three reasons a compiler team would still hand-write a recursive-descent parser.
  • Why is a construct-aware error message structurally easier in a top-down parser?
  • When would you choose a generator, and what would you accept in exchange?

Connections

Domains that do not exist yet
  • Testing & Reliability Engineering — Differential testing between two implementations
    A hand-written parser and a reference grammar are two implementations of the same specification, and keeping them honest is a differential-testing problem. The general technique is owned there; [[differential-testing]] is the compiler-specific version.
  • DevOps / Production Engineering — A code-generation step in the build
    Choosing a generator adds a build-time dependency, a generated artifact and a question about whether it is committed. Those are build-system concerns owned there, and they are part of the cost of the choice.