This post covers Chapter 10 of Crafting Interpreters: Functions. The following new syntax elements have been implemented: Expr::Call, Stmt::Function, and Stmt::Return. Lox now supports fun, return, and closures. This post discusses several implementation details that deserve attention.

🦀 Index of the Complete Series.

146-feature-image.png
rlox: A Rust Implementation of “Crafting Interpreters” – Functions

🚀 Note: You can download the code for this post from GitHub using:

git clone -b v0.5.0 https://github.com/behai-nguyen/rlox.git

Running the CLI Application

💥 The interactive mode is still available. However, valid expressions—such as ((4.5 / 2) * 2) == 4.50;—currently produce no output. I'm unsure when interactive mode will be fully restored, and it's not a current priority.

For now, Lox scripts can be executed via the CLI application. For example:

cargo run --release ./tests/data/function/book_make_counter.lox
Content of book_make_counter.lox:
fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1.0".
counter(); // "2.0".

This Lox script is at the beginning of the Local Functions and Closures section: it prints 1.0 and 2.0 — recall that we are Normalising Number Literals.

For more details, refer to this section of the README.md.

Updated Repository Layout

Legend: = updated, = new.

💥 Files not modified are omitted for brevity.

.
├── docs
│   └── RLoxGuide.md ★
├── README.md ★
├── src
│   ├── ast_printer.rs ★
│   ├── data_type.rs ★
│   ├── environment.rs ★
│   ├── expr.rs ★
│   ├── interpreter.rs ★
│   ├── lib.rs ★
│   ├── lox_callable.rs ☆
│   ├── lox_clock.rs ☆
│   ├── lox_function.rs ☆
│   ├── lox_return.rs ☆
│   ├── lox_runtime_error.rs ☆
│   ├── main.rs ★
│   ├── parser.rs ★
│   └── stmt.rs ★
├── tests
│   ├── data/ ☆ ➜ added more
│   ├── README.md ★
│   ├── test_common.rs ★
│   ├── test_control_flow.rs ★
│   ├── test_functions.rs ☆
│   ├── test_interpreter.rs ★
│   ├── test_parser.rs ★
│   └── test_statements_state.rs ★
└── tool
    └── generate_ast
        └── src
            └── main.rs ★

The introduction of the LoxCallable trait and the LoxFunction struct prompted substantial refactoring of the existing code. We begin by discussing these changes.

LoxCallable and Its Driven Refactorings

In the Java implementation, the first line of the visitCallExpr() method reads Object callee = evaluate(expr.callee);. Later, it checks if (!(callee instanceof LoxCallable)). This implies that evaluate() may return a LoxCallable object, alongside other supported data types.

In rlox, the evaluate() method returns Result<Value, LoxError>, where Value is an enum of supported types. To support callable objects, the LoxCallable trait must be added as a variant of Value: LoxCallable(Box<dyn LoxCallable>).

Rust trait objects do not automatically implement Clone or PartialEq. While this limitation isn't prominently documented in one place, the following resources and compiler guidance confirm it:

  1. Trait objects
  2. Using Trait Objects That Allow for Values of Different Types
  3. Trait Clone
  4. Trait PartialEq
  5. Traits Dyn compatibility

As a result, once LoxCallable(Box<dyn LoxCallable>) was added to Value, the enum could no longer derive Debug, Clone, and PartialEq automatically. Only #[derive(Debug)] remains, while explicit implementations of Clone and PartialEq replace the previous derive.

To make the LoxCallable trait usable as a trait object, we need helper traits that enable cloning and comparison. The trait is defined as follows:

48
49
50
51
52
53
pub trait LoxCallable: Debug + CloneLoxCallable + PartialEqLoxCallable {
    fn arity(&self) -> usize;
    fn call(&self, interpreter: &mut Interpreter, arguments: Vec<Value>) -> Result<Value, LoxRuntimeError>;
    fn as_any(&self) -> &dyn Any;
    fn to_string(&self) -> String;
}

💥 We discuss LoxRuntimeError in a later section.

The as_any() method enables downcasting to recover the concrete type, as described in LoxFunction and Its Driven Refactorings. Note the explicit PartialEq implementation:

55
56
57
58
59
impl PartialEq for Box<dyn LoxCallable> {
    fn eq(&self, other: &Self) -> bool {
        (**self).equals_callable(&**other)
    }
}

Unlike Clone, which operates on a single value and can delegate via clone_box(), PartialEq requires comparing two trait objects. Since Rust cannot infer the concrete types behind Box<dyn LoxCallable>, we must provide an explicit implementation. Even with PartialEqLoxCallable as a helper, the runtime lacks the type information needed to resolve == automatically.

LoxFunction and Its Driven Refactorings

Refactoring Interpreter's Output Destinations

In a previous post, we discussed the Interpreter's output destinations in detail. Consider the following Lox script from Chapter 10, featured in the Local Functions and Closures section:

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

LoxFunction enables the Interpreter to invoke functions like makeCounter() and count() in the script above. The print statement inside count() is executed via the visit_print_stmt() method. This means LoxFunction must have access to the Interpreter's output destination.

After experimentation, it became clear that the Interpreter's output field should be a trait object implementing the Write trait:

  • We introduced a new Writable trait for type erasure.
  • The Interpreter's output field was updated to Box<dyn Writable>. Methods using output remain unchanged.

The call() method of LoxFunction receives a mutable reference to the Interpreter and delegates execution to its methods. As a result, call() does not need direct access to the output destination.

Refactoring Test Helper Methods

The change to the Interpreter's output field required updates to the tests/test_common.rs module:

  1. extract_output_lines() — We downcast the Interpreter's output trait object to retrieve the Cursor<Vec<u8>> byte stream. The rest of the method remains unchanged.
  2. make_interpreter() — Removed generics; the parameter is now writer: impl Writable + 'static.
  3. Added helper methods: make_interpreter_stdout() and make_interpreter_byte_stream().
  4. In other methods — The parameter interpreter: &Interpreter<Cursor<Vec<u8>>> is now simply interpreter: &Interpreter.

LoxReturn and the LoxRuntimeError Enum

The original Java implementation uses the language’s exception mechanism to handle return statements. The author defines a custom unchecked exception to signal early exits from Lox functions, and Java’s try/catch makes this approach seamless. In Rust, we achieve the same effect using control flow via Result and early returns.

LoxReturn

Unlike the Java version, LoxReturn in Rust is simply a value holder. The Interpreter short-circuits execution by returning LoxReturn through a Result, allowing it to propagate up the call stack.

LoxRuntimeError Enum

Here is the original Java visitReturnStmt() method:

  @Override
  public Void visitReturnStmt(Stmt.Return stmt) {
    Object value = null;
    if (stmt.value != null) value = evaluate(stmt.value);

    throw new Return(value);
  }

As the author explains, Java exceptions are used to implement Lox’s return behavior. In Rust, we use Result and early returns to simulate this control flow. Here is the equivalent Rust implementation:

400
401
402
403
404
405
406
407
408
    fn visit_return_stmt(&mut self, stmt: &Return) -> Result<(), LoxRuntimeError> {
        let value = if let Some(expr) = &stmt.get_value() {
            self.evaluate(expr)?
        } else {
            Value::Nil
        };

        Err(LoxRuntimeError::Return(LoxReturn { value }))
    }

Previously, all expr::Visitor methods returned Result<Value, LoxError>, and stmt::Visitor methods returned Result<(), LoxError>. An Err(LoxError) indicated a runtime error.

With the introduction of LoxReturn, some visit_* methods now return one of three outcomes: Ok(T), Err(LoxError), or an early LoxReturn. The LoxRuntimeError enum encapsulates the latter two.

In LoxFunction's call() method:

47
48
49
50
51
52
53
54
55
        return match interpreter.execute_block(&self.declaration.get_body(), environment) {
            Err(LoxRuntimeError::Return(ret)) => {
                Ok(ret.value)
            }
            Err(LoxRuntimeError::Error(err)) => Err(LoxRuntimeError::Error(err)),
            Ok(_) => {
                Ok(Value::Nil)
            }
        }

✔️ A LoxReturn is translated into an Ok(T), representing the function’s return value. The following Interpreter methods are relevant:

Expressions and Statements Refactoring

As shown above, LoxRuntimeError replaces LoxError in expr.rs and stmt.rs. As discussed previously, these modules are generated by tool/generate_ast/src/main.rs. The updates were simple textual replacements:

  1. use super::lox_runtime_error::LoxRuntimeError; replaced use super::lox_error::LoxError;
  2. All instances of LoxError were replaced with LoxRuntimeError

Test Updates

The refactorings described in Refactoring Test Helper Methods led to several minor updates across existing test modules. These changes are straightforward and will not be detailed here.

Additional test scripts from the author's Crafting Interpreters test suite have been incorporated. These were used to expand both existing test methods and introduce new ones.

A new module, tests/test_functions.rs, was added. Its implementation follows the same structure and conventions as the existing test modules.

The Author's Benchmark Test Scripts

These benchmark scripts invoke the native clock() function, implemented in the lox_clock.rs module. The reported results are in seconds.

In the author's test/benchmark/ directory, several benchmark scripts are provided. The current implementation supports running the following three scripts via the CLI application:

test/benchmark/equality.lox

cargo run ./tests/data/benchmark/equality.lox

Output:

loop
41.23750400543213
elapsed
51.01371693611145
equals
9.776212930679321

test/benchmark/fib.lox

cargo run ./tests/data/benchmark/fib.lox

Output:

true
291.4927499294281

test/benchmark/string_equality.lox

cargo run ./tests/data/benchmark/string_equality.lox

Output:

loop
59.191839933395386
elapsed
63.91852593421936
equals
4.726686000823975

What’s Next

That wraps up Chapter 10: Functions—and the implementation discussion that came with it. There are still some warnings about dead code, which I’m okay with for now. With three more chapters remaining in Part II, I don’t plan on giving up at this stage.

Thanks for reading! I hope this post helps others on the same journey. As always—stay curious, stay safe 🦊

✿✿✿

Feature image sources:

🦀 Index of the Complete Series.