Skip to main content

decy_analyzer/
patterns.rs

1//! Pattern detection for identifying `Box<T>` and `Vec<T>` candidates.
2//!
3//! Analyzes HIR to find malloc/free patterns that can be replaced with safe Rust types.
4
5use decy_hir::{HirExpression, HirFunction, HirStatement};
6
7/// Represents a detected `Box<T>` pattern candidate.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct BoxCandidate {
10    /// Variable name that holds the allocated pointer
11    pub variable: String,
12    /// Statement index where malloc occurs
13    pub malloc_index: usize,
14    /// Statement index where free occurs (if found)
15    pub free_index: Option<usize>,
16}
17
18/// Represents a detected `Vec<T>` pattern candidate.
19#[derive(Debug, Clone, PartialEq)]
20pub struct VecCandidate {
21    /// Variable name that holds the allocated array pointer
22    pub variable: String,
23    /// Statement index where malloc occurs
24    pub malloc_index: usize,
25    /// Statement index where free occurs (if found)
26    pub free_index: Option<usize>,
27    /// Expression representing the array capacity (number of elements)
28    pub capacity_expr: Option<HirExpression>,
29}
30
31/// Pattern detector for identifying `Box<T>` candidates.
32#[derive(Debug, Clone)]
33pub struct PatternDetector;
34
35impl PatternDetector {
36    /// Create a new pattern detector.
37    pub fn new() -> Self {
38        Self
39    }
40
41    /// Analyze a function to find `Box<T>` candidates.
42    ///
43    /// Detects patterns like:
44    /// ```c
45    /// T* ptr = malloc(sizeof(T));
46    /// // ... use ptr ...
47    /// free(ptr);
48    /// ```
49    pub fn find_box_candidates(&self, func: &HirFunction) -> Vec<BoxCandidate> {
50        let mut candidates = Vec::new();
51        let body = func.body();
52
53        // Track malloc calls assigned to variables
54        for (idx, stmt) in body.iter().enumerate() {
55            if let Some(var_name) = self.is_malloc_assignment(stmt) {
56                // Look for corresponding free
57                let free_idx = self.find_free_call(body, idx + 1, &var_name);
58
59                candidates.push(BoxCandidate {
60                    variable: var_name,
61                    malloc_index: idx,
62                    free_index: free_idx,
63                });
64            }
65        }
66
67        candidates
68    }
69
70    /// Check if a statement is an assignment from malloc.
71    ///
72    /// Patterns matched:
73    /// - `T* ptr = malloc(...)`  (VariableDeclaration)
74    /// - `ptr = malloc(...)`     (Assignment)
75    fn is_malloc_assignment(&self, stmt: &HirStatement) -> Option<String> {
76        match stmt {
77            HirStatement::VariableDeclaration { name, initializer: Some(expr), .. } => {
78                if self.is_malloc_call(expr) {
79                    Some(name.clone())
80                } else {
81                    None
82                }
83            }
84            HirStatement::Assignment { target, value } => {
85                if self.is_malloc_call(value) {
86                    Some(target.clone())
87                } else {
88                    None
89                }
90            }
91            _ => None,
92        }
93    }
94
95    /// Check if an expression is a malloc call.
96    fn is_malloc_call(&self, expr: &HirExpression) -> bool {
97        matches!(
98            expr,
99            HirExpression::FunctionCall { function, .. } if function == "malloc"
100        )
101    }
102
103    /// Find a free call for a specific variable after a given statement index.
104    fn find_free_call(
105        &self,
106        body: &[HirStatement],
107        start_idx: usize,
108        var_name: &str,
109    ) -> Option<usize> {
110        for (offset, stmt) in body[start_idx..].iter().enumerate() {
111            if self.is_free_call(stmt, var_name) {
112                return Some(start_idx + offset);
113            }
114        }
115        None
116    }
117
118    /// Check if a statement is a free call for a specific variable.
119    ///
120    /// Free call detection requires ExpressionStatement support in HIR.
121    /// This will be implemented in a future phase when ExpressionStatement is added.
122    /// For now, free_index in BoxCandidate will always be None.
123    fn is_free_call(&self, _stmt: &HirStatement, _var_name: &str) -> bool {
124        // Free call detection requires ExpressionStatement support in HIR.
125        // This will be implemented in a future phase when ExpressionStatement is added.
126        // For now, free_index in BoxCandidate will always be None.
127        false
128    }
129
130    /// Analyze a function to find `Vec<T>` candidates.
131    ///
132    /// Detects patterns like:
133    /// ```c
134    /// T* arr = malloc(n * sizeof(T));
135    /// // ... use arr as array ...
136    /// free(arr);
137    /// ```
138    pub fn find_vec_candidates(&self, func: &HirFunction) -> Vec<VecCandidate> {
139        let mut candidates = Vec::new();
140        let body = func.body();
141
142        // Track malloc calls assigned to variables that use array allocation pattern
143        for (idx, stmt) in body.iter().enumerate() {
144            if let Some((var_name, malloc_expr)) = self.is_malloc_assignment_expr(stmt) {
145                // Check if this is an array allocation pattern (n * sizeof(T))
146                if self.is_array_size_expr(malloc_expr) {
147                    let capacity = self.extract_capacity(malloc_expr);
148
149                    // Look for corresponding free (same logic as Box)
150                    let free_idx = self.find_free_call(body, idx + 1, &var_name);
151
152                    candidates.push(VecCandidate {
153                        variable: var_name,
154                        malloc_index: idx,
155                        free_index: free_idx,
156                        capacity_expr: capacity,
157                    });
158                }
159            }
160        }
161
162        candidates
163    }
164
165    /// Check if a statement is an assignment from malloc, returning var name and malloc expr.
166    ///
167    /// Similar to is_malloc_assignment but returns the malloc call expression for analysis.
168    fn is_malloc_assignment_expr<'a>(
169        &self,
170        stmt: &'a HirStatement,
171    ) -> Option<(String, &'a HirExpression)> {
172        match stmt {
173            HirStatement::VariableDeclaration { name, initializer: Some(expr), .. } => {
174                if let HirExpression::FunctionCall { function, arguments } = expr {
175                    if function == "malloc" && !arguments.is_empty() {
176                        return Some((name.clone(), &arguments[0]));
177                    }
178                }
179                None
180            }
181            HirStatement::Assignment { target, value } => {
182                if let HirExpression::FunctionCall { function, arguments } = value {
183                    if function == "malloc" && !arguments.is_empty() {
184                        return Some((target.clone(), &arguments[0]));
185                    }
186                }
187                None
188            }
189            _ => None,
190        }
191    }
192
193    /// Check if an expression represents array allocation: n * sizeof(T) pattern
194    ///
195    /// Looks for multiplication expressions that indicate array sizing.
196    fn is_array_size_expr(&self, expr: &HirExpression) -> bool {
197        matches!(expr, HirExpression::BinaryOp { op: decy_hir::BinaryOperator::Multiply, .. })
198    }
199
200    /// Extract capacity from array size expression (n * sizeof(T))
201    ///
202    /// Returns the left operand of the multiplication, which typically
203    /// represents the number of elements (capacity).
204    fn extract_capacity(&self, expr: &HirExpression) -> Option<HirExpression> {
205        if let HirExpression::BinaryOp { op: decy_hir::BinaryOperator::Multiply, left, .. } = expr {
206            Some((**left).clone())
207        } else {
208            None
209        }
210    }
211}
212
213impl Default for PatternDetector {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use decy_hir::{HirParameter, HirType};
223
224    #[test]
225    fn test_pattern_detector_default() {
226        let detector = PatternDetector::default();
227        let func = HirFunction::new_with_body("test".to_string(), HirType::Void, vec![], vec![]);
228        assert!(detector.find_box_candidates(&func).is_empty());
229        assert!(detector.find_vec_candidates(&func).is_empty());
230    }
231
232    #[test]
233    fn test_vardecl_without_initializer() {
234        let func = HirFunction::new_with_body(
235            "test".to_string(),
236            HirType::Void,
237            vec![],
238            vec![HirStatement::VariableDeclaration {
239                name: "ptr".to_string(),
240                var_type: HirType::Pointer(Box::new(HirType::Int)),
241                initializer: None,
242            }],
243        );
244        let detector = PatternDetector::new();
245        assert!(detector.find_box_candidates(&func).is_empty());
246        assert!(detector.find_vec_candidates(&func).is_empty());
247    }
248
249    #[test]
250    fn test_vec_malloc_with_empty_arguments() {
251        // malloc() with no arguments — should not be detected
252        let func = HirFunction::new_with_body(
253            "test".to_string(),
254            HirType::Void,
255            vec![],
256            vec![HirStatement::VariableDeclaration {
257                name: "ptr".to_string(),
258                var_type: HirType::Pointer(Box::new(HirType::Int)),
259                initializer: Some(HirExpression::FunctionCall {
260                    function: "malloc".to_string(),
261                    arguments: vec![],
262                }),
263            }],
264        );
265        let detector = PatternDetector::new();
266        // box candidates: malloc with no args is still a malloc call
267        assert_eq!(detector.find_box_candidates(&func).len(), 1);
268        // vec candidates: needs args[0] to check for multiply pattern
269        assert!(detector.find_vec_candidates(&func).is_empty());
270    }
271
272    #[test]
273    fn test_detect_malloc_in_variable_declaration() {
274        let func = HirFunction::new_with_body(
275            "test".to_string(),
276            HirType::Void,
277            vec![],
278            vec![HirStatement::VariableDeclaration {
279                name: "ptr".to_string(),
280                var_type: HirType::Pointer(Box::new(HirType::Int)),
281                initializer: Some(HirExpression::FunctionCall {
282                    function: "malloc".to_string(),
283                    arguments: vec![HirExpression::IntLiteral(100)],
284                }),
285            }],
286        );
287
288        let detector = PatternDetector::new();
289        let candidates = detector.find_box_candidates(&func);
290
291        assert_eq!(candidates.len(), 1);
292        assert_eq!(candidates[0].variable, "ptr");
293        assert_eq!(candidates[0].malloc_index, 0);
294    }
295
296    #[test]
297    fn test_detect_malloc_in_assignment() {
298        let func = HirFunction::new_with_body(
299            "test".to_string(),
300            HirType::Void,
301            vec![HirParameter::new("ptr".to_string(), HirType::Pointer(Box::new(HirType::Int)))],
302            vec![HirStatement::Assignment {
303                target: "ptr".to_string(),
304                value: HirExpression::FunctionCall {
305                    function: "malloc".to_string(),
306                    arguments: vec![HirExpression::IntLiteral(50)],
307                },
308            }],
309        );
310
311        let detector = PatternDetector::new();
312        let candidates = detector.find_box_candidates(&func);
313
314        assert_eq!(candidates.len(), 1);
315        assert_eq!(candidates[0].variable, "ptr");
316        assert_eq!(candidates[0].malloc_index, 0);
317    }
318
319    #[test]
320    fn test_no_malloc_detected() {
321        let func = HirFunction::new_with_body(
322            "test".to_string(),
323            HirType::Int,
324            vec![],
325            vec![
326                HirStatement::VariableDeclaration {
327                    name: "x".to_string(),
328                    var_type: HirType::Int,
329                    initializer: Some(HirExpression::IntLiteral(42)),
330                },
331                HirStatement::Return(Some(HirExpression::Variable("x".to_string()))),
332            ],
333        );
334
335        let detector = PatternDetector::new();
336        let candidates = detector.find_box_candidates(&func);
337
338        assert_eq!(candidates.len(), 0);
339    }
340
341    #[test]
342    fn test_multiple_malloc_calls() {
343        let func = HirFunction::new_with_body(
344            "test".to_string(),
345            HirType::Void,
346            vec![],
347            vec![
348                HirStatement::VariableDeclaration {
349                    name: "ptr1".to_string(),
350                    var_type: HirType::Pointer(Box::new(HirType::Int)),
351                    initializer: Some(HirExpression::FunctionCall {
352                        function: "malloc".to_string(),
353                        arguments: vec![HirExpression::IntLiteral(100)],
354                    }),
355                },
356                HirStatement::VariableDeclaration {
357                    name: "ptr2".to_string(),
358                    var_type: HirType::Pointer(Box::new(HirType::Char)),
359                    initializer: Some(HirExpression::FunctionCall {
360                        function: "malloc".to_string(),
361                        arguments: vec![HirExpression::IntLiteral(200)],
362                    }),
363                },
364            ],
365        );
366
367        let detector = PatternDetector::new();
368        let candidates = detector.find_box_candidates(&func);
369
370        assert_eq!(candidates.len(), 2);
371        assert_eq!(candidates[0].variable, "ptr1");
372        assert_eq!(candidates[1].variable, "ptr2");
373    }
374
375    #[test]
376    fn test_malloc_from_other_function() {
377        // Should NOT detect allocate() as malloc
378        let func = HirFunction::new_with_body(
379            "test".to_string(),
380            HirType::Void,
381            vec![],
382            vec![HirStatement::VariableDeclaration {
383                name: "ptr".to_string(),
384                var_type: HirType::Pointer(Box::new(HirType::Int)),
385                initializer: Some(HirExpression::FunctionCall {
386                    function: "allocate".to_string(),
387                    arguments: vec![HirExpression::IntLiteral(100)],
388                }),
389            }],
390        );
391
392        let detector = PatternDetector::new();
393        let candidates = detector.find_box_candidates(&func);
394
395        assert_eq!(candidates.len(), 0);
396    }
397
398    // Vec candidate detection tests
399    #[test]
400    fn test_detect_vec_array_allocation_in_variable_declaration() {
401        // Pattern: int* arr = malloc(n * sizeof(int));
402        // Should be detected as Vec<i32> candidate
403        let n_expr = HirExpression::Variable("n".to_string());
404        let sizeof_expr = HirExpression::IntLiteral(4); // sizeof(int) = 4
405        let size_expr = HirExpression::BinaryOp {
406            op: decy_hir::BinaryOperator::Multiply,
407            left: Box::new(n_expr.clone()),
408            right: Box::new(sizeof_expr),
409        };
410
411        let func = HirFunction::new_with_body(
412            "test".to_string(),
413            HirType::Void,
414            vec![],
415            vec![HirStatement::VariableDeclaration {
416                name: "arr".to_string(),
417                var_type: HirType::Pointer(Box::new(HirType::Int)),
418                initializer: Some(HirExpression::FunctionCall {
419                    function: "malloc".to_string(),
420                    arguments: vec![size_expr],
421                }),
422            }],
423        );
424
425        let detector = PatternDetector::new();
426        let candidates = detector.find_vec_candidates(&func);
427
428        assert_eq!(candidates.len(), 1, "Should detect one Vec candidate");
429        assert_eq!(candidates[0].variable, "arr");
430        assert_eq!(candidates[0].malloc_index, 0);
431    }
432
433    #[test]
434    fn test_detect_vec_with_literal_capacity() {
435        // Pattern: int* arr = malloc(10 * sizeof(int));
436        let capacity = HirExpression::IntLiteral(10);
437        let sizeof_expr = HirExpression::IntLiteral(4);
438        let size_expr = HirExpression::BinaryOp {
439            op: decy_hir::BinaryOperator::Multiply,
440            left: Box::new(capacity.clone()),
441            right: Box::new(sizeof_expr),
442        };
443
444        let func = HirFunction::new_with_body(
445            "test".to_string(),
446            HirType::Void,
447            vec![],
448            vec![HirStatement::VariableDeclaration {
449                name: "arr".to_string(),
450                var_type: HirType::Pointer(Box::new(HirType::Int)),
451                initializer: Some(HirExpression::FunctionCall {
452                    function: "malloc".to_string(),
453                    arguments: vec![size_expr],
454                }),
455            }],
456        );
457
458        let detector = PatternDetector::new();
459        let candidates = detector.find_vec_candidates(&func);
460
461        assert_eq!(candidates.len(), 1);
462        assert_eq!(candidates[0].variable, "arr");
463        assert!(candidates[0].capacity_expr.is_some(), "Should extract capacity expression");
464    }
465
466    #[test]
467    fn test_vec_vs_box_distinction() {
468        // Box pattern: malloc(sizeof(T)) - single element
469        // Vec pattern: malloc(n * sizeof(T)) - array
470        let func = HirFunction::new_with_body(
471            "test".to_string(),
472            HirType::Void,
473            vec![],
474            vec![
475                // Box candidate: single element
476                HirStatement::VariableDeclaration {
477                    name: "single".to_string(),
478                    var_type: HirType::Pointer(Box::new(HirType::Int)),
479                    initializer: Some(HirExpression::FunctionCall {
480                        function: "malloc".to_string(),
481                        arguments: vec![HirExpression::IntLiteral(4)], // just sizeof(int)
482                    }),
483                },
484                // Vec candidate: array
485                HirStatement::VariableDeclaration {
486                    name: "array".to_string(),
487                    var_type: HirType::Pointer(Box::new(HirType::Int)),
488                    initializer: Some(HirExpression::FunctionCall {
489                        function: "malloc".to_string(),
490                        arguments: vec![HirExpression::BinaryOp {
491                            op: decy_hir::BinaryOperator::Multiply,
492                            left: Box::new(HirExpression::IntLiteral(10)),
493                            right: Box::new(HirExpression::IntLiteral(4)),
494                        }],
495                    }),
496                },
497            ],
498        );
499
500        let detector = PatternDetector::new();
501        let box_candidates = detector.find_box_candidates(&func);
502        let vec_candidates = detector.find_vec_candidates(&func);
503
504        // Box detector should find both (it's less specific)
505        // Vec detector should only find the array pattern
506        assert_eq!(vec_candidates.len(), 1, "Should find only array pattern");
507        assert_eq!(vec_candidates[0].variable, "array");
508
509        // The "single" allocation should be detected as Box only
510        // (Box detector will find it, Vec detector won't)
511        assert!(box_candidates.iter().any(|c| c.variable == "single"));
512    }
513
514    #[test]
515    fn test_no_vec_detected_for_non_array_malloc() {
516        // malloc without multiplication pattern should not be Vec
517        let func = HirFunction::new_with_body(
518            "test".to_string(),
519            HirType::Void,
520            vec![],
521            vec![HirStatement::VariableDeclaration {
522                name: "ptr".to_string(),
523                var_type: HirType::Pointer(Box::new(HirType::Int)),
524                initializer: Some(HirExpression::FunctionCall {
525                    function: "malloc".to_string(),
526                    arguments: vec![HirExpression::IntLiteral(100)],
527                }),
528            }],
529        );
530
531        let detector = PatternDetector::new();
532        let candidates = detector.find_vec_candidates(&func);
533
534        assert_eq!(candidates.len(), 0, "Should not detect non-array as Vec");
535    }
536
537    #[test]
538    fn test_multiple_vec_allocations() {
539        let size1 = HirExpression::BinaryOp {
540            op: decy_hir::BinaryOperator::Multiply,
541            left: Box::new(HirExpression::IntLiteral(10)),
542            right: Box::new(HirExpression::IntLiteral(4)),
543        };
544
545        let size2 = HirExpression::BinaryOp {
546            op: decy_hir::BinaryOperator::Multiply,
547            left: Box::new(HirExpression::Variable("count".to_string())),
548            right: Box::new(HirExpression::IntLiteral(8)),
549        };
550
551        let func = HirFunction::new_with_body(
552            "test".to_string(),
553            HirType::Void,
554            vec![],
555            vec![
556                HirStatement::VariableDeclaration {
557                    name: "arr1".to_string(),
558                    var_type: HirType::Pointer(Box::new(HirType::Int)),
559                    initializer: Some(HirExpression::FunctionCall {
560                        function: "malloc".to_string(),
561                        arguments: vec![size1],
562                    }),
563                },
564                HirStatement::VariableDeclaration {
565                    name: "arr2".to_string(),
566                    var_type: HirType::Pointer(Box::new(HirType::Double)),
567                    initializer: Some(HirExpression::FunctionCall {
568                        function: "malloc".to_string(),
569                        arguments: vec![size2],
570                    }),
571                },
572            ],
573        );
574
575        let detector = PatternDetector::new();
576        let candidates = detector.find_vec_candidates(&func);
577
578        assert_eq!(candidates.len(), 2, "Should detect both Vec candidates");
579        assert_eq!(candidates[0].variable, "arr1");
580        assert_eq!(candidates[1].variable, "arr2");
581    }
582
583    #[test]
584    fn test_wrong_function_name_not_detected() {
585        // Mutation testing found: changing == to != doesn't fail tests
586        // Test that non-malloc functions are NOT detected
587        let func = HirFunction::new_with_body(
588            "test".to_string(),
589            HirType::Void,
590            vec![],
591            vec![
592                HirStatement::VariableDeclaration {
593                    name: "ptr1".to_string(),
594                    var_type: HirType::Pointer(Box::new(HirType::Int)),
595                    initializer: Some(HirExpression::FunctionCall {
596                        function: "calloc".to_string(), // Not malloc!
597                        arguments: vec![HirExpression::IntLiteral(100)],
598                    }),
599                },
600                HirStatement::VariableDeclaration {
601                    name: "ptr2".to_string(),
602                    var_type: HirType::Pointer(Box::new(HirType::Int)),
603                    initializer: Some(HirExpression::FunctionCall {
604                        function: "realloc".to_string(), // Not malloc!
605                        arguments: vec![HirExpression::IntLiteral(100)],
606                    }),
607                },
608            ],
609        );
610
611        let detector = PatternDetector::new();
612        let box_candidates = detector.find_box_candidates(&func);
613        let vec_candidates = detector.find_vec_candidates(&func);
614
615        assert_eq!(box_candidates.len(), 0, "Should not detect calloc/realloc as malloc");
616        assert_eq!(vec_candidates.len(), 0, "Should not detect calloc/realloc as malloc");
617    }
618
619    #[test]
620    fn test_vec_assignment_with_array_malloc() {
621        // Mutation testing found: deleting Assignment branch in is_malloc_assignment_expr doesn't fail
622        // Test that Assignment statement with array malloc IS detected as Vec
623        let size_expr = HirExpression::BinaryOp {
624            op: decy_hir::BinaryOperator::Multiply,
625            left: Box::new(HirExpression::IntLiteral(10)),
626            right: Box::new(HirExpression::IntLiteral(4)),
627        };
628
629        let func = HirFunction::new_with_body(
630            "test".to_string(),
631            HirType::Void,
632            vec![HirParameter::new("arr".to_string(), HirType::Pointer(Box::new(HirType::Int)))],
633            vec![HirStatement::Assignment {
634                target: "arr".to_string(),
635                value: HirExpression::FunctionCall {
636                    function: "malloc".to_string(),
637                    arguments: vec![size_expr],
638                },
639            }],
640        );
641
642        let detector = PatternDetector::new();
643        let vec_candidates = detector.find_vec_candidates(&func);
644
645        assert_eq!(vec_candidates.len(), 1, "Should detect array malloc in Assignment as Vec");
646        assert_eq!(vec_candidates[0].variable, "arr");
647        assert_eq!(vec_candidates[0].malloc_index, 0);
648    }
649
650    #[test]
651    fn test_assignment_with_wrong_function_not_detected() {
652        // Mutation testing found: changing function == "malloc" to != doesn't fail in Assignment
653        // Test that Assignment with non-malloc function is NOT detected
654        let size_expr = HirExpression::BinaryOp {
655            op: decy_hir::BinaryOperator::Multiply,
656            left: Box::new(HirExpression::IntLiteral(10)),
657            right: Box::new(HirExpression::IntLiteral(4)),
658        };
659
660        let func = HirFunction::new_with_body(
661            "test".to_string(),
662            HirType::Void,
663            vec![HirParameter::new("arr".to_string(), HirType::Pointer(Box::new(HirType::Int)))],
664            vec![HirStatement::Assignment {
665                target: "arr".to_string(),
666                value: HirExpression::FunctionCall {
667                    function: "calloc".to_string(), // Not malloc!
668                    arguments: vec![size_expr],
669                },
670            }],
671        );
672
673        let detector = PatternDetector::new();
674        let vec_candidates = detector.find_vec_candidates(&func);
675
676        assert_eq!(vec_candidates.len(), 0, "Should not detect calloc in Assignment");
677    }
678}
679
680#[cfg(test)]
681mod property_tests {
682    use super::*;
683    use decy_hir::{HirExpression, HirFunction, HirStatement, HirType};
684    use proptest::prelude::*;
685
686    proptest! {
687        /// Property: Detector never panics on any function
688        #[test]
689        fn property_detector_never_panics(
690            func_name in "[a-z_][a-z0-9_]{0,10}",
691            var_name in "[a-z_][a-z0-9_]{0,10}",
692            size in 1i32..1000
693        ) {
694            let func = HirFunction::new_with_body(
695                func_name,
696                HirType::Void,
697                vec![],
698                vec![HirStatement::VariableDeclaration {
699                    name: var_name,
700                    var_type: HirType::Pointer(Box::new(HirType::Int)),
701                    initializer: Some(HirExpression::FunctionCall {
702                        function: "malloc".to_string(),
703                        arguments: vec![HirExpression::IntLiteral(size)],
704                    }),
705                }],
706            );
707
708            let detector = PatternDetector::new();
709            let _candidates = detector.find_box_candidates(&func);
710            // If we get here without panic, test passes
711        }
712
713        /// Property: Every malloc detection has a valid malloc_index
714        #[test]
715        fn property_malloc_index_valid(
716            var_name in "[a-z_][a-z0-9_]{0,10}",
717            size in 1i32..1000
718        ) {
719            let body = vec![
720                HirStatement::VariableDeclaration {
721                    name: "x".to_string(),
722                    var_type: HirType::Int,
723                    initializer: Some(HirExpression::IntLiteral(0)),
724                },
725                HirStatement::VariableDeclaration {
726                    name: var_name.clone(),
727                    var_type: HirType::Pointer(Box::new(HirType::Int)),
728                    initializer: Some(HirExpression::FunctionCall {
729                        function: "malloc".to_string(),
730                        arguments: vec![HirExpression::IntLiteral(size)],
731                    }),
732                },
733            ];
734
735            let func = HirFunction::new_with_body(
736                "test".to_string(),
737                HirType::Void,
738                vec![],
739                body.clone(),
740            );
741
742            let detector = PatternDetector::new();
743            let candidates = detector.find_box_candidates(&func);
744
745            // Should find exactly one candidate
746            prop_assert_eq!(candidates.len(), 1);
747            // Index should be valid (within body length)
748            prop_assert!(candidates[0].malloc_index < body.len());
749            // Index should point to the malloc statement
750            prop_assert_eq!(candidates[0].malloc_index, 1);
751        }
752
753        /// Property: Detected variable names match actual variable names
754        #[test]
755        fn property_variable_name_preserved(
756            var_name in "[a-z_][a-z0-9_]{0,10}",
757            size in 1i32..1000
758        ) {
759            let func = HirFunction::new_with_body(
760                "test".to_string(),
761                HirType::Void,
762                vec![],
763                vec![HirStatement::VariableDeclaration {
764                    name: var_name.clone(),
765                    var_type: HirType::Pointer(Box::new(HirType::Int)),
766                    initializer: Some(HirExpression::FunctionCall {
767                        function: "malloc".to_string(),
768                        arguments: vec![HirExpression::IntLiteral(size)],
769                    }),
770                }],
771            );
772
773            let detector = PatternDetector::new();
774            let candidates = detector.find_box_candidates(&func);
775
776            prop_assert_eq!(candidates.len(), 1);
777            prop_assert_eq!(&candidates[0].variable, &var_name);
778        }
779
780        /// Property: Detection is deterministic
781        #[test]
782        fn property_detection_deterministic(
783            var_name in "[a-z_][a-z0-9_]{0,10}",
784            size in 1i32..1000
785        ) {
786            let func = HirFunction::new_with_body(
787                "test".to_string(),
788                HirType::Void,
789                vec![],
790                vec![HirStatement::VariableDeclaration {
791                    name: var_name,
792                    var_type: HirType::Pointer(Box::new(HirType::Int)),
793                    initializer: Some(HirExpression::FunctionCall {
794                        function: "malloc".to_string(),
795                        arguments: vec![HirExpression::IntLiteral(size)],
796                    }),
797                }],
798            );
799
800            let detector = PatternDetector::new();
801            let candidates1 = detector.find_box_candidates(&func);
802            let candidates2 = detector.find_box_candidates(&func);
803
804            prop_assert_eq!(candidates1, candidates2);
805        }
806
807        // Vec candidate property tests
808        /// Property: Vec detector never panics
809        #[test]
810        fn property_vec_detector_never_panics(
811            func_name in "[a-z_][a-z0-9_]{0,10}",
812            var_name in "[a-z_][a-z0-9_]{0,10}",
813            capacity in 1i32..1000,
814            elem_size in 1i32..16
815        ) {
816            let size_expr = HirExpression::BinaryOp {
817                op: decy_hir::BinaryOperator::Multiply,
818                left: Box::new(HirExpression::IntLiteral(capacity)),
819                right: Box::new(HirExpression::IntLiteral(elem_size)),
820            };
821
822            let func = HirFunction::new_with_body(
823                func_name,
824                HirType::Void,
825                vec![],
826                vec![HirStatement::VariableDeclaration {
827                    name: var_name,
828                    var_type: HirType::Pointer(Box::new(HirType::Int)),
829                    initializer: Some(HirExpression::FunctionCall {
830                        function: "malloc".to_string(),
831                        arguments: vec![size_expr],
832                    }),
833                }],
834            );
835
836            let detector = PatternDetector::new();
837            let _candidates = detector.find_vec_candidates(&func);
838            // If we get here without panic, test passes
839        }
840
841        /// Property: Vec detection is deterministic
842        #[test]
843        fn property_vec_detection_deterministic(
844            var_name in "[a-z_][a-z0-9_]{0,10}",
845            capacity in 1i32..100,
846            elem_size in 1i32..16
847        ) {
848            let size_expr = HirExpression::BinaryOp {
849                op: decy_hir::BinaryOperator::Multiply,
850                left: Box::new(HirExpression::IntLiteral(capacity)),
851                right: Box::new(HirExpression::IntLiteral(elem_size)),
852            };
853
854            let func = HirFunction::new_with_body(
855                "test".to_string(),
856                HirType::Void,
857                vec![],
858                vec![HirStatement::VariableDeclaration {
859                    name: var_name,
860                    var_type: HirType::Pointer(Box::new(HirType::Int)),
861                    initializer: Some(HirExpression::FunctionCall {
862                        function: "malloc".to_string(),
863                        arguments: vec![size_expr],
864                    }),
865                }],
866            );
867
868            let detector = PatternDetector::new();
869            let candidates1 = detector.find_vec_candidates(&func);
870            let candidates2 = detector.find_vec_candidates(&func);
871
872            prop_assert_eq!(candidates1, candidates2);
873        }
874
875        /// Property: Detected variable names match actual variable names
876        #[test]
877        fn property_vec_variable_name_preserved(
878            var_name in "[a-z_][a-z0-9_]{0,10}",
879            capacity in 1i32..100,
880            elem_size in 1i32..16
881        ) {
882            let size_expr = HirExpression::BinaryOp {
883                op: decy_hir::BinaryOperator::Multiply,
884                left: Box::new(HirExpression::IntLiteral(capacity)),
885                right: Box::new(HirExpression::IntLiteral(elem_size)),
886            };
887
888            let func = HirFunction::new_with_body(
889                "test".to_string(),
890                HirType::Void,
891                vec![],
892                vec![HirStatement::VariableDeclaration {
893                    name: var_name.clone(),
894                    var_type: HirType::Pointer(Box::new(HirType::Int)),
895                    initializer: Some(HirExpression::FunctionCall {
896                        function: "malloc".to_string(),
897                        arguments: vec![size_expr],
898                    }),
899                }],
900            );
901
902            let detector = PatternDetector::new();
903            let candidates = detector.find_vec_candidates(&func);
904
905            if !candidates.is_empty() {
906                prop_assert_eq!(&candidates[0].variable, &var_name);
907            }
908        }
909
910        /// Property: Vec malloc_index is always valid
911        #[test]
912        fn property_vec_malloc_index_valid(
913            var_name in "[a-z_][a-z0-9_]{0,10}",
914            capacity in 1i32..100,
915            elem_size in 1i32..16
916        ) {
917            let size_expr = HirExpression::BinaryOp {
918                op: decy_hir::BinaryOperator::Multiply,
919                left: Box::new(HirExpression::IntLiteral(capacity)),
920                right: Box::new(HirExpression::IntLiteral(elem_size)),
921            };
922
923            let body = vec![
924                HirStatement::VariableDeclaration {
925                    name: "x".to_string(),
926                    var_type: HirType::Int,
927                    initializer: Some(HirExpression::IntLiteral(0)),
928                },
929                HirStatement::VariableDeclaration {
930                    name: var_name,
931                    var_type: HirType::Pointer(Box::new(HirType::Int)),
932                    initializer: Some(HirExpression::FunctionCall {
933                        function: "malloc".to_string(),
934                        arguments: vec![size_expr],
935                    }),
936                },
937            ];
938
939            let func = HirFunction::new_with_body(
940                "test".to_string(),
941                HirType::Void,
942                vec![],
943                body.clone(),
944            );
945
946            let detector = PatternDetector::new();
947            let candidates = detector.find_vec_candidates(&func);
948
949            for candidate in candidates {
950                prop_assert!(candidate.malloc_index < body.len());
951            }
952        }
953    }
954}