Exercise 1: Tracing Slot Assignment Across Nested Blocks — Possible Solution ==================================================================== THE PROGRAM ------------------------------ { var a = 1; { var b = 2; print a; print b; } print a; } HAND TRACE ------------------------------ Outer block begins -> scope_depth = 1 declare_local('a', depth=1) -> self.locals = [('a', 1)] 'a' occupies SLOT 0 Inner block begins -> scope_depth = 2 declare_local('b', depth=2) -> self.locals = [('a',1), ('b',2)] 'b' occupies SLOT 1 print a -> resolve_local('a') searches from the END backward: index 1 is 'b' (no match), index 0 is 'a' (match) -> slot 0 compiles to OP_GET_LOCAL 0 print b -> resolve_local('b') finds it at index 1 immediately -> slot 1 compiles to OP_GET_LOCAL 1 Inner block ends -> scope_depth back to 1 end_scope pops every local with depth > 1: just ('b', 2) emits ONE OP_POP, self.locals shrinks back to [('a', 1)] print a -> resolve_local('a') finds it at index 0 -> slot 0 compiles to OP_GET_LOCAL 0 (same slot as before -- 'a' never moved) Outer block ends -> scope_depth back to 0 end_scope pops ('a', 1): emits ONE more OP_POP RESULT ------------------------------ ['1', '2', '1'] WHY THIS WORKS AS AN ANSWER ------------------------------ 'a' is assigned slot 0 once, when it's first declared, and it KEEPS that slot for its entire lifetime -- including after 'b' is declared and after the inner block that declared 'b' has closed. This is the direct consequence of locals never moving: the runtime stack layout at any given point is [a, b] while both are alive, then just [a] again once 'b's block ends and its OP_POP runs -- 'a' is still sitting exactly where it always was, at index 0. resolve_local's own innermost-first search is what makes the SECOND 'print a' correctly resolve to slot 0 again rather than accidentally reusing whatever slot 'b' just vacated -- by the time that print statement compiles, 'b' has already been removed from self.locals entirely, so there's no chance of confusing the two.