v3 VM: Closures and GC
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Execute v2 bytecode and derive v3 closure/container instructions.
- Draw reachability graphs and terminate a graph traversal with visited state.
What you will practice
- Trace lexical environments through returned closures.
- Derive GC edges from this VM’s actual object kinds.
- Diagnose cycles, root retention, and repeated-collection failures.
In this lesson
Lesson
4 min readv3 VM: Closures and GC
v3 allocates arrays, maps, and closures as tracked heap objects. Numbers, strings, booleans, and nil are immediate for this collector. A Go string may use host memory, but it is not an independent Object in HeapCount. Run starts fresh execution state; GC is manually invoked and is neither concurrent nor moving.
Runtime contract before collection
MAKE_CLOSURE captures the current environment. A call makes a new environment whose parent is the captured environment, not the caller's environment. LOAD and STORE search that chain. Index mutation changes the shared container; arrays/maps/closures compare by identity. Missing map keys return nil, while invalid array indexes fail. Primitive map keys have no heap-object pointers.
Worked reconstruction
Suppose global m points to map M, whose value at key "saved" is array A. A holds closure C; C captures environment E containing back→M. Unrooted array U was allocated and popped:
global.m → M → A → C → E.back → M
U has no reachable incoming edge
| Step | Newly marked objects | Why traversal continues or stops |
|---|---|---|
| Root global.m | M | Visit map values |
| M["saved"] | A | Visit array elements |
| A[0] | C | Visit captured environment E |
| E.back | none | M already marked; cycle terminates |
| Sweep | remove U | U was never reached from a root |
HeapCount changes from four to three. E and the string key do not add to that count. If every root into the cycle is removed, all three objects become collectible; references inside an unreachable cycle do not preserve it.
Derive the traversal from the data types
Roots are stack values, all active frame environments, and the VM global environment. Arrays trace elements; maps trace values; closures trace captured environments; environments trace bindings and parent links. Mark a new object before recursing. Also keep a visited set for environments, because they can participate in closure cycles without an object mark bit.
Sweep only unmarked objects, then clear survivor marks. On the next collection, a surviving map may reference a newly added child; keeping its old mark and skipping traversal could collect that live child. A closure captures an entire environment here, so even unused bindings may remain reachable. This is conservative retention under this representation.
A lifetime test is more than a count test
Store an array into a global map, leave its local scope, then run GC twice. Both objects must remain. Remove the map reference and collect again; now they can disappear. A host Go variable holding an Object pointer is not an automatic VM root. Ordinary Run completion retains global/main roots; a subsequent Run starts a new heap and execution state.
Compare the author's mark-sweep chapter, but derive edges from this course's Value, Object, and Env types rather than copying another language's object graph.
Debugging lab
A collector passes “allocate array, pop, collect” but loses an array stored inside a rooted map. A later patch survives one GC and fails after adding a new child before the second. Identify both missing invariants.
Compare your diagnosis after attempting the lab
The first test covered isolated reclamation only; marking must traverse map values. The second failure can arise from uncleared survivor marks, which prevent fresh traversal of the newly installed edge. Test transitive retention and repeated collections separately.
Transfer task
Draw a rooted closure/environment cycle containing two arrays and an unreachable sibling array. Predict HeapCount before GC, after GC, and after removing the final root. Explain why host test references do not change these answers.
Carry forward
Carry a closure-lifetime trace and a repeated-GC fixture into BC2 and the capstone. Successful arithmetic execution alone is not evidence that heap reachability is correct.
Build bytecode-vm-v3. Keep a before/after trace of one failing test. Use the checkpoint to diagnose specific skills; passing it does not replace implementing and testing the artifact.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.