A complete interpreter for the Monkey programming language, implemented in Rust. This project is a Rust adaptation of the interpreter described in "Writing An Interpreter In Go" by Thorsten Ball.
I recently wrapped up Writing an Interpreter in Go, a book that demystified lexer, parser, ASTs, and evaluator logic. To deepen my understanding and sharpen my skills in Rust, I reimplemented the interpreter in Rustβand I couldn't be more satisfied with the outcome.
After over a decade in software engineeringβsplit between systems infrastructure and full-stack developmentβI realized that, despite my experience across multiple paradigms and languages, I never truly understood the mechanics behind interpreters. This project filled that gap, layer by layer.
Here's what I learned:
- The interpreter is built on clear, sequential stagesβlexing raw text, parsing into syntax trees, and evaluating with the right semantics.
- Working through the Monkey interpreter in Rust taught me how these pieces fit together in practiceβand how Rust's features impact implementation choices.
- It wasn't just about the codeβit was about learning how languages work under the hood.
This project reminded me how rewarding it is to rediscover fundamentalsβeven after years in tech. It's been one of my most fulfilling learning experiences in a while.
The Monkey interpreter supports:
- Variables with
letstatements and mutable assignment (variable = new_value) - Data types: integers, booleans, strings, arrays, and hash maps
- Control flow:
if/elseconditionals,whileloops, andforloops for iteration - Type casting:
int()and enhancedstring()builtin functions for type conversion - Operators: arithmetic (
+,-,*,/), comparison (<,>,<=,>=,==,!=), logical (&&,||), and unary (!,-) - Functions with closures and first-class support
- Built-in functions:
len,first,last,rest,push,puts,exit,int,string - Array indexing and hash map access
- Comments: single-line (
//) and multi-line (/* */) - String escape sequences:
\n,\t,\",\\,\r,\xHH(hex),\uHHHH(Unicode) - Enhanced error handling with structured error types implementing
std::error::Errortrait - Enhanced REPL with command history, special commands, and improved user experience
The interpreter follows a traditional compiler architecture:
- Lexer (
src/lexer/mod.rs): Tokenizes input text into tokens - Parser (
src/parser/mod.rs): Recursive descent parser with Pratt parsing for expressions - AST (
src/ast/mod.rs): Abstract Syntax Tree representation - Evaluator (
src/evaluator/mod.rs): Tree-walking interpreter that evaluates AST nodes - Object System (
src/object/mod.rs): Runtime value representation with environment scoping - REPL (
src/repl/mod.rs): Interactive Read-Eval-Print Loop
# Install from crates.io (when published)
cargo install monkey-interpreter-rs
# Or build from source
git clone https://github.com/samidarko/monkey-interpreter-rs
cd monkey-interpreter-rs
cargo build --release# Build the project
cargo build
# Run the enhanced interactive REPL
cargo run
# Run a Monkey program file
cargo run -- examples/fibonacci.monkey
cargo run -- examples/while_loops.monkey
cargo run -- examples/for_loops.monkey
# Run tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run benchmarks
cargo bench// Variables with mutable assignment
let x = 5;
x = x + 10; // x is now 15
// Type casting with builtin functions
let num_str = "42";
let num = int(num_str); // Convert string to integer
let result = string(num); // Convert back to string
let arr_str = string([1,2]); // Convert array to string: "[1, 2]"
let hash_str = string({"key": "value"}); // Convert hash to string
// Variables and functions
let fibonacci = fn(x) {
if (x == 0) {
0
} else {
if (x == 1) {
1
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};
fibonacci(10);
// While loops for iteration
let i = 0;
let sum = 0;
while (i < 10) {
sum = sum + i;
i = i + 1;
}
// For loops for array and hash iteration
let numbers = [1, 2, 3, 4, 5];
let total = 0;
for (num in numbers) {
total = total + num; // total becomes 15
}
let person = {"name": "John", "age": 30};
for (key in person) {
puts("Property: " + string(key));
}
// Arrays and higher-order functions
let map = fn(arr, f) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated
} else {
iter(rest(arr), push(accumulated, f(first(arr))));
}
};
iter(arr, []);
};
let a = [1, 2, 3, 4];
let double = fn(x) { x * 2; };
map(a, double); // [2, 4, 6, 8]
/* Multi-line comments
are also supported */
let person = {"name": "Alice", "age": 30};
person["name"]; // "Alice"
// Enhanced comparison and logical operators
let adult = person["age"] >= 18 && person["name"] != "";
if (adult) {
puts("Person is an adult named: " + person["name"]);
}
// String with escape sequences including hex and Unicode
puts("Hello\nWorld!\tTab\x21\u0020Unicode: \u2764\uFE0F\"Quote\"");
// Exit the REPL
exit(); // or exit(42) for custom exit codelen(array|string)- Returns length of arrays or stringsfirst(array)- Returns first element of an arraylast(array)- Returns last element of an arrayrest(array)- Returns array with all elements except the firstpush(array, element)- Returns new array with element appendedputs(args...)- Prints arguments to stdoutexit([code])- Exits the REPL with optional exit code
int(string)- Converts a string to an integerstring(value)- Converts integers, arrays, hash maps, booleans, null, or strings to string representation
help- Show available commands and language examplesclear- Clear the terminal screenhistory- Display command historyCtrl+CorCtrl+D- Exit the REPL
The interpreter features a robust error handling system designed for better developer experience and ecosystem integration:
All error types implement std::error::Error and provide detailed error information:
-
EvaluatorError: Runtime evaluation errors with categorized types:TypeError: Type mismatches and invalid operationsRuntimeError: General execution errorsIdentifierNotFound: Undefined variable accessDivisionByZero: Mathematical errorsInvalidFunctionCall: Function call errorsInvalidIndex: Array/hash indexing errors
-
ParserError: Parsing errors with position information -
LexerError: Tokenization errors with character position
The system supports automatic error propagation using the ? operator through From trait implementations, eliminating manual error conversion throughout the codebase.
All errors work seamlessly with popular Rust error handling crates like anyhow, eyre, and thiserror for larger applications.
# Test specific modules
cargo test lexer
cargo test parser
cargo test evaluator
# Run a specific test by name
cargo test fibonacci# Check code without building
cargo check
# Run clippy for linting
cargo clippy
# Format code
cargo fmt- π οΈ Enhanced Error Handling: Structured error types with
std::error::Errorimplementation for better ecosystem integration and automatic error conversions - π For Loops: Added iterator-style
for (variable in collection) { body }loops for arrays and hash maps with proper variable scoping - π While Loops: Added
while (condition) { body }loops for iterative programming patterns - π Variable Assignment: Added mutable variable assignment (
x = value) with proper operator precedence - π Enhanced Type Casting: Improved
string()builtin to support arrays, hash maps, booleans, null, and more types - π’ Type Conversion: New
int()builtin function for string-to-integer conversion - β‘ Enhanced REPL: Interactive shell with command history, special commands (
help,clear,history), and improved user experience - π Example Programs: Comprehensive example files including while loops, Fibonacci, basic demos, and more
- π Performance Benchmarks: Added benchmarking suite for performance monitoring
- Enhanced Error Reporting: Comprehensive error messages with line/column position tracking and contextual information
- Extended Operator Support: Added comparison operators (
<=,>=) and logical operators (&&,||) - Comment Support: Full single-line (
//) and multi-line (/* */) comment parsing - String Escape Sequences: Support for escape sequences (
\n,\t,\",\\,\r,\xHHhex,\uHHHHUnicode) - Modular Architecture: Split large files into logical modules for better maintainability
- Comprehensive Testing: 64+ tests covering all features including edge cases and error conditions
- Error Handling: Uses
Resulttypes throughout for proper error propagation - Memory Management: Uses
Rc<RefCell<>>for shared environment references in closures - Pattern Matching: Leverages Rust's powerful pattern matching for AST evaluation
- Type Safety: Rust's type system catches many interpreter bugs at compile time
- Position Tracking: Detailed source position tracking for enhanced debugging experience
- Separation of Concerns: Each phase (lexing, parsing, evaluation) is cleanly separated into modules
- Extensibility: Easy to add new language features by extending the AST and evaluator
- Testing: Comprehensive test suite covering all language features and error conditions
- Error Quality: Professional-quality error messages that help users identify and fix issues quickly
- Performance: Tree-walking interpreter optimized for clarity over speed
This project provided deep insights into:
- How programming languages are structured and implemented
- The relationship between syntax, semantics, and evaluation
- Rust's ownership model in the context of tree structures and shared state
- The beauty of recursive descent parsing and tree-walking evaluation
- How closures and environments work under the hood
This project is for educational purposes, inspired by "Writing An Interpreter In Go" by Thorsten Ball.