skip to content

questions

26

When a parser recovers from an unexpected token in panic mode, how does it choose where to resume?

level: middleimportance: must knowfreq 60%

answer

  1. report once, then stop listening
  2. skip to a token you can trust
  3. anchors: delimiters, closers, declaration keywords
  4. unwind rules until one accepts it
  5. consume the delimiter, leave the closer

basics

~20 s

Panic mode discards tokens until it reaches one from a synchronising set — a statement delimiter, block closer or declaration keyword — then unwinds its suspended rules until one accepts that token, and parses on.

solid answer

~50 s

Panic-mode recovery has two halves. First the parser reports the failure **once** and then discards input without trying to understand it, until the current token belongs to a **synchronising set** chosen for the construct being parsed — commonly a statement delimiter, a block closer, or a keyword that can only begin a fresh declaration. Second it unwinds its own state: returning from suspended recursive-descent calls, or popping parser stack states, until some rule can legally continue on that token. A statement delimiter is normally consumed, because it ends the broken construct; a block closer is normally left in place, because the enclosing rule is the one that needs it. The aim is not to guess what the author meant, but to reach a point where the rest of the file is parsed on its own merits, so a genuinely separate later mistake is still reported.

code

pseudocode · 15 lines
pseudocode
on parse_error(bad_token):
    report(bad_token, expected_set_of(current_state))
    suppress_reports = true

    while current is not in SYNC_SET and current is not END_OF_INPUT:
        advance()                       // discarded, never reported

    unwind rules until some rule can continue on current

    if current is a statement delimiter:
        advance()                       // consume it: the statement is over
    // a block closer or declaration keyword is LEFT for the enclosing rule

    suppress_reports = false
    resume()

go deeper

for a junior

Know that a real parser does not stop at the first mistake: it reports it, skips forward to a token that clearly starts something new, and carries on parsing from there.

for a middle

Explain both halves — discarding input up to an anchor and unwinding the parser's own suspended rules — and say why a statement delimiter is consumed while a block closer is left for the enclosing rule.

for a senior

Show how you would pick anchors for a real grammar, keep a stack of them for nested constructs, and stop recovery resuming so early that the same mistake is reported twice.

for a principal

Weigh recovery quality against front-end complexity: how much of an error list your users can trust, and whether one recovery policy can serve both a batch compile and an interactive editor.

## The problem panic mode solves A parser detects an error at the first token it cannot use. At that instant it knows two things: the input is wrong, and its own stack of half-finished rules describes a shape the text does not have. Stopping there is tolerable for a batch compile of a small file. It is not tolerable for a background syntax service in an editor, where the file is nearly always incomplete and the user still expects structure for the parts that are fine. **Panic-mode recovery** is the cheapest way back to solid ground: report once, throw input away until the input itself says a new construct is starting, and unwind the parser until some suspended rule can accept that token. ## Choosing the synchronising set The set of tokens the parser will stop skipping at is the whole design. A good synchronising token is one whose appearance is strong evidence about structure regardless of what came before it: - **Statement delimiters** — the token that ends one statement says the next statement begins after it. - **Block closers** — a closing bracket or block-ending keyword says the enclosing construct is over. - **Declaration keywords** — tokens that can only begin a top-level or member declaration. - **End of input** — the stop of last resort, which is what makes skipping guaranteed to terminate. The formal version of the same idea is to synchronise on the **FOLLOW set** of the rule that failed: those are exactly the tokens that can legally appear after that construct, so seeing one means the construct is finished. Hand-written recovery usually takes a smaller, hand-picked subset, because FOLLOW sets are computed across the whole grammar and contain tokens far too common to be trusted as anchors. ## Unwinding the parser's own state Skipping input is only half the job — the parser's state has to be unwound to match. The sequence is: 1. Report the error once, at the offending token, with the expected set for the current state. 2. Discard tokens until the current token is in the synchronising set, or is end of input. Discarded tokens produce no messages. 3. Unwind suspended rules — return from recursive-descent calls, or pop parser stack states — until a rule remains that can legally continue on the current token. 4. Decide whether to consume that token or leave it for the rule that will use it. 5. Resume normal parsing and re-enable reporting. Step 4 is the one that is most often wrong. A statement delimiter is normally **consumed**: it closes the broken statement, and leaving it hands the next rule a token it also cannot start with, producing a second message in the same place. A block closer is normally **left**, because the enclosing rule needs it; consuming it swallows the end of the block and makes everything after it look nested one level too deep. | Synchronising token | What it signals | Consume or leave | Typical failure if you get it wrong | |---|---|---|---| | Statement delimiter | The broken statement is over | Consume | An immediate second error on the same token | | Block closer | The enclosing construct is over | Leave | The rest of the file parses as if still inside the block | | Declaration keyword | A new member or top-level item begins | Leave | The declaration header is swallowed and its body parsed loose | | End of input | There is nothing to recover into | Leave | An endless skip loop if the loop forgets this case | ## Why a crude strategy survives Panic mode makes no attempt to reconstruct the author's intention, and that is exactly why it is robust. Provided each recovery round advances the input by at least one token, it terminates. It needs no per-rule repair logic, so it scales uniformly over a large grammar. And because it lands on a real construct boundary, the errors reported after resynchronising are usually **independent** of the first one — the property that makes a list of errors worth reading at all. The cost is precision. Everything between the failure and the anchor is unparsed, so no structure exists for that span, and one stray opening bracket can cost a whole block. That is acceptable for a batch compile and often unacceptable for an editor, which is why real front ends layer finer strategies on top: a single-token insertion or deletion attempted first, an error node recorded so a partial tree still covers the skipped range, and panic mode kept as the fallback that always works. ## Symptoms of a badly chosen anchor set - Anchors that are too common: recovery stops before the broken construct is really over and reports the same mistake again. - Anchors that are too rare: a small typo costs hundreds of lines of structure. - A single flat anchor set for a nested grammar: an inner rule consumes a token that belonged to an outer one, and nesting is wrong for the remainder of the file.

  • Why is panic-mode recovery guaranteed to terminate, and what must a recovery routine do to keep that guarantee?
    Because the skip loop is monotone in input position: it either advances past a token or stops at end of input. The guarantee only holds if every recovery round consumes at least one token. A routine that can resume without consuming anything must force one advance before retrying, otherwise the same token re-enters recovery indefinitely.
  • How do nested constructs change which synchronising set a recovery routine should use?
    Each active rule contributes its own anchors, so practical recovery keeps a stack of synchronising sets and stops at the innermost match. A token that closes an outer construct should unwind several rules at once rather than being consumed by the innermost one, which is why recovery pops frames until it finds a rule that can genuinely continue.
  • What is permanently lost for the span between the failure and the anchor?
    All of it: those tokens are never parsed, so no nodes exist for them and no later phase can say anything about that range. Front ends that serve editors compensate by recording an error node covering the skipped span, so the tree still spans the whole file and positions after it remain correct.

A reader who loses their place in a long list of instructions does not guess which step went missing; they scan forward to the next numbered heading and start again cleanly from there.

saying these in an interview costs you the question

  • Thinks the parser guesses what the author intended to write there
  • Emits a syntax error for every token it skips
  • Consumes a block closer during recovery, nesting the rest of the file wrongly
  • Believes recovery restores structure for the skipped span
  • Uses one very common token as the sole anchor, so recovery stops too early
  • Forgets end of input as a stopping condition for the skip loop
open as a page

When a generated bottom-up parser reads a schema file, what do its shift and reduce actions each do to the parse stack?

level: middleimportance: must knowfreq 68%

basics

~20 s

Shift pushes the next input token onto the parse stack; reduce recognises a handle, a complete right-hand side sitting on top, pops those symbols and pushes the rule's left-hand nonterminal. The generated table decides which, per state and lookahead.

open as a page

Why can a parsing expression grammar's ordered choice never produce two different parse trees for one input?

level: middleimportance: must knowfreq 62%

basics

~20 s

Ordered choice commits to the first alternative that succeeds at a position, so each rule yields at most one result there. The grammar defines one recognizer rather than a set of permitted derivations, so a second parse tree cannot exist.

open as a page

In a precedence-climbing parser, what makes an infix operator left-associative rather than right-associative?

level: middleimportance: must knowfreq 52%

basics

~20 s

The power handed to the right-operand recursion decides it: recursing with the operator's power plus one makes it left-associative, because an equally binding operator is then refused; recursing with the same power makes it right-associative.

open as a page

In precedence climbing, how does one minimum-binding-power loop parse an expression that a layered grammar would need a rule per level for?

level: middleimportance: must knowfreq 62%

basics

~20 s

A binding power is a number per operator saying how tightly it grips its neighbours. One loop parses an operand, then keeps absorbing operators whose power clears the caller's minimum, recursing once per right operand.

open as a page

In a hand-written recursive-descent parser, how does one function per grammar rule with one token of lookahead choose between a rule's alternatives?

level: middleimportance: must knowfreq 66%

basics

~20 s

Each grammar rule becomes one function. The function inspects the next token without consuming it, compares it with the set of tokens each alternative can begin with, and takes the single alternative whose set matches; nested rules become nested calls.

open as a page

A recursive-descent parser overflows its call stack having consumed no tokens at all — what property of the grammar causes this?

level: middleimportance: must knowfreq 58%

basics

~20 s

Left recursion: a rule whose first symbol is the rule itself, directly or through a chain. Its function calls itself before consuming anything, so every level re-enters at the same input position and the recursion never terminates.

open as a page

Why does one stray token make a parser report fifty syntax errors, and how is that cascade suppressed?

level: seniorimportance: must knowfreq 50%

basics

~10 s

After a failure the parser's state no longer matches the text, so every following token disagrees and becomes another message. Suppression combines real resynchronisation, a quiet window after each report, deduplication, and a cap.

open as a page

When a parser generator reports a shift-reduce conflict in one state of a grammar, what has it found and how does it resolve it?

level: seniorimportance: must knowfreq 56%

basics

~20 s

A shift-reduce conflict means one state can both shift the next token and reduce a completed rule on that same token, so the table is not deterministic. Generators conventionally prefer shift, silently binding the construct to the innermost open one.

open as a page

In an ordered-choice grammar for inline text annotations, `@notebook` parses as `@note` followed by the letters `book` — why?

level: seniorimportance: must knowfreq 54%

basics

~20 s

The choice lists note before notebook, and the first success wins, so the shorter literal matches and the longer alternative is never tried. A choice that has already succeeded is not retried when the rest of the rule struggles.

open as a page

When a parser prints 'expected one of X, Y or Z' at a bad token, where does that set come from?

level: middleimportance: should knowfreq 42%

basics

~20 s

From the parser's state at the moment it failed: the tokens that state could legally have accepted next. Tools then rename, group, rank and truncate that raw set, because printing it unedited produces an unreadable message.

open as a page

What does a bottom-up LR parser know at the moment it commits to a rule that a top-down LL parser does not?

level: middleimportance: should knowfreq 50%

basics

~20 s

An LR parser commits only after the whole right-hand side is already on its stack, plus the lookahead beyond it. An LL parser must choose the rule before reading any of it, from lookahead alone. More evidence, later decision.

open as a page

What does a packrat parser store in its memo table, and why does that make parse time linear in input length?

level: middleimportance: should knowfreq 47%

basics

~20 s

It stores the outcome of each rule at each input position: failure, or success with the end position and result. Because every rule-position pair is computed once and then reused, total work is proportional to input length times grammar size.

open as a page

Why does a recursive-descent parser need FOLLOW sets, and not just FIRST sets, when a grammar rule can derive the empty string?

level: middleimportance: should knowfreq 50%

basics

~20 s

An alternative that derives nothing begins with no token, so FIRST cannot describe it. The parser takes that empty branch when the next token is one that may legally appear immediately after the rule — that set is FOLLOW.

open as a page

Why is a reduce-reduce conflict between two rules that share a right-hand side more serious than a shift-reduce conflict?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A reduce-reduce conflict means identical stack contents could become two different nonterminals. The conventional default picks whichever rule was declared first, so the other construct is never built in that context and a whole feature quietly disappears.

open as a page

In a scannerless grammar with no token stage, why must every rule that may be followed by spacing say so explicitly?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Nothing discards whitespace for a scannerless parser: its rules match raw characters, so any space, tab or newline a rule does not consume is still sitting there and makes the next literal fail. Spacing is threaded through the rules by hand.

open as a page

In a precedence-climbing parser, why does a prefix operator need its own binding power, separate from the infix operator spelled the same way?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A prefix operator has no left operand, so it carries a single power of its own, used as the floor when parsing its argument. That number decides how far right the argument reaches, and it is usually higher than the infix power of the same symbol.

open as a page

You build an LL(1) prediction table before hand-writing a recursive-descent parser, and one cell holds two rules — what does that mean?

level: seniorimportance: should knowfreq 40%

basics

~20 s

That cell says two alternatives of the same rule are predicted by the same lookahead token, so one token cannot choose between them. The rule set is not LL(1), and a purely predictive parser for it cannot be written as written.

open as a page

A parser behind an editor reparses a half-typed file on every keystroke — what should it guarantee callers?

level: principalimportance: should knowfreq 32%

basics

~20 s

Totality: a tree for any input, spanning every byte, with unparsable spans held by explicit error nodes and fabricated tokens marked, plus one flag saying whether any error node exists — so each caller picks its own strictness.

open as a page

Your data-definition language's generated parser has shipped for years with a dozen unread conflicts in its build log, and a new construct must be added — how do you decide what to change?

level: principalimportance: should knowfreq 32%

basics

~20 s

Treat each unread conflict as an undocumented language decision: reproduce what the shipped parser does on inputs that reach it, decide whether that is the intended language, then reshape the rules or record the choice explicitly before adding anything new.

open as a page

When is a precedence-climbing binding-power table worth adopting for a formula evaluator whose users request a new operator every quarter, and what does it cost?

level: principalimportance: should knowfreq 33%

basics

~20 s

Adopt it when the operator set keeps growing or precedence must be data: each addition becomes one table row instead of a new rule layer and an edit to its neighbour. The cost is that precedence stops being readable from the code and becomes numbers you must document and regression-test.

open as a page

How does single-token insertion or deletion let a parser continue past a missing block closer?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Phrase-level repair edits the token stream locally: if fabricating the one token the state expected, or dropping the current stray token, lets parsing progress, the parser does that, marks the edit synthetic, and reports once.

open as a page

How do SLR, LALR and canonical LR(1) table constructions differ in state count and in which conflicts they avoid?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

SLR and LALR share the LR(0) state set, so their tables are the same size; SLR reduces on global FOLLOW sets, LALR on merged per-state lookaheads, and canonical LR(1) splits states to keep every lookahead exact, at a large size cost.

open as a page

In precedence climbing, why is the sub-expression inside brackets parsed with a minimum binding power of zero?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Brackets cancel the surrounding grip, so the inner parse starts from the loosest possible floor and can absorb every operator up to the closing bracket. Passing the ambient floor instead stops the inner parse early and reports a spurious error at the bracket.

open as a page

Your team must publish a normative grammar for an annotation syntax that other teams will implement independently — what is the risk of making it an ordered-choice grammar?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

An ordered-choice grammar defines its language operationally: the accepted set is whatever that recognizer accepts, with no independent description to check against. Conformance becomes behavioural agreement, so the specification must ship with a test corpus, not just rules.

open as a page

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%

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.

open as a page