skip to content

questions

5

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%

answer

  1. the grammar's shape becomes the code's shape
  2. one function owns exactly one rule
  3. look before you consume
  4. branch on each alternative's starting tokens
  5. match is optional, expect is mandatory

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.

solid answer

~40 s

Write one function per nonterminal: `parse_job`, `parse_block`, `parse_directives`, `parse_directive`. Each function is entered with the input positioned at the first token of its rule and returns with the input just past the last token of its rule, consuming nothing it does not own. Before committing it calls `peek()`, which reports the next token without consuming it, and branches on which alternative that token can begin — in a job-scheduling description language, `every` begins the schedule rule, `retry` the retry rule, `run` the command rule. Two helpers do all the consuming: `match(kind)` consumes only if the next token has that kind, and `expect(kind)` consumes it or raises a parse error naming what was wanted. Grammar nesting becomes call-stack nesting, so a block inside a block needs no extra machinery.

code

pseudocode · 19 lines
pseudocode
function parse_directives():
    items = empty list
    while peek() is one of 'every', 'retry', 'run':
        items.append(parse_directive())
    return items

function parse_directive():
    if peek() == 'every':
        expect('every'); d = expect(DURATION); return Schedule(d)
    if peek() == 'retry':
        expect('retry'); n = expect(NUMBER); return Retry(n)
    if peek() == 'run':
        expect('run'); c = expect(STRING); return Command(c)
    error('expected every, retry or run; found ' + peek())

function expect(kind):
    if peek() != kind:
        error('expected ' + kind + '; found ' + peek())
    return advance()        // consumes one token and returns it

go deeper

for a junior

Recall the core idea: one function per grammar rule, and the next token decides which branch that function takes. Being able to point at a rule and name the function that would parse it is enough at this stage.

for a middle

Explain the mechanics: peek inspects without consuming, expect consumes or fails, and each function enters at the first token of its rule and leaves just past the last. Say why disjoint starting-token sets are what make the branch decidable.

for a senior

Show the operational side: bound the recursion depth for untrusted input, keep each function's consumption boundary honest so callers can rely on it, and place the error at the function that knows which tokens it wanted.

for a principal

Frame the trade-off: this design makes the grammar implicit and unverified in exchange for control and readability. Decide deliberately whether the language is small and stable enough to accept a rule set that no tool checks.

## Taking the grammar literally A **recursive-descent parser** is a top-down parser in which every nonterminal of the grammar becomes one function. Parsing begins by calling the function for the start rule; that function calls the functions for the symbols on the right-hand side of its rule, left to right, and returns the fragment of tree it built. Recursion in the grammar — a block that may contain another block — becomes recursion in the code, which is where the name comes from. For a small job-scheduling description language the rules might be: ```pseudocode job -> 'job' NAME block block -> '{' directives '}' directives -> directive directives | (nothing) directive -> 'every' DURATION | 'retry' NUMBER | 'run' STRING ``` Four rules, four functions. Nothing else is needed: there is no table, no generated state machine, no separate driver loop. ## The contract every rule function honours A hand-written parser stays readable only while each function keeps the same two-part bargain: - **It is entered with the input positioned at the first token of its rule**, and it returns with the input positioned immediately after the last token of its rule. - **It consumes nothing it does not own.** The function for `block` consumes the braces and whatever `directives` consumes, then stops. It never reads past its own closing brace to help its caller decide something. Breaking that bargain in one function is the classic way a hand-written parser becomes unmaintainable, because every caller then has to know which tokens that one function might have eaten. ## One token of lookahead, and the three helpers A predictive parser never guesses and never backtracks. It examines — but does not consume — the next token, then commits. | helper | consumes a token | typically used for | |---|---|---| | `peek()` | never | deciding which alternative to take | | `match(kind)` | only when the kind matches | optional and repeated pieces | | `expect(kind)` | yes, or raises a parse error | a piece the rule requires | The decision procedure inside a function with several alternatives is mechanical: 1. For each alternative, work out the set of tokens it can begin with. 2. Check those sets do not overlap. If two alternatives can begin with the same token, one token of lookahead cannot separate them and this technique does not apply as written. 3. Branch on `peek()` against those sets; if the token belongs to none of them, raise an error that lists the tokens which would have been accepted. For `directive` the three sets are `{'every'}`, `{'retry'}` and `{'run'}` — disjoint, so the choice is a three-way branch on a single token. Because each token is inspected a bounded number of times and consumed exactly once, parsing runs in time linear in the number of tokens, where the input size is the token count rather than the character count. ## Why nesting is free The reason this technique suits nested configuration and description languages is that the **call stack is the parser's memory of depth**. A `block` inside a `block` is just `parse_block` calling `parse_directive` calling `parse_block` again; the pushdown behaviour that the grammar needs comes from the runtime's own call stack instead of an explicit stack the parser maintains. The shape of the code is the shape of the language, so a reviewer who knows the rules can read the parser, and a debugger's stack trace at the point of failure reads as the path through the grammar. ## What it buys, and what it costs - **Buys:** ordinary code you can step through; an error raised at the exact point of failure, inside the function that knows what it wanted next; a natural place to hang an ad-hoc rule (a directive legal only inside another directive) that a pure rule set expresses clumsily; tree construction and validation in the same pass. - **Costs:** the grammar exists only implicitly, spread across the functions, so nothing checks the alternatives for overlap — you find a clash by reasoning or by a failing test; an alternative that can derive nothing at all needs more than a first-token test to select; and a rule whose first symbol is the rule itself cannot be written this way at all, because the function would re-enter itself before consuming anything. One operational consequence follows directly from the call-stack trick: the stack depth equals the nesting depth of the input. A parser exposed to untrusted documents therefore needs an explicit depth counter checked on entry to the recursive functions, because unbounded nesting in a hostile input is a denial-of-service shape and a larger stack only moves the threshold.

  • Where does the parse tree get built in this design?
    In the same functions. Each rule function assembles a node from the values its sub-calls returned and returns it, so the tree grows as the recursion unwinds. Many hand-written parsers skip the literal parse tree and build the abstract syntax node directly, dropping punctuation such as braces that carried structure but carries no meaning afterwards.
  • What stops a hostile input from crashing this parser?
    An explicit nesting-depth counter, incremented on entry to each recursive rule function and checked against a limit. Because the parser stores nesting on the call stack, a document nested thousands of levels deep exhausts the stack before any rule is violated. The limit must be part of the parser, not a bigger stack, and exceeding it should be reported as an ordinary parse error.
  • Why is `match(kind)` worth having when `expect(kind)` exists?
    `match` expresses an optional piece without an error path: it consumes the token only if the kind matches and otherwise leaves the input untouched, returning a boolean the caller branches on. `expect` expresses a required piece and fails loudly. Mixing them up produces either a parser that swallows tokens it should have rejected or one that rejects legal optional syntax.

It is like a receptionist with a folder per department: she glances at the first word on the form, hands it to exactly one colleague, and never opens a folder that is not hers.

saying these in an interview costs you the question

  • Says the parser consumes the next token first, then decides the branch
  • Thinks recursive descent tries alternatives in order and backtracks
  • Believes nesting needs an explicit stack the parser maintains itself
  • Cannot say what expect does when the token kind is wrong
  • Assumes unbounded input nesting is safe because the stack grows
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 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

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

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