Evaluator Environment Architecture
Relevant source files
The following files were used as context for generating this wiki page:
The Evaluator Environment Architecture provides the infrastructure for variable binding, scope management, and procedure application within the SICP metacircular and lazy evaluators. Unlike the traditional Scheme implementation which relies on mutable shared state, this Rust implementation utilizes persistent data structures and functional state threading to manage environments safely and efficiently.
The architecture is primarily built upon im::HashMap, which allows for $O(1)$ cloning through structural sharing. This design choice enables closures to own their captured environments without the need for complex reference counting or interior mutability wrappers like Rc<RefCell<>>.
Sources: rust-examples/chapter4/src/section_4_1.rs:9-15, rust-examples/sicp-common/src/environment.rs
Core Environment Structure
The environment is implemented as a generic Environment<V> struct that wraps a persistent map of strings to values.
Persistence and Structural Sharing
The use of im::HashMap is central to the architecture. When an environment is extended (e.g., during a procedure call) or modified via a definition, a new version of the environment is created. Because the underlying data structure uses structural sharing, these operations are highly efficient in terms of memory and time.
The diagram shows how an extended environment logically contains its own bindings while referencing the parent global environment.
Sources: rust-examples/chapter4/src/section_4_1.rs:18-22, rust-examples/sicp-common/Cargo.toml:10
Environment API
The Environment struct provides a functional interface for managing bindings:
| Method | Description | Return Type |
|---|---|---|
new() | Creates a new, empty environment. | Environment<V> |
define(name, value) | Binds a value to a name in the current frame. | Environment<V> |
extend(bindings) | Extends the environment with a batch of new bindings. | Environment<V> |
lookup(name) | Retrieves a value by name from the environment. | Option<&V> |
Sources: rust-examples/chapter4/src/section_4_1.rs:185-230, rust-examples/chapter4/src/section_4_2.rs:107-120
Functional State Threading
A critical architectural pattern in the Rust implementation is Functional State Threading. In traditional Scheme, the eval procedure mutates a shared environment. In this project, eval returns a tuple containing the evaluated Value and the updated Environment.
Execution Flow
When a define expression is encountered, the evaluator produces a new environment containing the new binding. This new environment must be passed (threaded) to subsequent evaluations in a sequence to ensure visibility of the new variable.
This flowchart illustrates how the environment state is threaded through a sequence of evaluations.
Sources: rust-examples/chapter4/src/section_4_1.rs:23-28, rust-examples/chapter4/src/section_4_1.rs:242-258
Closure and Scope Architecture
The architecture handles lexical scoping by allowing Value::Closure to own its captured environment.
Owned Closures
When a Lambda is evaluated, the current environment is cloned. Because of the persistent nature of the environment, this clone is an $O(1)$ operation. The resulting Closure runtime value stores this cloned environment. When the closure is later applied, the evaluator extends this stored environment with the procedure's arguments, creating a local scope that correctly resolves free variables from the definition site.
Recursive Bindings
To support recursion without shared mutable state, the architecture uses a self_name field in the Closure and Procedure enums. During application, if a self_name is present, the closure is re-bound to that name within the local call environment before the body is evaluated.
// From section_4_1.rs
Value::Closure {
params: Vec<String>,
body: Vec<Expr>,
env: Environment<Value>, // Closure OWNS a copy
self_name: Option<String>, // Enables recursion
}
Sources: rust-examples/chapter4/src/section_4_1.rs:94-102, rust-examples/chapter4/src/section_4_1.rs:271-285, rust-examples/chapter4/src/section_4_2.rs:56-62
Variations: Lazy Evaluation Environment
In the lazy evaluator (section_4_2.rs), the environment architecture is augmented to support delayed evaluation through "Thunks."
Thunk Memoization
The lazy environment stores Value::Thunk(Rc<Thunk>). A Thunk captures both an expression and the environment in which it was created. To handle memoization without RefCell, the architecture utilizes std::cell::OnceCell. This allows a thunk to be forced exactly once, with the result being stored for future lookups while maintaining the overall safety of the persistent environment.
| Component | Responsibility |
|---|---|
actual_value | Evaluates an expression and forces any resulting thunks. |
force_it | Performs the memoization check and executes delayed expressions. |
delay_it | Packages an expression and environment into a Thunk. |
Sources: rust-examples/chapter4/src/section_4_2.rs:71-85, rust-examples/chapter4/src/section_4_2.rs:231-255
Conclusion
The Evaluator Environment Architecture successfully adapts Scheme's dynamic environment model to Rust's strict ownership and safety requirements. By replacing shared mutable lists with persistent hash maps and adopting functional state threading, the system achieves memory safety and deterministic cleanup without sacrificing the flexibility required for a metacircular evaluator. This architecture serves as the foundation for both the standard metacircular interpreter and the lazy evaluation variant.
Sources: rust-examples/chapter4/src/section_4_1.rs:9-35, rust-examples/chapter3/SECTION_3_3_SUMMARY.md:78-82