Lessons from Implementing Functions in My Interpreter (in Rust)

8/11/2026

I’ve been building a Lox interpreter in Rust. So far, I’ve implemented variables, expressions, and control flow. Today, I implemented functions, and it is way, way harder than I thought.

Before today, I took functions for granted. I use them every day, and I never really stopped to think about what they need to do behind the scenes.

Functions actually have two parts. The declaration, where we define the function. And the call, where we use it.

The declaration part is straightforward. When we write something like:

fun add(a, b) {
    return a + b;
}

All we have to do is store the name, the parameters, and the body. We store them in a struct and keep them under the function name as the key. When someone calls add, we just look up that struct by name.

Here’s what that looks like in code:

pub struct Function {
    pub name: String,
    pub params: Vec<String>,
    pub arity: usize,
    pub body: Stmt,
}

This part is easy. The trickier part is the call.

When we call a function, we pass arguments in parentheses. The parser stores the function name as the callee and the arguments in a vector.

Then comes the interesting part: how do we actually run the function?

The first thing we do is check for arity. Arity stands for the number of arguments a function accepts.

Here’s how that check looks:

if fun.arity != args.len() {
    return Err(RuntimeError::WrongNumberOfArguments);
}

We need to check that the number of arguments matches the number of parameters the function expects. If they don’t match, we throw an error.

But that is not the hard part. The hard part is the environment.

Where should the function arguments live while the function is running?

My first instinct was simple. Just store the parameters and arguments in the current environment, with the parameter name as the key and the argument value as the value.

But then I realized the problems.

Those parameters would be visible to every other part of the program, which is not what we want. Also, when the function ends, we will have to manually clean them up. And what if a variable with the same name already exists? It would be silently overwritten.

So I thought the obvious answer was to create a new environment just for the function. When the function is done executing, we throw that environment away.

So I did that. I created a new environment, ran the function inside it, and it crashed.

Why? Because the moment I created the new environment, the function declaration itself disappeared. The new environment had no idea the function existed.

So when I tried to look up the function by its name to run it, we got an error saying the variable does not exist.

Honestly, this was the most frustrating part for me. It took me a long time and analysis to figure this out. So this is the solution I came up with.

We do need a new environment for the function. But that new environment should not be empty. It should have the environment where the function was declared as its parent.

Here’s how I structured the environment:

pub struct Env {
    pub current: HashMap<String, RuntimeValue>,
    pub parent: Option<Rc<RefCell<Env>>>,
}

So when we want to look up the function signature, we walk up to the parent and find it. And, when we want to bind the parameters to the argument values, we do that in the child environment.

After the function finishes, we just point the current environment back to the parent.

let mut new_env = Env::new();
new_env.parent = Some(fun.env.clone());
// execute the function body
self.env = previous;

That single action does two things for us.

It clears the function’s local variables and parameters in one shot. And it makes sure one function cannot peek into another function’s variables.

This is what I am enjoying about building an interpreter. A familiar feature, like functions, looks completely different once you have to implement it yourself.

I still have one more piece to implement: the return statement. I’ll do that tomorrow. If it teaches me anything new or surprising, I will definitely share it.

0 Visitors