Skip to content

samidarko/writing_a_compiler_in_rust_monkey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

20 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Monkey Interpreter in Rust

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.

Project Background

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.

Features

The Monkey interpreter supports:

  • Variables with let statements and mutable assignment (variable = new_value)
  • Data types: integers, booleans, strings, arrays, and hash maps
  • Control flow: if/else conditionals, while loops, and for loops for iteration
  • Type casting: int() and enhanced string() 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::Error trait
  • Enhanced REPL with command history, special commands, and improved user experience

Architecture

The interpreter follows a traditional compiler architecture:

  1. Lexer (src/lexer/mod.rs): Tokenizes input text into tokens
  2. Parser (src/parser/mod.rs): Recursive descent parser with Pratt parsing for expressions
  3. AST (src/ast/mod.rs): Abstract Syntax Tree representation
  4. Evaluator (src/evaluator/mod.rs): Tree-walking interpreter that evaluates AST nodes
  5. Object System (src/object/mod.rs): Runtime value representation with environment scoping
  6. REPL (src/repl/mod.rs): Interactive Read-Eval-Print Loop

Usage

Installation

# 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

Building and Running

# 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

Example Monkey Code

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

Built-in Functions

Core Functions

  • len(array|string) - Returns length of arrays or strings
  • first(array) - Returns first element of an array
  • last(array) - Returns last element of an array
  • rest(array) - Returns array with all elements except the first
  • push(array, element) - Returns new array with element appended
  • puts(args...) - Prints arguments to stdout
  • exit([code]) - Exits the REPL with optional exit code

Type Conversion Functions

  • int(string) - Converts a string to an integer
  • string(value) - Converts integers, arrays, hash maps, booleans, null, or strings to string representation

Enhanced REPL Commands

  • help - Show available commands and language examples
  • clear - Clear the terminal screen
  • history - Display command history
  • Ctrl+C or Ctrl+D - Exit the REPL

Error Handling

The interpreter features a robust error handling system designed for better developer experience and ecosystem integration:

Structured Error Types

All error types implement std::error::Error and provide detailed error information:

  • EvaluatorError: Runtime evaluation errors with categorized types:

    • TypeError: Type mismatches and invalid operations
    • RuntimeError: General execution errors
    • IdentifierNotFound: Undefined variable access
    • DivisionByZero: Mathematical errors
    • InvalidFunctionCall: Function call errors
    • InvalidIndex: Array/hash indexing errors
  • ParserError: Parsing errors with position information

  • LexerError: Tokenization errors with character position

Automatic Error Conversion

The system supports automatic error propagation using the ? operator through From trait implementations, eliminating manual error conversion throughout the codebase.

Ecosystem Integration

All errors work seamlessly with popular Rust error handling crates like anyhow, eyre, and thiserror for larger applications.

Development

Running Specific Tests

# Test specific modules
cargo test lexer
cargo test parser
cargo test evaluator

# Run a specific test by name
cargo test fibonacci

Code Quality

# Check code without building
cargo check

# Run clippy for linting
cargo clippy

# Format code
cargo fmt

Implementation Highlights

Recent Enhancements

  • πŸ› οΈ Enhanced Error Handling: Structured error types with std::error::Error implementation 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, \xHH hex, \uHHHH Unicode)
  • Modular Architecture: Split large files into logical modules for better maintainability
  • Comprehensive Testing: 64+ tests covering all features including edge cases and error conditions

Rust-Specific Design Choices

  • Error Handling: Uses Result types 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

Architecture Benefits

  • 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

Learning Outcomes

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

License

This project is for educational purposes, inspired by "Writing An Interpreter In Go" by Thorsten Ball.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages