skip to content

In a precedence-climbing parser, why does a prefix operator need its own binding power, separate from the infix operator spelled the same way?

level: seniorimportance: should knowfreq 40%

answer

  1. fixity decides where it is consumed
  2. no left operand to grip
  3. one symbol, two table entries
  4. the power sets the argument's reach
  5. postfix wraps and continues, no recursion

basics

~20 s

A prefix operator has no left operand, so it carries a single power of its own, used as the floor when parsing its argument. That number decides how far right the argument reaches, and it is usually higher than the infix power of the same symbol.

solid answer

~50 s

A prefix operator appears where an operand is expected, so the operand step consumes it and calls the expression parser recursively with the prefix operator's **own** power to build its argument. That power is the argument's reach: with unary minus at 25, `+` at left power 10 and `^` at left power 30, the sub-parse refuses the addition but absorbs the exponent, giving `(-a) + b` and `-(a ^ b)` — the conventional mathematical reading. Reuse the infix minus power of 10 instead and `-a + b` silently becomes `-(a + b)`, because 10 clears a floor of 10. So one symbol gets two entries in the table, chosen by position: prefix where an operand was expected, infix after a complete operand. A postfix operator is the mirror case — only a left power, handled inside the loop with no recursion at all.

code

pseudocode · 20 lines
pseudocode
function parse_operand():
    t = take_token()
    if t is a literal or a name:
        return leaf(t)
    if t = "(":
        inner = parse_expr(0)
        expect(")")
        return inner
    if t is a prefix operator:
        arg = parse_expr(prefix_power(t))   # its own power is the floor
        return node(t, arg)
    error("expected an operand")

# inside parse_expr's loop, before the infix branch:
    if op is a postfix operator:
        if left_power(op) < min_bp:
            break
        take_token()
        left = node(op, left)
        continue                           # no right operand to parse

go deeper

for a junior

Know that unary minus and binary minus are different operators that happen to share a symbol, and that a parser distinguishes them by where the symbol appears.

for a middle

Explain that the prefix power is the floor for its argument, and show with one expression how changing that number moves unary minus above or below another operator.

for a senior

Recognise the failure signature: a fixity or power mistake here is accepted input with a wrong tree, so you look for value regressions over stored expressions rather than for parse errors.

for a principal

Own the fixity table as a published contract. Where unary minus sits relative to exponentiation is a language design decision users will rely on, so it is chosen and documented, not inherited by accident.

## Three fixities, two places in the parser A precedence-climbing parser has exactly two places where a token can be consumed, and an operator's fixity decides which one sees it. - **Infix** operators (`a + b`) have an operand on both sides. They are consumed by the loop, which already holds the left operand and recurses for the right one. - **Prefix** operators (`-a`) have no left operand. They appear exactly where an operand is expected, so the **operand step** consumes them and then calls the expression parser recursively to build the argument. - **Postfix** operators (`a!`) have no right operand. They are consumed by the loop, like an infix operator, but the branch wraps the left operand and continues without recursing. The position tells the parser which entry to use. A token found where an operand was expected can only be a prefix operator; the same token found after a complete operand can only be an infix or postfix one. That is why one symbol may hold two table entries with unrelated numbers. ## A prefix operator's power is the reach of its argument The single number a prefix operator carries is used as the floor for the recursion that parses its argument, so it decides how much of what follows is swallowed. Take unary minus at 25, against `+` at left power 10, `*` at left power 20 and `^` at left power 30: | input | floor for the argument | next operator's left power | result | |---|---|---|---| | `-a + b` | 25 | 10 — refused | `(-a) + b` | | `-a * b` | 25 | 20 — refused | `(-a) * b` | | `-a ^ b` | 25 | 30 — absorbed | `-(a ^ b)` | One number places unary minus between multiplication and exponentiation, which is the reading almost every expression language exposes. Move the number and the language changes shape: at 35 the last row becomes `(-a) ^ b`, and at 5 the first row becomes `-(a + b)`. Notice what the prefix power does **not** control: how tightly the resulting node grips anything on its left. By the time the node exists, it is simply the left operand in the caller's loop, and the caller's floor governs from there. ## One symbol, two entries The minus sign is the standard example. As an infix operator it might sit at left power 10 and right power 11; as a prefix operator it sits at 25. A parser that reuses the infix number for the prefix case produces a bug that is not a parse error: - With a prefix floor of 10, the argument of `-a + b` sees `+` at left power 10, which clears a floor of 10, so the addition is pulled inside and the tree becomes `-(a + b)`. - Over ordinary arithmetic `-(a + b)` and `(-a) + b` disagree for almost every input, and nothing in the parser complains. ## Postfix: a loop branch with no recursion A postfix operator carries only a left power. Inside the loop it is treated like an infix operator up to the point of consumption — its left power is compared against the current floor, so it can be refused and left for an outer call — but once consumed there is no right operand to parse. The branch wraps the current node and continues the loop, which is what makes chains such as `x!!` fall out for free: the second occurrence is just the next turn of the same loop. | fixity | left power | right power | consumed where | |---|---|---|---| | infix | yes | yes | the loop, recursing for the right operand | | prefix | none | acts as the argument's floor | the operand step | | postfix | yes | none | the loop, wrapping and continuing | ## The bugs this shape produces 1. **Prefix power set too low.** The argument absorbs operators that should have stayed outside it, as in `-a + b` becoming `-(a + b)`. 2. **Prefix power shared with the infix entry.** The same bug, arrived at by omission rather than by choice, because an equal power clears the floor. 3. **Prefix handled in the loop.** The loop only runs once a left operand exists, so a leading `-` is never reached and the input is rejected as a missing operand. 4. **Postfix given a right-operand recursion.** The recursion consumes whatever follows, so the postfix node steals the next operand from the surrounding expression. Each of these is a table or dispatch error, not a control-flow error, which is the general character of defects in this style of parser: the loop stays right and the numbers go wrong.

  • What breaks if a prefix operator's power is set too low?
    Its argument reaches too far right. With unary minus at power 5 and addition at left power 10, the argument recursion accepts the addition, so `-a + b` parses as `-(a + b)` instead of `(-a) + b`. The input is still accepted, so the defect shows up only as wrong values.
  • How is a postfix operator's power used, given it has no right operand?
    Only its left power participates. The loop compares it against the current floor exactly as for an infix operator, so an outer call can still claim it; once consumed, the branch wraps the existing node and continues. Setting that power high makes the postfix operator bind tighter than the surrounding infix operators.
  • How does the parser know whether a minus sign is prefix or infix?
    By position, not by the token itself. If the parser is looking for an operand, the only legal reading is prefix; if it already holds a complete operand and is looking for an operator, the only legal reading is infix or postfix. The two entries never compete, because the two states never overlap.

saying these in an interview costs you the question

  • Handles the prefix case in the operator loop rather than the operand step
  • Gives the prefix minus the same power as the infix minus
  • Thinks a postfix operator needs a right-operand recursion
  • Assumes one symbol can hold only one entry in the table
  • Believes the prefix power changes how the node binds on its left