v3 Language: Collections and Closures
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Pass the v2 integration gate and understand source-order binding.
What you will practice
- Track aliases and closure environments across function return.
- Identify v3 lexical additions without assigning them parser meaning.
- Distinguish primitive values from VM heap objects.
In this lesson
Lesson
3 min readv3 Language: Collections and Closures
The v3 milestone replaces named-only calls with callable values and adds
arrays, maps, indexing, and nested function declarations. The first exercise
extends the lexer with [, ], and :. Braces still have one token kind:
the parser decides whether they delimit a block or a map expression.
Worked reconstruction
Trace the value graph for:
fn make(start) {
var n = start;
fn next() { n = n + 1; return n; }
return next;
}
let counter = make(4);
let a = counter();
let b = counter();
The call to make creates environment E with start=4 and n=4. Creating next allocates a closure that refers to its code ID and E. Returning the closure keeps E reachable after make's call frame ends. The first counter call creates a new parameter environment whose parent is E, updates E.n to 5, and returns 5. The second updates the same binding to 6. Copying n=4 into each call would yield 5 twice and fail the shared-binding contract.
Aliases are part of observable behavior
For let a=[1,2]; let b=a; b[0]=9;, both names refer to one array; a[0] is now
9. A freshly constructed [9,2] has equal contents but a different identity.
Arrays, maps, and closures compare by identity in this VM. Passing them into a
function shares the object, while assigning a new object to a local parameter
only changes that binding.
Maps accept primitive keys (number, string, bool, nil) at runtime; a missing key returns nil. Array indexes must be numbers in range. Map literals evaluate pairs left-to-right; if keys repeat, the last source pair supplies the stored value. Index assignment evaluates target, then index, then value. These are execution rules: the lexer merely preserves the tokens needed to express them.
Heap boundary
Arrays, maps, and closures are tracked objects. Environments carry bindings and parent pointers; they are traversed by GC but do not add separate HeapCount entries. Numbers, strings, booleans, and nil are immediate with respect to this teaching collector, even though Go itself may allocate their representation. The Crafting Interpreters closure chapter explains the lifetime problem; its optimized stack/upvalue representation differs from this course's whole-environment capture.
Debugging lab
A returned counter closure observes the caller’s local n=100 instead of its defining n=4. What parent environment was chosen incorrectly? Would preserving only the function ID fix it?
Compare your diagnosis after attempting the lab
The call environment used the caller as parent instead of the environment captured when the closure was made. A function ID identifies code, not the defining bindings; it cannot replace the environment reference.
Transfer task
Create two counters from make(4). Predict interleaved calls A, A, B, A and draw which environment each closure retains. Then tokenize a map whose value is an array without interpreting either container.
Carry forward
Carry the value graph and new punctuation into v3 parsing. Preserve source-order evaluation and shared-binding examples for the VM and GC stages.
Build program-lexer-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.