In an editor that re-scans a buffer after every keystroke, why can typing one character re-tokenize the rest of the file?
answer
- state is carried, not local
- one character can open a mode
- checkpoint per line
- compare state and boundary together
- stop at the first re-convergence
basics
~20 sBecause the scanner carries state forward. One character can open a comment or a string, and every token after it is then classified under a different rule set, so the change propagates to the end of the buffer.
solid answer
~40 sA scanner's output at any position depends on the state it arrived in, and most characters leave that state unchanged - but a few do not. Typing an opening comment marker or an unmatched quote switches the mode for everything that follows, which is why one keystroke can repaint an entire file. An incremental scanner bounds the work by caching, per line or per region, the **scanner state at that point** plus the token boundaries. After an edit it restores the cached state just before the edited region and re-scans forward, comparing each new token's `(state, offset)` against the previous run. The first time both match, the old tokens from there on are still valid and re-scanning stops; until then it keeps going, in the worst case to the end.
go deeper
Know that scanning carries state from left to right, so a character that opens a comment or a string changes how everything after it is read.
Explain the checkpoint idea: store the scanner's carried state per line so a re-scan can start near the edit instead of at the top of the file.
Describe the re-convergence test on both carried state and token boundary, why either alone is unsound, and the span shift that unchanged tokens still need.
Decide what the checkpoint granularity and the worst case should be for the product: a transient full-buffer pass on an unmatched quote is usually the right trade against a more complex incremental scheme.
## Why the blast radius exceeds the edit Scanning is a left-to-right pass with carried state: the rule set in force, any open mode, any nesting depth. For most characters the state before and after an edit is identical, so the tokens after it are unchanged except for a shift in their offsets. But a handful of characters change the carried state itself: - an opening block-comment marker turns the remainder of the buffer into comment text until a closing marker is found; - an unmatched quote opens a string that runs to the end of the line, or to the end of the file in a multi-line string syntax; - deleting a closing marker has the same effect in reverse; - opening or closing an embedded-expression hole flips the mode stack for everything after it. That is the honest answer to the question: not every keystroke propagates, but the ones that alter carried state propagate to wherever the state next re-converges, which may be nowhere before end of input. ## What an incremental scanner caches To avoid re-scanning from the top of the file on every keystroke, the scanner stores checkpoints. A common arrangement is one checkpoint per line holding: - the scanner's full carried state at the line's first character - mode stack and any nesting depth, not just a mode name; - the offset that state corresponds to; - optionally the tokens produced on that line, so unchanged lines need no re-scan at all. The cache is what makes the work proportional to the damage rather than to the file. ## The re-scan algorithm 1. Find the checkpoint at or before the start of the edited region and restore its state; everything before it is untouched by construction. 2. Shift the offsets of all cached checkpoints after the edit by the edit's length delta, so old and new positions are comparable. 3. Re-scan forward from the checkpoint, emitting tokens. 4. After each emitted token, compare the pair `(carried state, offset)` with the pair recorded in the previous run at that shifted offset. 5. The first time both halves match, the scan has **re-converged**: from here on the old tokens are identical to what a full re-scan would produce, so stop and keep them. 6. If re-convergence never happens, the scan runs to end of input - which is exactly the unmatched-quote case. Step 4 is where the correctness lives, and both halves are needed. Matching offsets alone is not enough: the same position can be reached in a different mode, and then every token after it differs. Matching state alone is not enough either, since a token boundary that has shifted means the old tokens no longer line up with the text. ## What it buys, in practice | Edit | Typical re-scan extent | |---|---| | A letter inside an identifier | one token, then immediate re-convergence | | Inserting a space | one or two tokens | | Typing an opening comment marker | to the next closing marker, or end of file | | Typing one quote character | to the end of that string's scope, often end of file | | Deleting a closing brace of an embedded hole | end of file, because the mode stack never empties | So the common case is a few tokens and the pathological case is the whole buffer - and the pathological case is transient, because the user is usually one keystroke away from typing the closer that restores convergence. This is why a highlighter can visibly repaint an entire file for a moment when a quote is typed, then settle. ## Tokens are not the only thing that moves Even where tokens are unchanged, their **spans** are not: everything after the edit shifts by the delta. An implementation either rewrites the offsets or stores spans relative to a checkpoint so that shifting a checkpoint moves its tokens implicitly. Getting this wrong produces the classic symptom of decorations drifting one character further off with each edit, while the token classes themselves remain perfectly correct. ## The judgment this question is really probing The interviewer is checking whether the candidate sees tokenization as a stateful pass rather than a pure function of a window of text. The follow-through - cache the carried state, compare state and boundary, accept an unbounded worst case that resolves itself - is the design every incremental front end converges on, and the same convergence argument is what later stages reuse when they try to reuse a subtree instead of a token.
- Why must the re-convergence test compare both the carried state and the token boundary?Because either alone admits a wrong answer. The same offset can be reached with a different mode or nesting depth, and then every token after it is classified differently. Conversely the same state at a shifted boundary means the old tokens no longer line up with the text. Only both together prove the remaining old tokens are still exactly right.
- What has to happen to the tokens that are not re-scanned?Their spans shift by the edit's length delta even though their classes and lexemes are unchanged. Implementations either rewrite the offsets of everything after the edit or store spans relative to a checkpoint so moving the checkpoint moves its tokens implicitly. Skipping this is what makes editor decorations drift further off with every keystroke.
- Is the worst case acceptable if it is the whole buffer?Usually yes, because it is transient and linear. The pathological edits - an unmatched quote or an unclosed comment - are one keystroke away from being closed again, and one linear pass over a buffer is fast compared with a keystroke interval. The cache is there to keep the common case proportional to the edit, not to eliminate the worst case.
saying these in an interview costs you the question
- Thinks re-scanning only the edited line is always sufficient
- Treats tokenization as a pure function of nearby characters
- Compares only offsets when deciding that old tokens can be reused
- Caches a mode name but not the nesting depth or mode stack
- Forgets that unchanged tokens still need their spans shifted