Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Read the Codebase, Fix the Bug
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.
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.
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.
Examples and constraints 4 examples
rpn(['3', '4', '+'])7.0rpn(['5', '2', '-'])3.0rpn(['10', '2', '3', '-', '*'])-10.0rpn(['20', '5', '-', '3', '-'])12.0- 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
_applyso"-"returnsa - band"/"returnsa / 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
- 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.
- For ["5","2","-"], the stack is [5, 2]; b=2, a=5. You want a - b = 5 - 2 = 3.
- The current _apply returns b - a and b / a — both reversed. Swap them to a - b and a / b.
- 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.
| step | token | action (fixed) | stack after (fixed) | stack after (stub) |
|---|---|---|---|---|
| 1 | 10 | push 10.0 | [10.0] | [10.0] |
| 2 | 2 | push 2.0 | [10.0, 2.0] | [10.0, 2.0] |
| 3 | 3 | push 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] |
| 6 | end | return stack[-1] | -10.0 | 10.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 returns18.0.["8", "2", "/"]→4.0. Division is the second reversed branch; the stub returns0.25.["3", "4", "+", "2", "*"]→14.0on 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//orMath.truncwould return 3.["-3", "4", "+"]→1.0._is_opcompares the whole token, so"-3"is a number; a check on the first character would treat it as an operator.["2", "0", "/"]: Python raisesZeroDivisionError, while the TypeScript version returnsInfinity. 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
_applydirectly, not only throughrpn: assert_apply("-", 5, 2) == 3and_apply("/", 8, 2) == 4. Both fail on the stub and on the swap-the-pops patch, while every test that goes throughrpnpasses 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 2mean an operator is missing. Wrapfloat(t)so an unknown token such asxreports where it was instead of surfacing a bareValueError. - 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_ophas to report an arity andrpnhas 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.0Reference 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', '-', '*'])); // -> -10Questions 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.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Cosine SimilarityThe measure behind every embedding search and RAG system: how aligned are two vectors, ignoring their length? Dot product over the product of magnitudes — 1 identical, 0 orthogonal, -1 opposite. Solve it in Python or TypeScript.Read →
- Top-K RetrievalThe core of the "R" in RAG: given a query embedding and a set of document embeddings, return the indices of the k most similar docs by cosine similarity, with a stable tie-break. Solve it in Python or TypeScript.Read →