skip to content

When would you write a parser by hand for a description language instead of generating one from a grammar file?

level: principalimportance: nice to knowfreq 30%

answer

  1. who reads the failure message
  2. the rule set as a checked artefact
  3. conflicts reported at build time
  4. editors need trees for broken files
  5. drift between rules and code

basics

~20 s

Write it by hand when the quality of failure matters most — precise messages, recovery, partial trees for an editor — and when the grammar is small and stable. Generate it when the rule set is large, changes often, and must stay machine-checked.

solid answer

~50 s

The trade is **control against verification**. A hand-written recursive-descent parser is ordinary code: the error at each failure point is written by the function that knows what it wanted, recovery can be tuned construct by construct, a partial tree can be returned for an editor, and an ad-hoc rule that a clean rule set expresses badly is a few lines. The price is that the grammar exists only implicitly across the functions, so nothing checks it for overlapping alternatives, and every change relies on the author's reasoning. A generated parser inverts both: the rule set is a single checked artefact whose conflicts are reported when the parser is built, it accepts grammar shapes a one-token top-down parser cannot, and it shrinks with the language rather than with the hand-written code. Decide on who reads the failures, how fast the language changes, and whether anyone will own the rule set.

go deeper

for a junior

Recall the basic contrast: one parser is code you write and read directly, the other is produced from a file of rules. Knowing that both exist and that neither is universally better is the right level here.

for a middle

Name the concrete differences — where the error message comes from, whether anything checks the rules for conflicts, and what a language change costs in each option — rather than asserting a preference.

for a senior

Argue from the failure path: who reads the errors, whether an editor needs a tree for broken input, and how recovery is tuned. Be precise that a generator reports conflicts but a suppressed warning verifies nothing.

for a principal

Own the long-run cost: ownership of the code versus ownership of a rule set, drift between the written grammar and the shipped parser, and whether a second unshipped parser used as a test oracle is worth its price.

## Framing the decision This is a judgement call with no universally right answer, and the honest version of it names the forces rather than declaring a winner. The two options differ along axes that matter to different teams by different amounts. | axis | hand-written recursive descent | generated from a rule set | |---|---|---| | error messages | written at the failure point by the function that knows the expected set | generic by default; improving them means fighting the generated shape | | error recovery | tuned per construct, including resuming inside a half-written block | supported, but the strategy is largely the generator's | | grammar verification | none — the rules are implicit and unchecked | conflicts reported when the parser is built | | grammar class accepted | one-token top-down prediction, plus whatever local patches you hand-code | broader, depending on the family the generator implements | | cost of change | edit code, re-reason about the affected branches | edit rules, rebuild, read the reported conflicts | | build dependencies | none beyond the language it is written in | a generation step in the build, and a tool to keep current | | readability for a newcomer | the grammar must be reconstructed from the code | the rule set is the documentation | ## When hand-written wins 1. **The failures are read by humans who are not the authors.** A configuration or job-description language is edited by people who will make mistakes daily. The value of *"expected a directive — every, retry or run — but found a closing brace at line 12"* over *"parse error at line 12"* is not cosmetic; it is most of the product's perceived quality. 2. **The parse must survive bad input.** An editor needs a tree for a file that is being typed and is therefore broken most of the time. Deciding what to synthesise and where to resume is per-construct judgement, and having it in ordinary code is what makes it tractable. 3. **The language has rules a clean rule set expresses badly** — a keyword that is only reserved in one position, a directive legal only inside another, a limit on nesting depth. Each of those is a few lines in a function and a contortion in a rule set. 4. **The grammar is small and stable.** A dozen rules that change twice a year do not pay for a generation step. ## When generating wins 1. **The rule set is large or changes often.** Hand-written code grows roughly with the rules and every change forces re-reasoning about the branches it touches; a generated parser absorbs a rule change and tells you if it broke. 2. **The grammar must be an artefact others can read and check.** When the language is a published interface, the rule set is both the specification and the thing the parser is actually built from, which removes the drift between documentation and behaviour. 3. **The shape of the language resists one-token prediction.** If alternatives naturally share long prefixes or the structure is left-associative by nature, a family that accepts more grammars removes a class of problem instead of patching it. 4. **Nobody will own the hand-written code.** A generated parser degrades more gracefully under low ownership because the invariants live in the tool, not in a convention the original author kept in their head. ## The claims to be careful about - **"Hand-written is faster"** is not a given. It avoids a generic driver loop and some table indirection, which can matter, but parsing is rarely the bottleneck next to whatever happens to the tree afterwards. Treat it as a measurement, not an argument. - **"Hand-written accepts more grammars"** is false as stated. One-token top-down prediction accepts a strictly smaller class than the bottom-up family; a hand-written parser can exceed it only by local backtracking or extra lookahead that the author adds and maintains by hand. - **"A generator cannot give good errors"** is too strong. It is harder, and the good result usually comes from the generator's own recovery machinery plus per-rule annotations, rather than from anything as direct as an error raised inside a function. - **"Generated means verified"** is also too strong: a generator reports conflicts, and some generators resolve certain conflicts by a default preference with a warning. A suppressed warning is an unverified grammar wearing a checked artefact's clothes. ## A workable middle The common practical answer is to keep a written rule set as the specification even when the parser is hand-written, and to check the two against each other with a corpus of inputs plus a generated parser used only as an oracle in tests. That keeps the hand-written error quality while making the drift between the rules and the code visible instead of silent. It costs a second parser nobody ships, which is exactly the kind of cost a lead has to decide is worth paying.

  • What would make you reverse a decision to hand-write the parser two years later?
    The rule set growing past what one person can hold, or a run of bugs where a legal input was parsed as the wrong construct — the signature of overlapping alternatives nothing checks. Both say the implicit grammar has outgrown reasoning. A rising cost per language change, measured in review time rather than lines, is the earlier signal.
  • How do you keep a hand-written parser's grammar from drifting out of its documentation?
    Keep a written rule set as the specification and test the parser against a corpus derived from it, including inputs it must reject. Record every hand-coded deviation — an extra token of lookahead at one branch, a contextual keyword — next to the rule it patches. Undocumented deviations are how the written grammar quietly stops describing the parser.
  • Why does an editor integration push the decision towards hand-written?
    Because an editor parses broken input continuously and needs a usable tree anyway, with the damage localised to the construct being typed. That means per-construct decisions about what to synthesise and where to resume, which is judgement expressed as code. A parser that only distinguishes valid from invalid is not enough for that surface.

saying these in an interview costs you the question

  • Says a hand-written parser is always faster, with no measurement
  • Claims a generated parser cannot report useful positions or messages
  • Thinks a hand-written parser accepts a larger class of grammars
  • Ignores that the hand-written grammar is checked by nothing
  • Chooses on personal preference without asking who reads the errors