rlox: A Rust Implementation of “Crafting Interpreters” – Resolving and Binding
This post covers Chapter 11 of Crafting Interpreters: Resolving and Binding. No new syntax elements are introduced in this chapter. Instead, Chapter 11 serves as a kind of patch to Chapter 10: it ensures that variables are resolved within their correct closures
. The code for this chapter is relatively straightforward, but I made a mistake that introduced a subtle bug—one that took me a long time to diagnose and finally fix.
🦀 Index of the Complete Series.
![]() |
---|
rlox: A Rust Implementation of “Crafting Interpreters” – Resolving and Binding |
🚀 Note: You can download the code for this post from GitHub using:
git clone -b v0.5.1 https://github.com/behai-nguyen/rlox.git
💥 The interactive mode is still available. However, valid expressions—such as
((4.5 / 2) * 2) == 4.50;
—currently produce no output.
I’m not sure 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/closure/book_fun_in_closure.lox
Content of book_fun_in_closure.lox:
var a = "global";
{
fun showA() {
print a;
}
showA();
var a = "block";
showA();
}
This Lox script appears at the end of the Static Scope section. What do you think it prints?
For more details, refer to
this section of the
README.md
.
Legend: ★ = updated, ☆ = new.
💥 Files not modified are omitted for brevity.
.
├── docs
│ └── RLoxGuide.md ★
├── README.md ★
├── src
│ ├── ast_printer.rs ★
│ ├── environment.rs ★
│ ├── expr.rs ★
│ ├── interpreter.rs ★
│ ├── lib.rs ★
│ ├── lox_error_helper.rs ★
│ ├── lox_function.rs ★
│ ├── main.rs ★
│ ├── parser.rs ★
│ ├── resolver.rs ☆
│ ├── stmt.rs ★
│ ├── token.rs ★
│ └── token_type.rs ★
├── tests
│ ├── data/ ☆ ➜ only a few added
│ ├── README.md ★
│ ├── test_common.rs ★
│ ├── test_control_flow.rs ★
│ ├── test_functions.rs ★
│ ├── test_interpreter.rs ★
│ ├── test_parser.rs ★
│ ├── test_resolving_and_binding.rs ☆
│ ├── test_scanner.rs ★
│ └── test_statements_state.rs ★
└── tool
└── generate_ast
└── src
└── main.rs ★
❸
HashMap
, Pointer Identity,
and Their Driven Refactorings
The code in Chapter 11 is fairly straightforward. However, the new logic in the
Interpreter
module deserves some discussion. For storing resolved
variables, the author uses a
HashMap in Java:
32
private final Map<Expr, Integer> locals = new HashMap<>();
In the resolve() method, the visited expression is stored along with the number of scopes between the variable’s declaration and the current scope. See the reference:
87
88
89
void resolve(Expr expr, int depth) {
locals.put(expr, depth);
}
Later, when accessing a resolved variable, the lookUpVariable() method uses the stored depth to retrieve the correct binding:
482
483
484
485
486
487
488
489
private Object lookUpVariable(Token name, Expr expr) {
Integer distance = locals.get(expr);
if (distance != null) {
return environment.getAt(distance, name.lexeme);
} else {
return globals.get(name);
}
}
It’s clear that both methods operate on the actual
Expr
enum, rather
than on the variant structs associated with each enum. The Rust equivalent of Java’s
HashMap
is also
HashMap
.
⓵ The
Rc<T>
Pointer
I ran into some issues with the new code. After several iterations, it became clear that
Rc<T>
was the correct pointer type to use for the
Expr
and
Stmt
enums—rather than
Box<T>.
Rc<T>
implements
PartialEq,
Eq, and
Hash by delegating to T
.
This means two Rc<T>
instances are considered equal if their inner
T
values are equal. However, since variables being resolved may have the
same name, comparing by value isn’t sufficient. We need pointer identity instead—that is,
the HashMap
should key off the pointer addresses.
For this reason, the Interpreter
’s
locals
field uses raw
pointer keys:
72
locals: HashMap<*const Expr, usize>,
And in the Interpreter
’s
resolve()
method, we use the pointer address via
Rc::as_ptr()
:
137
138
139
140
pub fn resolve(&mut self, expr: Rc<Expr>, depth: usize) {
// Pointer identity, using pointer address: Rc::as_ptr(&expr).
self.locals.insert(Rc::as_ptr(&expr), depth);
}
Similarly, the Interpreter
’s
look_up_variable()
method:
142
143
144
145
146
147
148
149
fn look_up_variable(&self, name: &Token, expr: Rc<Expr>) -> Result<Value, LoxError> {
// Pointer identity, using pointer address: &Rc::as_ptr(&expr).
if let Some(&distance) = self.locals.get(&Rc::as_ptr(&expr)) {
Environment::get_at(&self.environment, name, distance)
} else {
self.globals.borrow().get(name)
}
}
💥 It’s essential that the Rc<Expr>
instance passed to
Interpreter::resolve()
is the same one later passed to
Interpreter::look_up_variable()
. We must not create a new instance using
Rc::new()
in between. This was the trap I fell into—in transitional code, I created a new
instance for the later method without realising it.
To arrive at the correct implementation, the existing code had to be refactored
significantly. Rc<Expr>
and Rc<Stmt>
instances
should be created only once. These refactorings are described in the next sections.
Before we move on, note that
Rc::clone()
increases the reference count but does not change the pointer address. This means we can
safely clone an Rc<Expr>
instance using either
Rc::clone(&expr)
or expr.clone()
.
⓶ The
src/token_type.rs
and
src/token.rs
Modules
● All relevant struct
s and enum
s must implement the
Eq and
Hash traits.
● In the src/token_type.rs
module, for the TokenType
enum,
simply add Eq
and Hash
to the existing
#[derive(...)]
attribute.
● In the src/token.rs
module:
-
The
LiteralValue
enum includes aNumber(f64)
variant. Sincef64
does not implementHash
by default—due to NaNs and rounding behavior—you must manually implementHash
forLiteralValue
. This can be done by converting thef64
to a bit pattern usingto_bits()
, which yields au64
that does implementHash
: please see the implementation. -
The
Eq
trait is a marker trait—it defines no methods or associated types. Its implementation is simple. -
Once
LiteralValue
implements bothHash
andEq
, you can then addEq
andHash
to the#[derive(...)]
attribute of theToken
struct: please see. -
Also, all
get_
prefixes were removed from getter methods to comply with idiomatic Rust naming conventions.
⓷ The
src/expr.rs
and
src/stmt.rs
Modules
● As mentioned at the outset, the Rc<T> smart pointer has replaced the Box<T> pointer. This enables shared ownership and identity preservation across the AST.
● All enum
s and variant struct
s now implement the
Eq and
Hash traits.
● 💡 Crucially, all Visitor<T>
’s visit_*
methods
now receive the outer enum
type directly—i.e., Rc<Expr>
and Rc<Stmt>
—rather than the inner variant struct
.
● 💡 Equally important, the accept()
and accept_ref()
methods
have been updated to take Rc<Expr>
and Rc<Stmt>
as parameters, replacing &mut self
and &self
. For example:
impl Expr {
pub fn accept<T>(expr: Rc<Expr>, visitor: &mut dyn Visitor<T>) -> Result<T, LoxRuntimeError> {
...
}
pub fn accept_ref<T>(expr: Rc<Expr>, visitor: &mut dyn Visitor<T>) -> Result<T, LoxRuntimeError> {
...
}
}
and
impl Stmt {
pub fn accept<T>(stmt: Rc<Stmt>, visitor: &mut dyn Visitor<T>) -> Result<T, LoxRuntimeError> {
...
}
pub fn accept_ref<T>(stmt: Rc<Stmt>, visitor: &mut dyn Visitor<T>) -> Result<T, LoxRuntimeError> {
...
}
}
This refactoring ensures that the original Rc<Expr>
and
Rc<Stmt>
instances are passed around without altering their pointer identity.
● As discussed previously, these modules are
generated by
tool/generate_ast/src/main.rs
.
This generator has been updated accordingly, and the helper function
get_constructor_overrides()
was removed.
● Also, all get_
prefixes were removed from getter methods to comply
with idiomatic Rust naming conventions.
⓸ The
src/parser.rs
Module
This is the starting point: the Parser
is responsible for creating all
Expr
and Stmt
instances. The refactoring is straightforward:
-
Methods that previously returned
Result<Expr, LoxError>
now returnResult<Rc<Expr>, LoxError>
. -
Methods that previously returned
Result<Stmt, LoxError>
now returnResult<Rc<Stmt>, LoxError>
. -
The public
parse()
method now returnsResult<Vec<Rc<Stmt>>, LoxError>
.
⓹ The
src/interpreter.rs
Module
-
The public
interpret()
method now takes a reference to a list ofRc<Stmt>
:&Vec<Rc<Stmt>>
. - This change propagates through to other helper methods and visitor implementations.
Changes to public methods in the main modules required minor updates across all existing test modules. These changes should be self-explanatory.
❹ The New Code: the
Resolver
The implementation closely mirrors the author’s original Java version. Here are a few noteworthy details:
-
The
scopes
field is implemented asVec<HashMap<String, bool>>
, representing a stack of lexical scopes. -
Instances of
Rc<Expr>
andRc<Stmt>
passed to theResolver
must be the exact same ones passed to theInterpreter
. Creating newRc
wrappers around existingExpr
orStmt
objects will break resolution, as pointer identity is used to track bindings.
⓶ Incorporating the
Resolver
As described
in the book, the
Parser
returns a list of statements: Vec<Rc<Stmt>>
. This list is first
passed to the Resolver
. If resolution succeeds, it is then passed to the
Interpreter
:
51
52
53
54
55
56
57
58
59
match resolver.resolve(&statements) {
Err(err) => println!("Resolve error: {}", err),
Ok(_) => {
match interpreter.interpret(&statements) {
Err(err) => println!("Evaluation error: {}", err),
Ok(_) => (),
}
}
}
See the full
run()
method.
● A new module,
tests/test_resolving_and_binding.rs
,
was added. Its structure and conventions follow those of the existing test modules.
● 💥 After incorporating the
Resolver
,
many test scripts in the previously introduced
tests/test_functions.rs
module began failing. These scripts were migrated to the new
tests/test_resolving_and_binding.rs
module to reflect their dependency on resolution.
● 👉 What do you think is the output of the script introduced at the beginning of this article?
● Only a handful of test scripts from the author’s Crafting Interpreters test suite have been incorporated to test the code of this chapter.
That wraps up Chapter 11: Resolving and Binding. There are still a few warnings about dead code, which I’m okay with for now. Two chapters remain in Part II, and I’m aiming to complete them during August 2025.
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/