v3 Language: Collections and Closures
Lesson, slides, and applied problem sets.
View SlidesLesson
v3 Language: Closures, Arrays, Maps
Why this module exists
This is the first module where language features require a real runtime contract:
- first-class function values
- heap-backed containers
- indexed access/mutation semantics
- closures over lexical environments
Without explicit rules, syntax appears to work while semantics drift.
1) Composite literals and evaluation order
Arrays
[1, 2, 3]
Evaluation is left-to-right for each element expression.
Maps
{"a": 1, "b": 2}
Evaluation is left-to-right by pair order. For each pair:
- evaluate key
- evaluate value
- insert/overwrite pair in map
In this pack, map keys are constrained to hashable primitive kinds (string, number, bool, nil). Attempts with non-hashable runtime keys are rejected.
2) Indexing as a read/write primitive
Read form:
value = xs[0]
Write form:
xs[1] = 42
m["a"] = 99
Semantics to keep explicit:
- container expression is evaluated first
- index/key expression is evaluated next
- for assignment, RHS is evaluated last
That ordering matters for side effects (e.g. f()[0] = g() must preserve call order).
3) Functions become values
In v3, function calls are expression-capable:
let f = makeAdder(10)
let z = f(2)
The key shift:
- function declarations are bindings
- those bindings can be passed, returned, and stored
- call site expects callable value semantics, not just identifier lookup
Compiler/VM responsibilities:
- closure/lambda creation emits callable value
CALLruntime instruction validates callee is callable- arity is still validated by analyzer when possible
4) Nested functions and lexical capture
Nested function example:
fn makeAdder(x) {
fn add(y) { return x + y; }
return add;
}
add captures x from defining scope, not from caller scope later.
Why this matters:
- same function can be returned and later invoked
- captured variable must remain valid while closure exists
- this directly motivates heap and GC in v3
5) Runtime-facing rules for container values
Arrays/maps/closures are not immutable scalars:
- aliasing is possible
- writes can mutate shared values
- equality is identity in this pack, not deep structural equality
Therefore:
arr1 == arr2is false unless they reference the same object- passing arrays to functions shares a reference-like behavior
This behavior must be documented before students assume copy-on-write semantics.
6) Memory model transition from v2 to v3
v3 introduces heap-lifecycle requirements:
- these values are allocated in the VM-managed heap
- garbage collection must be able to recover unreachable objects
- reference graphs can span frames through closures and captured environments
That is why v3 is the first true “memory-correctness” checkpoint.
7) Practice checkpoints (almost solved)
1) Array evaluation + [f(), g(), h()] should evaluate f then g then h.
2) Map key restrictions + {[1, 2]: 3} is invalid in this pack because array keys are not hashable.
3) Function value call + (fn add(y) { return y + 1; })(41) is legal if function expressions are supported; if not, the expected language rule is explicit in grammar.
4) Closure capture behavior + let x = 1; let f = fn() { return x }; x = 2; f()
Expected: f() observes either captured snapshot or live outer value depending on your capture strategy; this pack uses visible lexical value through environment semantics, so call-site expectations should be stated explicitly.
5) Index assignment side effects + xs[0] = xs[0] + 1 evaluates RHS using old xs[0], then writes back.
6) Equality rule + [1,2] == [1,2] is false under identity semantics.
The language is now about runtime truth and explicit value contracts, not just new syntax sugar.
Module Items
Program Lexer v3