//! A reusable lexical-scope stack for static analyses. //! //! Unlike [`EnvironmentStack`](crate::backend::environment::EnvironmentStack), //! which stores runtime *values*, this stores per-binding *analysis state* //! (for the resolver, a `bool` meaning "defined yet?"). It is generic over that //! state so other passes (a type checker, an unused-variable lint, ...) can //! reuse the same machinery. //! //! It starts **empty**: the global scope is intentionally untracked, so //! [`ScopeStack::resolve`] returning `None` means "not local — assume global". use std::collections::HashMap; #[derive(Debug)] pub struct ScopeStack { scopes: Vec>, } impl Default for ScopeStack { fn default() -> Self { Self::new() } } impl ScopeStack { pub fn new() -> Self { ScopeStack { scopes: Vec::new() } } pub fn begin_scope(&mut self) { self.scopes.push(HashMap::new()); } pub fn end_scope(&mut self) { self.scopes.pop(); } pub fn is_empty(&self) -> bool { self.scopes.is_empty() } pub fn depth(&self) -> usize { self.scopes.len() } /// Insert `name` with `state` into the innermost scope (no-op at global). pub fn declare(&mut self, name: impl Into, state: T) { if let Some(scope) = self.scopes.last_mut() { scope.insert(name.into(), state); } } /// Update an existing binding in the innermost scope (no-op if absent). pub fn set_local(&mut self, name: &str, state: T) { if let Some(scope) = self.scopes.last_mut() { if let Some(slot) = scope.get_mut(name) { *slot = state; } } } /// Look up `name` in the innermost scope only. pub fn get_local(&self, name: &str) -> Option<&T> { self.scopes.last().and_then(|scope| scope.get(name)) } /// Whether the innermost scope already declares `name`. pub fn declared_in_current(&self, name: &str) -> bool { self.scopes .last() .map_or(false, |scope| scope.contains_key(name)) } /// Distance (in scopes) from the innermost scope to the one declaring /// `name`, or `None` if it isn't in any scope (i.e. global). pub fn resolve(&self, name: &str) -> Option { self.scopes .iter() .rev() .enumerate() .find_map(|(distance, scope)| scope.contains_key(name).then_some(distance)) } } #[cfg(test)] mod tests { use super::*; #[test] fn declare_and_get_local() { let mut scopes: ScopeStack = ScopeStack::new(); scopes.begin_scope(); scopes.declare("a", false); assert_eq!(scopes.get_local("a"), Some(&false)); scopes.set_local("a", true); assert_eq!(scopes.get_local("a"), Some(&true)); assert_eq!(scopes.get_local("missing"), None); } #[test] fn resolve_returns_distance_from_innermost() { let mut scopes: ScopeStack = ScopeStack::new(); scopes.begin_scope(); scopes.declare("a", true); scopes.begin_scope(); scopes.declare("b", true); assert_eq!(scopes.resolve("b"), Some(0)); assert_eq!(scopes.resolve("a"), Some(1)); assert_eq!(scopes.resolve("missing"), None); } #[test] fn shadowing_and_end_scope() { let mut scopes: ScopeStack = ScopeStack::new(); scopes.begin_scope(); scopes.declare("a", true); scopes.begin_scope(); scopes.declare("a", false); assert_eq!(scopes.get_local("a"), Some(&false)); assert_eq!(scopes.resolve("a"), Some(0)); scopes.end_scope(); assert_eq!(scopes.get_local("a"), Some(&true)); assert_eq!(scopes.resolve("a"), Some(0)); } #[test] fn declared_in_current_checks_only_innermost() { let mut scopes: ScopeStack = ScopeStack::new(); scopes.begin_scope(); scopes.declare("a", true); scopes.begin_scope(); assert!(!scopes.declared_in_current("a")); scopes.declare("a", true); assert!(scopes.declared_in_current("a")); } #[test] fn global_scope_is_untracked() { let mut scopes: ScopeStack = ScopeStack::new(); // No scope pushed: declarations are no-ops and nothing resolves locally. scopes.declare("a", true); assert!(scopes.is_empty()); assert_eq!(scopes.resolve("a"), None); assert_eq!(scopes.get_local("a"), None); } }