Compilers — Build a Small Language from Start to Finish
Carrying the Environment from Declaration Time — Closures
In one line
A tree-walking interpreter is one function per AST node that says "when you meet this node, compute this." It finds names in an environment (a chain of name → value dictionaries, each pointing to the outer environment), and a function value carries along the environment at the time it was declared. This is a closure.
Why this was needed
Module 6's bytecode VM and module 8's machine code are fast, but they are far from defining meaning. A tree interpreter is the place where you can write rules most directly, such as "if the left side of && is false, do not compute the right side," "when a block ends, the inner names disappear," and "a function sees the variables of the place where it was declared." That is why this course takes the interpreter as the reference. The later VM and native code are correct only if, when they run the same program, they print the same lines as the interpreter.
But "write directly" does not mean "easy." If you write the interpreter in Python, Python's meanings sneak in.
- Python integers grow without end, but Mini's integers wrap at 64 bits (
9223372036854775807 + 1is negative). This is because machine code does that. - Python's
-7 // 2is -4, but Mini's-7 / 2is -3, like C. - In Python,
True == 1, andisinstance(True, int)is also true. In Mini,1 == trueis a run-time error.
If the side that defines the meaning lets such things slip, every later implementation is fitted to a wrong reference.
How it works
An environment is a chain. Each time you enter a block and each time you call a function, you create a new environment and make it point to its parent. Name lookup climbs the chain, and assignment changes the slot in the environment where that name lives — it does not create a new one in the current environment.
fn counter(start) { counter(10) 을 부르면
let n = start; [호출 환경 {start:10, n:10}] → [전역 {counter, …}]
fn next() { n = n + 1; next 를 선언하는 순간, 함수 값 = (next 의 선언, 지금 환경)
return n; }
return next; counter 가 끝나도 호출 환경은 next 가 붙잡고 있어 살아남는다
}
let a = counter(10); a() → 새 환경 {} → 부모는 붙잡아 둔 호출 환경 → n 을 11 로
print a(); print a(); 11, 12 — 같은 칸을 바꾸기 때문에 이어진다
The key is to take as the parent not the environment at the time the function value is called but the environment at the time it was declared (static scope). If you take the calling environment as the parent (dynamic scope), a() cannot find counter's n, and if there is a global with the same name, it changes that one by mistake. It is also wrong for a closure to copy the environment — two functions holding the same environment (one that increments the value and one that reads it) would see different slots.
A return exits several layers at once. A return inside an if inside a while must go straight back to the call site. In Python, the simplest way is to throw one exception (ReturnSignal) and catch it at the call site. When the call ends, you must always restore the call depth, and it is easy to forget that on the path that exits through the exception (a place that needs finally).
Run-time errors at the operator's position. If b is 0 in a / b, it raises "runtime error: division by zero" at the line:column of the /. Types that semantic analysis skipped without knowing (a true passed in as a parameter) are also caught here. If more than 200 calls are alive at the same time, it stops with "stack overflow" — if it hits Python's own recursion limit first, you end up showing the user a Python error.
What it looks like in the field
- CPython's
nonlocal. In a Python closure, to assign to an outer variable you must writenonlocal. Without it, the rule is that assignment creates a new name in the current environment — that chooses the exact opposite default from this module's "assignment changes the slot where the name lives," and the UnboundLocalError that results is one of the errors Python beginners meet most often. - Closures inside loops. When several closures hold a loop variable made with JavaScript's
var, they all see the same slot (the last value). That is whyletwas changed to create a new environment per iteration. When a new environment is created is, in itself, the meaning of the language. - Reference implementations. Many languages keep, alongside the specification document, a "slow but straight" interpreter as the reference and compare whether a fast compiler produces the same results. The job of this course's grader is the same.
What you will do in the next lab
In interp.py, you build the value rules (64-bit wrapping, division toward 0, separating bool from int), the environment chain, expression evaluation (short-circuit evaluation and run-time type checks), statements and blocks, if and while, functions with closures, return, and call depth, and run_source, which ties together parsing through execution. The grader compares the lines printed and the run-time errors against the reference on hundreds of random number pairs, the fixed programs, and 150 random programs.