Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°68

Read the Codebase, Fix the Bug

Medium AI EngineeringCode ReviewDebugging

The real skill an AI can’t fake for you: drop into unfamiliar code, trace how the pieces call each other, and fix the one that’s wrong without breaking the rest. Here a reverse-Polish calculator is spread across three functions and gets subtraction and division backwards. Read how operands flow from the loop into the math, then fix it at the source.

The problem

The starter code is a small RPN (reverse-Polish) evaluator in three functions: _is_op classifies a token, rpn runs the stack loop, and _apply does the arithmetic. rpn pops the top of the stack into b and the next into a, then calls _apply(op, a, b) — so for ["5", "2", "-"] it means a - b = 5 - 2 = 3. But _apply computes subtraction and division with the operands reversed, so those two operators give the wrong answer. Trace the operand order from rpn into _apply and fix the two buggy branches. Return the final value on the stack.

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

This RPN evaluator gets subtraction and division backwards. Read how rpn feeds operands into _apply, then fix the two reversed branches in _apply — without touching rpn’s (correct) pop order.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input rpn(['3', '4', '+'])
Output 7.0
addition is fine — order does not matter, so this already passes
EXAMPLE 2
Input rpn(['5', '2', '-'])
Output 3.0
the bug: _apply returns b - a = 2 - 5 = -3. Correct is a - b = 3
EXAMPLE 3
Input rpn(['10', '2', '3', '-', '*'])
Output -10.0
10 * (2 - 3) = -10 — a multi-op expression that only works once subtraction is fixed
EXAMPLE 4
Input rpn(['20', '5', '-', '3', '-'])
Output 12.0
edge case: chained subtraction runs left to right, (20 - 5) - 3; the reversed branches give 18.0
CONSTRAINTS
  • Do not change the operand order in rpn — it is correct (b = top of stack, a = the one below). The bug is in _apply.
  • Fix _apply so "-" returns a - b and "/" returns a / b. Leave "+" and "*" alone.
  • Non-operator tokens are pushed as float; the result is the single value left on the stack.
  • Assume well-formed input — no need to handle empty stacks or bad tokens.
Hints 4 ideas
HINTS — 4 IDEAS
  1. Start in rpn: it pops b (top) then a (next), then calls _apply(op, a, b). So inside _apply, a is the left operand and b is the right.
  2. For ["5","2","-"], the stack is [5, 2]; b=2, a=5. You want a - b = 5 - 2 = 3.
  3. The current _apply returns b - a and b / a — both reversed. Swap them to a - b and a / b.
  4. Addition and multiplication are commutative, which is why only "-" and "/" show the bug.
Approach, complexity & discussion — open after you solve

Start from brute force

The quickest-looking fix is at the call site: swap the two pops in rpn so a is the top of the stack and b the one below. Two reversals cancel, and all seven hidden tests pass. The evaluator still runs in O(n) time and O(n) stack space, so the cost does not change; what changes is which function is wrong. _apply(op, a, b) still computes b - a, so its contract stays broken: any other caller that passes left then right, or a direct test such as _apply("-", 5, 2), still gets -3. The patch turns the tests green while the bug stays where it lives, and it breaks the rule that rpn must not change.

The approach

Read the contract before the code. In rpn, an operator token pops b first, the top of the stack and the right operand, then pops a, the value pushed earlier and the left operand, and calls _apply(t, a, b). So the call promises _apply(op, left, right). Checking each branch against that promise finds the defect: + and * are commutative and come out right either way, while the stub’s - and / branches return b - a and b / a. The reference changes exactly those two lines to a - b and a / b and leaves _is_op and rpn untouched.

Why this is the right place: after each token, stack holds the values of the complete subexpressions read so far, in left-to-right order, so the value below the top is always the left operand. Confirm with a case where a wrong subtraction feeds another operator, 10 2 3 - *, and with chained subtraction, 20 5 - 3 -, which must give 12. The TypeScript apply gets the same two-line fix.

Walk through an example

The third example, ["10", "2", "3", "-", "*"], shows one reversed subtraction poisoning the multiply that consumes it; the last column is what the buggy stub computes.

steptokenaction (fixed)stack after (fixed)stack after (stub)
110push 10.0[10.0][10.0]
22push 2.0[10.0, 2.0][10.0, 2.0]
33push 3.0[10.0, 2.0, 3.0][10.0, 2.0, 3.0]
4-pop b = 3.0, a = 2.0; a - b = -1.0[10.0, -1.0][10.0, 1.0] (computed b - a)
5*pop b = -1.0, a = 10.0; a * b = -10.0[-10.0][10.0]
6endreturn stack[-1]-10.010.0

Complexity

O(n) time for n tokens: each token is classified once, each operand is pushed once, and each operator does two pops and one push. O(n) space for stack, which peaks when operands come first, as in 1 2 3 + +. The swap-the-pops patch costs exactly the same; it is rejected for correctness, not speed, because _apply stays wrong.

Edge cases to test

  • ["20", "5", "-", "3", "-"] → 12.0. Chained subtraction runs left to right, (20 − 5) − 3; the stub returns 18.0.
  • ["8", "2", "/"] → 4.0. Division is the second reversed branch; the stub returns 0.25.
  • ["3", "4", "+", "2", "*"] → 14.0 on both the fix and the stub. Tests that use only + and * can never catch this bug.
  • ["7", "2", "/"] → 3.5. Python’s / is true division; a port that used // or Math.trunc would return 3.
  • ["-3", "4", "+"] → 1.0. _is_op compares the whole token, so "-3" is a number; a check on the first character would treat it as an operator.
  • ["2", "0", "/"]: Python raises ZeroDivisionError, while the TypeScript version returns Infinity. The input is promised well-formed, but the two languages disagree here.

Common mistakes

  • “Fixing” rpn’s pop order instead of _apply — the pop order is right; the arithmetic is wrong.
  • Swapping only - and forgetting / (both are non-commutative and both are reversed).
  • Assuming addition/multiplication are also broken — they are commutative, so operand order does not matter there.
  • Not confirming with a nested case like [10, 2, 3, -, *], where a wrong subtraction order silently poisons the multiply.

Follow-ups an interviewer asks next

How do you prove the fix is in the right function?
Test _apply directly, not only through rpn: assert _apply("-", 5, 2) == 3 and _apply("/", 8, 2) == 4. Both fail on the stub and on the swap-the-pops patch, while every test that goes through rpn passes on that patch. A unit test at the boundary where the contract lives is what separates a real fix from two bugs cancelling out.
Make the evaluator reject malformed input.
Check that the stack holds at least two values before an operator pops, and raise an error naming the token and its position if it does not. At the end, require exactly one value, since leftovers like 1 2 mean an operator is missing. Wrap float(t) so an unknown token such as x reports where it was instead of surfacing a bare ValueError.
Add exponentiation or a unary minus.
Exponentiation is one more non-commutative branch, a ** b, and exactly where the same operand-order bug would reappear. Associativity needs no extra handling, because RPN token order already fixes the grouping. A unary operator such as negation pops one value instead of two, so _is_op has to report an arity and rpn has to pop that many.
Evaluate ordinary infix input like (3 + 4) * 2.
Convert it to RPN first with the shunting-yard algorithm: numbers go straight to the output, operators wait on a stack and move to the output when a new operator has lower precedence, or equal precedence and left associativity, and parentheses bracket the popping. The resulting token list feeds this evaluator unchanged, a common way to keep parsing separate from evaluation.

Where this shows up

This is code comprehension: the bug spans two functions, so you cannot fix it without reading how rpn feeds _apply. That “drop into unfamiliar code, trace the data flow, fix at the source without breaking the rest” skill is exactly what the AI era rewards — and what interview loops have started testing directly, because it is the half of the job AI does not do for you.

Reference solution

Reference solution — Python
def _is_op(t):
    return t in ('+', '-', '*', '/')


def _apply(op, a, b):
    if op == '+':
        return a + b
    if op == '-':
        return a - b
    if op == '*':
        return a * b
    return a / b


def rpn(tokens):
    stack = []
    for t in tokens:
        if _is_op(t):
            b = stack.pop()
            a = stack.pop()
            stack.append(_apply(t, a, b))
        else:
            stack.append(float(t))
    return stack[-1]


print(rpn(['5', '2', '-']))          # -> 3.0
print(rpn(['10', '2', '3', '-', '*']))  # -> -10.0
Reference solution — TypeScript
function isOp(t: string): boolean {
  return t === '+' || t === '-' || t === '*' || t === '/';
}

function apply(op: string, a: number, b: number): number {
  if (op === '+') return a + b;
  if (op === '-') return a - b;
  if (op === '*') return a * b;
  return a / b;
}

function rpn(tokens: string[]): number {
  const stack: number[] = [];
  for (const t of tokens) {
    if (isOp(t)) {
      const b = stack.pop()!;
      const a = stack.pop()!;
      stack.push(apply(t, a, b));
    } else {
      stack.push(parseFloat(t));
    }
  }
  return stack[stack.length - 1];
}

console.log(rpn(['5', '2', '-']));           // -> 3
console.log(rpn(['10', '2', '3', '-', '*'])); // -> -10

Questions about Read the Codebase, Fix the Bug

What is reverse Polish notation?

Reverse Polish notation, also called postfix notation, writes each operator after its operands, so 3 + 4 becomes 3 4 + and (3 + 4) * 2 becomes 3 4 + 2 *. Because the token order already says which operands each operator takes, RPN needs no parentheses and no precedence rules. It is evaluated with a stack: push every number, and on an operator pop two values, combine them, and push the result. When the tokens run out, the single value left on the stack is the answer.

Why does pop order matter in an RPN evaluator?

Because a stack hands values back in reverse. For 5 2 -, the 5 is pushed first and the 2 second, so the first pop returns 2, the right operand, and the second pop returns 5, the left operand. The left operand is always the one pushed earlier. Getting this backwards does nothing to addition or multiplication, which give the same result either way, but it turns 5 - 2 into 2 - 5 and 8 / 2 into 2 / 8. That is the whole bug in this challenge, split across two functions.

Why do addition and multiplication pass while subtraction and division fail?

Addition and multiplication are commutative: a + b equals b + a, and a * b equals b * a, so swapping the operands cannot change the answer. Subtraction and division are not, so a swapped pair returns the negative of the correct difference or the reciprocal of the correct quotient. That is also a lesson about testing. A suite that only exercised commutative operators would pass on the buggy code, so a good test set includes at least one case per non-commutative operator, plus a nested expression.

Should you fix the caller or the function it calls?

Fix the function that breaks its own contract. Here the parameter names and the call both say that _apply takes the left operand first, and _apply is the code that reverses them, so the fix belongs there. Swapping the pops in rpn would also make every rpn test pass, but it leaves _apply wrong for any other caller and hides the defect behind a second, compensating one. Then add a direct test of the function you changed, so the next reader can see where the contract is checked.

Finished this one? 0 / 76 Challenges done
NextShip or Stop Next on the “Agents & loop engineering” ladder · rung 9 of 9

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.