rusty_cpp/analysis/
scope_lifetime.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use crate::ir::{IrFunction, IrStatement, IrProgram, BasicBlock};
3use crate::parser::HeaderCache;
4use crate::parser::annotations::{FunctionSignature, LifetimeAnnotation};
5use crate::parser::safety_annotations::SafetyContext;
6use petgraph::graph::NodeIndex;
7use petgraph::Direction;
8
9/// Represents a scope in the program (function, block, loop, etc.)
10#[derive(Debug, Clone)]
11pub struct Scope {
12    #[allow(dead_code)]
13    pub id: usize,
14    pub parent: Option<usize>,
15    #[allow(dead_code)]
16    pub kind: ScopeKind,
17    /// Variables declared in this scope
18    pub local_variables: HashSet<String>,
19    /// References created in this scope
20    pub local_references: HashSet<String>,
21    /// Starting point in the CFG
22    #[allow(dead_code)]
23    pub entry_block: Option<NodeIndex>,
24    /// Ending point(s) in the CFG
25    #[allow(dead_code)]
26    pub exit_blocks: Vec<NodeIndex>,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30#[allow(dead_code)]
31pub enum ScopeKind {
32    Function,
33    Block,
34    Loop,
35    Conditional,
36}
37
38/// Tracks lifetimes with respect to program scopes
39#[derive(Debug)]
40pub struct ScopedLifetimeTracker {
41    /// All scopes in the program
42    scopes: HashMap<usize, Scope>,
43    /// Maps variables to their declaring scope
44    variable_scope: HashMap<String, usize>,
45    /// Maps lifetimes to their scope bounds
46    lifetime_scopes: HashMap<String, (usize, usize)>, // (start_scope, end_scope)
47    /// Active lifetime constraints
48    constraints: Vec<LifetimeConstraint>,
49    /// Counter for generating scope IDs
50    next_scope_id: usize,
51}
52
53#[derive(Debug, Clone)]
54pub struct LifetimeConstraint {
55    pub kind: ConstraintKind,
56    pub location: String,
57}
58
59#[derive(Debug, Clone)]
60#[allow(dead_code)]
61pub enum ConstraintKind {
62    Outlives { longer: String, shorter: String },
63    Equal { a: String, b: String },
64    BorrowedFrom { reference: String, source: String },
65    MustLiveUntil { lifetime: String, scope_id: usize },
66}
67
68impl ScopedLifetimeTracker {
69    pub fn new() -> Self {
70        Self {
71            scopes: HashMap::new(),
72            variable_scope: HashMap::new(),
73            lifetime_scopes: HashMap::new(),
74            constraints: Vec::new(),
75            next_scope_id: 0,
76        }
77    }
78    
79    /// Create a new scope
80    pub fn push_scope(&mut self, kind: ScopeKind, parent: Option<usize>) -> usize {
81        let id = self.next_scope_id;
82        self.next_scope_id += 1;
83        
84        self.scopes.insert(id, Scope {
85            id,
86            parent,
87            kind,
88            local_variables: HashSet::new(),
89            local_references: HashSet::new(),
90            entry_block: None,
91            exit_blocks: Vec::new(),
92        });
93        
94        id
95    }
96    
97    /// Add a variable to the current scope
98    pub fn declare_variable(&mut self, var: String, scope_id: usize) {
99        if let Some(scope) = self.scopes.get_mut(&scope_id) {
100            scope.local_variables.insert(var.clone());
101            self.variable_scope.insert(var, scope_id);
102        }
103    }
104    
105    /// Add a reference to the current scope
106    pub fn declare_reference(&mut self, ref_name: String, scope_id: usize) {
107        if let Some(scope) = self.scopes.get_mut(&scope_id) {
108            scope.local_references.insert(ref_name.clone());
109            self.variable_scope.insert(ref_name, scope_id);
110        }
111    }
112    
113    /// Check if a lifetime outlives another considering scopes
114    pub fn check_outlives(&self, longer: &str, shorter: &str) -> bool {
115        // Get the scope ranges for both lifetimes
116        let longer_range = self.lifetime_scopes.get(longer);
117        let shorter_range = self.lifetime_scopes.get(shorter);
118        
119        match (longer_range, shorter_range) {
120            (Some((l_start, l_end)), Some((s_start, s_end))) => {
121                // longer outlives shorter if it starts before or at the same time
122                // and ends after or at the same time
123                l_start <= s_start && l_end >= s_end
124            }
125            _ => {
126                // If we don't know the scopes, check if they're equal
127                longer == shorter
128            }
129        }
130    }
131    
132    /// Check if a variable is still alive in a given scope
133    pub fn is_alive_in_scope(&self, var: &str, scope_id: usize) -> bool {
134        if let Some(var_scope) = self.variable_scope.get(var) {
135            // Check if the variable's scope is an ancestor of the given scope
136            self.is_ancestor_scope(*var_scope, scope_id)
137        } else {
138            false
139        }
140    }
141    
142    /// Check if scope_a is an ancestor of scope_b
143    fn is_ancestor_scope(&self, scope_a: usize, scope_b: usize) -> bool {
144        if scope_a == scope_b {
145            return true;
146        }
147        
148        let mut current = scope_b;
149        while let Some(scope) = self.scopes.get(&current) {
150            if let Some(parent) = scope.parent {
151                if parent == scope_a {
152                    return true;
153                }
154                current = parent;
155            } else {
156                break;
157            }
158        }
159        
160        false
161    }
162    
163    /// Add a lifetime constraint
164    pub fn add_constraint(&mut self, constraint: LifetimeConstraint) {
165        self.constraints.push(constraint);
166    }
167    
168    /// Validate all lifetime constraints
169    pub fn validate_constraints(&self) -> Vec<String> {
170        let mut errors = Vec::new();
171        
172        for constraint in &self.constraints {
173            match &constraint.kind {
174                ConstraintKind::Outlives { longer, shorter } => {
175                    if !self.check_outlives(longer, shorter) {
176                        errors.push(format!(
177                            "Lifetime '{}' does not outlive '{}' at {}",
178                            longer, shorter, constraint.location
179                        ));
180                    }
181                }
182                ConstraintKind::BorrowedFrom { reference, source } => {
183                    // Skip alive check for function call results (temporaries)
184                    // These include operator* (dereference), operator-> (member access), etc.
185                    let is_function_call_result = source.starts_with("operator") ||
186                        source.contains("::") ||  // Qualified function calls
187                        source.starts_with("__") || // Compiler-generated temporaries
188                        source.starts_with("temp_"); // Explicit temporaries
189
190                    // Check that the source is alive where the reference is used (skip for temporaries)
191                    if !is_function_call_result {
192                        if let Some(ref_scope) = self.variable_scope.get(reference) {
193                            if !self.is_alive_in_scope(source, *ref_scope) {
194                                errors.push(format!(
195                                    "Reference '{}' borrows from '{}' which is not alive at {}",
196                                    reference, source, constraint.location
197                                ));
198                            }
199                        }
200                    }
201                }
202                ConstraintKind::MustLiveUntil { lifetime, scope_id } => {
203                    if let Some((_, end_scope)) = self.lifetime_scopes.get(lifetime) {
204                        if !self.is_ancestor_scope(*end_scope, *scope_id) && *end_scope != *scope_id {
205                            errors.push(format!(
206                                "Lifetime '{}' must live until scope {} but ends at scope {} at {}",
207                                lifetime, scope_id, end_scope, constraint.location
208                            ));
209                        }
210                    }
211                }
212                _ => {}
213            }
214        }
215        
216        errors
217    }
218}
219
220/// Analyze a function with scope-based lifetime tracking
221pub fn analyze_function_scopes(
222    function: &IrFunction,
223    header_cache: &HeaderCache,
224) -> Result<Vec<String>, String> {
225    let mut tracker = ScopedLifetimeTracker::new();
226    let mut errors = Vec::new();
227    
228    // Create function scope
229    let func_scope = tracker.push_scope(ScopeKind::Function, None);
230    
231    // Initialize function parameters
232    for (name, var_info) in &function.variables {
233        tracker.declare_variable(name.clone(), func_scope);
234        
235        // If it's a reference, track its lifetime
236        match &var_info.ty {
237            crate::ir::VariableType::Reference(_) |
238            crate::ir::VariableType::MutableReference(_) => {
239                tracker.declare_reference(name.clone(), func_scope);
240                // Assign a lifetime based on the parameter position
241                let lifetime = format!("'param_{}", name);
242                tracker.lifetime_scopes.insert(lifetime, (func_scope, func_scope));
243            }
244            _ => {}
245        }
246    }
247    
248    // Analyze each block in the CFG
249    let mut visited = HashSet::new();
250    let mut queue = VecDeque::new();
251    
252    // Start with entry blocks
253    for node in function.cfg.node_indices() {
254        if function.cfg.edges_directed(node, Direction::Incoming).count() == 0 {
255            queue.push_back((node, func_scope));
256        }
257    }
258    
259    while let Some((node_idx, current_scope)) = queue.pop_front() {
260        if visited.contains(&node_idx) {
261            continue;
262        }
263        visited.insert(node_idx);
264        
265        let block = &function.cfg[node_idx];
266        let block_errors = analyze_block(
267            block,
268            current_scope,
269            &mut tracker,
270            function,
271            header_cache,
272        )?;
273        errors.extend(block_errors);
274        
275        // Queue successor blocks
276        for neighbor in function.cfg.neighbors_directed(node_idx, Direction::Outgoing) {
277            queue.push_back((neighbor, current_scope));
278        }
279    }
280    
281    // Validate all constraints
282    errors.extend(tracker.validate_constraints());
283    
284    Ok(errors)
285}
286
287fn analyze_block(
288    block: &BasicBlock,
289    scope_id: usize,
290    tracker: &mut ScopedLifetimeTracker,
291    function: &IrFunction,
292    header_cache: &HeaderCache,
293) -> Result<Vec<String>, String> {
294    let mut errors = Vec::new();
295    
296    for statement in &block.statements {
297        match statement {
298            IrStatement::Borrow { from, to, .. } => {
299                // Skip alive check for function call results (temporaries)
300                // These include operator* (dereference), operator-> (member access), etc.
301                // Temporaries are alive for the duration of the full expression
302                let is_function_call_result = from.starts_with("operator") ||
303                    from.contains("::") ||  // Qualified function calls
304                    from.starts_with("__") || // Compiler-generated temporaries
305                    from.starts_with("temp_"); // Explicit temporaries
306
307                // Check that 'from' is alive in this scope (skip for temporaries)
308                if !is_function_call_result && !tracker.is_alive_in_scope(from, scope_id) {
309                    errors.push(format!(
310                        "Cannot borrow from '{}': variable is not alive in current scope",
311                        from
312                    ));
313                } else {
314                    // Create a borrow constraint
315                    tracker.add_constraint(LifetimeConstraint {
316                        kind: ConstraintKind::BorrowedFrom {
317                            reference: to.clone(),
318                            source: from.clone(),
319                        },
320                        location: format!("borrow of {} from {}", to, from),
321                    });
322                    
323                    // Track the new reference
324                    tracker.declare_reference(to.clone(), scope_id);
325                }
326            }
327            
328            IrStatement::Return { value } => {
329                if let Some(val) = value {
330                    // Check for dangling references
331                    if let Some(var_scope) = tracker.variable_scope.get(val) {
332                        // If returning a reference to a local variable, it's an error
333                        // Static variables are OK to return (they have 'static lifetime)
334                        if *var_scope != 0 && !is_parameter(val, function) {
335                            // Check if this is an OWNED local variable (not a reference alias)
336                            if let Some(var_info) = function.variables.get(val) {
337                                if !var_info.is_static {  // Static variables are safe to return
338                                    match var_info.ty {
339                                        crate::ir::VariableType::Reference(_) |
340                                        crate::ir::VariableType::MutableReference(_) => {
341                                            // Variable is a reference alias - it's NOT a local object
342                                            // The reference inherits the lifetime of what it was bound to
343                                            // We rely on dependency tracking for these cases
344                                        }
345                                        crate::ir::VariableType::Owned(_) => {
346                                            // Variable is an OWNED local object
347                                            // Returning a reference to it is dangerous
348                                            errors.push(format!(
349                                                "Returning reference to local variable '{}' - will create dangling reference",
350                                                val
351                                            ));
352                                        }
353                                        _ => {} // Pointers (Raw, UniquePtr, SharedPtr) are safe to return
354                                    }
355                                }
356                            }
357                        }
358                    }
359                }
360            }
361            
362            IrStatement::CallExpr { func, args, result } => {
363                // Check function signature if available
364                if let Some(signature) = header_cache.get_signature(func) {
365                    let call_errors = check_call_lifetimes(
366                        func,
367                        args,
368                        result.as_ref(),
369                        signature,
370                        scope_id,
371                        tracker,
372                    );
373                    errors.extend(call_errors);
374                }
375            }
376            
377            _ => {}
378        }
379    }
380    
381    Ok(errors)
382}
383
384fn check_call_lifetimes(
385    func_name: &str,
386    _args: &[String],
387    result: Option<&String>,
388    signature: &FunctionSignature,
389    scope_id: usize,
390    tracker: &mut ScopedLifetimeTracker,
391) -> Vec<String> {
392    let errors = Vec::new();
393    
394    // Check lifetime bounds from signature
395    for bound in &signature.lifetime_bounds {
396        // For each bound like 'a: 'b, ensure the constraint is satisfied
397        // This would require mapping signature lifetimes to actual argument lifetimes
398        tracker.add_constraint(LifetimeConstraint {
399            kind: ConstraintKind::Outlives {
400                longer: bound.longer.clone(),
401                shorter: bound.shorter.clone(),
402            },
403            location: format!("call to {}", func_name),
404        });
405    }
406    
407    // Check return lifetime
408    if let (Some(result_var), Some(return_lifetime)) = (result, &signature.return_lifetime) {
409        match return_lifetime {
410            LifetimeAnnotation::Ref(_) | LifetimeAnnotation::MutRef(_) => {
411                // The result is a reference - track it
412                tracker.declare_reference(result_var.clone(), scope_id);
413            }
414            LifetimeAnnotation::Owned => {
415                // The result is owned
416                tracker.declare_variable(result_var.clone(), scope_id);
417            }
418            _ => {}
419        }
420    }
421    
422    errors
423}
424
425fn is_parameter(var_name: &str, function: &IrFunction) -> bool {
426    // Check if this variable was declared as a parameter
427    // In a real implementation, we'd track this properly
428    // For now, use a heuristic
429    var_name.starts_with("param") || var_name.starts_with("arg") || 
430    (function.variables.get(var_name).map_or(false, |info| {
431        // If it's in the variables map but not initialized in the function body,
432        // it's likely a parameter
433        matches!(info.ownership, crate::ir::OwnershipState::Owned)
434    }))
435}
436
437/// Check lifetimes for the entire program with scope tracking
438/// Check if a file path is from a system header (not user code)
439fn is_system_header(file_path: &str) -> bool {
440    let system_paths = [
441        "/usr/include",
442        "/usr/local/include",
443        "/opt/homebrew/include",
444        "/Library/Developer",
445        "C:\\Program Files",
446        "/Applications/Xcode.app",
447    ];
448
449    for path in &system_paths {
450        if file_path.starts_with(path) {
451            return true;
452        }
453    }
454
455    // STL and system library patterns (works for relative paths too)
456    if file_path.contains("/include/c++/") ||
457       file_path.contains("/bits/") ||
458       file_path.contains("/ext/") ||
459       file_path.contains("stl_") ||
460       file_path.contains("/lib/gcc/") {
461        return true;
462    }
463
464    // Also skip project include directory
465    if file_path.contains("/include/rusty/") || file_path.contains("/include/unified_") {
466        return true;
467    }
468
469    false
470}
471
472pub fn check_scoped_lifetimes(
473    program: &IrProgram,
474    header_cache: &HeaderCache,
475    safety_context: &SafetyContext,
476) -> Result<Vec<String>, String> {
477    let mut all_errors = Vec::new();
478
479    for function in &program.functions {
480        // Skip system header functions
481        if is_system_header(&function.source_file) {
482            continue;
483        }
484
485        // Only check functions that should be analyzed (i.e., @safe functions)
486        // Bug #9 fix: undeclared functions should NOT be analyzed
487        if !safety_context.should_check_function(&function.name) {
488            continue;
489        }
490
491        let errors = analyze_function_scopes(function, header_cache)?;
492        all_errors.extend(errors);
493    }
494
495    Ok(all_errors)
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    
502    #[test]
503    fn test_scope_tracking() {
504        let mut tracker = ScopedLifetimeTracker::new();
505        
506        let func_scope = tracker.push_scope(ScopeKind::Function, None);
507        let block_scope = tracker.push_scope(ScopeKind::Block, Some(func_scope));
508        
509        tracker.declare_variable("x".to_string(), func_scope);
510        tracker.declare_variable("y".to_string(), block_scope);
511        
512        assert!(tracker.is_alive_in_scope("x", block_scope));
513        assert!(tracker.is_alive_in_scope("y", block_scope));
514        assert!(!tracker.is_alive_in_scope("y", func_scope));
515    }
516    
517    #[test]
518    fn test_outlives_checking() {
519        let mut tracker = ScopedLifetimeTracker::new();
520        
521        tracker.lifetime_scopes.insert("'a".to_string(), (0, 2));
522        tracker.lifetime_scopes.insert("'b".to_string(), (1, 2));
523        
524        assert!(tracker.check_outlives("'a", "'b"));
525        assert!(!tracker.check_outlives("'b", "'a"));
526    }
527}