This project is a feature-complete interpreter for the Lox programming language, implemented in idiomatic Rust. Based on Robert Nystrom's book "Crafting Interpreters", this implementation leverages Rust's strengths to create a memory-safe, performant interpreter with clean, maintainable code.
-
Complete language implementation including:
- Lexical scanning and tokenization
- Recursive descent parser with proper precedence
- AST (Abstract Syntax Tree) based evaluation
- Dynamic typing system
- Variables and assignment
- Control flow (if/else, while, for loops)
- First-class functions with closures
- Classes with inheritance
- Method calls with
this
binding - Superclass method access with
super
- Block scoping with lexical environments
- Comprehensive error reporting
-
Idiomatic Rust design showcasing:
- Strong type-safety through Rust's type system
- Memory safety without garbage collection
- Functional programming techniques
- Advanced pattern matching
- Zero-cost abstractions
- Smart pointers for managing object lifetimes
The interpreter implements a robust dynamic object system using Rust's enum types and smart pointers:
#[derive(Debug, Clone)]
pub enum Object {
Boolean(bool),
Callable(Function),
Class(Rc<RefCell<LoxClass>>),
Instance(Rc<RefCell<LoxInstance>>),
Null,
Number(f64),
String(String),
}
Classes and instances are managed with reference-counted cells (Rc<RefCell<T>>
) to allow for shared ownership and interior mutability, essential for modeling object-oriented concepts in a memory-safe way.
The environment uses a parent-pointer tree structure with Rc<RefCell<T>>
for efficient variable lookup while maintaining memory safety. This approach provides:
- Proper scoping for variables
- Clean handling of nested environments
- Support for closures that capture their lexical environment
- No need for manual memory management or GC
The interpreter implements the visitor pattern using Rust traits and generics, providing type-safe traversal of the AST while maintaining separation of concerns between syntax and execution.
impl expr::Visitor<Object> for Interpreter {
fn visit_binary_expr(
&mut self,
left: &Expr,
operator: &Token,
right: &Expr,
) -> Result<Object, Error> {
let l = self.evaluate(left)?;
let r = self.evaluate(right)?;
// Type checking and operation handling
match operator.token_type {
TokenType::Plus => match (l, r) {
(Object::Number(left_num), Object::Number(right_num)) => {
Ok(Object::Number(left_num + right_num))
}
(Object::String(left_str), Object::String(right_str)) => {
Ok(Object::String(left_str.clone() + &right_str))
}
_ => Err(Error::Runtime {
token: operator.clone(),
message: "Operands must be two numbers or two strings".to_string(),
}),
},
// Other operations...
}
}
// Other visitor methods...
}
The implementation includes a static analysis pass before execution:
pub fn resolve(&mut self, name: &Token, depth: usize) {
self.locals.insert(name.clone(), depth);
}
This resolution step:
- Improves variable lookup performance
- Ensures proper closure semantics
- Detects errors like referencing a variable in its own initializer
- Validates proper use of
this
andsuper
references
Error propagation uses Rust's Result
type with detailed error information, enabling:
- Clear error messages with source line information
- Proper error recovery in the parser
- Graceful handling of runtime errors
- Special handling for return statements via a custom Error variant
The codebase demonstrates functional programming techniques in Rust, such as:
- Monadic Error Handling: Using
Result
andOption
types as monads for clean error propagation - Combinators: Utilizing functions like
map
,unwrap_or
, and other functional combinators for more expressive code - Declarative Style: Favoring declarative over imperative code where appropriate
- Immutability: Preferring immutable data structures when possible
These approaches lead to more concise and robust code, reducing the likelihood of bugs while improving readability.
main.rs
- Program entry point, REPL and file execution logicscanner.rs
- Lexical scanner that converts source code to tokenstoken.rs
- Token definitions and utilitiesparser.rs
- Recursive descent parser that builds the ASTsyntax.rs
- AST node definitions and visitor implementationinterpreter.rs
- Tree-walk interpreter for executionenvironment.rs
- Variable scope and environment handlingresolver.rs
- Static analyzer for variable resolutionobject.rs
- Runtime value representationsclass.rs
- Class and instance implementationsfunction.rs
- Function call mechanism and closureserror.rs
- Error types and reportingbuild.rs
- Build-time code generation for keywords
var a = 1;
var b = 2;
print a + b; // Outputs: 3
var a = 1;
if (a > 2)
print "Greater than two";
else
print "Less than two";
fun makeCounter() {
var i = 0;
fun count() {
i = i + 1;
print i;
}
return count;
}
var counter = makeCounter();
counter(); // "1".
counter(); // "2".
class Doughnut {
cook() {
print "Fry until golden brown.";
}
}
class BostonCream < Doughnut {
cook() {
super.cook();
print "Pipe full of custard and coat with chocolate.";
}
}
BostonCream().cook();
class Cake {
taste() {
var adjective = "delicious";
print "The " + this.flavor + " cake is " + adjective + "!";
}
}
var cake = Cake();
cake.flavor = "German chocolate";
cake.taste(); // Prints "The German chocolate cake is delicious!".
- Rust stable or newer
- Cargo
cargo build --release
Execute a Lox script:
cargo run --release -- path/to/script.lox
Start the REPL:
cargo run --release
cargo test
The repository includes several example Lox programs in the examples/
directory:
examples/
├── assign.lox - Basic variable declaration and assignment
├── branching.lox - If/else control flow
├── class.lox - Comprehensive class example with methods and properties
├── eat_bacon.lox - Simple class with method call
├── fibonacci_for.lox - Fibonacci sequence using for loops
├── fibonacci_recursive.lox - Recursive Fibonacci implementation
├── fibonacci_while.lox - Fibonacci sequence using while loops
├── global_block_closure_scope.lox - Demonstrates closure scope resolution
├── incorrect_super.lox - Example of invalid super usage (for error testing)
├── inherit_method.lox - Basic inheritance example
├── instance.lox - Class instantiation example
├── logical.lox - Logical operators with short-circuit evaluation
├── make_counter.lox - Closure example with counter function
├── method.lox - Class method demonstration
├── print.lox - Basic printing of different types
├── scope.lox - Nested scope demonstration
├── super_method.lox - Superclass method access example
└── this.lox - Demonstration of this binding in methods
To run an example:
cargo run --release -- examples/class.lox
The project includes a GitHub Actions workflow that:
- Runs all tests
- Builds the release binary
- Executes the examples to verify interpreter functionality
This project served as a dual learning experience, deepening my understanding of both Rust and interpreter design:
- Ownership System: Implementing environments and variable scoping required careful consideration of Rust's ownership rules
- Smart Pointers: Using
Rc<RefCell<T>>
to create parent-pointer trees for lexical scoping and object representation - Error Handling: Leveraging Rust's Result type for robust error propagation throughout the interpreter
- Pattern Matching: Applying exhaustive pattern matching for elegant handling of AST nodes and object types
- Traits and Generics: Implementing the visitor pattern through Rust's trait system
- Functional Programming: Using combinators like
map
,unwrap_or
, and more declarative approaches vs. imperative code
- Lexical Analysis: Transforming raw source text into meaningful tokens
- Recursive Descent Parsing: Implementing a hand-written parser for grammar rules
- Abstract Syntax Trees: Representing code as data structures that can be traversed
- Type Systems: Runtime type checking and dynamic typing
- Evaluation Strategies: Implementing proper order of operations and short-circuit evaluation
- Environments: Creating a chain of environments for lexical variable lookup
- Closures: Capturing lexical environment for first-class functions
- Classes and Inheritance: Implementing an object system with method dispatch and inheritance
- Error Recovery: Implementing synchronization points for error recovery in the parser
Possible extensions to this project:
- Just-in-time compilation for performance improvement
- Standard library implementation
- Module system
- Additional language features like arrays and maps
- Static type checker
- Bytecode VM implementation (similar to Part III of Crafting Interpreters)
This project is based on Robert Nystrom's excellent book "Crafting Interpreters", which provides a clear and thorough explanation of interpreter implementation. The original Java code has been thoughtfully adapted to idiomatic Rust.
This project is licensed under the MIT License - see the LICENSE file for details.