Skip to main content

bock_interp/
env.rs

1//! Lexical environment (scope stack) for the Bock interpreter.
2
3use std::collections::HashMap;
4
5use crate::value::Value;
6
7/// A single scope frame: maps variable names to their current values.
8type Frame = HashMap<String, Value>;
9
10/// Nested lexical scopes for variable bindings.
11///
12/// The innermost scope is at the back of the `scopes` vec. Variable lookup
13/// walks from inner to outer, finding the nearest binding.
14#[derive(Debug, Clone, Default)]
15pub struct Environment {
16    scopes: Vec<Frame>,
17}
18
19impl Environment {
20    /// Create an environment with a single (global) scope.
21    #[must_use]
22    pub fn new() -> Self {
23        Self {
24            scopes: vec![Frame::new()],
25        }
26    }
27
28    /// Create an environment seeded with another environment\'s global (root)
29    /// frame — the bindings registered at program start (`Some`/`None`/`Ok`/
30    /// `Err`, top-level functions, constructors). Used for method-body
31    /// evaluation so those globals stay visible inside a method, mirroring the
32    /// way top-level function bodies clone the caller\'s env. Inner scopes of
33    /// `source` are intentionally dropped: a method does not capture the
34    /// caller\'s locals, only the globals.
35    #[must_use]
36    pub fn with_globals(source: &Environment) -> Self {
37        let root = source.scopes.first().cloned().unwrap_or_default();
38        Self { scopes: vec![root] }
39    }
40
41    /// Push a new inner scope.
42    pub fn push_scope(&mut self) {
43        self.scopes.push(Frame::new());
44    }
45
46    /// Pop the innermost scope. Does nothing if only the global scope remains.
47    pub fn pop_scope(&mut self) {
48        if self.scopes.len() > 1 {
49            self.scopes.pop();
50        }
51    }
52
53    /// Define (or redefine) a variable in the current (innermost) scope.
54    pub fn define(&mut self, name: impl Into<String>, value: Value) {
55        if let Some(frame) = self.scopes.last_mut() {
56            frame.insert(name.into(), value);
57        }
58    }
59
60    /// Look up a variable, searching from innermost to outermost scope.
61    #[must_use]
62    pub fn get(&self, name: &str) -> Option<&Value> {
63        for frame in self.scopes.iter().rev() {
64            if let Some(v) = frame.get(name) {
65                return Some(v);
66            }
67        }
68        None
69    }
70
71    /// Return all bindings visible in the current scope (inner scopes shadow outer).
72    #[must_use]
73    pub fn all_bindings(&self) -> Vec<(String, Value)> {
74        let mut seen = std::collections::HashSet::new();
75        let mut result = Vec::new();
76        for frame in self.scopes.iter().rev() {
77            for (name, value) in frame {
78                if seen.insert(name.clone()) {
79                    result.push((name.clone(), value.clone()));
80                }
81            }
82        }
83        result.sort_by(|a, b| a.0.cmp(&b.0));
84        result
85    }
86
87    /// Assign to an existing variable in the nearest enclosing scope that
88    /// contains it. Returns `false` if no binding was found.
89    pub fn assign(&mut self, name: &str, value: Value) -> bool {
90        for frame in self.scopes.iter_mut().rev() {
91            if frame.contains_key(name) {
92                frame.insert(name.to_string(), value);
93                return true;
94            }
95        }
96        false
97    }
98}
99
100/// A key identifying an effect + operation pair.
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct EffectOpKey {
103    /// The effect name (e.g. `"Log"`).
104    pub effect: String,
105    /// The operation name (e.g. `"log"`).
106    pub operation: String,
107}
108
109/// A single handler frame pushed by a `handling` block.
110///
111/// Maps effect names to `Value::Function` handler values.
112type HandlerFrame = HashMap<String, Value>;
113
114/// Three-layer algebraic effect handler stack.
115///
116/// Resolution order (innermost wins):
117/// 1. **Local** — pushed by `handling` blocks (dynamic stack)
118/// 2. **Module** — registered via `handle Effect with handler`
119/// 3. **Project** — global defaults from configuration
120#[derive(Debug, Clone, Default)]
121pub struct EffectStack {
122    /// Local handler stack: each entry maps effect names to handler values.
123    /// The back of the vec is the innermost (most recent) `handling` block.
124    local: Vec<HandlerFrame>,
125    /// Module-level handlers: `handle Effect with handler`.
126    module: HashMap<String, Value>,
127    /// Project-level default handlers.
128    project: HashMap<String, Value>,
129}
130
131impl EffectStack {
132    /// Create an empty effect stack.
133    #[must_use]
134    pub fn new() -> Self {
135        Self {
136            local: Vec::new(),
137            module: HashMap::new(),
138            project: HashMap::new(),
139        }
140    }
141
142    /// Returns `true` if no handlers are registered at any level.
143    #[must_use]
144    pub fn is_empty(&self) -> bool {
145        self.local.is_empty() && self.module.is_empty() && self.project.is_empty()
146    }
147
148    /// Push a new handler frame for a `handling` block.
149    pub fn push_handlers(&mut self, handlers: HashMap<String, Value>) {
150        self.local.push(handlers);
151    }
152
153    /// Pop the most recent handler frame (when leaving a `handling` block).
154    pub fn pop_handlers(&mut self) {
155        self.local.pop();
156    }
157
158    /// Register a module-level handler for an effect.
159    pub fn set_module_handler(&mut self, effect_name: impl Into<String>, handler: Value) {
160        self.module.insert(effect_name.into(), handler);
161    }
162
163    /// Register a project-level default handler for an effect.
164    pub fn set_project_handler(&mut self, effect_name: impl Into<String>, handler: Value) {
165        self.project.insert(effect_name.into(), handler);
166    }
167
168    /// Resolve a handler for the given effect using three-layer resolution.
169    ///
170    /// Returns `None` if no handler is registered at any level.
171    #[must_use]
172    pub fn resolve(&self, effect_name: &str) -> Option<&Value> {
173        // Layer 1: local (innermost handling block wins)
174        for frame in self.local.iter().rev() {
175            if let Some(handler) = frame.get(effect_name) {
176                return Some(handler);
177            }
178        }
179        // Layer 2: module
180        if let Some(handler) = self.module.get(effect_name) {
181            return Some(handler);
182        }
183        // Layer 3: project
184        self.project.get(effect_name)
185    }
186}
187
188// ─── Tests ────────────────────────────────────────────────────────────────────
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn define_and_get() {
196        let mut env = Environment::new();
197        env.define("x", Value::Int(42));
198        assert_eq!(env.get("x"), Some(&Value::Int(42)));
199    }
200
201    #[test]
202    fn inner_scope_shadows_outer() {
203        let mut env = Environment::new();
204        env.define("x", Value::Int(1));
205        env.push_scope();
206        env.define("x", Value::Int(2));
207        assert_eq!(env.get("x"), Some(&Value::Int(2)));
208        env.pop_scope();
209        assert_eq!(env.get("x"), Some(&Value::Int(1)));
210    }
211
212    #[test]
213    fn lookup_outer_from_inner() {
214        let mut env = Environment::new();
215        env.define("y", Value::Bool(true));
216        env.push_scope();
217        assert_eq!(env.get("y"), Some(&Value::Bool(true)));
218        env.pop_scope();
219    }
220
221    #[test]
222    fn assign_updates_nearest_binding() {
223        let mut env = Environment::new();
224        env.define("z", Value::Int(0));
225        env.push_scope();
226        let updated = env.assign("z", Value::Int(99));
227        assert!(updated);
228        env.pop_scope();
229        assert_eq!(env.get("z"), Some(&Value::Int(99)));
230    }
231
232    #[test]
233    fn assign_returns_false_for_unknown() {
234        let mut env = Environment::new();
235        assert!(!env.assign("nope", Value::Void));
236    }
237
238    #[test]
239    fn pop_global_scope_is_noop() {
240        let mut env = Environment::new();
241        env.define("k", Value::Int(7));
242        env.pop_scope(); // should not remove the global scope
243        assert_eq!(env.get("k"), Some(&Value::Int(7)));
244    }
245
246    // ── EffectStack tests ────────────────────────────────────────────────
247
248    #[test]
249    fn effect_stack_resolve_returns_none_when_empty() {
250        let stack = EffectStack::new();
251        assert!(stack.resolve("Log").is_none());
252    }
253
254    #[test]
255    fn effect_stack_project_layer() {
256        let mut stack = EffectStack::new();
257        stack.set_project_handler("Log", Value::Int(1));
258        assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
259    }
260
261    #[test]
262    fn effect_stack_module_overrides_project() {
263        let mut stack = EffectStack::new();
264        stack.set_project_handler("Log", Value::Int(1));
265        stack.set_module_handler("Log", Value::Int(2));
266        assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
267    }
268
269    #[test]
270    fn effect_stack_local_overrides_module() {
271        let mut stack = EffectStack::new();
272        stack.set_module_handler("Log", Value::Int(1));
273        let mut frame = HashMap::new();
274        frame.insert("Log".to_string(), Value::Int(2));
275        stack.push_handlers(frame);
276        assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
277    }
278
279    #[test]
280    fn effect_stack_innermost_local_wins() {
281        let mut stack = EffectStack::new();
282        let mut frame1 = HashMap::new();
283        frame1.insert("Log".to_string(), Value::Int(1));
284        stack.push_handlers(frame1);
285
286        let mut frame2 = HashMap::new();
287        frame2.insert("Log".to_string(), Value::Int(2));
288        stack.push_handlers(frame2);
289
290        assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
291    }
292
293    #[test]
294    fn effect_stack_pop_restores_outer() {
295        let mut stack = EffectStack::new();
296        let mut frame1 = HashMap::new();
297        frame1.insert("Log".to_string(), Value::Int(1));
298        stack.push_handlers(frame1);
299
300        let mut frame2 = HashMap::new();
301        frame2.insert("Log".to_string(), Value::Int(2));
302        stack.push_handlers(frame2);
303
304        stack.pop_handlers();
305        assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
306    }
307
308    #[test]
309    fn effect_stack_different_effects_in_same_frame() {
310        let mut stack = EffectStack::new();
311        let mut frame = HashMap::new();
312        frame.insert("Log".to_string(), Value::Int(1));
313        frame.insert("Clock".to_string(), Value::Int(2));
314        stack.push_handlers(frame);
315        assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
316        assert_eq!(stack.resolve("Clock"), Some(&Value::Int(2)));
317    }
318
319    #[test]
320    fn effect_stack_local_falls_through_to_module() {
321        let mut stack = EffectStack::new();
322        stack.set_module_handler("Clock", Value::Int(10));
323        let mut frame = HashMap::new();
324        frame.insert("Log".to_string(), Value::Int(1));
325        stack.push_handlers(frame);
326        // Log resolves from local
327        assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
328        // Clock falls through to module
329        assert_eq!(stack.resolve("Clock"), Some(&Value::Int(10)));
330    }
331}