Semantic Analysis: Names, Scopes, and Rules

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Before 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 read

Semantic 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.

Join Discord