Exercise 3: call_stack Balances Back to Empty After Success — Possible Solution ==================================================================== THE PROGRAM ------------------------------ fun c(x) { return x + 1; } fun b(x) { return c(x) + 1; } fun a(x) { return b(x) + 1; } print a(1); RESULT ------------------------------ interpreter.call_stack after a(1) completes: [] output: ['4'] No frames are left behind. The stack is exactly as empty after three nested calls as it was before any of them ran. THE RESPONSIBLE LINE ------------------------------ def call(self, interpreter, arguments, call_line=None): env = Environment(self.closure) for param, arg in zip(self.declaration.params, arguments): env.define(param, arg) interpreter.call_stack.append((self.declaration.name, call_line)) try: interpreter.execute_block(self.declaration.body, env) except ReturnException as r: return r.value except WispRuntimeError as e: if not hasattr(e, "wisp_stack"): e.wisp_stack = list(interpreter.call_stack) raise finally: interpreter.call_stack.pop() # <-- this line return None `interpreter.call_stack.pop()` sits in a `finally` block, which Python guarantees runs no matter how the `try` block above it exits -- whether it completes normally, raises a ReturnException that gets caught, or raises a WispRuntimeError that gets caught, re-raised, and propagates further. Every single call to call() pushes exactly once (the line right before the try) and pops exactly once (the finally), with no code path that can push without eventually popping. WOULD THE GUARANTEE STILL HOLD IF pop() WERE INSIDE THE try BLOCK? ------------------------------ No -- and this is the actual point of asking. If `interpreter. call_stack.pop()` were the last line inside the `try` block instead of in a `finally`, it would only run when the try body finishes WITHOUT raising anything past the except clauses already there. A ReturnException is always caught locally (by this exact call()), so that path would still pop correctly either way. But once `return r.value` executes inside `except ReturnException as r:`, the function returns immediately -- any code physically AFTER the except blocks but still nominally "inside the try" would never be reached via that path at all, since a `return` inside an except block exits the whole function on the spot. Moving the pop into the try body (after the interpreter.execute_block(...) call, say) would mean the pop is skipped entirely whenever ReturnException fires -- which is the NORMAL, everyday case for practically every function that uses `return` -- leaving a stale frame behind on every single successful call. The `finally` block's job is precisely to run regardless of which of those exits actually happened, which is why it -- and not any position inside `try` -- is what keeps the stack balanced across both the ordinary return path and the exceptional error path alike.