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.

149-feature-image.png
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

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/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.

Updated Repository Layout

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:

scanner-parser-resolver-interpret.png

❹ New Code

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:

  1. Updated the existing class_declaration() method.
  2. 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:

  1. Updated the existing visit_class_stmt() method — Resolves superclass references.
  2. Implemented the visit_super_expr() method — Resolves the super 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:

  1. Updated the existing visit_class_stmt() method — Evaluates the superclass, i.e., the entity on the right-hand side of <. For example, in BostonCream < Doughnut, Doughnut is the superclass.
  2. Implemented the visit_super_expr() method — Evaluates the super 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:

  1. Added the superclass field. Consequently, the constructor has been updated to accept a new superclass parameter.
  2. The find_method() method has been updated to search recursively in the superclass. Its return type is now an owned Option<LoxFunction> instead of a borrowed Option<&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:

  1. 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.
  2. 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:

  1. Successful evaluations are written to output as usual.
  2. When an error occurs:
    • We write the error message to output.
    • We also push the error message to the local string vector.

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.

❻ Test Updates

● 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:

  1. Method test_parser_inheritance_super_sub_class() and its helper get_inheritance_super_sub_class_script_results().
  2. Method test_parser_inheritance_calling_superclass_method() and its helper get_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:

  1. 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.
  2. Similarly, the assert_resolver_result(), assert_parser_result(), and assert_scanner_result() methods — Refactored to validate all expected error messages for failed scripts, per the respective bug fixes for Resolver, Parser, and Scanner.

⓶ 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.

What’s Next

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:

🦀 Index of the Complete Series.