rlox: A Rust Implementation of “Crafting Interpreters” – Inheritance
This post covers Chapter 13 of Crafting Interpreters: Inheritance. Class inheritance syntax <
— Class
<
SuperClass
— has been implemented. The final remaining syntax element, Expr::Super
, representing the super
keyword, has also been added. In this post, we briefly discuss the new code, followed by bug fixes and test updates.
🦀 Index of the Complete Series.
![]() |
---|
rlox: A Rust Implementation of “Crafting Interpreters” – Inheritance |
🚀 Note: You can download the code for this post from GitHub using:
git clone -b v0.6.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/scanning/sample.lox
Content of sample.lox:
class Doughnut {
cook() {
print "Fry until golden brown.";
}
}
class BostonCream < Doughnut {
cook() {
super.cook();
print "Pipe full of custard and coat with chocolate.";
}
}
fun cooking(num) {
var dish = BostonCream();
print num;
dish.cook();
}
for(var i=0; i<5; i=i+1) {
cooking(i);
}
This Lox script is from
here.
The Doughnut
and BostonCream
classes are from the book.
When I first explored the possibility of implementing the code in Rust,
I downloaded and compiled this repository and ran the script. It executed successfully,
and I was so inspired that I undertook the implementation myself. I’ve included it in the
scanner test. However, I can’t
remember why I named it sample.lox
instead of the original
example.lox
.
For more details on the Lox language, refer to
this section of the
README.md
.
Legend: ★ = updated, ☆ = new.
💥 Files not modified are omitted for brevity.
.
├── Cargo.toml
├── docs
│ └── RLoxGuide.md ★
├── README.md
├── src
│ ├── interpreter.rs ★
│ ├── lox_class.rs ★
│ ├── lox_error_helper.rs ★
│ ├── lox_error.rs ★
│ ├── lox_runtime_error.rs ★
│ ├── parser.rs ★
│ ├── resolver.rs ★
│ └── scanner.rs ★
└── tests
├── data/ ☆ ➜ some more were added
├── README.md ★
├── test_classes.rs ★
├── test_common.rs ★
├── test_inheritance.rs ☆
├── test_interpreter.rs ★
├── test_parser.rs ★
├── test_resolving_and_binding.rs ★
└── test_scanner.rs ★
Compared to Chapter 12, Chapter 13 was somewhat a breeze. I didn’t encounter any further errors that required debugging. We briefly describe the implementation of the new code. Next, we discuss bug fixes to the
Scanner
,
Parser
,
Resolver
, and
Interpreter
to ensure all errors are returned, along with any partial evaluation results—particularly in the case of the Interpreter
.
Finally, we discuss the test updates.
❸ Process Flow:
Scanner
, Parser
, Resolver
, and Interpreter
When the
Scanner
encounters an error, no token list is produced, so the
Parser
cannot and should not run.
Similarly, if the Parser
fails, no statement list is available, and the
Resolver
should not run. If the Resolver
fails, the
Interpreter
should not run either. While the statement list from the
Parser
remains available, attempting to evaluate unresolved statements
with the Interpreter
would be futile.
This interaction between the four components is illustrated in the flowchart below:
Chapter 13 does not introduce any new modules. Instead, new code is added to the following existing modules.
⓵ The existing src/parser.rs
module — Mirrors the Java version:
-
Updated the existing
class_declaration()
method. -
Updated the existing
primary()
method.
The new code supports superclass declarations via the
<
syntax, and enables access to superclass methods and properties using the super
keyword.
⓶ The existing
src/resolver.rs
module — Mirrors the Java version:
-
Updated the existing
visit_class_stmt()
method — Resolves superclass references. -
Implemented the
visit_super_expr()
method — Resolves thesuper
keyword.
Additionally, for methods that return a
LoxRuntimeError
enum on error, we now use the
runtime_error()
helper method to construct the error.
⓷ The existing
src/interpreter.rs
module — Mirrors the Java version:
-
Updated the existing
visit_class_stmt()
method — Evaluates the superclass, i.e., the entity on the right-hand side of<
. For example, inBostonCream < Doughnut
,Doughnut
is the superclass. -
Implemented the
visit_super_expr()
method — Evaluates thesuper
keyword.
The Rust code is more verbose than the Java version due to language constraints, but the implementation still closely mirrors the original.
As with the Resolver
, methods returning a
LoxRuntimeError
enum now use the
runtime_error()
helper method for error construction.
⓸ The existing
src/lox_class.rs
module — Mirrors the Java version:
-
Added the
superclass
field. Consequently, theconstructor
has been updated to accept a newsuperclass
parameter. -
The
find_method()
method has been updated to search recursively in the superclass. Its return type is now an ownedOption<LoxFunction>
instead of a borrowedOption<&LoxFunction>
.
❺ Bug Fix to Return Multiple Errors and Partial Evaluation Results
As of the Chapter 12 revision, the Scanner
, Parser
,
Resolver
, and Interpreter
returned only the first error
encountered—halting execution immediately. I overlooked this behavior, even while
implementing tests using the author-provided scripts. For some reason, I ignored
multiple errors in two scripts. This was a bug, though no debugging was required to fix it.
⓵ The Scanner
— Two bug fixes were made in the
scan_tokens()
method:
-
If the source text is blank, return an error immediately with the message
Source text is empty.
This is my own addition to handle empty input. -
To capture and return multiple errors, we maintain a local vector of strings.
Each time an error occurs, we push the message into the vector and continue scanning.
After scanning the full source, if the vector is empty, we return the token list. Otherwise, we return all error messages as a single string, separated by newline
\n
characters.
⓶ The Parser
— Similar to the Scanner
, in the
parse()
method, we maintain a local vector of strings.
When a token causes an error, we push the message into the vector and continue parsing.
After processing all tokens, if the vector is empty, we return the statement list.
Otherwise, we return all error messages as a single string, separated by newline
\n
characters.
⓷ The Resolver
— We apply the same approach. However, in the absence of errors, the
resolve()
method simply returns Ok(())
.
⓸ The Interpreter
— We follow the same pattern as the Resolver
.
The
interpret()
method now evaluates all
statements, capturing and returning both successful results and error messages.
💡 Recall that the Interpreter
has an
output: Box<dyn Writable>
field,
which writes all evaluation results to a buffer. In tests, we pass a byte stream to this field.
It is entirely reasonable to expect both successful and failed evaluations across statements. Therefore:
-
Successful evaluations are written to
output
as usual. -
When an error occurs:
-
We write the error message to
output
. - We also push the error message to the local string vector.
-
We write the error message to
Thus, the
Interpreter::output
buffer contains
everything when errors occur, and only evaluation results when successful.
The returned error contains only error messages. Naturally, when errors occur,
those messages are also present in the output
buffer.
● A new module,
tests/test_inheritance.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
tests/test_parser.rs
module, additional methods were
introduced to test Chapter 13 parsing error logic:
-
Method
test_parser_inheritance_super_sub_class()
and its helperget_inheritance_super_sub_class_script_results()
. -
Method
test_parser_inheritance_calling_superclass_method()
and its helperget_inheritance_calling_superclass_method_script_results()
.
● Regarding the author-provided
benchmark scripts — These are not included in integration tests. I ran them manually and recorded the results. See
README.md
.
The primary intention of this exercise is to demonstrate that the implementation is correct and can successfully run all author-provided test scripts.
● Regarding the
test scripts — All scripts are included except those in
test/limit
.
● In the
tests/test_resolving_and_binding.rs
and
tests/test_classes.rs
modules — Removed the Error:
prefix from failed Resolver
test outputs.
● Test refactoring triggered by bug fixes in point ❹:
⓵ In the
tests/test_common.rs
module:
-
The
assert_interpreter_result()
method — Refactored per the interpreter bug fix:- For both successful and failed scripts: validate all expected output entries.
- For failed scripts: also verify that all returned error messages appear in the expected output.
-
Similarly, the
assert_resolver_result()
,assert_parser_result()
, andassert_scanner_result()
methods — Refactored to validate all expected error messages for failed scripts, per the respective bug fixes forResolver
,Parser
, andScanner
.
⓶ In the existing
tests/test_parser.rs
module — Bug-fixed the helper method
get_for_loops_script_results()
to include multiple error messages for
./tests/data/for/statement_condition.lox
and
./tests/data/for/statement_initializer.lox
.
⓷ In the existing
tests/test_scanner.rs
module — Updated the helper method
get_generic_script_results()
to include the author-provided scripts
./tests/data/empty_file.lox
and
./tests/data/unexpected_character.lox
, as well as the new script
./tests/data/scanning/multi_errors.lox
. I had missed the two author-provided ones until halfway through writing this post.
⓸ In the existing
tests/test_interpreter.rs
module — Added a new test method
test_interpreter_precedence()
and its helper
get_precedence_script_results()
to test the author-provided script
./tests/data/precedence.lox
, which I had missed along with the two mentioned above.
That wraps up Chapter 13: Inheritance. I didn’t plan for this post to be quite this long—my apologies for its length.
There are still a few warnings about dead code, which I understand the reasons for. I’m leaving them as-is for now. I plan to do another round of refactoring across both the main code and test modules, without adding new features—so I can focus more clearly on areas I’m not fully happy with.
This post also marks the completion of Part II of the book. I’m not yet sure if I’ll take on Part III. I’m leaning toward it… but it would be a long undertaking, and I’m not committing to it in writing 😂.
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/
- https://www.anyrgb.com/en-clipart-23wno/download