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