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}