1use crate::ast::{ArithExpr, BodyLiteral, CompOp, CondExpr, FuncBody, FuncDef, Program};
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub enum FunctionError {
10 DuplicateDefinition {
12 name: String,
14 },
15 RecursionWithoutBaseCase {
17 name: String,
19 },
20 UndefinedFunction {
22 name: String,
24 },
25 MaxRecursionDepth {
27 name: String,
29 depth: u32,
31 },
32 NameConflict {
34 name: String,
36 },
37 ArityMismatch {
39 name: String,
41 expected: usize,
43 received: usize,
45 },
46 PredicateBodyRequiresRuleContext {
48 name: String,
50 },
51 UnsupportedPredicateTermArgument {
53 name: String,
55 parameter: String,
57 },
58 PredicateCallInConditionalBranch {
60 name: String,
62 },
63 InvalidPredicateBindingTarget {
65 name: String,
67 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#[derive(Debug, Clone)]
147pub struct RecursionWarning {
148 pub func_name: String,
150 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#[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 pub fn new() -> Self {
180 Self::default()
181 }
182
183 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 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 pub fn get(&self, name: &str) -> Option<&FuncDef> {
203 self.functions.get(name)
204 }
205
206 pub fn contains(&self, name: &str) -> bool {
208 self.functions.contains_key(name)
209 }
210
211 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 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 pub fn validate(&self) -> Result<(), FunctionError> {
316 for name in &self.registration_order {
317 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 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 pub fn from_program(program: &Program) -> Result<Self, FunctionError> {
340 let mut registry = Self::new();
341
342 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 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 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 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 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 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
492pub(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 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 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 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 assert!(reg.validate().is_ok());
631 }
632
633 #[test]
634 fn test_indirect_recursion() {
635 let mut reg = FunctionRegistry::new();
636
637 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 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 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 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 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 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 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 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}