dazzle_core/scheme/
environment.rs

1//! Scheme environment (variable bindings and lexical scoping)
2//!
3//! This module implements the environment for storing variable bindings,
4//! corresponding to OpenJade's `Interpreter` identifier management.
5//!
6//! ## OpenJade Correspondence
7//!
8//! OpenJade's `Interpreter` class maintains:
9//! - `identTable_`: Hash table of identifiers (symbols)
10//! - Environment frames (linked list for lexical scoping)
11//! - Global environment (built-in procedures + user definitions)
12//!
13//! ## Design
14//!
15//! We use a simple linked environment structure:
16//! - Each environment frame is a HashMap<Rc<str>, Value>
17//! - Parent pointer for lexical scoping
18//! - Gc-managed for safety
19
20// Suppress warnings from gc_derive macro (third-party crate issue)
21#![allow(non_local_definitions)]
22
23use crate::scheme::value::Value;
24use gc::{Gc, GcCell};
25use std::collections::HashMap;
26use std::rc::Rc;
27
28/// An environment frame (variable bindings)
29///
30/// Corresponds to OpenJade's environment frames in `Interpreter`.
31///
32/// ## Lexical Scoping
33///
34/// Each frame has:
35/// - `bindings`: Variables defined in this frame
36/// - `parent`: Enclosing scope (None for global environment)
37///
38/// Variable lookup walks up the parent chain until found or global reached.
39#[derive(Debug, Clone, gc::Trace, gc::Finalize)]
40pub struct Environment {
41    /// Variable bindings in this frame
42    bindings: GcCell<HashMap<Rc<str>, Value>>,
43
44    /// Parent environment (None for global scope)
45    parent: Option<Gc<Environment>>,
46}
47
48impl Environment {
49    /// Create a new global (top-level) environment
50    pub fn new_global() -> Gc<Self> {
51        Gc::new(Environment {
52            bindings: GcCell::new(HashMap::new()),
53            parent: None,
54        })
55    }
56
57    /// Create a new child environment extending the given parent
58    ///
59    /// Used for:
60    /// - Function calls (parameters in new scope)
61    /// - `let`, `let*`, `letrec` bindings
62    /// - Internal defines
63    pub fn extend(parent: Gc<Environment>) -> Gc<Self> {
64        Gc::new(Environment {
65            bindings: GcCell::new(HashMap::new()),
66            parent: Some(parent),
67        })
68    }
69
70    /// Define a variable in this environment (set binding)
71    ///
72    /// **Mutates** the current frame.
73    ///
74    /// Used for:
75    /// - `define` expressions
76    /// - Function parameter bindings
77    /// - `let` bindings
78    ///
79    /// ## DSSSL Note
80    ///
81    /// DSSSL is mostly functional, but `define` at top-level is imperative.
82    /// Internal `define`s in `lambda` bodies create a new frame.
83    pub fn define(&self, name: &str, value: Value) {
84        let symbol = Rc::from(name);
85        self.bindings.borrow_mut().insert(symbol, value);
86    }
87
88    /// Look up a variable in this environment or any parent
89    ///
90    /// Returns `None` if variable is undefined.
91    ///
92    /// Walks up the parent chain until:
93    /// - Variable found → return value
94    /// - Global reached and not found → return None
95    ///
96    /// Corresponds to OpenJade's `Interpreter::lookup()`.
97    pub fn lookup(&self, name: &str) -> Option<Value> {
98        let symbol = Rc::from(name);
99
100        // Check this frame first
101        if let Some(value) = self.bindings.borrow().get(&symbol) {
102            return Some(value.clone());
103        }
104
105        // Walk up parent chain
106        if let Some(ref parent) = self.parent {
107            return parent.lookup(name);
108        }
109
110        // Not found
111        None
112    }
113
114    /// Set an existing variable (mutation)
115    ///
116    /// Returns `Ok(())` if variable found and updated, `Err` if not found.
117    ///
118    /// Used for:
119    /// - `set!` expressions
120    ///
121    /// ## R4RS vs DSSSL
122    ///
123    /// R4RS allows `set!` on any variable.
124    /// DSSSL restricts mutation (mostly functional).
125    /// For now, we allow it (OpenJade does).
126    pub fn set(&self, name: &str, value: Value) -> Result<(), String> {
127        let symbol = Rc::from(name);
128
129        // Check this frame first
130        if self.bindings.borrow().contains_key(&symbol) {
131            self.bindings.borrow_mut().insert(symbol, value);
132            return Ok(());
133        }
134
135        // Walk up parent chain
136        if let Some(ref parent) = self.parent {
137            return parent.set(name, value);
138        }
139
140        // Variable not found
141        Err(format!("Undefined variable: {}", name))
142    }
143
144    /// Check if a variable is defined in this environment or any parent
145    pub fn is_defined(&self, name: &str) -> bool {
146        self.lookup(name).is_some()
147    }
148
149    /// Get the parent environment (if any)
150    pub fn parent(&self) -> Option<Gc<Environment>> {
151        self.parent.clone()
152    }
153
154    /// Get all bindings in this frame (not including parents)
155    ///
156    /// Used for debugging and introspection.
157    pub fn local_bindings(&self) -> Vec<(Rc<str>, Value)> {
158        self.bindings
159            .borrow()
160            .iter()
161            .map(|(k, v)| (k.clone(), v.clone()))
162            .collect()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_global_environment() {
172        let env = Environment::new_global();
173        env.define("x", Value::integer(42));
174
175        assert_eq!(
176            env.lookup("x").unwrap().is_integer(),
177            true
178        );
179        assert!(env.lookup("y").is_none());
180    }
181
182    #[test]
183    fn test_nested_environments() {
184        let global = Environment::new_global();
185        global.define("x", Value::integer(1));
186        global.define("y", Value::integer(2));
187
188        let local = Environment::extend(global.clone());
189        local.define("y", Value::integer(20)); // Shadow y
190        local.define("z", Value::integer(30));
191
192        // Local lookup: z is local, y shadows global, x from global
193        if let Value::Integer(n) = local.lookup("z").unwrap() {
194            assert_eq!(n, 30);
195        }
196        if let Value::Integer(n) = local.lookup("y").unwrap() {
197            assert_eq!(n, 20); // Shadowed
198        }
199        if let Value::Integer(n) = local.lookup("x").unwrap() {
200            assert_eq!(n, 1); // From global
201        }
202
203        // Global is unchanged
204        if let Value::Integer(n) = global.lookup("y").unwrap() {
205            assert_eq!(n, 2); // Not shadowed
206        }
207    }
208
209    #[test]
210    fn test_set_variable() {
211        let env = Environment::new_global();
212        env.define("x", Value::integer(1));
213
214        // Set existing variable
215        assert!(env.set("x", Value::integer(2)).is_ok());
216        if let Value::Integer(n) = env.lookup("x").unwrap() {
217            assert_eq!(n, 2);
218        }
219
220        // Set undefined variable fails
221        assert!(env.set("y", Value::integer(3)).is_err());
222    }
223
224    #[test]
225    fn test_is_defined() {
226        let global = Environment::new_global();
227        global.define("x", Value::integer(1));
228
229        let local = Environment::extend(global.clone());
230        local.define("y", Value::integer(2));
231
232        assert!(local.is_defined("x")); // From global
233        assert!(local.is_defined("y")); // From local
234        assert!(!local.is_defined("z")); // Undefined
235    }
236
237    #[test]
238    fn test_deep_nesting() {
239        let global = Environment::new_global();
240        global.define("a", Value::integer(1));
241
242        let env1 = Environment::extend(global.clone());
243        env1.define("b", Value::integer(2));
244
245        let env2 = Environment::extend(env1.clone());
246        env2.define("c", Value::integer(3));
247
248        let env3 = Environment::extend(env2.clone());
249        env3.define("d", Value::integer(4));
250
251        // Lookup walks all the way up
252        assert!(env3.is_defined("a"));
253        assert!(env3.is_defined("b"));
254        assert!(env3.is_defined("c"));
255        assert!(env3.is_defined("d"));
256    }
257}