Shunting-Yard Algorithm
Unary Operators
Algorithm Modification
Computer Science
Expression Parsing

How can I modify my Shunting-Yard Algorithm so it accepts unary operators?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The standard shunting-yard algorithm works well for binary operators such as +, -, *, and /, but it needs one extra rule to handle unary operators correctly. The key is to classify a token like - based on context, then give the unary form its own operator entry with its own precedence and associativity.

Why Unary Operators Break A Naive Parser

In infix expressions, the same symbol can mean different things. The character - is subtraction in 3 - 2, but negation in -2 or 5 * -x. A naive shunting-yard implementation usually treats every - as binary, which causes incorrect postfix output or stack underflow during evaluation.

The parser therefore needs to answer one question while scanning tokens:

  • does this operator expect one operand or two

That decision can be made locally from the previous token type.

Detect Unary Operators From Context

A - should usually be treated as unary if it appears:

  • at the beginning of the expression
  • after another operator
  • after a left parenthesis

It is binary if it appears after a number, identifier, or right parenthesis.

Here is a simple tokenizer and parser-oriented classifier in Python:

python
1from dataclasses import dataclass
2
3@dataclass
4class Token:
5    kind: str
6    value: str
7
8
9def classify_minus(previous_kind):
10    if previous_kind in {None, "operator", "lparen"}:
11        return Token("operator", "NEG")
12    return Token("operator", "-")

Using a separate internal symbol such as NEG is the cleanest approach. It avoids overloading - with two meanings deeper in the algorithm.

Give Unary Operators Their Own Precedence

Once unary minus becomes NEG, define it like any other operator. A common choice is:

  • higher precedence than multiplication and division
  • right associativity
  • arity of one
python
1OPERATORS = {
2    "+": {"prec": 1, "assoc": "left", "arity": 2},
3    "-": {"prec": 1, "assoc": "left", "arity": 2},
4    "*": {"prec": 2, "assoc": "left", "arity": 2},
5    "/": {"prec": 2, "assoc": "left", "arity": 2},
6    "NEG": {"prec": 3, "assoc": "right", "arity": 1},
7}

Right associativity matters for chains such as --x. Without it, the stack pop rules can produce the wrong order.

Adapt The Shunting-Yard Loop

The normal operator-handling logic still applies. The difference is that NEG is now just another operator with its own precedence.

python
1def to_postfix(tokens):
2    output = []
3    stack = []
4    previous_kind = None
5
6    for raw in tokens:
7        if raw.isdigit():
8            output.append(raw)
9            previous_kind = "operand"
10        elif raw == "(":
11            stack.append(raw)
12            previous_kind = "lparen"
13        elif raw == ")":
14            while stack and stack[-1] != "(":
15                output.append(stack.pop())
16            stack.pop()
17            previous_kind = "rparen"
18        else:
19            op = "NEG" if raw == "-" and previous_kind in {None, "operator", "lparen"} else raw
20            while stack and stack[-1] in OPERATORS:
21                top = OPERATORS[stack[-1]]
22                cur = OPERATORS[op]
23                should_pop = (
24                    top["prec"] > cur["prec"] or
25                    (top["prec"] == cur["prec"] and cur["assoc"] == "left")
26                )
27                if not should_pop:
28                    break
29                output.append(stack.pop())
30            stack.append(op)
31            previous_kind = "operator"
32
33    while stack:
34        output.append(stack.pop())
35
36    return output
37
38
39print(to_postfix(["-", "3", "+", "4"]))
40print(to_postfix(["5", "*", "-", "2"]))

This prints postfix forms where unary minus is explicit, such as ['3', 'NEG', '4', '+'].

Evaluating The Result

Your postfix evaluator must also understand operator arity.

python
1def eval_postfix(tokens):
2    stack = []
3    for token in tokens:
4        if token.isdigit():
5            stack.append(int(token))
6        elif token == "NEG":
7            stack.append(-stack.pop())
8        else:
9            b = stack.pop()
10            a = stack.pop()
11            stack.append({"+": a + b, "-": a - b, "*": a * b, "/": a / b}[token])
12    return stack[-1]
13
14expr = to_postfix(["5", "*", "-", "2"])
15print(expr)
16print(eval_postfix(expr))

The evaluator logic becomes simpler once unary and binary operators are no longer ambiguous.

Common Pitfalls

The most common mistake is trying to infer unary minus too late, after operators are already on the stack. Classification should happen when the token is first read.

Another mistake is keeping unary minus under the same - symbol internally. That creates confusion in both precedence rules and postfix evaluation.

A third issue is forgetting associativity. Unary operators are usually right-associative, and treating them as left-associative can break repeated unary expressions.

Summary

  • Detect unary operators from token context, not from the symbol alone.
  • Convert unary minus into a separate internal operator such as NEG.
  • Give unary operators their own precedence, associativity, and arity.
  • Keep the main shunting-yard loop almost unchanged once the operator table is correct.
  • Update the postfix evaluator so unary operators consume one operand instead of two.

Course illustration
Course illustration

All Rights Reserved.