The Precedence Problem

Type 3 + 4 * 2 into any calculator and you'll get 11. Type it into a left-to-right scanner — something that reads one token at a time, evaluates each operation as it encounters it — and you'll get 14. The scanner reads 3, then +, then 4, computes 3 + 4 = 7, then reads *, then 2, computes 7 * 2 = 14. Wrong answer. The multiplication was supposed to happen first.

This isn't a calculator bug. It's a fundamental problem with sequential evaluation. Operator precedence means that some operations need to happen before others, even if they appear after them in the text. Multiplication binds tighter than addition. Exponentiation binds tighter than multiplication. Parentheses override everything. The order you encounter operators is not the order you should evaluate them.

So what breaks? If you try to evaluate left-to-right, you'd need to “remember” the 3 + part, skip ahead to process 4 * 2 first, then come back to add 3 + 8. But how far ahead do you skip? What if there's another higher-precedence operator after the *? What if there are parentheses that change everything? You can't just scan forward arbitrarily — you need a systematic way to defer low-precedence operations until higher-precedence ones have resolved.

Think about what “defer” means here. You encounter + and you can't evaluate it yet — something more urgent might follow. So you save it. Then * arrives and it IS more urgent, so you process it. Then you come back to the + you saved. The operation you saved most recently is the one you come back to last. That's a specific pattern: last saved, first resolved.

That pattern is a stack.

The insight isn't that stacks can evaluate expressions. It's deeper than that: operator precedence is inherently a problem of deferred execution, and deferred execution with LIFO ordering is exactly what stacks do. The stack doesn't just help with expression evaluation — it naturally models the precedence relationships between operators.

But there's an elegant shortcut. What if you could transform the expression into a form where precedence is already baked in — where a simple left-to-right scan gives the correct answer? No precedence comparisons needed, no deferral logic, no parentheses to match. That form exists. It's called postfix notation (or reverse Polish notation), and evaluating it requires nothing more than a single stack and a single pass.

Evaluating Postfix

Before tackling precedence directly, let's start with the easy case. Postfix notation writes operators after their operands: 3 + 4 * 2 becomes 3 4 2 * +. It looks alien at first, but the evaluation rules are almost trivially simple.

You scan tokens left to right. If the token is a number, push it onto a stack. If the token is an operator, pop two operands off the stack, apply the operator, and push the result back. When you reach the end of the expression, the stack holds exactly one value: the answer.

No precedence table. No parentheses to match. No lookahead. Every operator consumes exactly two values from the top of the stack and produces exactly one. The order of the tokens already encodes the evaluation order — that's the whole point of postfix.

Walk through it below. Each number pushes automatically. Each operator will ask you to predict the result before the stack updates.

FIG. 1 — POSTFIX EVALUATION
3
4
2
*
+

Token 3 is a number — push directly to the stack. No decision needed.

— 3 4 2 * + — every operator consumes two, returns one —

Two operators, two predictions, one final result. The * grabbed the two most recent numbers (4 and 2) and produced 8. Then the + grabbed 3 and 8 — the original first operand and the multiplication result — and produced 11. The correct answer fell out of a purely mechanical left-to-right scan. No precedence comparisons happened during evaluation because the postfix ordering already handled it.

The same algorithm handles any combination of operators. With parentheses — (3 + 4) * 2 — the shunting-yard pushes ( onto the operator stack, and when ) arrives, it pops everything back to the matching (. The parentheses override precedence by creating a sub-expression boundary. The postfix for (3 + 4) * 2 is 3 4 + 2 * — addition happens first because the parentheses forced it. The postfix evaluator doesn't change at all. It still scans left to right, pushes numbers, pops two for each operator. The precedence decision happened during conversion, not during evaluation.

That raises the real question: how did 3 + 4 * 2 become 3 4 2 * + in the first place? The conversion is where the stack does its precedence work.

From Infix to Postfix

Dijkstra's shunting-yard algorithm converts infix expressions to postfix using an operator stack. The name comes from railroad switching yards where cars are rerouted onto different tracks — and that's almost literally what happens here. Numbers pass straight through to output. Operators route through a stack that enforces precedence ordering.

The rules are compact. Scan left to right. If the token is a number, send it directly to the output. If it's an operator, compare its precedence with whatever's on top of the operator stack. If the stack top has equal or higher precedence, pop it to the output first — that operator should evaluate before the current one. Then push the current operator onto the stack. When you've scanned every token, drain the remaining operators from the stack into the output.

The critical moment is the precedence comparison. When * arrives and + is on the stack, * has higher precedence — so + stays on the stack and * pushes on top. When the algorithm drains at the end, * pops first (it's on top), then +. The output order reflects evaluation order: higher-precedence operators first.

Step through the conversion of 3 + 4 * 2 below. Each operator decision point is a prediction gate — you decide whether to push or pop before the algorithm executes.

FIG. 2 — SHUNTING-YARD
1
function infixToPostfix(tokens: string[]): string[] {
2
  const output: string[] = []
3
  const ops: string[] = []
4
  const prec: Record<string, number> = { '+': 1, '-': 1, '*': 2, '/': 2 }
5
6
  for (const token of tokens) {
7
    if (!isNaN(Number(token))) {
8
      output.push(token)               // number → output
9
    } else {
10
      while (
11
        ops.length > 0 &&
12
        prec[ops[ops.length - 1]] >= prec[token]
13
      ) {
14
        output.push(ops.pop()!)         // pop higher-prec op
15
      }
16
      ops.push(token)                   // push current op
17
    }
18
  }
19
20
  while (ops.length > 0) {
21
    output.push(ops.pop()!)             // drain remaining ops
22
  }
23
24
  return output
25
}
Output
3

Number 3 goes straight to output — numbers never touch the operator stack.

— 3 + 4 * 2 — operator stack mediates precedence —

Two operator decisions, two correct predictions. The algorithm's logic reduces to a single comparison: is the stack top's precedence greater than or equal to the incoming operator's? If yes, pop. If no, push. That comparison, applied repeatedly, produces the correct postfix order for any expression — including expressions with parentheses, right-associative operators, and nested subexpressions (which the full shunting-yard algorithm handles with minor additions to the same loop).

Building the Evaluator

You've seen postfix evaluation and the shunting-yard conversion. Now build the evaluator yourself. The function below takes an array of postfix tokens and returns the numeric result. Four blanks correspond to the four critical operations: pushing an operand, popping the two operands for an operator (right first, then left), and pushing the result.

Pay attention to the pop order. The first pop() gives you the right operand (the one pushed most recently). The second pop() gives you the left operand. For addition and multiplication this doesn't matter (they're commutative), but for subtraction and division the order is essential: 5 3 - means 5 - 3, not 3 - 5.

FIG. 3 — BUILD THE EVALUATOR
1
function evalPostfix(tokens: string[]): number {
2
const stack: number[] = []
3
4
for (const token of tokens) {
5
if (!isNaN(Number(token))) {
6
___pushOperand___
7
} else {
8
const right = ___popRight___
9
const left = ___popLeft___
10
let result: number
11
switch (token) {
12
case '+': result = left + right; break
13
case '-': result = left - right; break
14
case '*': result = left * right; break
15
case '/': result = Math.trunc(left / right); break
16
default: throw new Error("Unknown op")
17
}
18
___pushResult___
19
}
20
}
21
22
return stack.pop()!
23
}
— Four blanks — push, pop right, pop left, push result —

Four blanks, one function, and you've built a complete postfix evaluator. Push numbers, pop pairs when operators arrive, push results. The same loop handles any expression — 2 3 + 4 *, 10 6 9 3 + - 11 * / *, arbitrary nesting — because postfix ordering already encodes the evaluation sequence. The stack just executes it mechanically.

That's the full expression evaluation pipeline: the shunting-yard algorithm converts infix to postfix (handling precedence), and the postfix evaluator runs through the result in a single pass (handling computation). Two stacks, two phases, zero parentheses. Every expression evaluator — from HP's RPN calculators to the bytecode interpreters inside Python, Java, and JavaScript — uses some variation of this pattern.