skip to content

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

level: seniorimportance: must knowfreq 54%

answer

  1. order of alternatives, not input length
  2. the shorter literal is written first
  3. first success wins, longer never tried
  4. no longest-match rule applies here
  5. longer alternative first, boundary predicate second

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.

solid answer

~40 s

This is prefix shadowing. The name rule reads `"note" / "notebook"`, and at the position after `@` the first alternative succeeds after four characters, so the second is never attempted; the enclosing rule resumes at `book`, which is left to whatever matches ordinary text. Nothing applies a longest-match preference, because a scannerless parser has no separate stage that could have grouped `notebook` into one unit first. Two changes fix it, and they do different jobs: write the longer alternative above the shorter one so it gets the first attempt, and add a not-predicate such as `!Letter` after the choice so that a name like `notepad` is rejected outright instead of being silently truncated to `note`.

code

pseudocode · 15 lines
pseudocode
Annotation <- "@" Name
Name       <- "note" / "notebook"     # bug: second alternative unreachable

# on "@notebook":
#   "@"  matches, position 1
#   Name tries "note"  -> succeeds, position 5
#   choice returns; "notebook" is never attempted
#   caller resumes at "book"

Name <- "notebook" / "note"           # fix 1: longer alternative first
Name <- ("notebook" / "note") !Letter # fix 2: also reject "@notepad"

# fix 2 alone does NOT work:
#   ("note" / "notebook") !Letter  commits to "note",
#   !Letter fails at "b", and the committed choice is not re-entered

go deeper

for a junior

Recall that the alternatives are tried in the written order and the first success ends the choice, so a short name listed above a longer one hides it.

for a middle

Explain the two mechanics involved: no alternative is chosen for being longer, and an alternative that has already succeeded is not revisited when the rest of the rule fails.

for a senior

Diagnose it from the symptom — stray text after a truncated name, and an error reported far from the cause — then fix ordering and boundary separately, and add a test per alternative.

for a principal

Decide whether a hand-ordered list of names is the right long-term shape at all, or whether the grammar should match a generic name and leave recognition to a checked step that scales as names are added.

## The defect The grammar looks reasonable on the page: ``` Annotation <- "@" Name Name <- "note" / "notebook" ``` At the position after `@` in `@notebook`, the choice tries `"note"` first. It succeeds, consuming four characters, and the choice returns that result. The alternative `"notebook"` is **never attempted at that position** — not because it would fail, but because nothing asks it to. `Annotation` then completes, and the caller resumes at `book`, which the surrounding text rule happily swallows. The document parses; it just parses into the wrong shape, which is the worst failure mode a grammar has. Two properties combine to produce it: - **First success wins.** Ordered choice compares position in the list, never how much input an alternative would consume. There is no longest-match rule anywhere in the formalism. - **A succeeded expression is committed.** Even when the enclosing rule later fails, the failure propagates outward to the nearest enclosing choice rather than back into the one that already returned. ## Why no earlier stage saved you In a design where a separate stage first groups characters into units, that stage would have produced one unit for the whole word before the grammar ever saw it, and a literal comparison against `note` would simply not match. A **scannerless** parser has no such stage: its rules match raw characters, and the only grouping that exists is the grouping the rules perform. So every place where one name is a prefix of another is a place where the grammar author, not the machine, has to decide what a boundary means. ## The two fixes, and what each one is for ``` Name <- "notebook" / "note" (1) longer alternative first Name <- ("notebook" / "note") !Letter (2) also require a boundary ``` 1. **Ordering** handles names that are both in the list. With `"notebook"` written first, `@notebook` matches the long name and `@note` still matches the short one, because the long alternative fails on that input and the choice falls through. 2. **The not-predicate** handles names that are *not* in the list. `!Letter` succeeds only when the next character is not a letter, and consumes nothing. On `@notepad`, `"notebook"` fails, `"note"` succeeds, and `!Letter` then fails at `p`, so the whole annotation is rejected — a loud error instead of a silent truncation. The order of these two fixes matters: the predicate alone does **not** repair the bug. With `("note" / "notebook") !Letter`, the choice still commits to `"note"` on `@notebook`, the predicate fails at `b`, and because a committed choice is not re-entered, the rule fails on a perfectly valid annotation. The predicate makes the grammar stricter; only the ordering makes the long name reachable. ## A third option: stop letting the choice decide how much to consume ``` Name <- Letter+ ``` One rule consumes the whole word, and a later step checks which annotation the text names. The hazard disappears because no choice is deciding a length any more. | Approach | Unknown name behaves as | Grows well with many names | |---|---|---| | Longest-first choice | parse error at the annotation | ordering must be maintained by hand | | Choice plus `!Letter` | parse error, reported at the name | same ordering duty, stricter boundary | | Generic name plus later check | parses, then fails a semantic check | new names need no grammar edit | The third row is usually what a maintained syntax settles on, because it also gives a better message: the parser can say *which* name it did not recognise, rather than pointing at the character where the last alternative died. ## What the symptom looks like in practice - A name that is a strict prefix of another silently wins, and the remainder shows up as stray text in the output. - Adding a new name to the end of a choice list appears to do nothing, because an earlier alternative already covers its prefix. - The reported error position is far from the real cause, since failure surfaces wherever the truncated remainder finally breaks a rule. - Reordering the list for tidiness changes behaviour, which is why alternative order belongs in review rather than in a formatting pass. The habit worth acquiring: whenever alternatives in one choice share a prefix, decide explicitly what should happen at the boundary, and encode that decision with ordering plus a predicate — the grammar will not raise the question for you.

  • Would matching the name as one generic run of letters avoid the problem entirely?
    Yes. A rule such as `Name <- Letter+` consumes the whole word, so no choice is deciding how much input to take and the ordering hazard disappears. The cost is that an unrecognised name becomes a semantic error checked after parsing rather than a parse failure, which usually produces a better message anyway.
  • Why can't the parser recover by backtracking once the rest of the annotation fails?
    Because an expression that has succeeded is committed. The failure of a later element propagates outward to the nearest enclosing choice that still has untried alternatives; it does not re-enter a choice that has already returned a result, so the shorter alternative is never reconsidered.
  • How would you catch this class of bug before it ships?
    Test every alternative of every choice at least once with input that only that alternative should accept. A shadowed alternative is unreachable, so its test fails immediately, whereas reading the grammar tends not to reveal it because each line looks correct on its own.

saying these in an interview costs you the question

  • Says the parser will prefer the longest matching alternative
  • Expects backtracking into the choice after a later rule fails
  • Blames a tokenizing stage that a scannerless parser does not have
  • Thinks adding the longer alternative anywhere in the list is enough
  • Claims the grammar is ambiguous and should have been rejected
  • Believes a boundary predicate alone repairs the shadowing