Beskid blog · Compiler
Parse Recovery: When the Compiler Says 'I See What You Meant' Instead of Just Dying
Most compilers hit a syntax error and give up. Beskid's parser recovers, finds more errors, and reports them all, because 'error on line 1' is a compiler crime and nobody has time for fix-one-error-rerun loops.
← All postsimport { BlogIndex } from ’../../../components/BlogIndex.astro’;
Most compilers have exactly one move when they hit a syntax error: stop. Print “unexpected token.” Exit. Make the developer fix that one error, recompile, and pray the next line isn’t also broken.
It’s a terrible developer experience, and we’ve all been trained to accept it.
Compilers that die on the first error
Section titled “Compilers that die on the first error”The traditional single-pass compiler loop goes: lex, parse, typecheck, codegen. If parsing fails, everything stops. You get one error message. You fix it. You recompile. You get another error. You fix that. Repeat. For a file with five syntax errors, that’s five compile cycles.
This isn’t just annoying. It’s a productivity tax that compounds with project size. A 200-line file with three missing semicolons shouldn’t require three round trips through the build system.
The commit that changed everything
Section titled “The commit that changed everything”On July 23, 2026, commit c765ef51 pinned the compiler submodule to the commit that landed parse recovery in Beskid. The message is characteristically terse (“land parse recovery and ISLE float/unsigned gaps”), but the diff underneath tells the real story. The parser no longer returns on first error. It tries small text repairs at the error site and keeps going.
The follow-up commit 0486fabd advanced the compiler pin again for an overflow fix in the recovery pipeline: the kind of bug that only shows up when you’re collecting dozens of candidate repairs instead of just one.
Together, they land the design documented in docs/superpowers/specs/2026-07-23-parse-recovery-heuristics-design.md: the parse-recovery heuristics spec.
How recovery actually works
Section titled “How recovery actually works”Recovery is not panic mode. It does not throw tokens away until it recognizes a keyword. When the parser hits a token it can’t fit into the current production, it generates a set of small repair candidates: insert a missing token, delete an unexpected one, or replace one token with another, each scored by how confidently it explains the error. It applies the best candidate, retries a strict parse, and repeats, capped so a genuinely broken file doesn’t spin forever.
The candidates come from a handful of specialized generators, each covering a slice of the grammar:
- Delimiters. Unclosed
(,[,{, strings, and code fences get their closer inserted. - Separators. A missing
;,,,:,=>, or.between two things that clearly belong together gets inserted. - Items. An incomplete
type,enum,impl,mod,use, orcontractdeclaration gets patched enough to keep parsing the rest of the item. - Expressions. Broken
matcharms, lambdas, literals, and calls get repaired locally. - Sync boundaries. When nothing local explains the error, recovery falls back to scanning forward for the next statement-starting keyword (
if,while,for,let,return, and friends) or a closing brace, and inserts the missing terminator there instead of guessing at a fix mid-expression.
The error is recorded either way, the parser moves on, and the next statement parses normally. If it has errors too, those get reported as well.
The result: instead of one error per compile, you get all of them, up to a fixed cap per file.
The LSP synergy
Section titled “The LSP synergy”Parse recovery isn’t just about batch compilation. It’s the prerequisite for a competent LSP.
Before recovery, the language server could only show one red squiggle at a time, at the first broken token. Everything after that was invisible to the parser, which meant no diagnostics, no syntax highlighting, no intellisense. The editor went dark after the first mistake.
With recovery, the LSP gets a complete(ish) AST no matter how many syntax errors exist. Red squiggles appear everywhere they’re needed. Completions work in a method body after a broken use line. Hover types resolve in code that follows a missing brace. The editor stays useful even when the code is broken, which, if we’re honest, is most of the time.
The ISLE float/unsigned gap fix
Section titled “The ISLE float/unsigned gap fix”The same commit that landed parse recovery also fixed float literal parsing in ISLE rules. This wasn’t a coincidence. Parse recovery exposed the bug by actually reaching ISLE rules that referenced float literals: code that the old parser never got to because it died on an earlier error. Recovery doesn’t just find more user errors; it finds more compiler bugs.
One of the float literal edge cases: unsigned integer gaps in the tokenizer meant that 0x1.0p0 (a hexadecimal float) could be mis-tokenized as an integer followed by a stray .0p0. Recovery made this visible by successfully parsing the surrounding context and reporting the tokenization mismatch as an error instead of crashing.
The therapist metaphor
Section titled “The therapist metaphor”The Book chapter “Read a diagnostic” (in The Compiler Is Not Your Therapist) makes the point directly: a compiler that says “error on line 1” and stops is like a therapist who says “you seem sad” and ends the session. The useful work hasn’t started.
A good diagnostic is specific, local, and actionable. But a good diagnostic system is also comprehensive: it tells you everything that’s wrong, not just the first thing. Parse recovery is how you get from “one specific error” to “all specific errors.”
The therapist metaphor extends further: the compiler shouldn’t judge. It shouldn’t speculate about what you probably meant. It should report what it knows and let you decide. Recovery heuristics walk this line carefully: they don’t guess at fixes, they find boundaries and resume. The errors are factual, not interpretive.
Why this matters
Section titled “Why this matters”A compiler that dies on the first error is a compiler that wastes your time. Every round trip through the build system costs context. Every “fix this one thing and try again” cycle pulls you out of flow. Multiply by the number of developers on a team, the number of compiles per day, the number of errors per compile: the tax is enormous and invisible because we’ve all normalized it.
Parse recovery is not a feature. It is basic respect for the developer’s attention. The compiler should work as hard as you do to understand your code, and when it can’t, it should tell you everything it knows, not just the first thing that went wrong.