Skip to main content

depyler_core/codegen/
scope_tracker.rs

1//! Scope tracking for variable declarations in code generation
2//!
3//! This module provides utilities to track variable declarations across
4//! nested scopes, ensuring correct `let` vs reassignment generation in Rust.
5
6use std::collections::HashSet;
7
8/// Tracks variable declarations across nested scopes
9///
10/// Used during code generation to determine whether a variable assignment
11/// should generate `let mut x = ...` (new declaration) or `x = ...` (reassignment).
12#[derive(Debug, Clone)]
13pub struct ScopeTracker {
14    declared_vars: 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 a single empty scope
25    pub fn new() -> Self {
26        Self {
27            declared_vars: vec![HashSet::new()],
28        }
29    }
30
31    /// Enter a new nested scope
32    ///
33    /// Variables declared in this scope will be removed when `exit_scope` is called.
34    pub fn enter_scope(&mut self) {
35        self.declared_vars.push(HashSet::new());
36    }
37
38    /// Exit the current scope
39    ///
40    /// Returns `true` if a scope was exited, `false` if already at the outermost scope.
41    pub fn exit_scope(&mut self) -> bool {
42        if self.declared_vars.len() > 1 {
43            self.declared_vars.pop();
44            true
45        } else {
46            false
47        }
48    }
49
50    /// Check if a variable is declared in any enclosing scope
51    pub fn is_declared(&self, var_name: &str) -> bool {
52        self.declared_vars
53            .iter()
54            .any(|scope| scope.contains(var_name))
55    }
56
57    /// Check if a variable is declared in the current (innermost) scope only
58    pub fn is_declared_in_current_scope(&self, var_name: &str) -> bool {
59        self.declared_vars
60            .last()
61            .is_some_and(|scope| scope.contains(var_name))
62    }
63
64    /// Declare a variable in the current scope
65    ///
66    /// Returns `true` if the variable was newly declared, `false` if it was already declared.
67    pub fn declare_var(&mut self, var_name: &str) -> bool {
68        if let Some(current_scope) = self.declared_vars.last_mut() {
69            current_scope.insert(var_name.to_string())
70        } else {
71            false
72        }
73    }
74
75    /// Get the number of nested scopes (1 = outermost only)
76    pub fn scope_depth(&self) -> usize {
77        self.declared_vars.len()
78    }
79
80    /// Get all declared variables across all scopes
81    pub fn all_declared_vars(&self) -> HashSet<String> {
82        self.declared_vars
83            .iter()
84            .flat_map(|scope| scope.iter().cloned())
85            .collect()
86    }
87
88    /// Get variables declared in the current scope only
89    pub fn current_scope_vars(&self) -> Option<&HashSet<String>> {
90        self.declared_vars.last()
91    }
92
93    /// Declare multiple variables at once in the current scope
94    ///
95    /// Useful for tuple unpacking: `let (a, b, c) = ...`
96    pub fn declare_vars<I>(&mut self, var_names: I)
97    where
98        I: IntoIterator,
99        I::Item: AsRef<str>,
100    {
101        for var in var_names {
102            self.declare_var(var.as_ref());
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    // ============ new / Default tests ============
112
113    #[test]
114    fn test_new_creates_single_scope() {
115        let tracker = ScopeTracker::new();
116        assert_eq!(tracker.scope_depth(), 1);
117    }
118
119    #[test]
120    fn test_default_creates_single_scope() {
121        let tracker = ScopeTracker::default();
122        assert_eq!(tracker.scope_depth(), 1);
123    }
124
125    #[test]
126    fn test_new_scope_is_empty() {
127        let tracker = ScopeTracker::new();
128        assert!(!tracker.is_declared("x"));
129    }
130
131    // ============ declare_var tests ============
132
133    #[test]
134    fn test_declare_var_returns_true_for_new() {
135        let mut tracker = ScopeTracker::new();
136        assert!(tracker.declare_var("x"));
137    }
138
139    #[test]
140    fn test_declare_var_returns_false_for_existing() {
141        let mut tracker = ScopeTracker::new();
142        tracker.declare_var("x");
143        assert!(!tracker.declare_var("x"));
144    }
145
146    #[test]
147    fn test_declared_var_is_found() {
148        let mut tracker = ScopeTracker::new();
149        tracker.declare_var("x");
150        assert!(tracker.is_declared("x"));
151    }
152
153    #[test]
154    fn test_undeclared_var_is_not_found() {
155        let tracker = ScopeTracker::new();
156        assert!(!tracker.is_declared("y"));
157    }
158
159    // ============ enter_scope / exit_scope tests ============
160
161    #[test]
162    fn test_enter_scope_increases_depth() {
163        let mut tracker = ScopeTracker::new();
164        tracker.enter_scope();
165        assert_eq!(tracker.scope_depth(), 2);
166    }
167
168    #[test]
169    fn test_exit_scope_decreases_depth() {
170        let mut tracker = ScopeTracker::new();
171        tracker.enter_scope();
172        tracker.exit_scope();
173        assert_eq!(tracker.scope_depth(), 1);
174    }
175
176    #[test]
177    fn test_exit_scope_returns_true_when_nested() {
178        let mut tracker = ScopeTracker::new();
179        tracker.enter_scope();
180        assert!(tracker.exit_scope());
181    }
182
183    #[test]
184    fn test_exit_scope_returns_false_at_outermost() {
185        let mut tracker = ScopeTracker::new();
186        assert!(!tracker.exit_scope());
187    }
188
189    #[test]
190    fn test_exit_scope_removes_vars() {
191        let mut tracker = ScopeTracker::new();
192        tracker.enter_scope();
193        tracker.declare_var("inner");
194        assert!(tracker.is_declared("inner"));
195        tracker.exit_scope();
196        assert!(!tracker.is_declared("inner"));
197    }
198
199    #[test]
200    fn test_parent_scope_vars_visible_in_child() {
201        let mut tracker = ScopeTracker::new();
202        tracker.declare_var("outer");
203        tracker.enter_scope();
204        assert!(tracker.is_declared("outer"));
205    }
206
207    #[test]
208    fn test_parent_scope_vars_survive_exit() {
209        let mut tracker = ScopeTracker::new();
210        tracker.declare_var("outer");
211        tracker.enter_scope();
212        tracker.declare_var("inner");
213        tracker.exit_scope();
214        assert!(tracker.is_declared("outer"));
215    }
216
217    // ============ is_declared_in_current_scope tests ============
218
219    #[test]
220    fn test_is_declared_in_current_scope_true() {
221        let mut tracker = ScopeTracker::new();
222        tracker.declare_var("x");
223        assert!(tracker.is_declared_in_current_scope("x"));
224    }
225
226    #[test]
227    fn test_is_declared_in_current_scope_false_for_parent() {
228        let mut tracker = ScopeTracker::new();
229        tracker.declare_var("outer");
230        tracker.enter_scope();
231        assert!(!tracker.is_declared_in_current_scope("outer"));
232        assert!(tracker.is_declared("outer")); // but it's visible
233    }
234
235    #[test]
236    fn test_is_declared_in_current_scope_false_for_undeclared() {
237        let tracker = ScopeTracker::new();
238        assert!(!tracker.is_declared_in_current_scope("x"));
239    }
240
241    // ============ all_declared_vars tests ============
242
243    #[test]
244    fn test_all_declared_vars_empty() {
245        let tracker = ScopeTracker::new();
246        assert!(tracker.all_declared_vars().is_empty());
247    }
248
249    #[test]
250    fn test_all_declared_vars_single_scope() {
251        let mut tracker = ScopeTracker::new();
252        tracker.declare_var("a");
253        tracker.declare_var("b");
254        let vars = tracker.all_declared_vars();
255        assert!(vars.contains("a"));
256        assert!(vars.contains("b"));
257        assert_eq!(vars.len(), 2);
258    }
259
260    #[test]
261    fn test_all_declared_vars_multiple_scopes() {
262        let mut tracker = ScopeTracker::new();
263        tracker.declare_var("outer");
264        tracker.enter_scope();
265        tracker.declare_var("inner");
266        let vars = tracker.all_declared_vars();
267        assert!(vars.contains("outer"));
268        assert!(vars.contains("inner"));
269        assert_eq!(vars.len(), 2);
270    }
271
272    // ============ current_scope_vars tests ============
273
274    #[test]
275    fn test_current_scope_vars_returns_some() {
276        let tracker = ScopeTracker::new();
277        assert!(tracker.current_scope_vars().is_some());
278    }
279
280    #[test]
281    fn test_current_scope_vars_contains_declared() {
282        let mut tracker = ScopeTracker::new();
283        tracker.declare_var("x");
284        let vars = tracker.current_scope_vars().unwrap();
285        assert!(vars.contains("x"));
286    }
287
288    #[test]
289    fn test_current_scope_vars_excludes_parent() {
290        let mut tracker = ScopeTracker::new();
291        tracker.declare_var("outer");
292        tracker.enter_scope();
293        tracker.declare_var("inner");
294        let vars = tracker.current_scope_vars().unwrap();
295        assert!(!vars.contains("outer"));
296        assert!(vars.contains("inner"));
297    }
298
299    // ============ declare_vars tests ============
300
301    #[test]
302    fn test_declare_vars_multiple() {
303        let mut tracker = ScopeTracker::new();
304        tracker.declare_vars(["a", "b", "c"]);
305        assert!(tracker.is_declared("a"));
306        assert!(tracker.is_declared("b"));
307        assert!(tracker.is_declared("c"));
308    }
309
310    #[test]
311    fn test_declare_vars_empty() {
312        let mut tracker = ScopeTracker::new();
313        tracker.declare_vars(Vec::<&str>::new());
314        assert!(tracker.all_declared_vars().is_empty());
315    }
316
317    #[test]
318    fn test_declare_vars_with_strings() {
319        let mut tracker = ScopeTracker::new();
320        let vars = vec!["x".to_string(), "y".to_string()];
321        tracker.declare_vars(vars);
322        assert!(tracker.is_declared("x"));
323        assert!(tracker.is_declared("y"));
324    }
325
326    // ============ scope_depth tests ============
327
328    #[test]
329    fn test_scope_depth_nested() {
330        let mut tracker = ScopeTracker::new();
331        assert_eq!(tracker.scope_depth(), 1);
332        tracker.enter_scope();
333        assert_eq!(tracker.scope_depth(), 2);
334        tracker.enter_scope();
335        assert_eq!(tracker.scope_depth(), 3);
336        tracker.exit_scope();
337        assert_eq!(tracker.scope_depth(), 2);
338        tracker.exit_scope();
339        assert_eq!(tracker.scope_depth(), 1);
340    }
341
342    // ============ shadowing tests ============
343
344    #[test]
345    fn test_redeclare_in_nested_scope() {
346        let mut tracker = ScopeTracker::new();
347        tracker.declare_var("x");
348        tracker.enter_scope();
349        // Redeclaring in nested scope shadows
350        assert!(tracker.declare_var("x"));
351        assert!(tracker.is_declared_in_current_scope("x"));
352    }
353
354    #[test]
355    fn test_shadow_persists_only_in_scope() {
356        let mut tracker = ScopeTracker::new();
357        tracker.declare_var("x");
358        tracker.enter_scope();
359        tracker.declare_var("x"); // shadow
360        tracker.exit_scope();
361        // Original x still visible from outer scope
362        assert!(tracker.is_declared("x"));
363    }
364
365    // ============ complex scenario tests ============
366
367    #[test]
368    fn test_deeply_nested_scopes() {
369        let mut tracker = ScopeTracker::new();
370        tracker.declare_var("a");
371        tracker.enter_scope();
372        tracker.declare_var("b");
373        tracker.enter_scope();
374        tracker.declare_var("c");
375        tracker.enter_scope();
376        tracker.declare_var("d");
377
378        // All visible
379        assert!(tracker.is_declared("a"));
380        assert!(tracker.is_declared("b"));
381        assert!(tracker.is_declared("c"));
382        assert!(tracker.is_declared("d"));
383
384        // Only d in current scope
385        assert!(tracker.is_declared_in_current_scope("d"));
386        assert!(!tracker.is_declared_in_current_scope("a"));
387
388        // Exit scopes one by one
389        tracker.exit_scope();
390        assert!(!tracker.is_declared("d"));
391        tracker.exit_scope();
392        assert!(!tracker.is_declared("c"));
393        tracker.exit_scope();
394        assert!(!tracker.is_declared("b"));
395        assert!(tracker.is_declared("a"));
396    }
397
398    #[test]
399    fn test_function_like_usage() {
400        // Simulate: fn foo(x, y) { let z = ...; if cond { let w = ...; } }
401        let mut tracker = ScopeTracker::new();
402
403        // Function parameters
404        tracker.declare_vars(["x", "y"]);
405
406        // Local variable
407        tracker.declare_var("z");
408
409        // If block
410        tracker.enter_scope();
411        tracker.declare_var("w");
412        assert!(tracker.is_declared("w"));
413        assert!(tracker.is_declared("x")); // params visible
414        tracker.exit_scope();
415
416        // w no longer visible
417        assert!(!tracker.is_declared("w"));
418        assert!(tracker.is_declared("x")); // params still visible
419        assert!(tracker.is_declared("z")); // local still visible
420    }
421}