cp-04 — Symbol Tables & the Resolver Pass
Status: ✅ Built · all tests passing
A second compiler pass that runs between parsing and execution. The resolver:
- maintains a scope stack (lexical scopes seen so far),
- annotates every variable use with the lexical depth at which its binding lives,
- statically detects a class of bugs the cp-03 interpreter would only catch at runtime — or worse, miss entirely.
The interpreter is then changed to do O(depth) lookups using the annotation — no more hash-map walk up the parent chain.
What's new vs cp-03
| Aspect | cp-03 | cp-04 |
|---|---|---|
| Phases | lex → parse → interpret | lex → parse → resolve → interpret |
| Variable lookup | dynamic walk of parent envs | static depth + one hash lookup at the right scope |
let vs var | parsed but treated identically | let immutable (resolver rejects assignment), var mutable |
| Errors caught | mostly at runtime | undefined, redecl, self-init, assign-to-let, top-level return |
Source layout (src/cpp/)
src/
token.hpp / lexer.{hpp,cpp} # unchanged from cp-03
value.hpp # unchanged
ast.hpp # adds DeclKind + depth fields
parser.{hpp,cpp} # records DeclKind & node line numbers
environment.hpp # adds getAt(depth)/assignAt(depth)
resolver.{hpp,cpp} # NEW — the static-analysis pass
interpreter.{hpp,cpp} # uses depth when available
main.cpp # runs resolver before interpreter
tests/test_resolver.cpp # 15 assertions (regression + new diagnostics)
Build & test
cd src/cpp
cmake -S . -B build -G "Unix Makefiles"
cmake --build build -j
ctest --test-dir build --output-on-failure
Expected: cp-04 tests: 15/15 PASS.
Sample diagnostics
$ cat > bad.ml <<'EOF'
{ let x = 1; let x = 2; x = 3; return 99; }
EOF
$ build/mli bad.ml
[line 1] resolver: redeclaration of 'x' in the same scope (previous at line 1)
[line 1] resolver: cannot assign to immutable binding 'x' (declared with 'let')
[line 1] resolver: 'return' outside of a function
The resolver reports all problems at once, then main exits 1 without
running a single instruction. This is the same shape as a C compiler:
fail in the front end, never reach codegen.
See src/cpp/steps/ for the build-up
01-why-a-separate-pass.md— what runtime-only resolution costs us02-ast-changes.md— addingDeclKindand thedepthslot03-the-resolver-walk.md— visitor over Stmt/Expr, scope stack04-declare-then-define.md— the trick that catcheslet x = x;05-let-vs-var.md— immutability check on assignment06-getAt-and-fast-lookup.md— wiring depth into the interpreter07-error-recovery.md— collecting diagnostics instead of throwing