Semantic Analysis: Names, Scopes, and Rules
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Build v2 Program/Stmt/Expr trees.
- Use a stack of maps and distinguish declaration from assignment.
What you will practice
- Trace lexical scope and declared-before-use lookup.
- Register a function before its own body without hoisting later declarations.
- Classify name/arity errors separately from dynamic runtime types.
In this lesson
Lesson
3 min readSemantic Analysis: Names, Scopes, and Rules
Analyze(prog) returns diagnostics in traversal order. It checks declarations,
name lookup, const bindings, return placement, and direct named-call arity.
It does not produce a typed AST or infer expression types in v2.
Worked reconstruction
Analyze let x=1; { let x=2; x=3; } x=4;:
| Visit | Scope stack, outer first | Lookup/update target |
|---|---|---|
| outer declaration | {x:var} |
Introduce outer x |
| enter block | {x:var}, {} |
New current scope |
| inner declaration | {x:var}, {x:var} |
Shadowing is legal |
| inner assignment | same | Nearest x is inner |
| exit block | {x:var} |
Inner binding disappears |
| final assignment | same | Nearest x is outer |
Duplicates are checked only in the current scope; lookup searches outward.
An initializer is analyzed before its new name is inserted. Therefore
let x=x; fails when no outer x exists, while an inner let x=x; can refer to
an already declared outer x.
Function order is part of this language
The analyzer traverses declarations in source order. It inserts a function's
name and arity before analyzing its body, so direct recursion works. It does
not predeclare every later function: f(); fn f(){} is use before
binding. A blanket two-pass hoisting strategy would change this contract.
For fn f(a){ return a; } f(1,2);, the name resolves but arity fails. For
let f=1; f();, v2 rejects calling a non-function name. v3 will permit variable
bindings to hold closures and defer that particular dynamic decision.
Diagnostic boundaries
const k; is a const-initializer error; k=2 after a valid const is an
assignment error. return outside a function is rejected. But let s="x"-1;
passes these name rules and fails in the VM. Later static types move some
errors earlier; they do not retroactively change this v2 analyzer.
General binding design is discussed in the author's Crafting Interpreters binding chapter. Follow this pack's source-order and AST contracts when applying those ideas.
Debugging lab
An implementation registers every function before visiting any statements. It accepts later(); fn later(){ return 1; }. Explain why this is wrong for this course even though another language could permit it.
Compare your diagnosis after attempting the lab
The local rule is declaration before use, with self-recursion enabled by inserting the current function before its body. Global hoisting accepts a program the specified analyzer must reject. Language policy determines the pass structure.
Transfer task
Construct one legal shadowing example, one same-scope duplicate, and one recursive function. Write the scope snapshot at the first disputed identifier. Add a wrong-arity call without changing its otherwise valid function body.
Carry forward
Carry a semantically accepted Program into compilation; do not assume acceptance means every runtime arithmetic operation will succeed.
Build semantic-analysis. 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.