v3 VM: Closures and GC

Lesson, slides, and applied problem sets.

View Slides

Lesson

v3 VM + GC: Closures, Object Lifetimes, and Memory Reclamation

The language now has three heap-backed value kinds:

  • arrays
  • maps
  • closures

At that point, VM correctness depends on a defined memory model, not just op execution.


1) Heap object model contract

Scalars are immediate values. Heap objects are explicit and tracked:

  • ValueArray
  • ValueMap
  • ValueClosure

Why these must be heap values:

  • closures can outlive the creating frame
  • containers can be shared by reference
  • object identity is required (== is identity in this pack)

Allocation rule:

  • every array/map/closure allocation goes through VM heap list
  • heap count reflects live allocated objects

2) Closure lifecycle at runtime

Call flow:

  1. OpMakeClosure builds closure value from function ID + current env
  2. runtime store in stack/local as reference
  3. call instruction executes closure in new frame
  4. new frame environment parent points to closure's captured environment

This is lexical scoping in execution terms.

Failure modes if broken:

  • unresolved free variable at call-time
  • missing captured variable after GC due to incomplete marking

3) Mark-and-sweep algorithm (contract version)

Phases:

Mark

  • from roots:
    • operand stack
    • all active frames and frame environments
    • module/global environment reachable from frames
  • recursively mark child references:
    • array elements
    • map keys and values
    • closure captured environment + function reference

Sweep

  • iterate heap objects
  • reclaim unmarked nodes
  • reset mark bit on survivors for next GC cycle

No object survives a full sweep unless directly or transitively reachable from roots.


4) Object graph details that often fail

For maps, mark both:

  • keys
  • values

If keys are skipped, objects referenced only as keys become collectible incorrectly.

For closures, mark the closure object and captured env chain.

For arrays, mark every element recursively (including nested arrays/maps/closures).

Marking must be idempotent:

  • an object may be reached multiple times
  • marked bit prevents repeated traversal and recursion loops

5) Collector correctness checks

  • roots include current stack + active env chains
  • do not rely only on current frame locals
  • clear marks after sweep to avoid false retention/resurrection behavior

If marks are not cleared, a survivor from prior cycle can become immortal.


6) Runtime errors remain runtime-only

Runtime still owns operational validation:

  • OpAdd wrong types
  • arithmetic type violations
  • index out of bounds / invalid index kind
  • map access with non-hashable key
  • calling non-callable value

Static type validity in earlier phases does not replace runtime guards.


7) Practical checks (almost solved)

1) Reachability through closure + Array stored in local a, passed into closure, then a binding dropped from local scope. Expected surviving object: array remains reachable via closure capture.

2) Identity semantics + Two separate arrays with same contents are not equal unless same object identity.

3) Key marking + array cannot be map key in this pack. Non-primitive keys should be rejected before map insertion.

4) Mark completeness + If only stack and env are roots and map keys are not marked, map-referenced objects can be collected incorrectly.

5) Scope retention + Closed-over variable bindings remain valid after outer function returns.

GC bugs here appear as late runtime errors; this module prevents that by enforcing clear root contracts.


Module Items

  • Bytecode VM v3

    Execute v3 bytecode with closures, collections, and GC.

    hard Upgrade to Pro to access hard problems
Join Discord