Semantic Analysis: Names, Scopes, and Rules

Lesson, slides, and applied problem sets.

View Slides

Lesson

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:

  1. declaration pass on top-level scope:
    • register all function names first
  2. 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:

  1. walk inward → outward
  2. if not found -> undefined name
  3. if found and const -> const assignment error
  4. 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 return is semantic error
  • nested function returns should be scoped to nearest enclosing function

A pragmatic strategy:

  • carry inFunctionDepth counter during traversal
  • increment entering function body
  • decrement leaving function body
  • return valid when counter > 0

With this, you can also support useful messages:

  • return outside function
  • return 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: x
  • redeclare in same scope: x
  • cannot assign to const: pi
  • return outside function
  • arity 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 x and inner x are legal
  • y undefined 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 fact is 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.


Module Items

Join Discord