Exercise 1: Predicting the Heap by Hand — Possible Solution ==================================================================== THE PROGRAM ------------------------------ fun makeAdder(x) { fun adder(y) { return x + y; } return adder; } var keep = makeAdder(10); makeAdder(20); makeAdder(30); print keep(5); HAND PREDICTION ------------------------------ Objects allocated, in order: 1. Closure for the top-level script itself (allocated explicitly when compile_and_run sets up the VM, before any Wisp code runs) 2. Closure for `makeAdder` (created when the `fun makeAdder` statement executes at the top level) 3. Upvalue capturing x=10 (created when makeAdder(10) is called and OP_CLOSURE for `adder` needs to capture x) 4. Closure for `adder`, wrapping upvalue #3 (same call) 5. Upvalue capturing x=20 (from makeAdder(20)) 6. Closure for `adder`, wrapping upvalue #5 7. Upvalue capturing x=30 (from makeAdder(30)) 8. Closure for `adder`, wrapping upvalue #7 Total allocated: 8 SURVIVORS, predicted ------------------------------ - #2 (makeAdder's own closure) -- reachable forever via the global 'makeAdder' - #3 and #4 (keep's own upvalue and closure) -- reachable via the global 'keep' #1 (the script's own closure) -- NOT reachable once the program has finished: nothing in globals or on the stack points back to it, and self.frames is empty after run() returns. Easy to predict wrong if you forget the script itself is a Closure too. #5/#6 and #7/#8 -- both pairs are pushed by their own OP_CALL, then immediately discarded by OP_POP (each `makeAdder(20);` and `makeAdder(30);` is a bare ExpressionStmt). Garbage. PREDICTED: heap=8, survivors=3 ({makeAdder closure, keep closure, keep upvalue}), collected=5. VERIFIED RESULT ------------------------------ heap before: 8 collected: 5 heap after: 3 survivors: ['adder', 'makeAdder'] (the two Closure names -- keep's own upvalue has no separate "name" to print) Matches the hand prediction exactly. WHY THIS WORKS AS AN ANSWER ------------------------------ The one genuinely easy detail to miss is object #1 -- it's tempting to only count objects the WISP PROGRAM itself visibly creates (via `fun` statements and calls), forgetting that compile_and_run's own setup code allocates a Closure for the top-level script exactly the same way OP_CLOSURE would for any Wisp-declared function. Once the script finishes, that closure has genuinely nothing referencing it -- it was only ever reachable via self.frames while the script was still running, and self.frames is empty by the time collect_garbage() gets a chance to run afterward.