Skip to main content

xlog_logic/
function.rs

1//! Function registry and validation for user-defined functions.
2
3use crate::ast::{ArithExpr, BodyLiteral, CompOp, CondExpr, FuncBody, FuncDef, Program};
4use std::collections::{HashMap, HashSet};
5
6/// Errors related to functions
7#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub enum FunctionError {
10    /// Duplicate function definition
11    DuplicateDefinition {
12        /// Function name that was defined more than once.
13        name: String,
14    },
15    /// Recursive function without base case
16    RecursionWithoutBaseCase {
17        /// Recursive function missing a terminating branch.
18        name: String,
19    },
20    /// Undefined function called
21    UndefinedFunction {
22        /// Function name that could not be resolved.
23        name: String,
24    },
25    /// Maximum recursion depth exceeded
26    MaxRecursionDepth {
27        /// Function name whose expansion exceeded the recursion limit.
28        name: String,
29        /// Maximum recursion depth that was reached.
30        depth: u32,
31    },
32    /// Function name conflicts with predicate
33    NameConflict {
34        /// Name reused by both a function and a predicate.
35        name: String,
36    },
37    /// Function call supplied the wrong number of arguments.
38    ArityMismatch {
39        /// Function being called.
40        name: String,
41        /// Declared parameter count.
42        expected: usize,
43        /// Supplied argument count.
44        received: usize,
45    },
46    /// A predicate body was expanded without an ordinary rule or constraint body.
47    PredicateBodyRequiresRuleContext {
48        /// Predicate-bodied function being called.
49        name: String,
50    },
51    /// A non-term arithmetic argument cannot occupy a predicate-body term position.
52    UnsupportedPredicateTermArgument {
53        /// Predicate-bodied function being called.
54        name: String,
55        /// Parameter used in a term position.
56        parameter: String,
57    },
58    /// A predicate-bodied call appeared in a conditional result branch.
59    PredicateCallInConditionalBranch {
60        /// Predicate-bodied function being called.
61        name: String,
62    },
63    /// A non-variable argument was substituted into an `is` target.
64    InvalidPredicateBindingTarget {
65        /// Predicate-bodied function being called.
66        name: String,
67        /// Parameter used as the binding target.
68        parameter: String,
69    },
70}
71
72impl std::fmt::Display for FunctionError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            FunctionError::DuplicateDefinition { name } => {
76                write!(f, "error[E0501]: duplicate function definition `{}`", name)
77            }
78            FunctionError::RecursionWithoutBaseCase { name } => {
79                writeln!(
80                    f,
81                    "error[E0502]: recursive function `{}` without base case",
82                    name
83                )?;
84                write!(
85                    f,
86                    "  = help: use conditional form: `if <condition> then <base> else <recursive>`"
87                )
88            }
89            FunctionError::UndefinedFunction { name } => {
90                write!(f, "error[E0503]: undefined function `{}`", name)
91            }
92            FunctionError::MaxRecursionDepth { name, depth } => {
93                write!(
94                    f,
95                    "error[E0504]: maximum recursion depth ({}) exceeded in function `{}`",
96                    depth, name
97                )
98            }
99            FunctionError::NameConflict { name } => {
100                write!(
101                    f,
102                    "error[E0505]: `{}` is already defined as a predicate",
103                    name
104                )
105            }
106            FunctionError::ArityMismatch {
107                name,
108                expected,
109                received,
110            } => {
111                let argument = if *expected == 1 { "argument" } else { "arguments" };
112                write!(
113                    f,
114                    "error[E0508]: function `{name}` expects {expected} {argument} but received {received}"
115                )
116            }
117            FunctionError::PredicateBodyRequiresRuleContext { name } => write!(
118                f,
119                "error[E0509]: predicate-bodied function `{name}` requires a surrounding rule or constraint body"
120            ),
121            FunctionError::UnsupportedPredicateTermArgument { name, parameter } => write!(
122                f,
123                "error[E0510]: predicate-bodied function `{name}` cannot use an arithmetic expression argument for parameter `{parameter}` in a term position"
124            ),
125            FunctionError::PredicateCallInConditionalBranch { name } => write!(
126                f,
127                "error[E0511]: predicate-bodied function `{name}` cannot be expanded inside a conditional branch"
128            ),
129            FunctionError::InvalidPredicateBindingTarget { name, parameter } => write!(
130                f,
131                "error[E0512]: predicate-bodied function `{name}` cannot substitute a non-variable argument for binding target `{parameter}`"
132            ),
133        }
134    }
135}
136
137impl std::error::Error for FunctionError {}
138
139impl From<FunctionError> for xlog_core::XlogError {
140    fn from(e: FunctionError) -> Self {
141        xlog_core::XlogError::Compilation(e.to_string())
142    }
143}
144
145/// Warning for potentially infinite recursion
146#[derive(Debug, Clone)]
147pub struct RecursionWarning {
148    /// Name of the function with potential infinite recursion.
149    pub func_name: String,
150    /// Descriptive warning message.
151    pub message: String,
152}
153
154impl std::fmt::Display for RecursionWarning {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        writeln!(
157            f,
158            "warning[W0502]: potentially infinite recursion in `{}`",
159            self.func_name
160        )?;
161        writeln!(f, "  {}", self.message)?;
162        write!(
163            f,
164            "  = note: base case may be unreachable with given recursive call"
165        )
166    }
167}
168
169/// Registry of user-defined functions
170#[derive(Debug, Default)]
171pub struct FunctionRegistry {
172    functions: HashMap<String, FuncDef>,
173    call_graph: HashMap<String, Vec<String>>,
174    registration_order: Vec<String>,
175}
176
177impl FunctionRegistry {
178    /// Create an empty function registry.
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Register a function
184    pub fn register(&mut self, func: FuncDef) -> Result<(), FunctionError> {
185        if self.functions.contains_key(&func.name) {
186            return Err(FunctionError::DuplicateDefinition {
187                name: func.name.clone(),
188            });
189        }
190
191        // Build call graph
192        let name = func.name.clone();
193        let calls = Self::extract_calls(&func.body);
194        self.call_graph.insert(name.clone(), calls);
195        self.functions.insert(name.clone(), func);
196        self.registration_order.push(name);
197
198        Ok(())
199    }
200
201    /// Get a function by name
202    pub fn get(&self, name: &str) -> Option<&FuncDef> {
203        self.functions.get(name)
204    }
205
206    /// Check if a function exists
207    pub fn contains(&self, name: &str) -> bool {
208        self.functions.contains_key(name)
209    }
210
211    /// Extract function calls from a body
212    fn extract_calls(body: &FuncBody) -> Vec<String> {
213        let mut calls = Vec::new();
214        Self::extract_calls_from_body(body, &mut calls);
215        calls
216    }
217
218    fn extract_calls_from_body(body: &FuncBody, calls: &mut Vec<String>) {
219        match body {
220            FuncBody::Arithmetic(expr) => Self::extract_calls_from_expr(expr, calls),
221            FuncBody::Conditional(cond) => {
222                Self::extract_calls_from_expr(&cond.cond_left, calls);
223                Self::extract_calls_from_expr(&cond.cond_right, calls);
224                Self::extract_calls_from_body(&cond.then_branch, calls);
225                Self::extract_calls_from_body(&cond.else_branch, calls);
226            }
227            FuncBody::Predicate { body, .. } => {
228                for literal in body {
229                    if let BodyLiteral::IsExpr(binding) = literal {
230                        Self::extract_calls_from_expr(&binding.expr, calls);
231                    }
232                }
233            }
234        }
235    }
236
237    fn extract_calls_from_expr(expr: &ArithExpr, calls: &mut Vec<String>) {
238        match expr {
239            ArithExpr::FuncCall { name, args } => {
240                if !calls.contains(name) {
241                    calls.push(name.clone());
242                }
243                for arg in args {
244                    Self::extract_calls_from_expr(arg, calls);
245                }
246            }
247            ArithExpr::Add(l, r)
248            | ArithExpr::Sub(l, r)
249            | ArithExpr::Mul(l, r)
250            | ArithExpr::Div(l, r)
251            | ArithExpr::Mod(l, r)
252            | ArithExpr::Min(l, r)
253            | ArithExpr::Max(l, r)
254            | ArithExpr::Pow(l, r) => {
255                Self::extract_calls_from_expr(l, calls);
256                Self::extract_calls_from_expr(r, calls);
257            }
258            ArithExpr::Abs(e) | ArithExpr::Cast(e, _) => {
259                Self::extract_calls_from_expr(e, calls);
260            }
261            ArithExpr::Variable(_) | ArithExpr::Integer(_) | ArithExpr::Float(_) => {}
262            ArithExpr::Conditional {
263                cond_left,
264                cond_right,
265                then_expr,
266                else_expr,
267                ..
268            } => {
269                Self::extract_calls_from_expr(cond_left, calls);
270                Self::extract_calls_from_expr(cond_right, calls);
271                Self::extract_calls_from_expr(then_expr, calls);
272                Self::extract_calls_from_expr(else_expr, calls);
273            }
274        }
275    }
276
277    /// Check if a function is recursive (calls itself directly or indirectly)
278    pub fn is_recursive(&self, name: &str) -> bool {
279        self.reaches(name, name)
280    }
281
282    fn reaches(&self, from: &str, target: &str) -> bool {
283        let mut pending = vec![from];
284        let mut visited = HashSet::new();
285        while let Some(current) = pending.pop() {
286            if !visited.insert(current) {
287                continue;
288            }
289            let Some(calls) = self.call_graph.get(current) else {
290                continue;
291            };
292            for call in calls {
293                if call == target {
294                    return true;
295                }
296                if self.functions.contains_key(call) {
297                    pending.push(call);
298                }
299            }
300        }
301        false
302    }
303
304    fn recursive_component_has_base_case(&self, name: &str) -> bool {
305        self.registration_order.iter().any(|candidate| {
306            self.functions
307                .get(candidate)
308                .is_some_and(|func| Self::has_base_case(&func.body))
309                && self.reaches(name, candidate)
310                && self.reaches(candidate, name)
311        })
312    }
313
314    /// Validate all functions
315    pub fn validate(&self) -> Result<(), FunctionError> {
316        for name in &self.registration_order {
317            // Check that all called functions exist
318            if let Some(calls) = self.call_graph.get(name) {
319                for call in calls {
320                    if !self.functions.contains_key(call) && !is_builtin(call) {
321                        return Err(FunctionError::UndefinedFunction { name: call.clone() });
322                    }
323                }
324            }
325
326            // Check recursive functions have base case
327            if self.is_recursive(name) && !self.recursive_component_has_base_case(name) {
328                return Err(FunctionError::RecursionWithoutBaseCase { name: name.clone() });
329            }
330        }
331        Ok(())
332    }
333
334    fn has_base_case(body: &FuncBody) -> bool {
335        matches!(body, FuncBody::Conditional(_))
336    }
337
338    /// Build registry from a program
339    pub fn from_program(program: &Program) -> Result<Self, FunctionError> {
340        let mut registry = Self::new();
341
342        // Check for name conflicts with predicates
343        let pred_names: HashSet<_> = program.predicates.iter().map(|p| p.name.clone()).collect();
344
345        for func in &program.functions {
346            if pred_names.contains(&func.name) {
347                return Err(FunctionError::NameConflict {
348                    name: func.name.clone(),
349                });
350            }
351            registry.register(func.clone())?;
352        }
353
354        registry.validate()?;
355        Ok(registry)
356    }
357
358    /// Get all registered functions
359    pub fn functions(&self) -> impl Iterator<Item = &FuncDef> {
360        self.registration_order
361            .iter()
362            .filter_map(|name| self.functions.get(name))
363    }
364
365    /// Analyze recursive function for potential infinite recursion
366    pub fn analyze_recursion(&self, func: &FuncDef) -> Option<RecursionWarning> {
367        if !self.is_recursive(&func.name) {
368            return None;
369        }
370
371        match &func.body {
372            FuncBody::Conditional(cond) => self.check_convergence(func, cond),
373            _ => None,
374        }
375    }
376
377    fn check_convergence(&self, func: &FuncDef, cond: &CondExpr) -> Option<RecursionWarning> {
378        // Find recursive calls in else branch
379        let recursive_calls = Self::find_recursive_calls_in_body(&func.name, &cond.else_branch);
380
381        for call_args in recursive_calls {
382            if call_args.is_empty() {
383                continue;
384            }
385
386            // Simple pattern check: if condition is var <= k and recursive uses var + n
387            // This is a warning sign (moving away from base case)
388            if let (ArithExpr::Variable(var), CompOp::Le | CompOp::Lt) =
389                (&cond.cond_left, cond.cond_op)
390            {
391                if let ArithExpr::Add(left, right) = &call_args[0] {
392                    if let (ArithExpr::Variable(arg_var), ArithExpr::Integer(n)) =
393                        (left.as_ref(), right.as_ref())
394                    {
395                        if arg_var == var && *n > 0 {
396                            return Some(RecursionWarning {
397                                func_name: func.name.clone(),
398                                message: format!(
399                                    "recursive call increases `{}`, but base case requires it to decrease",
400                                    var
401                                ),
402                            });
403                        }
404                    }
405                }
406            }
407        }
408
409        None
410    }
411
412    fn find_recursive_calls_in_body(name: &str, body: &FuncBody) -> Vec<Vec<ArithExpr>> {
413        let mut calls = Vec::new();
414        match body {
415            FuncBody::Arithmetic(expr) => {
416                Self::find_recursive_calls_in_expr(name, expr, &mut calls);
417            }
418            FuncBody::Conditional(cond) => {
419                Self::find_recursive_calls_in_expr(name, &cond.cond_left, &mut calls);
420                Self::find_recursive_calls_in_expr(name, &cond.cond_right, &mut calls);
421                calls.extend(Self::find_recursive_calls_in_body(name, &cond.then_branch));
422                calls.extend(Self::find_recursive_calls_in_body(name, &cond.else_branch));
423            }
424            FuncBody::Predicate { body, .. } => {
425                for literal in body {
426                    if let BodyLiteral::IsExpr(binding) = literal {
427                        Self::find_recursive_calls_in_expr(name, &binding.expr, &mut calls);
428                    }
429                }
430            }
431        }
432        calls
433    }
434
435    fn find_recursive_calls_in_expr(name: &str, expr: &ArithExpr, calls: &mut Vec<Vec<ArithExpr>>) {
436        match expr {
437            ArithExpr::FuncCall {
438                name: fn_name,
439                args,
440            } if fn_name == name => {
441                calls.push(args.clone());
442            }
443            ArithExpr::Add(l, r)
444            | ArithExpr::Sub(l, r)
445            | ArithExpr::Mul(l, r)
446            | ArithExpr::Div(l, r)
447            | ArithExpr::Mod(l, r)
448            | ArithExpr::Min(l, r)
449            | ArithExpr::Max(l, r)
450            | ArithExpr::Pow(l, r) => {
451                Self::find_recursive_calls_in_expr(name, l, calls);
452                Self::find_recursive_calls_in_expr(name, r, calls);
453            }
454            ArithExpr::Abs(e) | ArithExpr::Cast(e, _) => {
455                Self::find_recursive_calls_in_expr(name, e, calls);
456            }
457            ArithExpr::FuncCall { args, .. } => {
458                for arg in args {
459                    Self::find_recursive_calls_in_expr(name, arg, calls);
460                }
461            }
462            ArithExpr::Conditional {
463                cond_left,
464                cond_right,
465                then_expr,
466                else_expr,
467                ..
468            } => {
469                Self::find_recursive_calls_in_expr(name, cond_left, calls);
470                Self::find_recursive_calls_in_expr(name, cond_right, calls);
471                Self::find_recursive_calls_in_expr(name, then_expr, calls);
472                Self::find_recursive_calls_in_expr(name, else_expr, calls);
473            }
474            _ => {}
475        }
476    }
477
478    /// Validate all functions, collecting warnings
479    pub fn validate_with_warnings(&self) -> (Result<(), FunctionError>, Vec<RecursionWarning>) {
480        let mut warnings = Vec::new();
481
482        for func in self.functions() {
483            if let Some(warning) = self.analyze_recursion(func) {
484                warnings.push(warning);
485            }
486        }
487
488        (self.validate(), warnings)
489    }
490}
491
492/// Check if a name is a built-in function
493pub(crate) fn is_builtin(name: &str) -> bool {
494    matches!(name, "abs" | "min" | "max" | "pow" | "cast")
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::ast::FuncParam;
501    use xlog_core::XlogError;
502
503    #[test]
504    fn test_function_error_into_xlog() {
505        let err = FunctionError::UndefinedFunction {
506            name: "foo".to_string(),
507        };
508        let xlog_err: XlogError = err.into();
509        let msg = xlog_err.to_string();
510        assert!(msg.contains("foo"), "Expected 'foo' in: {msg}");
511    }
512
513    fn make_arith_func(name: &str, body: ArithExpr) -> FuncDef {
514        FuncDef {
515            name: name.to_string(),
516            params: vec![FuncParam {
517                name: "X".to_string(),
518                typ: None,
519            }],
520            return_type: None,
521            body: FuncBody::Arithmetic(body),
522            is_private: false,
523        }
524    }
525
526    #[test]
527    fn test_register_function() {
528        let mut reg = FunctionRegistry::new();
529        let func = make_arith_func("square", ArithExpr::Variable("X".to_string()));
530        assert!(reg.register(func).is_ok());
531    }
532
533    #[test]
534    fn test_duplicate_error() {
535        let mut reg = FunctionRegistry::new();
536        let func = make_arith_func("f", ArithExpr::Variable("X".to_string()));
537        reg.register(func.clone()).unwrap();
538        let result = reg.register(func);
539        assert!(matches!(
540            result,
541            Err(FunctionError::DuplicateDefinition { .. })
542        ));
543    }
544
545    #[test]
546    fn test_recursive_detection() {
547        let mut reg = FunctionRegistry::new();
548
549        // f calls itself
550        let f = FuncDef {
551            name: "f".to_string(),
552            params: vec![],
553            return_type: None,
554            body: FuncBody::Arithmetic(ArithExpr::FuncCall {
555                name: "f".to_string(),
556                args: vec![],
557            }),
558            is_private: false,
559        };
560        reg.register(f).unwrap();
561
562        assert!(reg.is_recursive("f"));
563    }
564
565    #[test]
566    fn test_get_function() {
567        let mut reg = FunctionRegistry::new();
568        let func = make_arith_func("square", ArithExpr::Variable("X".to_string()));
569        reg.register(func).unwrap();
570
571        assert!(reg.get("square").is_some());
572        assert!(reg.get("nonexistent").is_none());
573    }
574
575    #[test]
576    fn test_contains_function() {
577        let mut reg = FunctionRegistry::new();
578        let func = make_arith_func("square", ArithExpr::Variable("X".to_string()));
579        reg.register(func).unwrap();
580
581        assert!(reg.contains("square"));
582        assert!(!reg.contains("nonexistent"));
583    }
584
585    #[test]
586    fn test_undefined_function_error() {
587        let mut reg = FunctionRegistry::new();
588
589        // Function that calls an undefined function
590        let f = FuncDef {
591            name: "f".to_string(),
592            params: vec![],
593            return_type: None,
594            body: FuncBody::Arithmetic(ArithExpr::FuncCall {
595                name: "undefined_func".to_string(),
596                args: vec![],
597            }),
598            is_private: false,
599        };
600        reg.register(f).unwrap();
601
602        let result = reg.validate();
603        assert!(matches!(
604            result,
605            Err(FunctionError::UndefinedFunction { .. })
606        ));
607    }
608
609    #[test]
610    fn test_builtin_function_allowed() {
611        let mut reg = FunctionRegistry::new();
612
613        // Function that calls built-in functions
614        let f = FuncDef {
615            name: "f".to_string(),
616            params: vec![FuncParam {
617                name: "X".to_string(),
618                typ: None,
619            }],
620            return_type: None,
621            body: FuncBody::Arithmetic(ArithExpr::FuncCall {
622                name: "abs".to_string(),
623                args: vec![ArithExpr::Variable("X".to_string())],
624            }),
625            is_private: false,
626        };
627        reg.register(f).unwrap();
628
629        // Should not error because abs is a built-in
630        assert!(reg.validate().is_ok());
631    }
632
633    #[test]
634    fn test_indirect_recursion() {
635        let mut reg = FunctionRegistry::new();
636
637        // f calls g
638        let f = FuncDef {
639            name: "f".to_string(),
640            params: vec![],
641            return_type: None,
642            body: FuncBody::Arithmetic(ArithExpr::FuncCall {
643                name: "g".to_string(),
644                args: vec![],
645            }),
646            is_private: false,
647        };
648
649        // g calls f (indirect recursion)
650        let g = FuncDef {
651            name: "g".to_string(),
652            params: vec![],
653            return_type: None,
654            body: FuncBody::Arithmetic(ArithExpr::FuncCall {
655                name: "f".to_string(),
656                args: vec![],
657            }),
658            is_private: false,
659        };
660
661        reg.register(f).unwrap();
662        reg.register(g).unwrap();
663
664        assert!(reg.is_recursive("f"));
665        assert!(reg.is_recursive("g"));
666    }
667
668    #[test]
669    fn test_functions_iterator() {
670        let mut reg = FunctionRegistry::new();
671        let f1 = make_arith_func("f1", ArithExpr::Variable("X".to_string()));
672        let f2 = make_arith_func("f2", ArithExpr::Variable("X".to_string()));
673        reg.register(f1).unwrap();
674        reg.register(f2).unwrap();
675
676        let names: HashSet<_> = reg.functions().map(|f| f.name.as_str()).collect();
677        assert!(names.contains("f1"));
678        assert!(names.contains("f2"));
679        assert_eq!(names.len(), 2);
680    }
681
682    #[test]
683    fn test_recursion_warning_display() {
684        let warning = RecursionWarning {
685            func_name: "fib".to_string(),
686            message: "recursive call increases `N`".to_string(),
687        };
688        let msg = warning.to_string();
689        assert!(msg.contains("W0502"));
690        assert!(msg.contains("infinite recursion"));
691        assert!(msg.contains("fib"));
692    }
693
694    #[test]
695    fn test_analyze_non_recursive() {
696        let mut reg = FunctionRegistry::new();
697        let func = make_arith_func("square", ArithExpr::Variable("X".to_string()));
698        reg.register(func.clone()).unwrap();
699
700        // Non-recursive functions shouldn't trigger warnings
701        assert!(reg.analyze_recursion(&func).is_none());
702    }
703
704    #[test]
705    fn test_analyze_recursive_with_proper_convergence() {
706        use crate::ast::CondExpr;
707
708        let mut reg = FunctionRegistry::new();
709
710        // Proper factorial: if N <= 1 then 1 else N * fact(N - 1)
711        let factorial = FuncDef {
712            name: "fact".to_string(),
713            params: vec![FuncParam {
714                name: "N".to_string(),
715                typ: None,
716            }],
717            return_type: None,
718            body: FuncBody::Conditional(CondExpr {
719                cond_left: ArithExpr::Variable("N".to_string()),
720                cond_op: CompOp::Le,
721                cond_right: ArithExpr::Integer(1),
722                then_branch: Box::new(FuncBody::Arithmetic(ArithExpr::Integer(1))),
723                else_branch: Box::new(FuncBody::Arithmetic(ArithExpr::Mul(
724                    Box::new(ArithExpr::Variable("N".to_string())),
725                    Box::new(ArithExpr::FuncCall {
726                        name: "fact".to_string(),
727                        args: vec![ArithExpr::Sub(
728                            Box::new(ArithExpr::Variable("N".to_string())),
729                            Box::new(ArithExpr::Integer(1)),
730                        )],
731                    }),
732                ))),
733            }),
734            is_private: false,
735        };
736
737        reg.register(factorial.clone()).unwrap();
738
739        // Proper convergence (N - 1) shouldn't trigger warning
740        assert!(reg.analyze_recursion(&factorial).is_none());
741    }
742
743    #[test]
744    fn test_analyze_recursive_with_divergence() {
745        use crate::ast::CondExpr;
746
747        let mut reg = FunctionRegistry::new();
748
749        // Bad function: if N <= 1 then 1 else f(N + 1)
750        // This increases N, which diverges from the base case
751        let bad_func = FuncDef {
752            name: "badfunc".to_string(),
753            params: vec![FuncParam {
754                name: "N".to_string(),
755                typ: None,
756            }],
757            return_type: None,
758            body: FuncBody::Conditional(CondExpr {
759                cond_left: ArithExpr::Variable("N".to_string()),
760                cond_op: CompOp::Le,
761                cond_right: ArithExpr::Integer(1),
762                then_branch: Box::new(FuncBody::Arithmetic(ArithExpr::Integer(1))),
763                else_branch: Box::new(FuncBody::Arithmetic(ArithExpr::FuncCall {
764                    name: "badfunc".to_string(),
765                    args: vec![ArithExpr::Add(
766                        Box::new(ArithExpr::Variable("N".to_string())),
767                        Box::new(ArithExpr::Integer(1)),
768                    )],
769                })),
770            }),
771            is_private: false,
772        };
773
774        reg.register(bad_func.clone()).unwrap();
775
776        // Should trigger a warning about potential infinite recursion
777        let warning = reg.analyze_recursion(&bad_func);
778        assert!(warning.is_some());
779        assert!(warning.unwrap().message.contains("increases"));
780    }
781
782    #[test]
783    fn test_validate_with_warnings() {
784        use crate::ast::CondExpr;
785
786        let mut reg = FunctionRegistry::new();
787
788        // Bad function that will generate a warning
789        let bad_func = FuncDef {
790            name: "diverging".to_string(),
791            params: vec![FuncParam {
792                name: "X".to_string(),
793                typ: None,
794            }],
795            return_type: None,
796            body: FuncBody::Conditional(CondExpr {
797                cond_left: ArithExpr::Variable("X".to_string()),
798                cond_op: CompOp::Lt,
799                cond_right: ArithExpr::Integer(0),
800                then_branch: Box::new(FuncBody::Arithmetic(ArithExpr::Integer(0))),
801                else_branch: Box::new(FuncBody::Arithmetic(ArithExpr::FuncCall {
802                    name: "diverging".to_string(),
803                    args: vec![ArithExpr::Add(
804                        Box::new(ArithExpr::Variable("X".to_string())),
805                        Box::new(ArithExpr::Integer(1)),
806                    )],
807                })),
808            }),
809            is_private: false,
810        };
811
812        reg.register(bad_func).unwrap();
813
814        let (result, warnings) = reg.validate_with_warnings();
815        assert!(result.is_ok());
816        assert_eq!(warnings.len(), 1);
817        assert!(warnings[0].func_name == "diverging");
818    }
819}