rlox: A Rust Implementation of “Crafting Interpreters” – Functions
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.
![]() |
---|
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:
- Trait objects
- Using Trait Objects That Allow for Values of Different Types
- Trait Clone
- Trait PartialEq
- 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
'soutput
field was updated toBox<dyn Writable>
. Methods usingoutput
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:
-
extract_output_lines()
— We downcast theInterpreter
'soutput
trait object to retrieve theCursor<Vec<u8>>
byte stream. The rest of the method remains unchanged. -
make_interpreter()
— Removed generics; the parameter is nowwriter: impl Writable + 'static
. -
Added helper methods:
make_interpreter_stdout()
andmake_interpreter_byte_stream()
. -
In other methods — The parameter
interpreter: &Interpreter<Cursor<Vec<u8>>>
is now simplyinterpreter: &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:
-
visit_call_expr()
— Note:Ok(func.call(self, arguments)?)
. -
visit_function_stmt()
— Converts a function declaration into a runtime representation and stores it in the current environment. -
visit_return_stmt()
— Note:Err(LoxRuntimeError::Return(...))
.
⓷ 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:
-
use super::lox_runtime_error::LoxRuntimeError;
replaceduse super::lox_error::LoxError;
-
All instances of
LoxError
were replaced withLoxRuntimeError
❻ 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.
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:
cargo run ./tests/data/benchmark/equality.lox
Output:
loop
41.23750400543213
elapsed
51.01371693611145
equals
9.776212930679321
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:
- https://www.omgubuntu.co.uk/2024/03/ubuntu-24-04-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://www.rust-lang.org/
- https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/
- https://craftinginterpreters.com/