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 {
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_pattern_detector_default() {
253        let detector = PatternDetector::default();
254        let func = HirFunction::new_with_body(
255            "test".to_string(),
256            HirType::Void,
257            vec![],
258            vec![],
259        );
260        assert!(detector.find_box_candidates(&func).is_empty());
261        assert!(detector.find_vec_candidates(&func).is_empty());
262    }
263
264    #[test]
265    fn test_vardecl_without_initializer() {
266        let func = HirFunction::new_with_body(
267            "test".to_string(),
268            HirType::Void,
269            vec![],
270            vec![HirStatement::VariableDeclaration {
271                name: "ptr".to_string(),
272                var_type: HirType::Pointer(Box::new(HirType::Int)),
273                initializer: None,
274            }],
275        );
276        let detector = PatternDetector::new();
277        assert!(detector.find_box_candidates(&func).is_empty());
278        assert!(detector.find_vec_candidates(&func).is_empty());
279    }
280
281    #[test]
282    fn test_vec_malloc_with_empty_arguments() {
283        // malloc() with no arguments — should not be detected
284        let func = HirFunction::new_with_body(
285            "test".to_string(),
286            HirType::Void,
287            vec![],
288            vec![HirStatement::VariableDeclaration {
289                name: "ptr".to_string(),
290                var_type: HirType::Pointer(Box::new(HirType::Int)),
291                initializer: Some(HirExpression::FunctionCall {
292                    function: "malloc".to_string(),
293                    arguments: vec![],
294                }),
295            }],
296        );
297        let detector = PatternDetector::new();
298        // box candidates: malloc with no args is still a malloc call
299        assert_eq!(detector.find_box_candidates(&func).len(), 1);
300        // vec candidates: needs args[0] to check for multiply pattern
301        assert!(detector.find_vec_candidates(&func).is_empty());
302    }
303
304    #[test]
305    fn test_detect_malloc_in_variable_declaration() {
306        let func = HirFunction::new_with_body(
307            "test".to_string(),
308            HirType::Void,
309            vec![],
310            vec![HirStatement::VariableDeclaration {
311                name: "ptr".to_string(),
312                var_type: HirType::Pointer(Box::new(HirType::Int)),
313                initializer: Some(HirExpression::FunctionCall {
314                    function: "malloc".to_string(),
315                    arguments: vec![HirExpression::IntLiteral(100)],
316                }),
317            }],
318        );
319
320        let detector = PatternDetector::new();
321        let candidates = detector.find_box_candidates(&func);
322
323        assert_eq!(candidates.len(), 1);
324        assert_eq!(candidates[0].variable, "ptr");
325        assert_eq!(candidates[0].malloc_index, 0);
326    }
327
328    #[test]
329    fn test_detect_malloc_in_assignment() {
330        let func = HirFunction::new_with_body(
331            "test".to_string(),
332            HirType::Void,
333            vec![HirParameter::new(
334                "ptr".to_string(),
335                HirType::Pointer(Box::new(HirType::Int)),
336            )],
337            vec![HirStatement::Assignment {
338                target: "ptr".to_string(),
339                value: HirExpression::FunctionCall {
340                    function: "malloc".to_string(),
341                    arguments: vec![HirExpression::IntLiteral(50)],
342                },
343            }],
344        );
345
346        let detector = PatternDetector::new();
347        let candidates = detector.find_box_candidates(&func);
348
349        assert_eq!(candidates.len(), 1);
350        assert_eq!(candidates[0].variable, "ptr");
351        assert_eq!(candidates[0].malloc_index, 0);
352    }
353
354    #[test]
355    fn test_no_malloc_detected() {
356        let func = HirFunction::new_with_body(
357            "test".to_string(),
358            HirType::Int,
359            vec![],
360            vec![
361                HirStatement::VariableDeclaration {
362                    name: "x".to_string(),
363                    var_type: HirType::Int,
364                    initializer: Some(HirExpression::IntLiteral(42)),
365                },
366                HirStatement::Return(Some(HirExpression::Variable("x".to_string()))),
367            ],
368        );
369
370        let detector = PatternDetector::new();
371        let candidates = detector.find_box_candidates(&func);
372
373        assert_eq!(candidates.len(), 0);
374    }
375
376    #[test]
377    fn test_multiple_malloc_calls() {
378        let func = HirFunction::new_with_body(
379            "test".to_string(),
380            HirType::Void,
381            vec![],
382            vec![
383                HirStatement::VariableDeclaration {
384                    name: "ptr1".to_string(),
385                    var_type: HirType::Pointer(Box::new(HirType::Int)),
386                    initializer: Some(HirExpression::FunctionCall {
387                        function: "malloc".to_string(),
388                        arguments: vec![HirExpression::IntLiteral(100)],
389                    }),
390                },
391                HirStatement::VariableDeclaration {
392                    name: "ptr2".to_string(),
393                    var_type: HirType::Pointer(Box::new(HirType::Char)),
394                    initializer: Some(HirExpression::FunctionCall {
395                        function: "malloc".to_string(),
396                        arguments: vec![HirExpression::IntLiteral(200)],
397                    }),
398                },
399            ],
400        );
401
402        let detector = PatternDetector::new();
403        let candidates = detector.find_box_candidates(&func);
404
405        assert_eq!(candidates.len(), 2);
406        assert_eq!(candidates[0].variable, "ptr1");
407        assert_eq!(candidates[1].variable, "ptr2");
408    }
409
410    #[test]
411    fn test_malloc_from_other_function() {
412        // Should NOT detect allocate() as malloc
413        let func = HirFunction::new_with_body(
414            "test".to_string(),
415            HirType::Void,
416            vec![],
417            vec![HirStatement::VariableDeclaration {
418                name: "ptr".to_string(),
419                var_type: HirType::Pointer(Box::new(HirType::Int)),
420                initializer: Some(HirExpression::FunctionCall {
421                    function: "allocate".to_string(),
422                    arguments: vec![HirExpression::IntLiteral(100)],
423                }),
424            }],
425        );
426
427        let detector = PatternDetector::new();
428        let candidates = detector.find_box_candidates(&func);
429
430        assert_eq!(candidates.len(), 0);
431    }
432
433    // Vec candidate detection tests
434    #[test]
435    fn test_detect_vec_array_allocation_in_variable_declaration() {
436        // Pattern: int* arr = malloc(n * sizeof(int));
437        // Should be detected as Vec<i32> candidate
438        let n_expr = HirExpression::Variable("n".to_string());
439        let sizeof_expr = HirExpression::IntLiteral(4); // sizeof(int) = 4
440        let size_expr = HirExpression::BinaryOp {
441            op: decy_hir::BinaryOperator::Multiply,
442            left: Box::new(n_expr.clone()),
443            right: Box::new(sizeof_expr),
444        };
445
446        let func = HirFunction::new_with_body(
447            "test".to_string(),
448            HirType::Void,
449            vec![],
450            vec![HirStatement::VariableDeclaration {
451                name: "arr".to_string(),
452                var_type: HirType::Pointer(Box::new(HirType::Int)),
453                initializer: Some(HirExpression::FunctionCall {
454                    function: "malloc".to_string(),
455                    arguments: vec![size_expr],
456                }),
457            }],
458        );
459
460        let detector = PatternDetector::new();
461        let candidates = detector.find_vec_candidates(&func);
462
463        assert_eq!(candidates.len(), 1, "Should detect one Vec candidate");
464        assert_eq!(candidates[0].variable, "arr");
465        assert_eq!(candidates[0].malloc_index, 0);
466    }
467
468    #[test]
469    fn test_detect_vec_with_literal_capacity() {
470        // Pattern: int* arr = malloc(10 * sizeof(int));
471        let capacity = HirExpression::IntLiteral(10);
472        let sizeof_expr = HirExpression::IntLiteral(4);
473        let size_expr = HirExpression::BinaryOp {
474            op: decy_hir::BinaryOperator::Multiply,
475            left: Box::new(capacity.clone()),
476            right: Box::new(sizeof_expr),
477        };
478
479        let func = HirFunction::new_with_body(
480            "test".to_string(),
481            HirType::Void,
482            vec![],
483            vec![HirStatement::VariableDeclaration {
484                name: "arr".to_string(),
485                var_type: HirType::Pointer(Box::new(HirType::Int)),
486                initializer: Some(HirExpression::FunctionCall {
487                    function: "malloc".to_string(),
488                    arguments: vec![size_expr],
489                }),
490            }],
491        );
492
493        let detector = PatternDetector::new();
494        let candidates = detector.find_vec_candidates(&func);
495
496        assert_eq!(candidates.len(), 1);
497        assert_eq!(candidates[0].variable, "arr");
498        assert!(
499            candidates[0].capacity_expr.is_some(),
500            "Should extract capacity expression"
501        );
502    }
503
504    #[test]
505    fn test_vec_vs_box_distinction() {
506        // Box pattern: malloc(sizeof(T)) - single element
507        // Vec pattern: malloc(n * sizeof(T)) - array
508        let func = HirFunction::new_with_body(
509            "test".to_string(),
510            HirType::Void,
511            vec![],
512            vec![
513                // Box candidate: single element
514                HirStatement::VariableDeclaration {
515                    name: "single".to_string(),
516                    var_type: HirType::Pointer(Box::new(HirType::Int)),
517                    initializer: Some(HirExpression::FunctionCall {
518                        function: "malloc".to_string(),
519                        arguments: vec![HirExpression::IntLiteral(4)], // just sizeof(int)
520                    }),
521                },
522                // Vec candidate: array
523                HirStatement::VariableDeclaration {
524                    name: "array".to_string(),
525                    var_type: HirType::Pointer(Box::new(HirType::Int)),
526                    initializer: Some(HirExpression::FunctionCall {
527                        function: "malloc".to_string(),
528                        arguments: vec![HirExpression::BinaryOp {
529                            op: decy_hir::BinaryOperator::Multiply,
530                            left: Box::new(HirExpression::IntLiteral(10)),
531                            right: Box::new(HirExpression::IntLiteral(4)),
532                        }],
533                    }),
534                },
535            ],
536        );
537
538        let detector = PatternDetector::new();
539        let box_candidates = detector.find_box_candidates(&func);
540        let vec_candidates = detector.find_vec_candidates(&func);
541
542        // Box detector should find both (it's less specific)
543        // Vec detector should only find the array pattern
544        assert_eq!(vec_candidates.len(), 1, "Should find only array pattern");
545        assert_eq!(vec_candidates[0].variable, "array");
546
547        // The "single" allocation should be detected as Box only
548        // (Box detector will find it, Vec detector won't)
549        assert!(box_candidates.iter().any(|c| c.variable == "single"));
550    }
551
552    #[test]
553    fn test_no_vec_detected_for_non_array_malloc() {
554        // malloc without multiplication pattern should not be Vec
555        let func = HirFunction::new_with_body(
556            "test".to_string(),
557            HirType::Void,
558            vec![],
559            vec![HirStatement::VariableDeclaration {
560                name: "ptr".to_string(),
561                var_type: HirType::Pointer(Box::new(HirType::Int)),
562                initializer: Some(HirExpression::FunctionCall {
563                    function: "malloc".to_string(),
564                    arguments: vec![HirExpression::IntLiteral(100)],
565                }),
566            }],
567        );
568
569        let detector = PatternDetector::new();
570        let candidates = detector.find_vec_candidates(&func);
571
572        assert_eq!(candidates.len(), 0, "Should not detect non-array as Vec");
573    }
574
575    #[test]
576    fn test_multiple_vec_allocations() {
577        let size1 = HirExpression::BinaryOp {
578            op: decy_hir::BinaryOperator::Multiply,
579            left: Box::new(HirExpression::IntLiteral(10)),
580            right: Box::new(HirExpression::IntLiteral(4)),
581        };
582
583        let size2 = HirExpression::BinaryOp {
584            op: decy_hir::BinaryOperator::Multiply,
585            left: Box::new(HirExpression::Variable("count".to_string())),
586            right: Box::new(HirExpression::IntLiteral(8)),
587        };
588
589        let func = HirFunction::new_with_body(
590            "test".to_string(),
591            HirType::Void,
592            vec![],
593            vec![
594                HirStatement::VariableDeclaration {
595                    name: "arr1".to_string(),
596                    var_type: HirType::Pointer(Box::new(HirType::Int)),
597                    initializer: Some(HirExpression::FunctionCall {
598                        function: "malloc".to_string(),
599                        arguments: vec![size1],
600                    }),
601                },
602                HirStatement::VariableDeclaration {
603                    name: "arr2".to_string(),
604                    var_type: HirType::Pointer(Box::new(HirType::Double)),
605                    initializer: Some(HirExpression::FunctionCall {
606                        function: "malloc".to_string(),
607                        arguments: vec![size2],
608                    }),
609                },
610            ],
611        );
612
613        let detector = PatternDetector::new();
614        let candidates = detector.find_vec_candidates(&func);
615
616        assert_eq!(candidates.len(), 2, "Should detect both Vec candidates");
617        assert_eq!(candidates[0].variable, "arr1");
618        assert_eq!(candidates[1].variable, "arr2");
619    }
620
621    #[test]
622    fn test_wrong_function_name_not_detected() {
623        // Mutation testing found: changing == to != doesn't fail tests
624        // Test that non-malloc functions are NOT detected
625        let func = HirFunction::new_with_body(
626            "test".to_string(),
627            HirType::Void,
628            vec![],
629            vec![
630                HirStatement::VariableDeclaration {
631                    name: "ptr1".to_string(),
632                    var_type: HirType::Pointer(Box::new(HirType::Int)),
633                    initializer: Some(HirExpression::FunctionCall {
634                        function: "calloc".to_string(), // Not malloc!
635                        arguments: vec![HirExpression::IntLiteral(100)],
636                    }),
637                },
638                HirStatement::VariableDeclaration {
639                    name: "ptr2".to_string(),
640                    var_type: HirType::Pointer(Box::new(HirType::Int)),
641                    initializer: Some(HirExpression::FunctionCall {
642                        function: "realloc".to_string(), // Not malloc!
643                        arguments: vec![HirExpression::IntLiteral(100)],
644                    }),
645                },
646            ],
647        );
648
649        let detector = PatternDetector::new();
650        let box_candidates = detector.find_box_candidates(&func);
651        let vec_candidates = detector.find_vec_candidates(&func);
652
653        assert_eq!(
654            box_candidates.len(),
655            0,
656            "Should not detect calloc/realloc as malloc"
657        );
658        assert_eq!(
659            vec_candidates.len(),
660            0,
661            "Should not detect calloc/realloc as malloc"
662        );
663    }
664
665    #[test]
666    fn test_vec_assignment_with_array_malloc() {
667        // Mutation testing found: deleting Assignment branch in is_malloc_assignment_expr doesn't fail
668        // Test that Assignment statement with array malloc IS detected as Vec
669        let size_expr = HirExpression::BinaryOp {
670            op: decy_hir::BinaryOperator::Multiply,
671            left: Box::new(HirExpression::IntLiteral(10)),
672            right: Box::new(HirExpression::IntLiteral(4)),
673        };
674
675        let func = HirFunction::new_with_body(
676            "test".to_string(),
677            HirType::Void,
678            vec![HirParameter::new(
679                "arr".to_string(),
680                HirType::Pointer(Box::new(HirType::Int)),
681            )],
682            vec![HirStatement::Assignment {
683                target: "arr".to_string(),
684                value: HirExpression::FunctionCall {
685                    function: "malloc".to_string(),
686                    arguments: vec![size_expr],
687                },
688            }],
689        );
690
691        let detector = PatternDetector::new();
692        let vec_candidates = detector.find_vec_candidates(&func);
693
694        assert_eq!(
695            vec_candidates.len(),
696            1,
697            "Should detect array malloc in Assignment as Vec"
698        );
699        assert_eq!(vec_candidates[0].variable, "arr");
700        assert_eq!(vec_candidates[0].malloc_index, 0);
701    }
702
703    #[test]
704    fn test_assignment_with_wrong_function_not_detected() {
705        // Mutation testing found: changing function == "malloc" to != doesn't fail in Assignment
706        // Test that Assignment with non-malloc function is NOT detected
707        let size_expr = HirExpression::BinaryOp {
708            op: decy_hir::BinaryOperator::Multiply,
709            left: Box::new(HirExpression::IntLiteral(10)),
710            right: Box::new(HirExpression::IntLiteral(4)),
711        };
712
713        let func = HirFunction::new_with_body(
714            "test".to_string(),
715            HirType::Void,
716            vec![HirParameter::new(
717                "arr".to_string(),
718                HirType::Pointer(Box::new(HirType::Int)),
719            )],
720            vec![HirStatement::Assignment {
721                target: "arr".to_string(),
722                value: HirExpression::FunctionCall {
723                    function: "calloc".to_string(), // Not malloc!
724                    arguments: vec![size_expr],
725                },
726            }],
727        );
728
729        let detector = PatternDetector::new();
730        let vec_candidates = detector.find_vec_candidates(&func);
731
732        assert_eq!(
733            vec_candidates.len(),
734            0,
735            "Should not detect calloc in Assignment"
736        );
737    }
738}
739
740#[cfg(test)]
741mod property_tests {
742    use super::*;
743    use decy_hir::{HirExpression, HirFunction, HirStatement, HirType};
744    use proptest::prelude::*;
745
746    proptest! {
747        /// Property: Detector never panics on any function
748        #[test]
749        fn property_detector_never_panics(
750            func_name in "[a-z_][a-z0-9_]{0,10}",
751            var_name in "[a-z_][a-z0-9_]{0,10}",
752            size in 1i32..1000
753        ) {
754            let func = HirFunction::new_with_body(
755                func_name,
756                HirType::Void,
757                vec![],
758                vec![HirStatement::VariableDeclaration {
759                    name: var_name,
760                    var_type: HirType::Pointer(Box::new(HirType::Int)),
761                    initializer: Some(HirExpression::FunctionCall {
762                        function: "malloc".to_string(),
763                        arguments: vec![HirExpression::IntLiteral(size)],
764                    }),
765                }],
766            );
767
768            let detector = PatternDetector::new();
769            let _candidates = detector.find_box_candidates(&func);
770            // If we get here without panic, test passes
771        }
772
773        /// Property: Every malloc detection has a valid malloc_index
774        #[test]
775        fn property_malloc_index_valid(
776            var_name in "[a-z_][a-z0-9_]{0,10}",
777            size in 1i32..1000
778        ) {
779            let body = vec![
780                HirStatement::VariableDeclaration {
781                    name: "x".to_string(),
782                    var_type: HirType::Int,
783                    initializer: Some(HirExpression::IntLiteral(0)),
784                },
785                HirStatement::VariableDeclaration {
786                    name: var_name.clone(),
787                    var_type: HirType::Pointer(Box::new(HirType::Int)),
788                    initializer: Some(HirExpression::FunctionCall {
789                        function: "malloc".to_string(),
790                        arguments: vec![HirExpression::IntLiteral(size)],
791                    }),
792                },
793            ];
794
795            let func = HirFunction::new_with_body(
796                "test".to_string(),
797                HirType::Void,
798                vec![],
799                body.clone(),
800            );
801
802            let detector = PatternDetector::new();
803            let candidates = detector.find_box_candidates(&func);
804
805            // Should find exactly one candidate
806            prop_assert_eq!(candidates.len(), 1);
807            // Index should be valid (within body length)
808            prop_assert!(candidates[0].malloc_index < body.len());
809            // Index should point to the malloc statement
810            prop_assert_eq!(candidates[0].malloc_index, 1);
811        }
812
813        /// Property: Detected variable names match actual variable names
814        #[test]
815        fn property_variable_name_preserved(
816            var_name in "[a-z_][a-z0-9_]{0,10}",
817            size in 1i32..1000
818        ) {
819            let func = HirFunction::new_with_body(
820                "test".to_string(),
821                HirType::Void,
822                vec![],
823                vec![HirStatement::VariableDeclaration {
824                    name: var_name.clone(),
825                    var_type: HirType::Pointer(Box::new(HirType::Int)),
826                    initializer: Some(HirExpression::FunctionCall {
827                        function: "malloc".to_string(),
828                        arguments: vec![HirExpression::IntLiteral(size)],
829                    }),
830                }],
831            );
832
833            let detector = PatternDetector::new();
834            let candidates = detector.find_box_candidates(&func);
835
836            prop_assert_eq!(candidates.len(), 1);
837            prop_assert_eq!(&candidates[0].variable, &var_name);
838        }
839
840        /// Property: Detection is deterministic
841        #[test]
842        fn property_detection_deterministic(
843            var_name in "[a-z_][a-z0-9_]{0,10}",
844            size in 1i32..1000
845        ) {
846            let func = HirFunction::new_with_body(
847                "test".to_string(),
848                HirType::Void,
849                vec![],
850                vec![HirStatement::VariableDeclaration {
851                    name: var_name,
852                    var_type: HirType::Pointer(Box::new(HirType::Int)),
853                    initializer: Some(HirExpression::FunctionCall {
854                        function: "malloc".to_string(),
855                        arguments: vec![HirExpression::IntLiteral(size)],
856                    }),
857                }],
858            );
859
860            let detector = PatternDetector::new();
861            let candidates1 = detector.find_box_candidates(&func);
862            let candidates2 = detector.find_box_candidates(&func);
863
864            prop_assert_eq!(candidates1, candidates2);
865        }
866
867        // Vec candidate property tests
868        /// Property: Vec detector never panics
869        #[test]
870        fn property_vec_detector_never_panics(
871            func_name in "[a-z_][a-z0-9_]{0,10}",
872            var_name in "[a-z_][a-z0-9_]{0,10}",
873            capacity in 1i32..1000,
874            elem_size in 1i32..16
875        ) {
876            let size_expr = HirExpression::BinaryOp {
877                op: decy_hir::BinaryOperator::Multiply,
878                left: Box::new(HirExpression::IntLiteral(capacity)),
879                right: Box::new(HirExpression::IntLiteral(elem_size)),
880            };
881
882            let func = HirFunction::new_with_body(
883                func_name,
884                HirType::Void,
885                vec![],
886                vec![HirStatement::VariableDeclaration {
887                    name: var_name,
888                    var_type: HirType::Pointer(Box::new(HirType::Int)),
889                    initializer: Some(HirExpression::FunctionCall {
890                        function: "malloc".to_string(),
891                        arguments: vec![size_expr],
892                    }),
893                }],
894            );
895
896            let detector = PatternDetector::new();
897            let _candidates = detector.find_vec_candidates(&func);
898            // If we get here without panic, test passes
899        }
900
901        /// Property: Vec detection is deterministic
902        #[test]
903        fn property_vec_detection_deterministic(
904            var_name in "[a-z_][a-z0-9_]{0,10}",
905            capacity in 1i32..100,
906            elem_size in 1i32..16
907        ) {
908            let size_expr = HirExpression::BinaryOp {
909                op: decy_hir::BinaryOperator::Multiply,
910                left: Box::new(HirExpression::IntLiteral(capacity)),
911                right: Box::new(HirExpression::IntLiteral(elem_size)),
912            };
913
914            let func = HirFunction::new_with_body(
915                "test".to_string(),
916                HirType::Void,
917                vec![],
918                vec![HirStatement::VariableDeclaration {
919                    name: var_name,
920                    var_type: HirType::Pointer(Box::new(HirType::Int)),
921                    initializer: Some(HirExpression::FunctionCall {
922                        function: "malloc".to_string(),
923                        arguments: vec![size_expr],
924                    }),
925                }],
926            );
927
928            let detector = PatternDetector::new();
929            let candidates1 = detector.find_vec_candidates(&func);
930            let candidates2 = detector.find_vec_candidates(&func);
931
932            prop_assert_eq!(candidates1, candidates2);
933        }
934
935        /// Property: Detected variable names match actual variable names
936        #[test]
937        fn property_vec_variable_name_preserved(
938            var_name in "[a-z_][a-z0-9_]{0,10}",
939            capacity in 1i32..100,
940            elem_size in 1i32..16
941        ) {
942            let size_expr = HirExpression::BinaryOp {
943                op: decy_hir::BinaryOperator::Multiply,
944                left: Box::new(HirExpression::IntLiteral(capacity)),
945                right: Box::new(HirExpression::IntLiteral(elem_size)),
946            };
947
948            let func = HirFunction::new_with_body(
949                "test".to_string(),
950                HirType::Void,
951                vec![],
952                vec![HirStatement::VariableDeclaration {
953                    name: var_name.clone(),
954                    var_type: HirType::Pointer(Box::new(HirType::Int)),
955                    initializer: Some(HirExpression::FunctionCall {
956                        function: "malloc".to_string(),
957                        arguments: vec![size_expr],
958                    }),
959                }],
960            );
961
962            let detector = PatternDetector::new();
963            let candidates = detector.find_vec_candidates(&func);
964
965            if !candidates.is_empty() {
966                prop_assert_eq!(&candidates[0].variable, &var_name);
967            }
968        }
969
970        /// Property: Vec malloc_index is always valid
971        #[test]
972        fn property_vec_malloc_index_valid(
973            var_name in "[a-z_][a-z0-9_]{0,10}",
974            capacity in 1i32..100,
975            elem_size in 1i32..16
976        ) {
977            let size_expr = HirExpression::BinaryOp {
978                op: decy_hir::BinaryOperator::Multiply,
979                left: Box::new(HirExpression::IntLiteral(capacity)),
980                right: Box::new(HirExpression::IntLiteral(elem_size)),
981            };
982
983            let body = vec![
984                HirStatement::VariableDeclaration {
985                    name: "x".to_string(),
986                    var_type: HirType::Int,
987                    initializer: Some(HirExpression::IntLiteral(0)),
988                },
989                HirStatement::VariableDeclaration {
990                    name: var_name,
991                    var_type: HirType::Pointer(Box::new(HirType::Int)),
992                    initializer: Some(HirExpression::FunctionCall {
993                        function: "malloc".to_string(),
994                        arguments: vec![size_expr],
995                    }),
996                },
997            ];
998
999            let func = HirFunction::new_with_body(
1000                "test".to_string(),
1001                HirType::Void,
1002                vec![],
1003                body.clone(),
1004            );
1005
1006            let detector = PatternDetector::new();
1007            let candidates = detector.find_vec_candidates(&func);
1008
1009            for candidate in candidates {
1010                prop_assert!(candidate.malloc_index < body.len());
1011            }
1012        }
1013    }
1014}