skip to content

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%

answer

  1. the conflicts already have behaviour
  2. archaeology before design
  3. pin the current tree with a test
  4. each reversal is a compatibility change
  5. ratchet the count so it cannot rise

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.

solid answer

~60 s

Start from the fact that the parser already has behaviour for every one of those conflicts — a tie-break chose it. So the first work is **archaeology, not design**: for each conflict, build the smallest input that reaches the state, parse it, and pin the resulting tree with a test. That converts a log line into a stated language rule, and it is safe to do before touching anything. Then ask whether the new construct's rules touch any conflicted state. If they do not, add it and fail the build on any **new** conflict. If they do, the choice is between reshaping the rules so the decision is deferrable, declaring the resolution explicitly where the generator supports it, and regenerating with a construction that does not merge states — which only helps if the conflict was a merge artefact. Fixing the whole backlog first is usually the wrong trade: changing a tie-break changes what existing files mean, so each one is a compatibility decision with its own owner and its own migration.

go deeper

for a junior

Take away the habit: a conflict line in a build log is a language decision made for you, so ask what it decided before assuming it is noise.

for a middle

Be able to reach a conflicted state with a real input and read the tree the shipped parser produces, which is the evidence every later decision rests on.

for a senior

Sequence the work so nothing user-visible changes until it is meant to: pin current behaviour with tests, then change one decision at a time.

for a principal

Own it as contract management — which readings the language guarantees, who approves reversing one, what migration each needs, and the build policy that stops the backlog growing again.

## First principle: the conflicts already have behaviour A conflict in a shipped grammar is not an open question — the generator answered it with a default and users have been relying on the answer, possibly for years. That reframes the task. You are not deciding what the language should do; you are discovering what it already does, and then deciding which of those decisions to keep. This matters because the tempting first move — "clean up the conflicts before adding anything" — is a **compatibility change in disguise**. Every tie-break you reverse changes the tree for some existing file. ## The archaeology pass 1. For each conflict, read the state's items and find the token in dispute. 2. Write the shortest input that reaches the state, and parse it with the **shipped** parser. 3. Record the resulting tree as a test. Now the decision is written down and any future regeneration that changes it will fail loudly. 4. Mark each one: intended, unintended-but-relied-upon, or plainly wrong. After this pass the backlog is no longer a risk of unknown size. Nothing has changed for users and the team can reason about each item separately. ## Deciding per conflict | Finding | Sensible move | What it costs | |---|---|---| | The default matches the documented intent | keep it, declare it explicitly if the generator allows | a grammar-file edit, no behaviour change | | Two rules are the same shape for a semantic reason | unify into one nonterminal, classify in a later pass | a change to the pass that consumes the tree | | The conflict vanishes without state merging | either pay for the larger table or separate the contexts | table size, or grammar rework | | The default is wrong and users depend on it | treat as a breaking language change with a migration | notice period, tooling, rewrites of existing files | | The default is wrong and nothing reaches it | fix the rules now | almost nothing — do these first | ## Then, and only then, the new construct - **Does it reach a conflicted state?** If not, this is an ordinary change. Add the rules, regenerate, and check the conflict count is unchanged. - **Does it add a conflict?** Do not resolve it by default. Either shape the rules so the construct is distinguishable at the point of decision, or declare the resolution where that is supported, and pin the intended tree with a test. - **Does it need the parser to know something only a later pass knows?** Then the distinction is not syntactic and no grammar change will fix it — parse a neutral form and decide later. ## The policy that stops the backlog returning The reason a dozen conflicts accumulated is that the build tolerated them silently. Once the archaeology pass is done, the durable move is a **ratchet**: record the current count, fail the build if it rises, and require every conflict that remains to have a test and a one-line note saying which reading it encodes. A conflict with a test is a documented language rule; one without is a trap waiting for whoever changes the grammar next. - Prefer reversible steps: tests first, rules later. - Prefer removing a conflict by making the language clearer over removing it by enlarging the table. - Never reorder rules to make a report go away; that changes which construct disappears without making the parser able to tell them apart. - Treat the grammar as the language's public contract — its readers include every file anyone has already written. ## What a strong answer sounds like Sequencing and ownership, not cleverness. The candidate who says "regenerate with the biggest table construction and see what happens" has missed that most conflicts survive it and that the ones that do not were never grammar defects. The candidate who says "pin the current behaviour, then change one decision at a time with a migration for each" is describing how a language stays maintainable while it keeps shipping.

  • Why not fix the whole backlog before adding the new construct?
    Because each fix can change the tree for files that already exist, so the backlog is a set of compatibility decisions, not a cleanup task. Batching them hides which change broke which file. Pin the behaviour first, then take them one at a time with their own migration.
  • What would make you accept a permanent conflict in the build?
    When the default's reading is the one the language intends, the grammar cannot express it more directly without hurting readability, and a test pins the tree. The conflict is then a documented rule rather than an unknown; the build should still refuse to let the count rise.
  • How do you decide whether a distinction belongs in the grammar at all?
    Ask whether the text alone determines it. If telling the two constructs apart requires knowing what a name was declared as elsewhere, no rule set can do it. Parse a neutral form and let the pass that holds that knowledge classify it.

saying these in an interview costs you the question

  • Proposes clearing every conflict first, without checking existing behaviour
  • Reaches for a larger table construction as the default remedy
  • Treats reversing a tie-break as a refactor rather than a language change
  • Resolves reports by reordering rules until the log is quiet
  • Leaves the resolved decisions undocumented and untested