Semantic Analysis: Names, Scopes, and Rules
Lesson, slides, and applied problem sets.
View SlidesLesson
Semantic Analysis: Names, Scopes, and Rules
Why semantic analysis exists
Parsing checks syntax. Semantic analysis checks meaning.
Core questions before execution:
- Is every identifier known at its use site?
- Can each name be assigned according to its binding kind?
- Do control constructs appear only where legal?
- Are function boundaries and arity contracts respected?
If any of these are wrong, runtime errors are avoided or made easier to interpret.
1) Scope chain as a first-class policy
A clean analyzer uses an explicit scope stack:
- each block creates a new environment
- inner scopes shadow outer names
- lookups search current → parent → root
Binding record needs at least:
- name
- kind (
var,const,fun,param) - declaration location
Rule for duplicate declarations:
- same name in same scope is illegal
- same name in parent and child is legal shadowing
This distinction is one of the biggest learner pain points, and one that improves quickly with this mental model.
2) Two-phase function strategy (declaration + body validation)
For functions, single-pass name checks are often not enough.
Recommended analyzer shape:
- declaration pass on top-level scope:
- register all function names first
- body pass:
- validate parameters/locals/body statements with full visibility
Benefits:
- recursive calls can resolve known function names
- arity validation can happen at each call site
Without this pattern, valid recursive examples are often wrongly flagged as “undefined function.”
3) Assignment and const rules
const policy:
- must be initialized at declaration (if your grammar supports it)
- cannot be mutated later
STORE target resolution:
- walk inward → outward
- if not found -> undefined name
- if found and const -> const assignment error
- else mutate nearest binding
This rule mirrors VM behavior and avoids mismatches between compile-time and runtime diagnostics.
4) Return placement and control-flow legality
return is only legal inside function scope.
- top-level
returnis semantic error - nested function returns should be scoped to nearest enclosing function
A pragmatic strategy:
- carry
inFunctionDepthcounter during traversal - increment entering function body
- decrement leaving function body
returnvalid when counter > 0
With this, you can also support useful messages:
return outside functionreturn in global scope at line X
5) Call + arity validation
Call semantics at analysis time:
- resolve callee name
- verify declared parameter count equals actual call arity
- optionally track unknown variadic support
This catches obvious runtime call defects before bytecode emission and reduces debug ambiguity later.
6) Error quality as curriculum, not decoration
For this pack, analyzer should be:
- deterministic in order
- single-source in shape (same issue, same message order)
- short but specific
Good messages:
undefined name: xredeclare in same scope: xcannot assign to const: pireturn outside functionarity mismatch: add expects 2, got 3
Prefer source location in every message when available.
7) Open-ended future growth
You can keep extending from this semantic base:
- flow-sensitive initialization tracking
- type system checks
- modules/imports
- closures and upvalues
The important part is keeping stage contract stable: AST in, analyzer diagnostics/typed graph out.
8) Practice checkpoints (almost solved)
1) Shadowing check
Program:
let x = 1
{ let x = 2; let y = x }
y
Expected:
- first
xand innerxare legal yundefined in outer scope (undefined name: y)
2) Const assignment
Program: const pi = 3 then pi = 4 Expected:
- semantic error:
cannot assign to const: pi
3) Recursive function declaration
Program: fn fact(n) { if n == 0 { return 1 } return n * fact(n-1) } Expected:
- function
factis known before body pass - recursive call accepted
4) Return placement
Program: return 1 Expected:
return outside function
5) Arity mismatch
fn add(a,b) {}; add(1)
Expected:
- arity mismatch: expects 2, got 1
6) Unknown symbol in expression
x + 1 with no declaration of x
Expected:
undefined name: x
This module is less about syntax and more about turning language rules into enforceable compiler behavior.