Step 1 of 3 · Reading · ~3 min
Functions — Reusable Code
Control Flow & Functions
Function Declaration & Calls
Functions are where your interpreter stops being a calculator and becomes a real programming language: they introduce a new kind of runtime value (a callable), a new environment per invocation, and a new statement/expression pair (fun declaration, call expression).
Parsing
A function declaration is a name, a parenthesized parameter list, and a block body:
fun add(a, b) { return a + b; }
Model it as a statement node holding the name token, a list of parameter tokens, and a list of body statements. A call is a postfix expression: after parsing a primary expression, keep checking for a following ( and wrap what you have in a Call node — this is what lets you chain calls like f()() or handle a call on any expression that evaluates to a function.
Functions as runtime values
The key architectural move: a function declaration doesn't execute anything by itself — it creates a callable object and binds it to the function's name in the current environment, exactly like a var declaration binds a value. This is what "functions are first-class values" means in practice: you can store one in a variable, pass it as an argument, or return it, because at runtime it's just another value sitting in an environment slot.
Note closure, not "the global environment" — storing the defining environment rather than always using globals is exactly what makes closures possible two lessons from now. Even before you need that, it's the correct model: each call to a function creates a brand-new environment for its parameters and locals, chained onto the environment where the function was defined.
Calling a function
When the interpreter evaluates a Call expression:
- Evaluate
callee— it must produce something callable (raise a runtime error otherwise, e.g. calling a number:"can only call functions and classes"). - Evaluate each argument expression, left to right, into a list of values.
- Check arity: if
len(arguments) != callable.arity(), raise a runtime error likef"Expected {arity} arguments but got {len(arguments)}."before ever entering the function body. - Delegate to
callable.call(interpreter, arguments).
Edge cases to watch
- Arity mismatches must be caught before executing any of the function body — don't let a missing argument silently become
nil. - No explicit
return: a function that falls off the end of its body should evaluate tonil, not raise an error or return the last expression's value. - Recursion and mutual recursion work automatically as long as each call gets a fresh environment — you don't need anything special yet, just don't accidentally share one environment across calls.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…