Why do compiler front ends run a separate scanning stage instead of parsing the raw characters directly?
answer
- weakest machine that suffices
- regular below, stack above
- trivia handled once, not everywhere
- alphabet shrinks to token classes
- boundary leaks on context-dependent splits
basics
~20 sBecause the two jobs need different machine power and different rules. Token shapes are regular, so a finite-state pass handles them in one linear sweep, which keeps whitespace, comments and longest-match logic out of every grammar rule.
solid answer
~40 sSplitting the front end matches each job to the weakest machine that can do it. Token shapes - identifiers, numbers, operators - are **regular**, so one finite-state pass recognises them with no stack and no backtracking beyond a token's length. Nesting is not regular, so the parser needs a stack; making it also handle characters would force whitespace and comment handling into every production and blow up its lookahead. The split also puts longest-match and keyword resolution in exactly one place, and shrinks the parser's alphabet from thousands of characters to a few dozen token classes, which is what keeps parse tables small. The cost is that the boundary leaks: some splitting decisions genuinely need parse context, and those cases have to be patched explicitly.
go deeper
Know that the scanner runs first and hands the parser tokens, and that this is why grammar rules never mention spaces or comments.
Explain the power argument - token shapes are regular and need no stack, nesting is not - and what the split does to the parser's terminal alphabet and rule count.
Name a concrete case where the boundary leaks and describe the patch actually used, such as the parser splitting a compound token or the scanner running under a mode stack.
Weigh a scannerless or context-fed design against the conventional split for a specific language: extensible syntax and error quality on one side, throughput and grammar simplicity on the other.
## Two jobs, two amounts of machine power The front end has two recognition problems, and they sit at different levels of the language hierarchy. - **Token shapes are regular.** An identifier is a letter followed by letters and digits; a number is a run of digits with an optional fractional part. Nothing about them requires counting or remembering how deep you are. A finite-state recogniser handles them in one pass, left to right, with a fixed amount of memory. - **Phrase structure is not regular.** Matching brackets, nesting expressions and blocks inside blocks require unbounded memory - a stack - which is exactly what a parser has. Running the weaker machine first is the design principle. The scanner does the part that a stackless one-pass machine can do, and the parser only deals with what genuinely needs the stack. ## What the split buys | Concern | Handled by the scanner | Cost if the parser did it | |---|---|---| | Whitespace between symbols | skipped once, in one loop | optional-trivia allowed in every rule | | Comments | skipped once | a comment could appear between any two symbols | | Longest match | one rule, applied globally | resolved ad hoc per production | | Keyword versus identifier | one lookup | duplicated across rules | | Terminal alphabet | a few dozen classes | thousands of character values | The last row is the one with the biggest mechanical effect. A parser's tables or its lookahead sets are indexed by terminal symbol. Handing it token classes instead of characters shrinks that dimension by orders of magnitude, and it means a single rule matches `while ( x )` and `while(x)` alike because the two produce the same class sequence. There is a speed argument too. The scanner's inner loop is a table step per character with no allocation and no stack activity, and it is the only stage that touches every character. Everything after it works on a sequence one or two orders of magnitude shorter. ## What the split costs The boundary is not clean, because a few splitting decisions genuinely depend on grammatical context: 1. **Compound closers.** A rule set with both `>` and `>>` will munch `>>` as one token, which is wrong when the source is closing two nested type arguments. The parser knows which it is; the scanner does not. 2. **Strings with embedded expressions.** Inside a string hole, the language is code again - including another string, including another hole. That nesting is not something a flat pass can track without extra state. 3. **Contextual (soft) keywords.** A word that is a keyword only in one syntactic position cannot be classified before the position is known. 4. **Type-name versus variable-name ambiguities.** Some languages need to know what a name *is* before they can decide how a construct parses at all. ## How the leak is patched Each of these is handled by a small, named exception rather than by abandoning the split: - the parser **splits a compound token** it received, when it knows the context; - the scanner runs in **modes**, pushed and popped, so that the rule set in force depends on what construct is open; - ambiguous words are emitted as plain identifiers and **the parser matches on the lexeme text** where it needs to; - in the hardest cases the parser feeds information back and the scanner re-scans a region under a different rule set. A **scannerless** design avoids the leak by writing one grammar over characters, and pays for it with a bigger grammar, weaker error messages at the character level, and the loss of the fast linear pre-pass. That trade is taken deliberately in tools where context-sensitivity dominates - a language with user-extensible syntax, say - and rejected almost everywhere else. ## How to answer this in an interview The strong answer names the power difference first (regular versus context-free), then the engineering consequences (grammar size, table size, one place for trivia and longest match), then admits the leak and names one case. An answer that stops at *it is faster* misses the reason the grammar is readable at all, which is the effect practitioners feel every day.
- Name a decision that the scanner genuinely cannot make on its own.Splitting a compound closing token. With rules for both `>` and `>>`, longest match takes `>>`, which is wrong when the source closes two nested type arguments. Only the parser knows which construct is open, so the usual repair is to emit the compound token and let the parser break it apart where its context permits.
- What does a scannerless design give up?The short terminal alphabet and the fast linear pre-pass. One grammar over characters must allow trivia between symbols wherever it can occur, which enlarges the grammar and complicates its tables, and errors are reported at character granularity rather than at token granularity. In return it can make splitting decisions with full parse context.
- Does the split imply the scanner runs to completion before the parser starts?No. The two stages are usually interleaved, with the parser pulling one token at a time on demand. The split is about responsibility, not about scheduling - and pull-based scanning is what makes mode switching possible, since the parser can change the rule set in force before the next token is requested.
saying these in an interview costs you the question
- Says the only reason for the split is speed
- Claims a parser cannot recognise regular languages at all
- Thinks the token stream is always fully built before parsing begins
- Believes the scanner and parser boundary has no context-dependent cases
- Assumes scannerless parsing is simply wrong rather than a trade