Exercise 3: Reproducing the Loop-Capture Bug, Then the Fix — Possible Solution ==================================================================== THE BUG (desugared for-loop, closures capture the shared counter i) ------------------------------ var f0 = nil; var f1 = nil; var f2 = nil; { var i = 0; while (i < 3) { { fun grab() { return i; } if (i == 0) { f0 = grab; } if (i == 1) { f1 = grab; } if (i == 2) { f2 = grab; } } i = i + 1; } } print f0(); print f1(); print f2(); RESULT: ['3', '3', '3'] THE FIX (a fresh local captured instead of i itself) ------------------------------ ... same shape, but inside the inner block, BEFORE fun grab(): var captured = i; fun grab() { return captured; } ... (grab now returns 'captured', not 'i') RESULT: ['0', '1', '2'] WHY captured GETS A FRESH UPVALUE EVERY ITERATION BUT i NEVER DOES ------------------------------ `i` is declared exactly ONCE -- by the `var i = 0;` statement that sits OUTSIDE the while loop entirely, as part of the for-loop's own one-time desugared setup. Every one of the three grab() closures created across the loop's three iterations calls resolve_upvalue('i'), which walks out to the SAME enclosing local slot every single time -- there is only one 'i' local for the compiler to ever find, so capture_upvalue is called with the SAME abs_slot three separate times, and (per Exercise 2's own dedup finding) returns the SAME Upvalue object all three times. All three closures end up sharing one upvalue, which only ever closes once -- when the OUTER block (the one holding `var i`) finally ends, after the whole loop has already finished and i has already been incremented to 3. `captured`, by contrast, is declared by a `var captured = i;` statement sitting INSIDE the loop's own per-iteration body block. That block's own begin_scope()/end_scope() pair runs fresh on every single iteration -- end_scope emits OP_CLOSE_UPVALUE for 'captured' at the end of EACH iteration (closing whatever Upvalue existed for it, capturing that iteration's own value), and the NEXT iteration's begin_scope declares a brand new local also named 'captured', at what may even be the very same stack slot number, but as a completely distinct entry in self.locals with its own fresh Upvalue once a closure captures it. Three iterations, three declarations, three closes, three independent Upvalue objects -- one holding 0, one holding 1, one holding 2. The structural rule this confirms: an upvalue's own identity is tied to a specific DECLARATION of a local, not to a variable NAME. `i` is one declaration, referenced three times. `captured` is three separate declarations that happen to share one name.