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