v3 Semantics: Nested Functions

Lesson, slides, and applied problem sets.

View Slides

Lesson

v3 Semantics: Closures and Const Function Bindings

Why this module exists

With nested functions and lexical scope, “name exists” is no longer a simple local question.

The analyzer now has to distinguish:

  • local declarations
  • nested scopes
  • captured outer variables
  • function bindings that are constant references

This module adds strict rules so closures remain predictable before code generation.


1) Lexical capture as an analyzer responsibility

A nested function body may read a variable from outer scope:

let x = 1
fn make() {
  fn inner() { return x }
  return inner
}

This is valid and should be treated as capture, even though no VM closure cell may be implemented yet.

Analyzer perspective:

  • outer declaration remains in scope chain
  • inner use resolves to nearest binding
  • captured binding is valid even if not currently mutable

Without this rule, many valid nested-function programs are rejected too early.


2) Core semantic rules in v3

Keep these contracts explicit:

  • duplicate declarations in the same scope are errors
  • undefined identifiers are errors at use site
  • const must be initialized before use
  • assigning to const is an error
  • return only inside function bodies
  • function declarations are const bindings (can’t be reassigned)
  • call checks:
    • callee must be known (when direct identifier call)
    • arity must match declaration

In this version, function objects are still treated as values for lookup and arity purposes, but there is no full closure heap model yet.


3) Function-level return tracking

Nested function bodies require context tracking:

  • an inFunctionDepth counter on traversal
  • each function body increments before walking children
  • return allowed only when depth > 0

This also catches incorrect returns in nested closures and in top-level modules.


4) Dynamic typing policy

This language still does not perform static type checking.

Analyzer responsibilities stop at:

  • binding correctness
  • control-flow legality
  • symbol resolution
  • function signature consistency

Runtime type mismatches are deferred to VM and become executable errors.


5) Why closure-like behavior is tricky

Pitfalls to call out in implementation:

  • allowing read access but accidentally rewriting capture as local
  • rejecting capture for read-only usage only (should allow in expression context)
  • function declarations shadowing outer functions with same name in nested scopes
  • call arity checks that ignore recursive declaration order

Good semantic analysis should be strict without overreaching into runtime policy.


6) Practice checkpoints (almost solved)

1) Valid lexical capture
Input: let x = 1; fn f(){ fn g(){ x } }
Expected: no semantic error for x inside g.

2) Invalid top-level return
Input: return 1 at program root
Expected: return outside function.

3) Const binding immutability
const f = fn(){}; f = fn(){}
Expected: cannot assign to const.

4) Duplicate in same scope
let a = 1; let a = 2 in same block
Expected: redeclare error.

5) Capture + assignment
let x = 1; fn f(){ x = 2 } with nested function
Expected: if your policy forbids assignment to captured consts, signal error where policy says so; otherwise allow only if binding is mutable and properly resolved to nearest mutable scope.

6) Direct call arity
fn add(a, b) {}; add(1)
Expected: arity mismatch before codegen.

This version’s goal is deterministic name and scope behavior before deeper VM features.


Module Items

  • Semantic Analysis v3

    Validate v3 programs with nested scopes and closures.

    hard Upgrade to Pro to access hard problems
Join Discord