v3 Semantics: Nested Functions
Lesson, slides, and applied problem sets.
View SlidesLesson
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
constmust be initialized before use- assigning to
constis an error returnonly inside function bodies- function declarations are
constbindings (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
inFunctionDepthcounter on traversal - each function body increments before walking children
returnallowed 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 immutabilityconst f = fn(){}; f = fn(){}
Expected: cannot assign to const.
4) Duplicate in same scopelet a = 1; let a = 2 in same block
Expected: redeclare error.
5) Capture + assignmentlet 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 arityfn 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