Skip to main content

debtmap/analyzers/
closure_analyzer.rs

1//! Closure analysis for purity detection.
2//!
3//! This module provides dedicated analysis for closure expressions,
4//! including capture detection, capture mode inference, and purity
5//! propagation for higher-order functions and iterator chains.
6
7use std::collections::HashSet;
8use syn::{Expr, ExprClosure, visit::Visit};
9
10use super::purity_detector::{MutationScope, PurityDetector};
11use super::scope_tracker::ScopeTracker;
12use crate::core::PurityLevel;
13
14/// Result of closure purity analysis
15#[derive(Debug, Clone)]
16pub struct ClosurePurity {
17    pub level: PurityLevel,
18    pub confidence: f32,
19    pub captures: Vec<Capture>,
20    pub has_nested_closures: bool,
21}
22
23/// Capture mode for closure variables
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CaptureMode {
26    /// By-value capture (move closure)
27    ByValue,
28    /// By-reference capture (immutable)
29    ByRef,
30    /// By-mutable-reference capture
31    ByMutRef,
32}
33
34/// Information about a captured variable
35#[derive(Debug, Clone)]
36pub struct Capture {
37    pub var_name: String,
38    pub mode: CaptureMode,
39    pub is_mutated: bool,
40    pub scope: MutationScope,
41}
42
43/// Dedicated analyzer for closure expressions
44#[derive(Debug)]
45pub struct ClosureAnalyzer<'a> {
46    /// Parent scope for determining captures
47    parent_scope: &'a ScopeTracker,
48    /// Detected captures
49    captures: Vec<Capture>,
50    /// Confidence reduction factors
51    confidence_penalties: Vec<&'static str>,
52}
53
54impl<'a> ClosureAnalyzer<'a> {
55    /// Create new closure analyzer with parent scope
56    pub fn new(parent_scope: &'a ScopeTracker) -> Self {
57        Self {
58            parent_scope,
59            captures: Vec::new(),
60            confidence_penalties: Vec::new(),
61        }
62    }
63
64    /// Main entry point: Analyze a closure expression
65    pub fn analyze_closure(&mut self, closure: &ExprClosure) -> ClosurePurity {
66        // Step 1: Create isolated detector for closure body
67        let mut body_detector = PurityDetector::new();
68
69        // Step 2: Register closure parameters in body scope
70        for input in &closure.inputs {
71            if let syn::Pat::Ident(pat_ident) = input {
72                body_detector
73                    .scope_mut()
74                    .add_local_var(pat_ident.ident.to_string());
75            }
76        }
77
78        // Step 3: Analyze closure body
79        body_detector.visit_expr(&closure.body);
80
81        // Step 4: Detect captures (free variables)
82        self.captures = self.find_captures(closure, &body_detector);
83
84        // Step 5: Infer capture modes from usage
85        self.infer_capture_modes(closure, &body_detector);
86
87        // Step 6: Check for nested closures
88        let has_nested_closures = self.contains_nested_closures(&closure.body);
89        if has_nested_closures {
90            self.confidence_penalties.push("nested_closures");
91        }
92
93        // Step 7: Determine purity level
94        let level = self.determine_purity_level(&body_detector);
95
96        // Step 8: Calculate confidence
97        let confidence = self.calculate_confidence(&body_detector);
98
99        ClosurePurity {
100            level,
101            confidence,
102            captures: self.captures.clone(),
103            has_nested_closures,
104        }
105    }
106
107    /// Detect captured variables (free variables in closure body)
108    fn find_captures(
109        &self,
110        closure: &ExprClosure,
111        _body_detector: &PurityDetector,
112    ) -> Vec<Capture> {
113        // Collect parameter names
114        let mut params: HashSet<String> = HashSet::new();
115        for input in &closure.inputs {
116            if let syn::Pat::Ident(pat_ident) = input {
117                params.insert(pat_ident.ident.to_string());
118            }
119        }
120
121        // Walk body and find variable references
122        let mut visitor = CaptureDetector {
123            params: &params,
124            parent_scope: self.parent_scope,
125            captures: Vec::new(),
126        };
127        visitor.visit_expr(&closure.body);
128
129        visitor.captures
130    }
131
132    /// Infer capture modes based on usage patterns
133    fn infer_capture_modes(&mut self, closure: &ExprClosure, body_detector: &PurityDetector) {
134        // Check for 'move' keyword
135        let has_move = closure.capture.is_some();
136
137        for capture in &mut self.captures {
138            // 'move' forces by-value capture
139            if has_move {
140                capture.mode = CaptureMode::ByValue;
141                continue;
142            }
143
144            // Check if captured variable is mutated in closure body
145            let is_mutated = body_detector
146                .local_mutations()
147                .iter()
148                .any(|m| m.target == capture.var_name);
149
150            capture.is_mutated = is_mutated;
151
152            // Infer mode: mutated → mut ref, otherwise immut ref
153            capture.mode = if is_mutated {
154                CaptureMode::ByMutRef
155            } else {
156                CaptureMode::ByRef
157            };
158
159            // Determine scope (local to function vs external)
160            capture.scope = if self.parent_scope.is_local(&capture.var_name) {
161                MutationScope::Local
162            } else {
163                MutationScope::External
164            };
165        }
166    }
167
168    /// Check if closure body contains nested closures
169    fn contains_nested_closures(&self, expr: &Expr) -> bool {
170        let mut visitor = ClosureDetector { found: false };
171        visitor.visit_expr(expr);
172        visitor.found
173    }
174
175    /// Determine purity level based on closure behavior
176    fn determine_purity_level(&self, body_detector: &PurityDetector) -> PurityLevel {
177        // Has I/O or unsafe operations?
178        if body_detector.has_io_operations() || body_detector.has_unsafe_blocks() {
179            return PurityLevel::Impure;
180        }
181
182        // Modifies external state?
183        if body_detector.modifies_external_state() {
184            return PurityLevel::Impure;
185        }
186
187        // Check captured variable mutations
188        let mutates_external = self
189            .captures
190            .iter()
191            .any(|c| c.is_mutated && c.scope == MutationScope::External);
192
193        if mutates_external {
194            return PurityLevel::Impure;
195        }
196
197        // Mutates local captures only?
198        let mutates_local = self
199            .captures
200            .iter()
201            .any(|c| c.is_mutated && c.scope == MutationScope::Local);
202
203        if mutates_local || !body_detector.local_mutations().is_empty() {
204            return PurityLevel::LocallyPure;
205        }
206
207        // Accesses external state (reads)?
208        if body_detector.accesses_external_state() {
209            return PurityLevel::ReadOnly;
210        }
211
212        // No side effects detected
213        PurityLevel::StrictlyPure
214    }
215
216    /// Calculate confidence score with penalty factors
217    fn calculate_confidence(&self, body_detector: &PurityDetector) -> f32 {
218        let mut confidence: f32 = 1.0;
219
220        // Reduce confidence for nested closures
221        if self.confidence_penalties.contains(&"nested_closures") {
222            confidence *= 0.85;
223        }
224
225        // Reduce confidence for external state access
226        if body_detector.accesses_external_state() {
227            confidence *= 0.80;
228        }
229
230        // Reduce confidence for multiple captures
231        if self.captures.len() > 3 {
232            confidence *= 0.90;
233        }
234
235        // Reduce confidence if capture modes were inferred
236        if self.captures.iter().any(|c| c.mode != CaptureMode::ByValue) {
237            confidence *= 0.95;
238        }
239
240        confidence.clamp(0.5, 1.0)
241    }
242}
243
244/// Helper visitor to detect captured variables
245struct CaptureDetector<'a> {
246    params: &'a HashSet<String>,
247    parent_scope: &'a ScopeTracker,
248    captures: Vec<Capture>,
249}
250
251impl<'ast, 'a> Visit<'ast> for CaptureDetector<'a> {
252    fn visit_expr(&mut self, expr: &'ast Expr) {
253        if let Expr::Path(path) = expr
254            && let Some(ident) = path.path.get_ident()
255        {
256            let name = ident.to_string();
257
258            // Not a parameter and not a standard construct?
259            if !self.params.contains(&name) && name != "self" && name != "Self" {
260                // Check if it's in parent scope (captured)
261                if self.parent_scope.is_local(&name) || self.parent_scope.is_self(&name) {
262                    // Add if not already captured
263                    if !self.captures.iter().any(|c| c.var_name == name) {
264                        self.captures.push(Capture {
265                            var_name: name,
266                            mode: CaptureMode::ByRef, // Default, refined later
267                            is_mutated: false,
268                            scope: MutationScope::Local,
269                        });
270                    }
271                }
272            }
273        }
274
275        syn::visit::visit_expr(self, expr);
276    }
277}
278
279/// Helper visitor to detect nested closures
280struct ClosureDetector {
281    found: bool,
282}
283
284impl<'ast> Visit<'ast> for ClosureDetector {
285    fn visit_expr(&mut self, expr: &'ast Expr) {
286        if matches!(expr, Expr::Closure(_)) {
287            self.found = true;
288            return;
289        }
290        syn::visit::visit_expr(self, expr);
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use syn::parse_quote;
298
299    #[test]
300    fn test_simple_closure_no_captures() {
301        let closure: ExprClosure = parse_quote!(|x| x * 2);
302        let parent_scope = ScopeTracker::new();
303        let mut analyzer = ClosureAnalyzer::new(&parent_scope);
304
305        let result = analyzer.analyze_closure(&closure);
306
307        assert_eq!(result.level, PurityLevel::StrictlyPure);
308        assert!(result.captures.is_empty());
309        assert!(!result.has_nested_closures);
310    }
311
312    #[test]
313    fn test_closure_with_capture() {
314        let closure: ExprClosure = parse_quote!(|x| x + y);
315        let mut parent_scope = ScopeTracker::new();
316        parent_scope.add_local_var("y".to_string());
317        let mut analyzer = ClosureAnalyzer::new(&parent_scope);
318
319        let result = analyzer.analyze_closure(&closure);
320
321        assert_eq!(result.captures.len(), 1);
322        assert_eq!(result.captures[0].var_name, "y");
323        assert_eq!(result.captures[0].mode, CaptureMode::ByRef);
324    }
325
326    #[test]
327    fn test_move_closure() {
328        let closure: ExprClosure = parse_quote!(move |x| x + y);
329        let mut parent_scope = ScopeTracker::new();
330        parent_scope.add_local_var("y".to_string());
331        let mut analyzer = ClosureAnalyzer::new(&parent_scope);
332
333        let result = analyzer.analyze_closure(&closure);
334
335        assert_eq!(result.captures.len(), 1);
336        assert_eq!(result.captures[0].mode, CaptureMode::ByValue);
337    }
338
339    #[test]
340    fn test_nested_closure_detection() {
341        let closure: ExprClosure = parse_quote!(|x| {
342            let f = |y| y * 2;
343            f(x)
344        });
345        let parent_scope = ScopeTracker::new();
346        let mut analyzer = ClosureAnalyzer::new(&parent_scope);
347
348        let result = analyzer.analyze_closure(&closure);
349
350        assert!(result.has_nested_closures);
351        assert!(result.confidence < 0.9); // Confidence reduced
352    }
353}