The Borrow Checker and the Primate: Inside jnsahaj/monkey-rs
How translating a Go-based interpreter into Rust forces a collision between garbage-collected assumptions and strict memory safety.

While the primary goal was to get good at Rust, learning about how compilers and interpreters work has left me with profound knowledge applicable immediately.
- Porting Thorsten Ball's Go-based Monkey interpreter to Rust exposes a fundamental conflict between garbage-collected closures and strict ownership rules.
- The project circumvents the borrow checker's limitations on shared environments by wrapping state in `Rc<RefCell<Environment>>`, mimicking GC behavior through reference counting and interior mutability.
- The dual implementation features both a recursive tree-walking interpreter and a performant stack-based virtual machine, illuminating the trade-offs between simplicity and execution speed.
- Rebuilding a known specification in a new language isolates syntax and paradigm constraints from underlying logic, offering an accelerated path to mastery.
The Memory Model Collision
Thorsten Ball’s highly regarded "Writing an Interpreter in Go" provides a pragmatic architecture for executing a custom language called Monkey. It relies heavily on closures and shared environments. In Go, these constructs are trivial. The language's garbage collector quietly monitors the heap, cleaning up dangling references when a closure's environment outlives its parent scope.
Rust, however, forbids this casual relationship with memory. The compiler's ownership model demands explicit lifetimes and single ownership. When jnsahaj set out to port the Monkey interpreter to Rust, this fundamental difference created an immediate architectural collision. The Go code assumed a safety net that simply did not exist in the target language.
Escaping the Borrow Checker
To implement functional closures, the interpreter must allow multiple guest functions to hold references to the same overarching scope. A pure functional approach—passing the entire state matrix through every function call—would require a complete rewrite of the book's architecture. Instead, the project needed an escape hatch.
use std::cell::RefCell;
use std::rc::Rc;
pub type MutEnv = Rc<RefCell<Environment>>;
The solution lies in src/common/environment.rs. By wrapping the Environment struct in Rc<RefCell<T>>, the author leverages Rust's interior mutability and reference counting. The Rc (Reference Counted) smart pointer allows multiple closures to own the environment simultaneously. The RefCell defers borrow checking to runtime, allowing the environment to be mutated even when shared.
Porting as Pedagogy
The value of jnsahaj/monkey-rs isn't in introducing a novel programming language paradigm. Its value is pedagogical. By tackling a known specification—where the logic of parsing if-statements and evaluating infix operators is already solved—the developer isolates the challenge entirely to the constraints of the host language.
The project serves as a clear blueprint for systems-minded engineers looking to understand how high-level abstractions map to low-level memory constraints.
Two Paths to Execution
The repository houses a dual implementation. The frontend parser generates an Abstract Syntax Tree (AST) that can be fed into either a tree-walking interpreter (src/interpreter) or a stack-based virtual machine (src/compiler).
The interpreter evaluates the AST recursively. It is conceptually simple and straightforward to debug, as every node implements the Display trait. However, this recursive evaluation is slow. The compiler, conversely, flattens the AST into custom bytecode, pushing and popping instructions onto a stack array. It trades the elegance of recursion for the raw speed of linear execution.
| Feature | Tree-Walker (Interpreter) | Bytecode VM (Compiler) |
|---|---|---|
| Execution Target | Abstract Syntax Tree (AST) | Flattened Bytecode |
| State Management | Recursive function calls | Linear stack array |
| Debugging | High-level string re-parsing | Low-level hex dumps |
The Elegance of Pratt Parsing
A hidden gem within the codebase is the parser implementation in src/common/parser.rs. Instead of relying on immense, nested match statements or parser-generator tools, the project employs a Top-Down Operator Precedence parser, often called a Pratt parser.
By utilizing a custom expect_peek! macro and leveraging Rust's powerful enum pattern matching, the logic required to parse complex mathematical expressions and operator precedence is reduced to highly readable, idiomatic code. It demonstrates how Rust's type system can make writing manual parsers a surprisingly ergonomic experience.