use std::collections::HashMap;
use crate::value::Value;
type Frame = HashMap<String, Value>;
#[derive(Debug, Clone, Default)]
pub struct Environment {
scopes: Vec<Frame>,
}
impl Environment {
#[must_use]
pub fn new() -> Self {
Self {
scopes: vec![Frame::new()],
}
}
pub fn push_scope(&mut self) {
self.scopes.push(Frame::new());
}
pub fn pop_scope(&mut self) {
if self.scopes.len() > 1 {
self.scopes.pop();
}
}
pub fn define(&mut self, name: impl Into<String>, value: Value) {
if let Some(frame) = self.scopes.last_mut() {
frame.insert(name.into(), value);
}
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Value> {
for frame in self.scopes.iter().rev() {
if let Some(v) = frame.get(name) {
return Some(v);
}
}
None
}
#[must_use]
pub fn all_bindings(&self) -> Vec<(String, Value)> {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for frame in self.scopes.iter().rev() {
for (name, value) in frame {
if seen.insert(name.clone()) {
result.push((name.clone(), value.clone()));
}
}
}
result.sort_by(|a, b| a.0.cmp(&b.0));
result
}
pub fn assign(&mut self, name: &str, value: Value) -> bool {
for frame in self.scopes.iter_mut().rev() {
if frame.contains_key(name) {
frame.insert(name.to_string(), value);
return true;
}
}
false
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EffectOpKey {
pub effect: String,
pub operation: String,
}
type HandlerFrame = HashMap<String, Value>;
#[derive(Debug, Clone, Default)]
pub struct EffectStack {
local: Vec<HandlerFrame>,
module: HashMap<String, Value>,
project: HashMap<String, Value>,
}
impl EffectStack {
#[must_use]
pub fn new() -> Self {
Self {
local: Vec::new(),
module: HashMap::new(),
project: HashMap::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.local.is_empty() && self.module.is_empty() && self.project.is_empty()
}
pub fn push_handlers(&mut self, handlers: HashMap<String, Value>) {
self.local.push(handlers);
}
pub fn pop_handlers(&mut self) {
self.local.pop();
}
pub fn set_module_handler(&mut self, effect_name: impl Into<String>, handler: Value) {
self.module.insert(effect_name.into(), handler);
}
pub fn set_project_handler(&mut self, effect_name: impl Into<String>, handler: Value) {
self.project.insert(effect_name.into(), handler);
}
#[must_use]
pub fn resolve(&self, effect_name: &str) -> Option<&Value> {
for frame in self.local.iter().rev() {
if let Some(handler) = frame.get(effect_name) {
return Some(handler);
}
}
if let Some(handler) = self.module.get(effect_name) {
return Some(handler);
}
self.project.get(effect_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn define_and_get() {
let mut env = Environment::new();
env.define("x", Value::Int(42));
assert_eq!(env.get("x"), Some(&Value::Int(42)));
}
#[test]
fn inner_scope_shadows_outer() {
let mut env = Environment::new();
env.define("x", Value::Int(1));
env.push_scope();
env.define("x", Value::Int(2));
assert_eq!(env.get("x"), Some(&Value::Int(2)));
env.pop_scope();
assert_eq!(env.get("x"), Some(&Value::Int(1)));
}
#[test]
fn lookup_outer_from_inner() {
let mut env = Environment::new();
env.define("y", Value::Bool(true));
env.push_scope();
assert_eq!(env.get("y"), Some(&Value::Bool(true)));
env.pop_scope();
}
#[test]
fn assign_updates_nearest_binding() {
let mut env = Environment::new();
env.define("z", Value::Int(0));
env.push_scope();
let updated = env.assign("z", Value::Int(99));
assert!(updated);
env.pop_scope();
assert_eq!(env.get("z"), Some(&Value::Int(99)));
}
#[test]
fn assign_returns_false_for_unknown() {
let mut env = Environment::new();
assert!(!env.assign("nope", Value::Void));
}
#[test]
fn pop_global_scope_is_noop() {
let mut env = Environment::new();
env.define("k", Value::Int(7));
env.pop_scope(); assert_eq!(env.get("k"), Some(&Value::Int(7)));
}
#[test]
fn effect_stack_resolve_returns_none_when_empty() {
let stack = EffectStack::new();
assert!(stack.resolve("Log").is_none());
}
#[test]
fn effect_stack_project_layer() {
let mut stack = EffectStack::new();
stack.set_project_handler("Log", Value::Int(1));
assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
}
#[test]
fn effect_stack_module_overrides_project() {
let mut stack = EffectStack::new();
stack.set_project_handler("Log", Value::Int(1));
stack.set_module_handler("Log", Value::Int(2));
assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
}
#[test]
fn effect_stack_local_overrides_module() {
let mut stack = EffectStack::new();
stack.set_module_handler("Log", Value::Int(1));
let mut frame = HashMap::new();
frame.insert("Log".to_string(), Value::Int(2));
stack.push_handlers(frame);
assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
}
#[test]
fn effect_stack_innermost_local_wins() {
let mut stack = EffectStack::new();
let mut frame1 = HashMap::new();
frame1.insert("Log".to_string(), Value::Int(1));
stack.push_handlers(frame1);
let mut frame2 = HashMap::new();
frame2.insert("Log".to_string(), Value::Int(2));
stack.push_handlers(frame2);
assert_eq!(stack.resolve("Log"), Some(&Value::Int(2)));
}
#[test]
fn effect_stack_pop_restores_outer() {
let mut stack = EffectStack::new();
let mut frame1 = HashMap::new();
frame1.insert("Log".to_string(), Value::Int(1));
stack.push_handlers(frame1);
let mut frame2 = HashMap::new();
frame2.insert("Log".to_string(), Value::Int(2));
stack.push_handlers(frame2);
stack.pop_handlers();
assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
}
#[test]
fn effect_stack_different_effects_in_same_frame() {
let mut stack = EffectStack::new();
let mut frame = HashMap::new();
frame.insert("Log".to_string(), Value::Int(1));
frame.insert("Clock".to_string(), Value::Int(2));
stack.push_handlers(frame);
assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
assert_eq!(stack.resolve("Clock"), Some(&Value::Int(2)));
}
#[test]
fn effect_stack_local_falls_through_to_module() {
let mut stack = EffectStack::new();
stack.set_module_handler("Clock", Value::Int(10));
let mut frame = HashMap::new();
frame.insert("Log".to_string(), Value::Int(1));
stack.push_handlers(frame);
assert_eq!(stack.resolve("Log"), Some(&Value::Int(1)));
assert_eq!(stack.resolve("Clock"), Some(&Value::Int(10)));
}
}