This post covers Chapter 12 of Crafting Interpreters: Classes. The following new syntax elements have been implemented: Stmt::Class, Expr::Get, Expr::Set, and Expr::This. Lox now supports class, this, and init. While implementing this chapter, I encountered two stack overflow bugs and several cases where author-provided test scripts produced incorrect results. This post discusses those issues in detail.

🦀 Index of the Complete Series.

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

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

git clone -b v0.6.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 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/constructor/call_init_explicitly.lox
Content of call_init_explicitly.lox:
class Foo {
  init(arg) {
    print "Foo.init(" + arg + ")";
    this.field = "init";
  }
}

var foo = Foo("one"); // expect: Foo.init(one)
foo.field = "field";

var foo2 = foo.init("two"); // expect: Foo.init(two)
print foo2; // expect: Foo instance

// Make sure init() doesn't create a fresh instance.
print foo.field; // expect: init

This Lox script is an author provided one, it can be found here. The output of this Lox script should be as noted: Foo.init(one), Foo.init(two), Foo instance, and init.

For more details on the Lox language, 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
│   ├── environment.rs ★
│   ├── interpreter.rs ★
│   ├── lib.rs ★
│   ├── lox_callable.rs ★
│   ├── lox_class.rs ☆
│   ├── lox_clock.rs ★
│   ├── lox_error_helper.rs ★
│   ├── lox_error.rs ★
│   ├── lox_function.rs ★
│   ├── lox_instance.rs ☆
│   ├── lox_return.rs ★
│   ├── main.rs ★
│   ├── parser.rs ★
│   ├── resolver.rs ★
│   └── value.rs ★ ➜ formerly data_type.rs
└── tests
    ├── data/ ☆ ➜ a lot more were added
    ├── README.md ★
    ├── test_classes.rs ☆
    ├── test_interpreter.rs ★
    └── test_parser.rs ★

The code for Chapter 12 initially appeared straightforward. However, testing it against the relevant scripts from the author’s Crafting Interpreters test suite revealed some existing flaws in my earlier implementation, as well as mistakes introduced during development. Working through these issues deepened my understanding of Rust, particularly around ownership, pointer identity, and trait object behavior. This post explores those challenges in detail.

#[derive(Debug)] Can Potentially Cause Stack Overflow

Consider the following Chapter 11 code:

  1. pub struct LoxFunction — The LoxFunction struct implemented Debug via the #[derive(Debug, Clone, PartialEq)] attribute.
  2. DataType aka Value enum — The Value::LoxCallable variant was defined as Box<dyn LoxCallable>.

These were still in place while I was converting code for Chapter 12. I needed to do some debug logging and happened to add a debug print inside the Interpreter’s visit_call_expr() method, like so:

    fn visit_call_expr(&mut self, expr: Rc<Expr>) -> Result<Value, LoxRuntimeError> {
        let call = unwrap_expr!(expr, Call);

        let callee: Value = self.evaluate(Rc::clone(call.callee()))?;

        println!("Interpreter::visit_call_expr() callee {:?}", callee);
        ...
    }

The variable callee was a Value::LoxCallable(Box<dyn LoxCallable>), whose concrete type was LoxFunction. The LoxFunction’s closure was an Environment, and its enclosing field was also an Environment.

In other words, Environment is a recursive structure. This recursive nesting caused the debug print to explode and eventually led to a stack overflow when the Interpreter tried to clone or traverse it. The println! statement above produced between 240K and 320K of repeated AST text before crashing.

💡 The solution was to override Debug for LoxFunction to avoid printing the full closure:

  1. Removed the #[derive(Debug)] attribute from pub struct LoxFunction.
  2. Manually implemented std::fmt::Debug for pub struct LoxFunction.

PartialEq on Box<dyn LoxCallable> Can Potentially Cause Stack Overflow

This stack overflow occurred when the codebase was still largely in the same state as the first stack overflow. That is:

  1. pub struct LoxFunction — The LoxFunction struct derived PartialEq via #[derive(Debug, Clone, PartialEq)].
  2. DataType aka Value enum — The Value::LoxCallable variant was still defined as Box<dyn LoxCallable>.
  3. In the manual PartialEq implementation for Value, the comparison for Value::LoxCallable was written as (DataType::LoxCallable(a), DataType::LoxCallable(b)) => a == b, 🤬.
  4. The variables involved in the comparison were Value::LoxCallable(Box<dyn LoxCallable>), whose concrete type was LoxFunction.

Box<dyn LoxCallable> doesn’t support PartialEq out of the box. So when Rust attempts to evaluate the equality comparison, it will:

  1. Try to resolve PartialEq for the trait object.
  2. Since PartialEq was derived on LoxFunction, it will compare all fields.
  3. That includes the closure field, which is an Environment.
  4. Environment can contain values that include functions, which contain environments, and so on. This leads to infinite recursion and ultimately a stack overflow.

💡 The solution involved two key fixes:

Compare the data pointer only—check identity, not equality. Replace the offending line (DataType::LoxCallable(a), DataType::LoxCallable(b)) => a == b, with:

61
62
	(Value::LoxCallable(a), Value::LoxCallable(b)) =>
		std::ptr::eq(a.as_ref(), b.as_ref()),

See the full updated impl PartialEq for Value.

Manually implement PartialEq for LoxFunction. This gives you full control over the equality logic: compare closure by identity, and declaration by value.

Box<dyn LoxCallable> Breaks Pointer Identity

When I ran the author-provided test script test/operator/equals_method.lox, which contains the following:

// Bound methods have identity equality.
class Foo {
  method() {}
}

var foo = Foo();
var fooMethod = foo.method;

// Same bound method.
print fooMethod == fooMethod; // expect: true

// Different closurizations.
print foo.method == foo.method; // expect: false

💥 I got false and false instead of the expected true and false.

The manual Clone implementation for DataType correctly cloned Box<dyn LoxCallable> using: DataType::LoxCallable(callable) => DataType::LoxCallable(callable.clone_box()). However, this creates a new boxed object, which breaks pointer identity.

💡 The fix is to use Rc<dyn LoxCallable> instead of Box<dyn LoxCallable>. Since Rc::clone() preserves pointer identity, this change ensures correct behavior. See the refactored pub enum Value.

With Rc<dyn LoxCallable>, we can safely derive Clone using #[derive(Clone)], eliminating the need for a manual implementation.

After this change, the test script now produces the correct result.

❹ The New LoxInstance::get() and Interpreter::visit_get_expr() Methods

The src/lox_instance.rs is a new module introduced in Chapter 12. It defines and implements the LoxInstance struct, which is also added as a variant of pub enum Value: LoxInstance(Rc<RefCell<LoxInstance>>).

At this stage of development, I ran the author-provided script referenced in the beginning and got the wrong output: Foo.init(one), Foo.init(two), Foo instance, and field. 💥 The last entry should have been init.

After several debugging iterations, I discovered the issue: in Interpreter::visit_get_expr(), I passed a &LoxInstance to LoxInstance::get(). That method then created a new Rc<RefCell<LoxInstance>> from the borrowed reference, which broke pointer identity.

However, Interpreter::visit_get_expr() already had access to the original Rc<RefCell<LoxInstance>>. I should have passed that directly to LoxInstance::get() instead of a reference. Then, inside LoxInstance::get(), I could simply clone it using Rc::clone(), which preserves pointer identity.

This fix allows the script — test/constructor/call_init_explicitly.lox — to run correctly.

❺ Other Side-Effect Refactorings

By side-effect, I mean refactorings triggered by bug fixes in existing code or by the introduction of new code.

⓵ With the addition of the two LoxCallable and LoxInstance variants to the Value enum, the former src/data_type.rs module was renamed to src/value.rs. The enum is now named Value; DataType has been removed.

⓶ As discussed in , we replaced Box<dyn LoxCallable> with Rc<dyn LoxCallable> in the src/lox_callable.rs module. This change allowed us to simplify the module down to a trait declaration. We also removed the to_string() method in favor of Display. That means any concrete type implementing the LoxCallable trait must also implement Display. See:

  1. The existing src/lox_clock.rs.
  2. The existing src/lox_function.rs.
  3. The new src/lox_class.rs.

⓷ Up to Chapter 11, the src/environment.rs module’s ancestor() and get_at() methods included a name: &Token parameter. However, the original Java methods do not.

I’ve been reading and coding chapter by chapter without looking ahead, so I didn’t initially understand why getAt() lacked a Token name in the Java version. In Chapter 12, getAt() is called in LoxFunction.java without a Token name, which isn’t available at that point in the code anyway.

To match the Java version, I refactored the src/environment.rs module. This involved:

  1. Adding a new get_by_name() helper method.
  2. Removing the name: &Token parameter from both ancestor() and get_at().

The original purpose of the name: &Token parameter was to provide error context, specifically line numbers. But get_at() is only used for internally resolved variables. By the time it’s called in the Interpreter, resolution has already guaranteed the variable exists, so error context isn’t needed. If it fails, it’s a bug in the Interpreter, not user code. So the name: &Token was unnecessary.

❻ New Code

Apart from the src/lox_instance.rs module, which we discussed above, the other code additions are fairly straightforward and closely mirror the Java version from the book.

⓵ The new src/lox_class.rs module — Mirrors the Java version, though the call() method in Rust is particularly interesting.

⓶ The existing src/parser.rs module — Mirrors the Java version:

  1. Added a new class_declaration() method.
  2. Updated the existing declaration() method to invoke class_declaration().
  3. Updated the existing methods call(), assignment(), and primary() as per Chapter 12.

⓷ Implemented the following methods in the existing src/resolver.rs module, as described in Chapter 12 — visit_class_stmt(), visit_get_expr(), visit_set_expr(), and visit_this_expr().

⓸ Updated the existing src/interpreter.rs module:

  1. Implemented the following methods as per Chapter 12: visit_class_stmt(), visit_get_expr(), visit_set_expr(), and visit_this_expr().
  2. Updated the existing methods is_truthy() and stringify() to handle the new Value::LoxInstance(Rc<RefCell<LoxInstance>>) enum variant.
  3. Updated the existing look_up_variable() method — In response to the Environment::get_at() signature refactoring discussed earlier.

❼ Test Updates

● A new module, tests/test_classes.rs, was added. Its structure and conventions follow those of the existing test modules. It incorporates a relevant set of author-provided test scripts.

● In the existing module tests/test_parser.rs, additional methods were introduced to test Chapter 12 parsing error logic:

  1. Method test_parser_classes_field_and_property() and its helper get_classes_field_and_property_script_results().
  2. Method test_parser_classes_methods_on_classes() and its helper get_classes_methods_on_classes_script_results().
  3. Method test_parser_classes_this() and its helper get_classes_this_script_results().

● The update to the existing tests/test_interpreter.rs module was minor: a few adjustments were made in response to the removal of the get_ prefix from getter methods in the src/lox_error.rs module.

What’s Next

That wraps up Chapter 12: Classes. I apologise if this post feels a bit long, but I believe it’s important to document everything thoroughly for my future self—especially after encountering so many bugs in this chapter. I’m still not entirely satisfied with the code; there’s room for improvement, and I may revisit it for refactoring later on.

There are still a few warnings about dead code, which I’m okay with for now.
Only one chapter remains in Part II, and about 10 days left until the end of August 2025.
Hopefully, I’ll be able to complete it by then.

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.