skip to content

How do you rewrite a flat expression grammar so that operator precedence and associativity follow from the rule shape?

level: middleimportance: should knowfreq 52%

answer

  1. one layer per binding strength
  2. tighter operators sit lower down
  3. expression, term, factor
  4. left recursion groups to the left
  5. brackets at the bottom re-enter the top

basics

~20 s

Split the single expression rule into one layer per precedence level — expression, term, factor — so a tighter operator is reachable only by descending. Recurse on the left for left-associative operators and on the right for right-associative ones.

solid answer

~50 s

Replace the flat `E -> E op E | atom` with a layer per binding strength: `expr -> expr '+' term | expr '-' term | term`, then `term -> term '*' factor | term '/' factor | factor`, then `factor -> number | '(' expr ')'`. Because a `*` can only be reached by first descending from `expr` into `term`, the string `1 + 2 * 3` has exactly one tree, with the multiplication beneath the addition — precedence is now the shape of the rules rather than a setting somewhere. Associativity comes from which side recurses: `term -> term '*' factor` recurses left, so a chain groups left; a right-associative operator is written `factor -> base '^' factor | base` instead. Brackets sit in the bottom layer and re-enter the top, which is how a user overrides the layering.

code

pseudocode · 8 lines
pseudocode
expr   -> expr '+' term   | expr '-' term   | term
term   -> term '*' factor | term '/' factor | factor
factor -> base '^' factor | base
base   -> number | '(' expr ')'

-- 1 + 2 * 3   has one tree: 1 + (2 * 3)
-- 8 - 3 - 2   has one tree: (8 - 3) - 2   [left recursion]
-- 2 ^ 3 ^ 2   has one tree: 2 ^ (3 ^ 2)   [right recursion]

go deeper

for a junior

Remember the three-layer skeleton by name — expression, term, factor — and that the tighter operator lives in the lower rule. Being able to write it down is the expectation here.

for a middle

Explain the mechanism, not just the shape: a tighter operator is reachable only by descending, and the recursive side of each rule decides the grouping. Show the non-associative variant too.

for a senior

Weigh the construction against its costs — layer count, descent depth, non-local edits — and know that it preserves the accepted language exactly, which is what makes it safe to apply to a grammar whose text is already stored.

for a principal

Decide where the meaning of an expression language is specified at all. Encoding it in the grammar makes it reviewable and reimplementable; leaving it to each parser's settings makes it a per-implementation detail that will drift.

## Precedence is a shape, not a setting The flat rule `E -> E '+' E | E '*' E | number` is ambiguous twice over: `1 + 2 * 3` has one tree with the multiplication on top and another with the addition on top, and `1 - 2 - 3` has one tree grouping left and another grouping right. Neither problem is solved by telling a parser what to prefer — that only hides it behind an implementation. The repair is to write a different grammar for the same language, one whose *structure* permits only the tree you want. ## The layered rule set The standard construction gives each precedence level its own nonterminal, ordered from loosest at the top to tightest at the bottom: ``` expr -> expr '+' term | expr '-' term | term term -> term '*' factor | term '/' factor | factor factor -> base '^' factor | base base -> number | '(' expr ')' ``` Two mechanisms are at work, and they are worth separating because interviews probe them separately. **Precedence comes from the descent.** An `expr` may contain a `+` directly, but to reach a `*` it must first go through `term`. So in `1 + 2 * 3`, the multiplication can only appear as one of the addition's operands, never the other way round: the tree is `1 + (2 * 3)` and there is no second one. Operators of the *same* precedence share a layer — `+` and `-` are alternatives of `expr`, not layers of their own. **Associativity comes from the recursive side.** At each layer, exactly one side of the operator recurses back to the same nonterminal and the other side drops to the layer below: | Rule shape | Grouping of `a op b op c` | Note | |---|---|---| | `A -> A op B`, else `B` | `(a op b) op c` | left-associative; the standard shape for `+ - * /` | | `A -> B op A`, else `B` | `a op (b op c)` | right-associative; used for exponent-style and assignment-style operators | | `A -> B op B`, else `B` | not derivable | non-associative; chaining is a syntax error | | `A -> A op A`, else `B` | both | ambiguous — this is the shape you are removing | So `term -> term '*' factor` forces `2 * 3 * 4` to group as `(2 * 3) * 4`, while `factor -> base '^' factor` forces `2 ^ 3 ^ 2` to group as `2 ^ (3 ^ 2)`. ## Where brackets fit The bottom layer contains `'(' expr ')'`, which jumps straight back to the top. That is deliberate: - It gives a user the only escape from the layering, so `(1 + 2) * 3` becomes writable. - It does not re-open the ambiguity, because the recursion is fenced by two literal terminals — every bracketed group has a determined extent, so no string gains a second tree. - It keeps the atom rule tiny, which is where you later add other primaries such as names or calls. ## Costs and limits The construction is not free, and a good answer names the price: - **One layer per precedence level.** A language with a dozen levels needs a dozen nonterminals, and deriving a bare number walks all twelve. The cost is paid in rule count and in descent depth, not in correctness. - **Editing the ladder is not local.** Inserting a new precedence level between two existing ones means re-pointing the layer above it and the layer below it, so the grammar has to be read as a whole. - **It removes ambiguity only where it is applied.** Layering the arithmetic operators does nothing for an unrelated ambiguity elsewhere in the grammar, such as an optional trailing clause that could attach to two different constructs. - **Unary operators need care.** A prefix operator usually gets its own layer just above the atoms so that it binds tighter than the binary operators but still allows a bracketed operand underneath. ## Why this is the answer interviewers want The question is really "do you understand that a grammar encodes meaning, not just syntax?". A candidate who says "set the precedence in the parser" has located the meaning in an implementation, where a second implementation cannot see it. A candidate who writes three rules and points at which side recurses has put the meaning where every reader of the grammar — including a future reimplementation — is forced to find it. That is also why the layered grammar is the right fix when rules written in the language are already stored: it changes the number of trees per string without changing which strings are legal, so nothing that was written before becomes unparseable.

  • How do you make an operator non-associative, so that a chain of it is a syntax error?
    Recurse on neither side: write the layer as `rel -> sum op sum | sum`. Both operands drop to the layer below, so `a op b op c` has no derivation at all and the parser rejects it. This is how comparison-style operators are kept from chaining.
  • What happens to the grammar as the operator set grows to a dozen precedence levels?
    You get a dozen nonterminals and a dozen-step descent for every atom. The grammar stays unambiguous and correct; the cost is rule count, reading effort and the non-local edit needed to insert a new level between two existing ones.
  • Does layering the rules change which strings the language accepts?
    No. The layered grammar generates exactly the same set of strings as the flat one; it just derives each of them in exactly one way. That is precisely why it is the safe fix when text written in the old grammar is already stored somewhere.

saying these in an interview costs you the question

  • Says the order operators are listed in one rule sets their precedence
  • Puts precedence in the parser rather than in the grammar's shape
  • Believes left and right recursion produce the same grouping
  • Claims a layered grammar no longer needs brackets
  • Says every operator needs a precedence level of its own