Skip to main content

kaish_kernel/validator/
scope_tracker.rs

1//! Variable scope tracking for validation.
2//!
3//! Tracks which variables are bound in each scope without caring about values.
4//! Used to detect possibly undefined variable references.
5
6use std::collections::HashSet;
7
8/// Tracks variable bindings across nested scopes.
9///
10/// Unlike the interpreter's Scope which holds values, this only tracks names
11/// for static validation purposes.
12pub struct ScopeTracker {
13    /// Stack of scope frames, each containing bound variable names.
14    frames: Vec<HashSet<String>>,
15}
16
17impl Default for ScopeTracker {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl ScopeTracker {
24    /// Create a new scope tracker with built-in special variables.
25    pub fn new() -> Self {
26        let mut tracker = Self {
27            frames: vec![HashSet::new()],
28        };
29
30        // Register built-in special variables
31        tracker.bind_builtins();
32
33        tracker
34    }
35
36    /// Register all built-in and special shell variables.
37    fn bind_builtins(&mut self) {
38        let builtins = [
39            // Exit status
40            "?",
41            // Positional parameters
42            "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
43            // Special arrays
44            "@", "#", "*",
45            // Environment defaults
46            "HOME", "PATH", "PWD", "OLDPWD", "USER", "SHELL", "TERM",
47            // Script info
48            "LINENO", "FUNCNAME", "BASH_SOURCE",
49            // IFS for word splitting
50            "IFS",
51            // Random
52            "RANDOM",
53            // Process info
54            "PID", "PPID", "UID", "EUID",
55        ];
56
57        for name in builtins {
58            self.bind(name);
59        }
60    }
61
62    /// Push a new scope frame.
63    ///
64    /// Variables bound after this call are local to the new frame
65    /// until `pop_frame` is called.
66    pub fn push_frame(&mut self) {
67        self.frames.push(HashSet::new());
68    }
69
70    /// Pop the current scope frame.
71    ///
72    /// Variables bound in this frame are forgotten.
73    /// Panics if trying to pop the global frame.
74    pub fn pop_frame(&mut self) {
75        if self.frames.len() > 1 {
76            self.frames.pop();
77        }
78    }
79
80    /// Bind a variable name in the current scope.
81    pub fn bind(&mut self, name: impl Into<String>) {
82        if let Some(frame) = self.frames.last_mut() {
83            frame.insert(name.into());
84        }
85    }
86
87    /// Check if a variable is bound in any scope.
88    ///
89    /// Searches from innermost to outermost scope.
90    pub fn is_bound(&self, name: &str) -> bool {
91        self.frames.iter().rev().any(|frame| frame.contains(name))
92    }
93
94    /// Check if a variable name should skip undefined warnings.
95    ///
96    /// Variables starting with underscore are conventionally external
97    /// or intentionally unchecked.
98    pub fn should_skip_undefined_check(name: &str) -> bool {
99        name.starts_with('_')
100    }
101
102    /// Get the current nesting depth.
103    pub fn depth(&self) -> usize {
104        self.frames.len()
105    }
106
107    /// List all bound variables (for debugging).
108    #[allow(dead_code)]
109    pub fn all_bound(&self) -> Vec<&str> {
110        self.frames
111            .iter()
112            .flat_map(|f| f.iter().map(|s| s.as_str()))
113            .collect()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn new_has_builtins() {
123        let tracker = ScopeTracker::new();
124        assert!(tracker.is_bound("?"));
125        assert!(tracker.is_bound("HOME"));
126        assert!(tracker.is_bound("PATH"));
127        assert!(tracker.is_bound("0"));
128        assert!(tracker.is_bound("@"));
129    }
130
131    #[test]
132    fn bind_and_lookup() {
133        let mut tracker = ScopeTracker::new();
134        assert!(!tracker.is_bound("MY_VAR"));
135        tracker.bind("MY_VAR");
136        assert!(tracker.is_bound("MY_VAR"));
137    }
138
139    #[test]
140    fn nested_scopes() {
141        let mut tracker = ScopeTracker::new();
142
143        tracker.bind("OUTER");
144        assert!(tracker.is_bound("OUTER"));
145
146        tracker.push_frame();
147        tracker.bind("INNER");
148        assert!(tracker.is_bound("INNER"));
149        assert!(tracker.is_bound("OUTER")); // Still visible from outer scope
150
151        tracker.pop_frame();
152        assert!(!tracker.is_bound("INNER")); // Gone
153        assert!(tracker.is_bound("OUTER")); // Still there
154    }
155
156    #[test]
157    fn underscore_convention() {
158        assert!(ScopeTracker::should_skip_undefined_check("_EXTERNAL"));
159        assert!(ScopeTracker::should_skip_undefined_check("__private"));
160        assert!(!ScopeTracker::should_skip_undefined_check("NORMAL"));
161    }
162
163    #[test]
164    fn depth_tracking() {
165        let mut tracker = ScopeTracker::new();
166        assert_eq!(tracker.depth(), 1);
167
168        tracker.push_frame();
169        assert_eq!(tracker.depth(), 2);
170
171        tracker.push_frame();
172        assert_eq!(tracker.depth(), 3);
173
174        tracker.pop_frame();
175        assert_eq!(tracker.depth(), 2);
176
177        tracker.pop_frame();
178        assert_eq!(tracker.depth(), 1);
179
180        // Can't pop the global frame
181        tracker.pop_frame();
182        assert_eq!(tracker.depth(), 1);
183    }
184}