Exercise 3: Naive vs. Correct Under Nested Shadowing — Possible Solution ==================================================================== THE PROGRAM ------------------------------ var x = 1; { var x = 2; { x = 99; } print x; } print x; RESULTS ------------------------------ NaiveEnvironment -> ['2', '1'] Environment -> ['99', '1'] The two implementations DISAGREE on the middle block's own "print x" (2 vs. 99) and AGREE on the outer "print x" (both 1). WHY THE MIDDLE VALUE DIFFERS ------------------------------ Walk both implementations through entering the innermost block: CORRECT (Environment, live chain): global.values = {'x': 1.0} middle.values = {'x': 2.0}, middle.enclosing -> global innermost.values = {}, innermost.enclosing -> middle (empty -- no var here) innermost.assign('x', 99.0): 'x' not in innermost.values -> fall through innermost.enclosing.assign('x',99) -> ask middle middle.assign('x', 99.0): 'x' IS in middle.values -> middle.values['x'] = 99.0 <- mutates the REAL middle scope print x (still inside the middle block) reads middle.values['x'] -> 99 NAIVE (NaiveEnvironment, snapshot copy): global.values = {'x': 1.0} middle.values = dict(global.values); middle.values['x'] = 2.0 = {'x': 2.0} (a COPY of global, then x redefined) innermost.values = dict(middle.values) = {'x': 2.0} (a COPY of middle, taken HERE) innermost.assign('x', 99.0): 'x' IS in innermost.values (it's 2.0, copied) -> innermost.values['x'] = 99.0 <- mutates ONLY the innermost copy print x (inside the middle block) reads middle.values['x'], which was NEVER touched -- still 2.0 -> prints 2 WHY THE OUTER VALUE MATCHES BY COINCIDENCE, NOT DESIGN ------------------------------ Both implementations print 1 for the final, outermost `print x;` because NEITHER assignment (`x = 99;`) ever had any legitimate reason to reach the global scope in the first place -- in the CORRECT version, `x = 99;` finds `x` already present in the middle environment's own values (from `var x = 2;`) and stops the chain walk right there, never even asking global. The outer `x` was never a candidate for either implementation to touch for this specific program, so both give the "right" answer for it -- not because the naive implementation is secretly fine, but because this particular program's outer scope was never in the blast radius of the bug. A FOLLOW-UP CHECK, RUN TO CONFIRM (not just assumed) ------------------------------ Changing the middle block's `var x = 2;` to a plain `x = 2;` (assignment, not a new declaration) was tried directly and gives a genuinely more interesting result than expected: NAIVE : ['2', '1'] <- UNCHANGED CORRECT: ['99', '99'] <- both prints now show 99 The correct, chained version changes because `x = 2;` (assignment, no `var`) no longer creates a local binding in the middle scope -- it walks the chain and mutates GLOBAL's own `x` directly, which the later `x = 99;` then also reaches, so every read afterward correctly sees 99. The naive version's output does NOT change at all. That's the real lesson here: NaiveEnvironment copies every enclosing variable into the new block's own dict the instant the block is entered -- which means every name from an outer scope already looks "locally defined" from the block's own point of view, whether it was declared with `var` in this block or not. The naive `assign()` can never tell the difference between "this block shadowed the variable on purpose" and "this assignment was meant to reach the real outer variable" -- both cases look identical to it (a key already present in `self.values`), so it always mutates its own local copy and never the outer scope, even in a case (like this follow-up) where mutating the outer scope was exactly the intended behavior. The var/no-var distinction that `Environment.define()` vs. `Environment.assign()` was built to preserve is invisible to `NaiveEnvironment` -- not handled incorrectly in some cases, but structurally impossible for it to handle at all.