Files
rlox/src/middleend/scope_stack.rs
T

147 lines
4.5 KiB
Rust
Raw Normal View History

//! 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<T> {
scopes: Vec<HashMap<String, T>>,
}
impl<T> Default for ScopeStack<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> ScopeStack<T> {
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<String>, 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<usize> {
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<bool> = 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<bool> = 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<bool> = 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<bool> = 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<bool> = 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);
}
}