147 lines
4.5 KiB
Rust
147 lines
4.5 KiB
Rust
//! 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);
|
||
|
|
}
|
||
|
|
}
|