rusty_cpp/analysis/
lifetime_inference.rs

1use std::collections::{HashMap, HashSet};
2use crate::ir::{IrFunction, IrStatement, BorrowKind};
3use crate::debug_println;
4
5/// Check if a return type string represents a reference
6fn returns_reference(return_type: &str) -> bool {
7    // Check for reference types: &, const &, const Type&, Type&, etc.
8    return_type.contains('&') && !return_type.contains("&&") // Exclude rvalue references
9}
10
11/// Represents an inferred lifetime for a variable
12#[derive(Debug, Clone, PartialEq)]
13pub struct InferredLifetime {
14    pub name: String,
15    /// The point where this lifetime begins (statement index)
16    pub start: usize,
17    /// The point where this lifetime ends (statement index)
18    pub end: usize,
19    /// Variables that this lifetime depends on
20    pub dependencies: HashSet<String>,
21}
22
23/// Infers lifetimes for local variables based on their usage patterns
24pub struct LifetimeInferencer {
25    /// Maps variable names to their inferred lifetimes
26    lifetimes: HashMap<String, InferredLifetime>,
27    /// Tracks the last use of each variable
28    last_use: HashMap<String, usize>,
29    /// Tracks when variables are first defined
30    first_def: HashMap<String, usize>,
31    /// Counter for generating unique lifetime names
32    lifetime_counter: usize,
33}
34
35impl LifetimeInferencer {
36    pub fn new() -> Self {
37        Self {
38            lifetimes: HashMap::new(),
39            last_use: HashMap::new(),
40            first_def: HashMap::new(),
41            lifetime_counter: 0,
42        }
43    }
44    
45    /// Generate a unique lifetime name
46    fn gen_lifetime(&mut self) -> String {
47        let name = format!("'inferred_{}", self.lifetime_counter);
48        self.lifetime_counter += 1;
49        name
50    }
51    
52    /// Infer lifetimes for all variables in a function
53    pub fn infer_function_lifetimes(&mut self, function: &IrFunction) -> HashMap<String, InferredLifetime> {
54        // REASSIGNMENT FIX: Initialize all declared variables with first_def = 0
55        // This ensures variables exist in the lifetime tracking even if their
56        // declaration doesn't generate IR (e.g., int x = 42 with literal)
57        for (var_name, _var_info) in &function.variables {
58            self.first_def.entry(var_name.clone()).or_insert(0);
59            self.last_use.entry(var_name.clone()).or_insert(0);
60        }
61
62        // First pass: collect all variable definitions and uses from IR statements
63        let mut statement_index = 0;
64
65        for node_idx in function.cfg.node_indices() {
66            let block = &function.cfg[node_idx];
67
68            for statement in &block.statements {
69                self.process_statement(statement, statement_index);
70                statement_index += 1;
71            }
72        }
73        
74        // Second pass: create lifetime ranges
75        let first_def_clone = self.first_def.clone();
76        for (var, &first_def_idx) in &first_def_clone {
77            let last_use_idx = self.last_use.get(var).copied().unwrap_or(first_def_idx);
78            
79            let lifetime = InferredLifetime {
80                name: self.gen_lifetime(),
81                start: first_def_idx,
82                end: last_use_idx,
83                dependencies: HashSet::new(),
84            };
85            
86            self.lifetimes.insert(var.clone(), lifetime);
87        }
88        
89        // Third pass: infer dependencies between lifetimes
90        statement_index = 0;
91        for node_idx in function.cfg.node_indices() {
92            let block = &function.cfg[node_idx];
93            
94            for statement in &block.statements {
95                self.infer_dependencies(statement, statement_index);
96                statement_index += 1;
97            }
98        }
99        
100        self.lifetimes.clone()
101    }
102    
103    /// Process a statement to track variable definitions and uses
104    fn process_statement(&mut self, statement: &IrStatement, index: usize) {
105        match statement {
106            IrStatement::Assign { lhs, rhs } => {
107                self.first_def.entry(lhs.clone()).or_insert(index);
108                self.last_use.insert(lhs.clone(), index);
109                // REASSIGNMENT FIX: Also track usage of RHS variable
110                // This extends the lifetime of variables used on the right side
111                if let crate::ir::IrExpression::Variable(rhs_var) = rhs {
112                    self.last_use.insert(rhs_var.clone(), index);
113                }
114            }
115            
116            IrStatement::Move { from, to } => {
117                self.last_use.insert(from.clone(), index);
118                self.first_def.entry(to.clone()).or_insert(index);
119                self.last_use.insert(to.clone(), index);
120            }
121            
122            IrStatement::Borrow { from, to, .. } => {
123                self.last_use.insert(from.clone(), index);
124                self.first_def.entry(to.clone()).or_insert(index);
125                self.last_use.insert(to.clone(), index);
126            }
127            
128            IrStatement::CallExpr { args, result, .. } => {
129                for arg in args {
130                    self.last_use.insert(arg.clone(), index);
131                }
132                if let Some(res) = result {
133                    self.first_def.entry(res.clone()).or_insert(index);
134                    self.last_use.insert(res.clone(), index);
135                }
136            }
137            
138            IrStatement::Return { value } => {
139                if let Some(val) = value {
140                    self.last_use.insert(val.clone(), index);
141                }
142            }
143            
144            _ => {}
145        }
146    }
147    
148    /// Infer dependencies between lifetimes based on borrows and moves
149    fn infer_dependencies(&mut self, statement: &IrStatement, _index: usize) {
150        match statement {
151            IrStatement::Borrow { from, to, kind } => {
152                // The lifetime of 'to' depends on 'from'
153                if let Some(to_lifetime) = self.lifetimes.get_mut(to) {
154                    to_lifetime.dependencies.insert(from.clone());
155                    
156                    // For mutable borrows, the lifetime must be exclusive
157                    if matches!(kind, BorrowKind::Mutable) {
158                        // Mark that this lifetime requires exclusive access
159                        // In a full implementation, we'd track this for conflict detection
160                    }
161                }
162            }
163            
164            IrStatement::Move { from, to } => {
165                // After a move, 'from' lifetime ends and 'to' lifetime begins
166                if let Some(from_lifetime) = self.lifetimes.get(from).cloned() {
167                    if let Some(to_lifetime) = self.lifetimes.get_mut(to) {
168                        // 'to' inherits dependencies from 'from'
169                        to_lifetime.dependencies.extend(from_lifetime.dependencies);
170                    }
171                }
172            }
173            
174            _ => {}
175        }
176    }
177    
178    /// Check if two lifetimes overlap
179    pub fn lifetimes_overlap(&self, a: &str, b: &str) -> bool {
180        if let (Some(lifetime_a), Some(lifetime_b)) = (self.lifetimes.get(a), self.lifetimes.get(b)) {
181            // Check if the ranges [start_a, end_a] and [start_b, end_b] overlap
182            !(lifetime_a.end < lifetime_b.start || lifetime_b.end < lifetime_a.start)
183        } else {
184            false
185        }
186    }
187    
188    /// Get the inferred lifetime for a variable
189    #[allow(dead_code)]
190    pub fn get_lifetime(&self, var: &str) -> Option<&InferredLifetime> {
191        self.lifetimes.get(var)
192    }
193    
194    /// Check if a variable is alive at a given point
195    pub fn is_alive_at(&self, var: &str, point: usize) -> bool {
196        if let Some(lifetime) = self.lifetimes.get(var) {
197            point >= lifetime.start && point <= lifetime.end
198        } else {
199            false
200        }
201    }
202}
203
204/// Perform lifetime inference and validation
205pub fn infer_and_validate_lifetimes(function: &IrFunction) -> Result<Vec<String>, String> {
206    let mut inferencer = LifetimeInferencer::new();
207    let lifetimes = inferencer.infer_function_lifetimes(function);
208    let mut errors = Vec::new();
209
210    // Check for conflicts between inferred lifetimes
211    let mut statement_index = 0;
212    for node_idx in function.cfg.node_indices() {
213        let block = &function.cfg[node_idx];
214
215        for statement in &block.statements {
216            match statement {
217                IrStatement::Borrow { from, to, kind } => {
218                    // Check that 'from' is alive when borrowed
219                    // Note: We should only check this if we have actually tracked the variable
220                    if inferencer.lifetimes.contains_key(from) && !inferencer.is_alive_at(from, statement_index) {
221                        errors.push(format!(
222                            "Cannot borrow '{}': variable is not alive at this point",
223                            from
224                        ));
225                    }
226                    
227                    // For mutable borrows, check for conflicts
228                    if matches!(kind, BorrowKind::Mutable) {
229                        // Check if there are other borrows of 'from' that overlap with 'to'
230                        for (other_var, other_lifetime) in &lifetimes {
231                            if other_var != to && other_lifetime.dependencies.contains(from) {
232                                if inferencer.lifetimes_overlap(to, other_var) {
233                                    // Only error if the other borrow is also mutable or we have a mutable borrow
234                                    // Multiple immutable borrows are allowed
235                                    errors.push(format!(
236                                        "Cannot create mutable borrow '{}': '{}' is already borrowed by '{}'",
237                                        to, from, other_var
238                                    ));
239                                }
240                            }
241                        }
242                    }
243                }
244                
245                IrStatement::Move { from, to } => {
246                    // Check that 'from' is alive when moved
247                    if inferencer.lifetimes.contains_key(from) && !inferencer.is_alive_at(from, statement_index) {
248                        errors.push(format!(
249                            "Cannot move '{}': variable is not alive at this point",
250                            from
251                        ));
252                    }
253                }
254                
255                IrStatement::Return { value } => {
256                    // PHASE 2: If function returns a reference but return value is None,
257                    // it's returning a temporary (e.g., return 42;)
258                    if value.is_none() && returns_reference(&function.return_type) {
259                        errors.push(format!(
260                            "Cannot return reference to temporary value in function '{}'",
261                            function.name
262                        ));
263                    }
264
265                    if let Some(val) = value {
266                        // PHASE 2: If function returns a reference, check what we're returning
267                        if returns_reference(&function.return_type) {
268                            if let Some(var_info) = function.variables.get(val) {
269                                let is_param = is_parameter(val, function);
270
271                                // Check based on variable type
272                                match &var_info.ty {
273                                    crate::ir::VariableType::Reference(_) |
274                                    crate::ir::VariableType::MutableReference(_) => {
275                                        // Variable is already a reference (alias) - it's NOT a local object
276                                        // Check its dependencies instead of flagging as "returning local"
277                                        if let Some(lifetime) = lifetimes.get(val) {
278                                            // Check if it depends on local variables
279                                            for dep in &lifetime.dependencies {
280                                                if !is_parameter(dep, function) && !var_info.is_static {
281                                                    errors.push(format!(
282                                                        "Potential dangling reference: returning '{}' which depends on local variable '{}'",
283                                                        val, dep
284                                                    ));
285                                                }
286                                            }
287                                        }
288                                        // If no dependencies tracked, the reference alias is safe to return
289                                        // (it inherits the lifetime of whatever it was bound to)
290                                    }
291                                    _ => {
292                                        // Variable is an OWNED local object (not a reference alias)
293                                        // Returning a reference to it is dangerous
294                                        if !is_param && !var_info.is_static {
295                                            errors.push(format!(
296                                                "Cannot return reference to local variable '{}'",
297                                                val
298                                            ));
299                                        }
300                                    }
301                                }
302                            }
303                        }
304                    }
305                }
306                
307                _ => {}
308            }
309            
310            statement_index += 1;
311        }
312    }
313    
314    Ok(errors)
315}
316
317fn is_parameter(var_name: &str, function: &IrFunction) -> bool {
318    // Check if variable is marked as a parameter in the IR
319    function.variables.get(var_name)
320        .map(|var_info| var_info.is_parameter)
321        .unwrap_or(false)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::ir::BasicBlock;
328    use petgraph::graph::Graph;
329    
330    #[test]
331    fn test_lifetime_inference() {
332        let mut inferencer = LifetimeInferencer::new();
333        
334        // Simulate processing statements
335        inferencer.process_statement(&IrStatement::Assign {
336            lhs: "x".to_string(),
337            rhs: crate::ir::IrExpression::Variable("temp".to_string()),
338        }, 0);
339        
340        inferencer.process_statement(&IrStatement::Borrow {
341            from: "x".to_string(),
342            to: "ref_x".to_string(),
343            kind: BorrowKind::Immutable,
344        }, 1);
345        
346        inferencer.process_statement(&IrStatement::Return {
347            value: Some("ref_x".to_string()),
348        }, 2);
349        
350        // Create a dummy function for testing
351        let mut cfg = Graph::new();
352        let block = BasicBlock {
353            id: 0,
354            statements: vec![],
355            terminator: None,
356        };
357        cfg.add_node(block);
358        
359        let function = IrFunction {
360            name: "test".to_string(),
361            cfg,
362            variables: HashMap::new(),
363            return_type: "void".to_string(),
364            source_file: "test.cpp".to_string(),
365            is_method: false,
366            method_qualifier: None,
367            class_name: None,
368            template_parameters: vec![],
369            lifetime_params: HashMap::new(),
370            param_lifetimes: Vec::new(),
371            return_lifetime: None,
372            lifetime_constraints: Vec::new(),
373        };
374        
375        let lifetimes = inferencer.infer_function_lifetimes(&function);
376        
377        // Check that lifetimes were inferred
378        assert!(inferencer.first_def.contains_key("x"));
379        assert!(inferencer.first_def.contains_key("ref_x"));
380        assert_eq!(inferencer.last_use.get("x"), Some(&1));
381        assert_eq!(inferencer.last_use.get("ref_x"), Some(&2));
382    }
383    
384    #[test]
385    fn test_lifetime_overlap() {
386        let mut inferencer = LifetimeInferencer::new();
387        
388        inferencer.lifetimes.insert("a".to_string(), InferredLifetime {
389            name: "'a".to_string(),
390            start: 0,
391            end: 5,
392            dependencies: HashSet::new(),
393        });
394        
395        inferencer.lifetimes.insert("b".to_string(), InferredLifetime {
396            name: "'b".to_string(),
397            start: 3,
398            end: 7,
399            dependencies: HashSet::new(),
400        });
401        
402        inferencer.lifetimes.insert("c".to_string(), InferredLifetime {
403            name: "'c".to_string(),
404            start: 6,
405            end: 10,
406            dependencies: HashSet::new(),
407        });
408        
409        assert!(inferencer.lifetimes_overlap("a", "b")); // [0,5] and [3,7] overlap
410        assert!(!inferencer.lifetimes_overlap("a", "c")); // [0,5] and [6,10] don't overlap
411        assert!(inferencer.lifetimes_overlap("b", "c")); // [3,7] and [6,10] overlap
412    }
413}