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 /// Get all bindings in this environment and all parents
166 ///
167 /// Returns a HashMap of all variable bindings, with child bindings
168 /// shadowing parent bindings. Used for syncing to VM globals.
169 pub fn all_bindings(&self) -> HashMap<String, Value> {
170 let mut result = HashMap::new();
171
172 // Walk up parent chain first (so child bindings override parent)
173 if let Some(ref parent) = self.parent {
174 result = parent.all_bindings();
175 }
176
177 // Add/override with this frame's bindings
178 for (name, value) in self.bindings.borrow().iter() {
179 result.insert(name.to_string(), value.clone());
180 }
181
182 result
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_global_environment() {
192 let env = Environment::new_global();
193 env.define("x", Value::integer(42));
194
195 assert_eq!(
196 env.lookup("x").unwrap().is_integer(),
197 true
198 );
199 assert!(env.lookup("y").is_none());
200 }
201
202 #[test]
203 fn test_nested_environments() {
204 let global = Environment::new_global();
205 global.define("x", Value::integer(1));
206 global.define("y", Value::integer(2));
207
208 let local = Environment::extend(global.clone());
209 local.define("y", Value::integer(20)); // Shadow y
210 local.define("z", Value::integer(30));
211
212 // Local lookup: z is local, y shadows global, x from global
213 if let Value::Integer(n) = local.lookup("z").unwrap() {
214 assert_eq!(n, 30);
215 }
216 if let Value::Integer(n) = local.lookup("y").unwrap() {
217 assert_eq!(n, 20); // Shadowed
218 }
219 if let Value::Integer(n) = local.lookup("x").unwrap() {
220 assert_eq!(n, 1); // From global
221 }
222
223 // Global is unchanged
224 if let Value::Integer(n) = global.lookup("y").unwrap() {
225 assert_eq!(n, 2); // Not shadowed
226 }
227 }
228
229 #[test]
230 fn test_set_variable() {
231 let env = Environment::new_global();
232 env.define("x", Value::integer(1));
233
234 // Set existing variable
235 assert!(env.set("x", Value::integer(2)).is_ok());
236 if let Value::Integer(n) = env.lookup("x").unwrap() {
237 assert_eq!(n, 2);
238 }
239
240 // Set undefined variable fails
241 assert!(env.set("y", Value::integer(3)).is_err());
242 }
243
244 #[test]
245 fn test_is_defined() {
246 let global = Environment::new_global();
247 global.define("x", Value::integer(1));
248
249 let local = Environment::extend(global.clone());
250 local.define("y", Value::integer(2));
251
252 assert!(local.is_defined("x")); // From global
253 assert!(local.is_defined("y")); // From local
254 assert!(!local.is_defined("z")); // Undefined
255 }
256
257 #[test]
258 fn test_deep_nesting() {
259 let global = Environment::new_global();
260 global.define("a", Value::integer(1));
261
262 let env1 = Environment::extend(global.clone());
263 env1.define("b", Value::integer(2));
264
265 let env2 = Environment::extend(env1.clone());
266 env2.define("c", Value::integer(3));
267
268 let env3 = Environment::extend(env2.clone());
269 env3.define("d", Value::integer(4));
270
271 // Lookup walks all the way up
272 assert!(env3.is_defined("a"));
273 assert!(env3.is_defined("b"));
274 assert!(env3.is_defined("c"));
275 assert!(env3.is_defined("d"));
276 }
277}