rusty_cpp/analysis/
lifetime_checker.rs

1use crate::parser::annotations::{LifetimeAnnotation, FunctionSignature, LifetimeBound};
2use crate::parser::HeaderCache;
3use crate::parser::safety_annotations::SafetyContext;
4use crate::ir::{IrProgram, IrStatement, IrFunction, VariableType};
5use std::collections::{HashMap, HashSet};
6use crate::debug_println;
7
8/// Tracks lifetime information for variables in the current scope
9#[derive(Debug, Clone)]
10pub struct LifetimeScope {
11    /// Maps variable names to their lifetimes
12    variable_lifetimes: HashMap<String, String>,
13    /// Active lifetime constraints
14    constraints: Vec<LifetimeBound>,
15    /// Variables that own their data (not references)
16    owned_variables: HashSet<String>,
17}
18
19impl LifetimeScope {
20    pub fn new() -> Self {
21        Self {
22            variable_lifetimes: HashMap::new(),
23            constraints: Vec::new(),
24            owned_variables: HashSet::new(),
25        }
26    }
27    
28    /// Assign a lifetime to a variable
29    pub fn set_lifetime(&mut self, var: String, lifetime: String) {
30        self.variable_lifetimes.insert(var, lifetime);
31    }
32    
33    /// Mark a variable as owned (not a reference)
34    pub fn mark_owned(&mut self, var: String) {
35        self.owned_variables.insert(var);
36    }
37    
38    /// Get the lifetime of a variable
39    pub fn get_lifetime(&self, var: &str) -> Option<&String> {
40        self.variable_lifetimes.get(var)
41    }
42    
43    /// Check if a variable is owned
44    pub fn is_owned(&self, var: &str) -> bool {
45        self.owned_variables.contains(var)
46    }
47    
48    /// Add a lifetime constraint
49    #[allow(dead_code)]
50    pub fn add_constraint(&mut self, constraint: LifetimeBound) {
51        self.constraints.push(constraint);
52    }
53    
54    /// Check if lifetime 'a outlives lifetime 'b
55    pub fn check_outlives(&self, longer: &str, shorter: &str) -> bool {
56        // If they're the same lifetime, it trivially outlives itself
57        if longer == shorter {
58            return true;
59        }
60
61        // Check scope-based lifetimes: 'scope_X outlives 'scope_Y if X < Y
62        // Lower scope number = outer scope = longer lifetime
63        if let (Some(longer_scope), Some(shorter_scope)) = (
64            longer.strip_prefix("'scope_"),
65            shorter.strip_prefix("'scope_")
66        ) {
67            if let (Ok(longer_depth), Ok(shorter_depth)) = (
68                longer_scope.parse::<usize>(),
69                shorter_scope.parse::<usize>()
70            ) {
71                return longer_depth <= shorter_depth;
72            }
73        }
74
75        // Check explicit constraints
76        for constraint in &self.constraints {
77            if constraint.longer == longer && constraint.shorter == shorter {
78                return true;
79            }
80        }
81
82        // Implement transitive outlives checking
83        // If 'a: 'b and 'b: 'c, then 'a: 'c
84        self.check_outlives_transitive(longer, shorter, &mut HashSet::new())
85    }
86    
87    /// Check outlives relationship with transitive closure
88    fn check_outlives_transitive(&self, longer: &str, shorter: &str, visited: &mut HashSet<String>) -> bool {
89        // Avoid infinite recursion
90        if visited.contains(longer) {
91            return false;
92        }
93        visited.insert(longer.to_string());
94        
95        // Find all lifetimes that 'longer' outlives directly
96        for constraint in &self.constraints {
97            if constraint.longer == longer {
98                // Check if we found the target
99                if constraint.shorter == shorter {
100                    return true;
101                }
102                
103                // Try transitively through this intermediate lifetime
104                if self.check_outlives_transitive(&constraint.shorter, shorter, visited) {
105                    return true;
106                }
107            }
108        }
109        
110        false
111    }
112}
113
114/// Check lifetime constraints in a program using header annotations
115/// Check if a file path is from a system header (not user code)
116fn is_system_header(file_path: &str) -> bool {
117    let system_paths = [
118        "/usr/include",
119        "/usr/local/include",
120        "/opt/homebrew/include",
121        "/Library/Developer",
122        "C:\\Program Files",
123        "/Applications/Xcode.app",
124    ];
125
126    for path in &system_paths {
127        if file_path.starts_with(path) {
128            return true;
129        }
130    }
131
132    // STL and system library patterns (works for relative paths too)
133    if file_path.contains("/include/c++/") ||
134       file_path.contains("/bits/") ||
135       file_path.contains("/ext/") ||
136       file_path.contains("stl_") ||
137       file_path.contains("/lib/gcc/") {
138        return true;
139    }
140
141    // Also skip project include directory
142    if file_path.contains("/include/rusty/") || file_path.contains("/include/unified_") {
143        return true;
144    }
145
146    false
147}
148
149pub fn check_lifetimes_with_annotations(
150    program: &IrProgram,
151    header_cache: &HeaderCache,
152    safety_context: &SafetyContext
153) -> Result<Vec<String>, String> {
154    let mut errors = Vec::new();
155
156    for function in &program.functions {
157        // Skip system header functions
158        if is_system_header(&function.source_file) {
159            continue;
160        }
161
162        // Only check functions that should be analyzed (i.e., @safe functions)
163        // Bug #9 fix: undeclared functions should NOT be analyzed
164        if !safety_context.should_check_function(&function.name) {
165            continue;
166        }
167
168        let mut scope = LifetimeScope::new();
169        let function_errors = check_function_lifetimes(function, &mut scope, header_cache)?;
170        errors.extend(function_errors);
171    }
172
173    Ok(errors)
174}
175
176fn check_function_lifetimes(
177    function: &IrFunction,
178    scope: &mut LifetimeScope,
179    header_cache: &HeaderCache
180) -> Result<Vec<String>, String> {
181    let mut errors = Vec::new();
182    
183    // Initialize lifetimes for function parameters and variables
184    // For now, give each variable a unique lifetime based on its name
185    for (name, var_info) in &function.variables {
186        match &var_info.ty {
187            crate::ir::VariableType::Reference(_) |
188            crate::ir::VariableType::MutableReference(_) => {
189                // References get a lifetime based on their name
190                scope.set_lifetime(name.clone(), format!("'{}", name));
191            }
192            _ => {
193                // Owned types don't have lifetimes
194                scope.mark_owned(name.clone());
195            }
196        }
197    }
198    
199    // Track scope depth for lifetime inference
200    let mut scope_depth = 0;
201    let mut variable_scopes: HashMap<String, usize> = HashMap::new();
202
203    // First pass: assign scope depths to variables based on where they're declared/used
204    for node_idx in function.cfg.node_indices() {
205        let block = &function.cfg[node_idx];
206
207        for statement in &block.statements {
208            match statement {
209                IrStatement::EnterScope => {
210                    scope_depth += 1;
211                }
212                IrStatement::ExitScope => {
213                    if scope_depth > 0 {
214                        scope_depth -= 1;
215                    }
216                }
217                IrStatement::Assign { lhs, .. } |
218                IrStatement::Borrow { to: lhs, .. } => {
219                    // Record the scope depth where this variable is first assigned
220                    if !variable_scopes.contains_key(lhs) {
221                        variable_scopes.insert(lhs.clone(), scope_depth);
222                    }
223                }
224                IrStatement::CallExpr { args, result, .. } => {
225                    // Record result variable if present
226                    if let Some(lhs) = result {
227                        if !variable_scopes.contains_key(lhs) {
228                            variable_scopes.insert(lhs.clone(), scope_depth);
229                        }
230                    }
231                    // For arguments that don't have lifetimes yet, record their scope
232                    for arg in args {
233                        if !variable_scopes.contains_key(arg) && scope.is_owned(arg) {
234                            variable_scopes.insert(arg.clone(), scope_depth);
235                        }
236                    }
237                }
238                _ => {}
239            }
240        }
241    }
242
243    // Assign scope-based lifetimes to owned variables
244    // Variables not in variable_scopes must have been declared before any tracked statements (scope 0)
245    // This includes function parameters and variables declared at the start of the function
246    for (var_name, var_info) in &function.variables {
247        if scope.is_owned(var_name) {
248            // Check if this variable was assigned/declared in a tracked statement
249            if let Some(&depth) = variable_scopes.get(var_name) {
250                // Variable was assigned/declared, use that scope
251                let lifetime = format!("'scope_{}", depth);
252                scope.set_lifetime(var_name.clone(), lifetime.clone());
253            } else {
254                // Variable exists but was never assigned/declared in statements
255                // It must be a parameter or declared at function start (scope 0)
256                let lifetime = "'scope_0".to_string();
257                scope.set_lifetime(var_name.clone(), lifetime.clone());
258            }
259        }
260    }
261
262    // Reset scope depth for statement processing
263    scope_depth = 0;
264
265    // Check each statement in the function
266    for node_idx in function.cfg.node_indices() {
267        let block = &function.cfg[node_idx];
268
269        for (idx, statement) in block.statements.iter().enumerate() {
270            match statement {
271                IrStatement::EnterScope => {
272                    scope_depth += 1;
273                }
274                IrStatement::ExitScope => {
275                    if scope_depth > 0 {
276                        scope_depth -= 1;
277                    }
278                }
279                IrStatement::CallExpr { func, args, result } => {
280                    // Check if we have annotations for this function
281                    if let Some(signature) = header_cache.get_signature(func) {
282                        let call_errors = check_function_call(
283                            func,
284                            args,
285                            result.as_ref(),
286                            signature,
287                            scope
288                        );
289                        errors.extend(call_errors);
290                    }
291                }
292                
293                IrStatement::Borrow { from, to, .. } => {
294                    // When creating a reference, the new reference has the same lifetime
295                    // as the source or a shorter one
296                    if let Some(from_lifetime) = scope.get_lifetime(from) {
297                        scope.set_lifetime(to.clone(), from_lifetime.clone());
298                    } else if scope.is_owned(from) {
299                        // Borrowing from owned data creates a new lifetime
300                        scope.set_lifetime(to.clone(), format!("'{}", to));
301                    }
302                }
303                
304                IrStatement::Return { value } => {
305                    // Check that returned references have appropriate lifetimes
306                    if let Some(value) = value {
307                        let return_errors = check_return_lifetime(value, function, scope);
308                        errors.extend(return_errors);
309                    }
310                }
311                
312                _ => {}
313            }
314        }
315    }
316    
317    Ok(errors)
318}
319
320fn check_function_call(
321    func_name: &str,
322    args: &[String],
323    result: Option<&String>,
324    signature: &FunctionSignature,
325    scope: &LifetimeScope
326) -> Vec<String> {
327    let mut errors = Vec::new();
328    
329    // Check that we have the right number of arguments
330    if args.len() != signature.param_lifetimes.len() {
331        // Signature doesn't match, skip lifetime checking
332        return errors;
333    }
334    
335    // Collect the actual lifetimes of arguments
336    let mut arg_lifetimes = Vec::new();
337    for (i, arg) in args.iter().enumerate() {
338        if let Some(lifetime) = scope.get_lifetime(arg) {
339            arg_lifetimes.push(Some(lifetime.clone()));
340        } else if scope.is_owned(arg) {
341            arg_lifetimes.push(None); // Owned value
342        } else {
343            arg_lifetimes.push(Some(format!("'arg{}", i)));
344        }
345    }
346    
347    // Check parameter lifetime requirements
348    // Note: In C++, passing owned values to const reference parameters is legal (creates temporary)
349    // So we only check for ownership transfer violations
350    for (i, (arg, expected)) in args.iter().zip(&signature.param_lifetimes).enumerate() {
351        if let Some(expected_lifetime) = expected {
352            match expected_lifetime {
353                LifetimeAnnotation::Ref(_expected) | LifetimeAnnotation::MutRef(_expected) => {
354                    // In C++, you can pass owned values to reference parameters
355                    // The compiler creates a temporary reference
356                    // So we don't error here - just note that owned values will create temporaries
357                }
358                LifetimeAnnotation::Owned => {
359                    // The argument should transfer ownership
360                    if !scope.is_owned(arg) {
361                        errors.push(format!(
362                            "Function '{}' expects ownership of parameter {}, but '{}' is a reference",
363                            func_name, i + 1, arg
364                        ));
365                    }
366                }
367                _ => {}
368            }
369        }
370    }
371    
372    // Check lifetime bounds
373    for bound in &signature.lifetime_bounds {
374        // Map lifetime names from signature to actual argument lifetimes
375        let longer_lifetime = map_lifetime_to_actual(&bound.longer, &arg_lifetimes);
376        let shorter_lifetime = map_lifetime_to_actual(&bound.shorter, &arg_lifetimes);
377
378        if let (Some(longer), Some(shorter)) = (longer_lifetime, shorter_lifetime) {
379            let outlives = scope.check_outlives(&longer, &shorter);
380            if !outlives {
381                errors.push(format!(
382                    "Lifetime constraint violated in call to '{}': '{}' must outlive '{}'",
383                    func_name, longer, shorter
384                ));
385            }
386        }
387    }
388    
389    // Check return lifetime
390    if let (Some(_result_var), Some(return_lifetime)) = (result, &signature.return_lifetime) {
391        match return_lifetime {
392            LifetimeAnnotation::Ref(ret_lifetime) | LifetimeAnnotation::MutRef(ret_lifetime) => {
393                // The return value is a reference that borrows from one of the parameters
394                // Map the return lifetime to the actual argument lifetime
395                let actual_lifetime = map_lifetime_to_actual(ret_lifetime, &arg_lifetimes);
396                if let Some(_lifetime) = actual_lifetime {
397                    // The result variable gets this lifetime
398                    // Note: We're not modifying scope here as it's borrowed
399                    // In a real implementation, we'd need mutable access
400                }
401            }
402            LifetimeAnnotation::Owned => {
403                // The return value is owned, no lifetime constraints
404            }
405            _ => {}
406        }
407    }
408    
409    errors
410}
411
412fn check_return_lifetime(
413    value: &str,
414    function: &IrFunction,
415    scope: &LifetimeScope
416) -> Vec<String> {
417    let mut errors = Vec::new();
418
419    // First, check if the returned value is actually a reference type
420    // Returning pointer values (Raw, Owned) is safe - only references are dangerous
421    if let Some(var_info) = function.variables.get(value) {
422        match &var_info.ty {
423            VariableType::Reference(_) | VariableType::MutableReference(_) => {
424                // Variable is a REFERENCE type (alias) - it's not a local object itself
425                // Check if its lifetime is tied to a local OWNED variable
426                if let Some(lifetime) = scope.get_lifetime(value) {
427                    // Check if this lifetime is tied to a local owned variable
428                    for (var_name, other_var_info) in &function.variables {
429                        // Skip self-referential check (variable tracking its own name as lifetime)
430                        if var_name == value {
431                            continue;
432                        }
433                        // Only flag if the dependency is an OWNED local variable
434                        // Reference aliases that depend on other references are OK
435                        // (they inherit the lifetime of whatever they're bound to)
436                        let is_owned_type = matches!(
437                            other_var_info.ty,
438                            VariableType::Owned(_)
439                        );
440                        if is_owned_type && lifetime.contains(var_name) && !is_parameter(var_name, function) {
441                            errors.push(format!(
442                                "Returning reference to local variable '{}' - this will create a dangling reference",
443                                var_name
444                            ));
445                        }
446                    }
447                }
448            }
449            VariableType::Owned(_) => {
450                // Variable is an OWNED local object - returning a reference to it is dangerous
451                // (This case is handled elsewhere - the function returns a reference but
452                // the variable itself is not a reference type, so we're taking &local)
453            }
454            // Pointer types (Raw, UniquePtr, SharedPtr) are safe to return
455            // The pointer value is copied, heap memory persists after function return
456            _ => {}
457        }
458    }
459
460    errors
461}
462
463fn is_parameter(var_name: &str, function: &IrFunction) -> bool {
464    // Check if variable is marked as a parameter in the IR
465    function.variables.get(var_name)
466        .map(|var_info| var_info.is_parameter)
467        .unwrap_or(false)
468}
469
470fn map_lifetime_to_actual(lifetime_name: &str, arg_lifetimes: &[Option<String>]) -> Option<String> {
471    // Map lifetime parameter names like 'a, 'b to actual argument lifetimes
472    match lifetime_name {
473        "a" => arg_lifetimes.get(0).and_then(|l| l.clone()),
474        "b" => arg_lifetimes.get(1).and_then(|l| l.clone()),
475        "c" => arg_lifetimes.get(2).and_then(|l| l.clone()),
476        _ => Some(format!("'{}", lifetime_name)),
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    
484    #[test]
485    fn test_lifetime_scope() {
486        let mut scope = LifetimeScope::new();
487        
488        scope.set_lifetime("ref1".to_string(), "'a".to_string());
489        scope.mark_owned("value".to_string());
490        
491        assert_eq!(scope.get_lifetime("ref1"), Some(&"'a".to_string()));
492        assert!(scope.is_owned("value"));
493        assert!(!scope.is_owned("ref1"));
494    }
495    
496    #[test]
497    fn test_outlives_checking() {
498        let mut scope = LifetimeScope::new();
499
500        scope.add_constraint(LifetimeBound {
501            longer: "a".to_string(),
502            shorter: "b".to_string(),
503        });
504
505        assert!(scope.check_outlives("a", "b"));
506        assert!(scope.check_outlives("a", "a")); // Self outlives
507        assert!(!scope.check_outlives("b", "a")); // Not declared
508    }
509
510    #[test]
511    fn test_check_return_lifetime_pointer_is_safe() {
512        use crate::ir::{VariableInfo, OwnershipState, ControlFlowGraph};
513        use std::collections::HashMap;
514
515        // Create a function with a pointer variable (Raw type)
516        let mut variables = HashMap::new();
517        variables.insert("p".to_string(), VariableInfo {
518            name: "p".to_string(),
519            ty: VariableType::Raw("void*".to_string()),
520            ownership: OwnershipState::Owned,
521            lifetime: None,
522            is_parameter: false,
523            is_static: false,
524            scope_level: 1,
525            has_destructor: false,
526            declaration_index: 0,
527        });
528
529        let function = IrFunction {
530            name: "allocate".to_string(),
531            cfg: ControlFlowGraph::new(),
532            variables,
533            return_type: "void*".to_string(),
534            source_file: "test.cpp".to_string(),
535            is_method: false,
536            method_qualifier: None,
537            class_name: None,
538            template_parameters: vec![],
539            lifetime_params: HashMap::new(),
540            param_lifetimes: vec![],
541            return_lifetime: None,
542            lifetime_constraints: vec![],
543        };
544
545        let mut scope = LifetimeScope::new();
546        scope.set_lifetime("p".to_string(), "'p".to_string());
547
548        // Returning a pointer should NOT produce any errors
549        let errors = check_return_lifetime("p", &function, &scope);
550        assert!(errors.is_empty(), "Returning pointer value should be safe, got: {:?}", errors);
551    }
552
553    #[test]
554    fn test_check_return_lifetime_reference_is_unsafe() {
555        use crate::ir::{VariableInfo, OwnershipState, ControlFlowGraph};
556        use std::collections::HashMap;
557
558        // Create a function with a reference variable
559        let mut variables = HashMap::new();
560        variables.insert("local".to_string(), VariableInfo {
561            name: "local".to_string(),
562            ty: VariableType::Owned("int".to_string()),
563            ownership: OwnershipState::Owned,
564            lifetime: None,
565            is_parameter: false,
566            is_static: false,
567            scope_level: 1,
568            has_destructor: false,
569            declaration_index: 0,
570        });
571        variables.insert("ref".to_string(), VariableInfo {
572            name: "ref".to_string(),
573            ty: VariableType::Reference("int".to_string()),
574            ownership: OwnershipState::Owned,
575            lifetime: None,
576            is_parameter: false,
577            is_static: false,
578            scope_level: 1,
579            has_destructor: false,
580            declaration_index: 1,
581        });
582
583        let function = IrFunction {
584            name: "bad_return".to_string(),
585            cfg: ControlFlowGraph::new(),
586            variables,
587            return_type: "int&".to_string(),
588            source_file: "test.cpp".to_string(),
589            is_method: false,
590            method_qualifier: None,
591            class_name: None,
592            template_parameters: vec![],
593            lifetime_params: HashMap::new(),
594            param_lifetimes: vec![],
595            return_lifetime: None,
596            lifetime_constraints: vec![],
597        };
598
599        let mut scope = LifetimeScope::new();
600        // The reference's lifetime is tied to the local variable
601        scope.set_lifetime("ref".to_string(), "'local".to_string());
602
603        // Returning a reference to local should produce an error
604        let errors = check_return_lifetime("ref", &function, &scope);
605        assert!(!errors.is_empty(), "Returning reference to local should be flagged as unsafe");
606        assert!(errors[0].contains("local"), "Error should mention the local variable");
607    }
608
609    #[test]
610    fn test_check_return_lifetime_owned_is_safe() {
611        use crate::ir::{VariableInfo, OwnershipState, ControlFlowGraph};
612        use std::collections::HashMap;
613
614        // Create a function with an owned variable (like unique_ptr)
615        let mut variables = HashMap::new();
616        variables.insert("ptr".to_string(), VariableInfo {
617            name: "ptr".to_string(),
618            ty: VariableType::UniquePtr("int".to_string()),
619            ownership: OwnershipState::Owned,
620            lifetime: None,
621            is_parameter: false,
622            is_static: false,
623            scope_level: 1,
624            has_destructor: true,
625            declaration_index: 0,
626        });
627
628        let function = IrFunction {
629            name: "create".to_string(),
630            cfg: ControlFlowGraph::new(),
631            variables,
632            return_type: "std::unique_ptr<int>".to_string(),
633            source_file: "test.cpp".to_string(),
634            is_method: false,
635            method_qualifier: None,
636            class_name: None,
637            template_parameters: vec![],
638            lifetime_params: HashMap::new(),
639            param_lifetimes: vec![],
640            return_lifetime: None,
641            lifetime_constraints: vec![],
642        };
643
644        let mut scope = LifetimeScope::new();
645        scope.set_lifetime("ptr".to_string(), "'ptr".to_string());
646
647        // Returning owned value should NOT produce any errors
648        let errors = check_return_lifetime("ptr", &function, &scope);
649        assert!(errors.is_empty(), "Returning owned value should be safe, got: {:?}", errors);
650    }
651}