1use std::collections::{HashMap, HashSet};
16
17use crate::ast::{
18 BinOp, Expr, FnBody, FnDef, MatchArm, Pattern, Spanned, Stmt, TailCallData, TopLevel, TypeDef,
19};
20use crate::call_graph;
21use crate::codegen::proof_lower::ProofLowerInputs;
22
23use super::{ProofModeIssue, RecursionPlan};
24
25pub(crate) fn expr_to_dotted_name(expr: &Spanned<Expr>) -> Option<String> {
26 match &expr.node {
27 Expr::Ident(name) => Some(name.clone()),
28 Expr::Attr(obj, field) => expr_to_dotted_name(obj).map(|p| format!("{}.{}", p, field)),
29 _ => None,
30 }
31}
32
33pub(crate) fn local_name_of(expr: &Spanned<Expr>) -> Option<&str> {
41 match &expr.node {
42 Expr::Ident(name) => Some(name.as_str()),
43 Expr::Resolved { name, .. } => Some(name.as_str()),
44 _ => None,
45 }
46}
47
48pub(crate) fn call_matches(name: &str, target: &str) -> bool {
54 name == target
55}
56
57pub(crate) fn call_is_in_set(name: &str, targets: &HashSet<String>) -> bool {
58 call_matches_any(name, targets)
59}
60
61pub(crate) fn canonical_callee_name(name: &str, targets: &HashSet<String>) -> Option<String> {
62 if targets.contains(name) {
63 return Some(name.to_string());
64 }
65 name.rsplit('.')
66 .next()
67 .filter(|last| targets.contains(*last))
68 .map(ToString::to_string)
69}
70
71pub(crate) fn call_matches_any(name: &str, targets: &HashSet<String>) -> bool {
72 if targets.contains(name) {
73 return true;
74 }
75 match name.rsplit('.').next() {
76 Some(last) => targets.contains(last),
77 None => false,
78 }
79}
80
81pub(crate) fn is_int_minus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
82 match &expr.node {
83 Expr::BinOp(BinOp::Sub, left, right) => {
84 local_name_of(left).is_some_and(|id| id == param_name)
85 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
86 }
87 Expr::FnCall(callee, args) => {
88 let Some(name) = expr_to_dotted_name(callee) else {
89 return false;
90 };
91 (name == "Int.sub" || name == "int.sub")
92 && args.len() == 2
93 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
94 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
95 }
96 _ => false,
97 }
98}
99
100pub(crate) fn collect_calls_from_expr<'a>(
101 expr: &'a Spanned<Expr>,
102 out: &mut Vec<(String, Vec<&'a Spanned<Expr>>)>,
103) {
104 match &expr.node {
105 Expr::FnCall(callee, args) => {
106 if let Some(name) = expr_to_dotted_name(callee) {
107 out.push((name, args.iter().collect()));
108 }
109 collect_calls_from_expr(callee, out);
110 for arg in args {
111 collect_calls_from_expr(arg, out);
112 }
113 }
114 Expr::TailCall(boxed) => {
115 let TailCallData {
116 target: name, args, ..
117 } = boxed.as_ref();
118 out.push((name.clone(), args.iter().collect()));
119 for arg in args {
120 collect_calls_from_expr(arg, out);
121 }
122 }
123 Expr::Attr(obj, _) => collect_calls_from_expr(obj, out),
124 Expr::BinOp(_, left, right) => {
125 collect_calls_from_expr(left, out);
126 collect_calls_from_expr(right, out);
127 }
128 Expr::Neg(inner) => collect_calls_from_expr(inner, out),
129 Expr::Match { subject, arms, .. } => {
130 collect_calls_from_expr(subject, out);
131 for arm in arms {
132 collect_calls_from_expr(&arm.body, out);
133 }
134 }
135 Expr::Constructor(_, inner) => {
136 if let Some(inner) = inner {
137 collect_calls_from_expr(inner, out);
138 }
139 }
140 Expr::ErrorProp(inner) => collect_calls_from_expr(inner, out),
141 Expr::InterpolatedStr(parts) => {
142 for p in parts {
143 if let crate::ast::StrPart::Parsed(e) = p {
144 collect_calls_from_expr(e, out);
145 }
146 }
147 }
148 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
149 for item in items {
150 collect_calls_from_expr(item, out);
151 }
152 }
153 Expr::MapLiteral(entries) => {
154 for (k, v) in entries {
155 collect_calls_from_expr(k, out);
156 collect_calls_from_expr(v, out);
157 }
158 }
159 Expr::RecordCreate { fields, .. } => {
160 for (_, v) in fields {
161 collect_calls_from_expr(v, out);
162 }
163 }
164 Expr::RecordUpdate { base, updates, .. } => {
165 collect_calls_from_expr(base, out);
166 for (_, v) in updates {
167 collect_calls_from_expr(v, out);
168 }
169 }
170 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
171 }
172}
173
174pub(crate) fn collect_calls_from_body(body: &FnBody) -> Vec<(String, Vec<&Spanned<Expr>>)> {
175 let mut out = Vec::new();
176 for stmt in body.stmts() {
177 match stmt {
178 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => collect_calls_from_expr(expr, &mut out),
179 }
180 }
181 out
182}
183
184pub(crate) fn collect_list_tail_binders_from_expr(
185 expr: &Spanned<Expr>,
186 list_param_name: &str,
187 tails: &mut HashSet<String>,
188) {
189 match &expr.node {
190 Expr::Match { subject, arms, .. } => {
191 if local_name_of(subject).is_some_and(|id| id == list_param_name) {
192 for MatchArm { pattern, .. } in arms {
193 if let Pattern::Cons(_, tail) = pattern {
194 tails.insert(tail.clone());
195 }
196 }
197 }
198 for arm in arms {
199 collect_list_tail_binders_from_expr(&arm.body, list_param_name, tails);
200 }
201 collect_list_tail_binders_from_expr(subject, list_param_name, tails);
202 }
203 Expr::FnCall(callee, args) => {
204 collect_list_tail_binders_from_expr(callee, list_param_name, tails);
205 for arg in args {
206 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
207 }
208 }
209 Expr::TailCall(boxed) => {
210 let TailCallData {
211 target: _, args, ..
212 } = boxed.as_ref();
213 for arg in args {
214 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
215 }
216 }
217 Expr::Attr(obj, _) => collect_list_tail_binders_from_expr(obj, list_param_name, tails),
218 Expr::BinOp(_, left, right) => {
219 collect_list_tail_binders_from_expr(left, list_param_name, tails);
220 collect_list_tail_binders_from_expr(right, list_param_name, tails);
221 }
222 Expr::Neg(inner) => collect_list_tail_binders_from_expr(inner, list_param_name, tails),
223 Expr::Constructor(_, inner) => {
224 if let Some(inner) = inner {
225 collect_list_tail_binders_from_expr(inner, list_param_name, tails);
226 }
227 }
228 Expr::ErrorProp(inner) => {
229 collect_list_tail_binders_from_expr(inner, list_param_name, tails)
230 }
231 Expr::InterpolatedStr(parts) => {
232 for p in parts {
233 if let crate::ast::StrPart::Parsed(e) = p {
234 collect_list_tail_binders_from_expr(e, list_param_name, tails);
235 }
236 }
237 }
238 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
239 for item in items {
240 collect_list_tail_binders_from_expr(item, list_param_name, tails);
241 }
242 }
243 Expr::MapLiteral(entries) => {
244 for (k, v) in entries {
245 collect_list_tail_binders_from_expr(k, list_param_name, tails);
246 collect_list_tail_binders_from_expr(v, list_param_name, tails);
247 }
248 }
249 Expr::RecordCreate { fields, .. } => {
250 for (_, v) in fields {
251 collect_list_tail_binders_from_expr(v, list_param_name, tails);
252 }
253 }
254 Expr::RecordUpdate { base, updates, .. } => {
255 collect_list_tail_binders_from_expr(base, list_param_name, tails);
256 for (_, v) in updates {
257 collect_list_tail_binders_from_expr(v, list_param_name, tails);
258 }
259 }
260 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
261 }
262}
263
264pub(crate) fn collect_list_tail_binders(fd: &FnDef, list_param_name: &str) -> HashSet<String> {
265 let mut tails = HashSet::new();
266 for stmt in fd.body.stmts() {
267 match stmt {
268 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
269 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
270 }
271 }
272 }
273 tails
274}
275
276pub(crate) fn recursive_constructor_binders(
277 td: &TypeDef,
278 variant_name: &str,
279 binders: &[String],
280) -> Vec<String> {
281 let variant_short = variant_name.rsplit('.').next().unwrap_or(variant_name);
282 match td {
283 TypeDef::Sum { name, variants, .. } => variants
284 .iter()
285 .find(|variant| variant.name == variant_short)
286 .map(|variant| {
287 variant
288 .fields
289 .iter()
290 .zip(binders.iter())
291 .filter_map(|(field_ty, binder)| {
292 (field_ty.trim() == name).then_some(binder.clone())
293 })
294 .collect()
295 })
296 .unwrap_or_default(),
297 TypeDef::Product { .. } => Vec::new(),
298 }
299}
300
301pub(crate) fn grow_recursive_subterm_binders_from_expr(
302 expr: &Spanned<Expr>,
303 tracked: &HashSet<String>,
304 td: &TypeDef,
305 out: &mut HashSet<String>,
306) {
307 match &expr.node {
308 Expr::Match { subject, arms, .. } => {
309 if let Some(subject_name) = local_name_of(subject)
310 && tracked.contains(subject_name)
311 {
312 for arm in arms {
313 if let Pattern::Constructor(variant_name, binders) = &arm.pattern {
314 out.extend(recursive_constructor_binders(td, variant_name, binders));
315 }
316 }
317 }
318 grow_recursive_subterm_binders_from_expr(subject, tracked, td, out);
319 for arm in arms {
320 grow_recursive_subterm_binders_from_expr(&arm.body, tracked, td, out);
321 }
322 }
323 Expr::FnCall(callee, args) => {
324 grow_recursive_subterm_binders_from_expr(callee, tracked, td, out);
325 for arg in args {
326 grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
327 }
328 }
329 Expr::Attr(obj, _) => grow_recursive_subterm_binders_from_expr(obj, tracked, td, out),
330 Expr::BinOp(_, left, right) => {
331 grow_recursive_subterm_binders_from_expr(left, tracked, td, out);
332 grow_recursive_subterm_binders_from_expr(right, tracked, td, out);
333 }
334 Expr::Neg(inner) => grow_recursive_subterm_binders_from_expr(inner, tracked, td, out),
335 Expr::Constructor(_, Some(inner)) | Expr::ErrorProp(inner) => {
336 grow_recursive_subterm_binders_from_expr(inner, tracked, td, out)
337 }
338 Expr::InterpolatedStr(parts) => {
339 for part in parts {
340 if let crate::ast::StrPart::Parsed(inner) = part {
341 grow_recursive_subterm_binders_from_expr(inner, tracked, td, out);
342 }
343 }
344 }
345 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
346 for item in items {
347 grow_recursive_subterm_binders_from_expr(item, tracked, td, out);
348 }
349 }
350 Expr::MapLiteral(entries) => {
351 for (k, v) in entries {
352 grow_recursive_subterm_binders_from_expr(k, tracked, td, out);
353 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
354 }
355 }
356 Expr::RecordCreate { fields, .. } => {
357 for (_, v) in fields {
358 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
359 }
360 }
361 Expr::RecordUpdate { base, updates, .. } => {
362 grow_recursive_subterm_binders_from_expr(base, tracked, td, out);
363 for (_, v) in updates {
364 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
365 }
366 }
367 Expr::TailCall(boxed) => {
368 for arg in &boxed.args {
369 grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
370 }
371 }
372 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } | Expr::Constructor(_, None) => {}
373 }
374}
375
376pub(crate) fn collect_recursive_subterm_binders(
377 fd: &FnDef,
378 param_name: &str,
379 param_type: &str,
380 inputs: &ProofLowerInputs,
381) -> HashSet<String> {
382 let Some(td) = inputs.find_type_def(param_type) else {
383 return HashSet::new();
384 };
385 let mut tracked: HashSet<String> = HashSet::from([param_name.to_string()]);
386 loop {
387 let mut discovered = HashSet::new();
388 for stmt in fd.body.stmts() {
389 match stmt {
390 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
391 grow_recursive_subterm_binders_from_expr(expr, &tracked, td, &mut discovered);
392 }
393 }
394 }
395 let before = tracked.len();
396 tracked.extend(discovered);
397 if tracked.len() == before {
398 break;
399 }
400 }
401 tracked.remove(param_name);
402 tracked
403}
404
405pub(crate) fn int_countdown_native_arms(
415 fd: &FnDef,
416 param_index: usize,
417) -> Option<(i64, Spanned<Expr>, Spanned<Expr>)> {
418 let (param_name, _) = fd.params.get(param_index)?;
419 if fd.body.stmts().len() != 1 {
424 return None;
425 }
426 let tail = fd.body.tail_expr()?;
427 let Expr::Match { subject, arms, .. } = &tail.node else {
428 return None;
429 };
430 if !is_ident(subject, param_name) {
431 return None;
432 }
433 if arms.len() != 2 {
434 return None;
435 }
436 let mut literal_arm: Option<(i64, Spanned<Expr>)> = None;
437 let mut wildcard_arm_body: Option<Spanned<Expr>> = None;
438 for arm in arms {
439 match &arm.pattern {
440 Pattern::Literal(crate::ast::Literal::Int(n)) => {
441 literal_arm = Some((*n, (*arm.body).clone()));
442 }
443 Pattern::Wildcard | Pattern::Ident(_) => {
444 wildcard_arm_body = Some((*arm.body).clone());
445 }
446 _ => return None,
447 }
448 }
449 let (literal_value, literal_arm_body) = literal_arm?;
450 let wildcard_arm_body = wildcard_arm_body?;
451
452 if literal_value != 0 {
465 return None;
466 }
467
468 let mut lit_calls = Vec::new();
469 collect_calls_from_expr(&literal_arm_body, &mut lit_calls);
470 if lit_calls.iter().any(|(n, _)| call_matches(n, &fd.name)) {
471 return None;
472 }
473 let mut wc_calls = Vec::new();
474 collect_calls_from_expr(&wildcard_arm_body, &mut wc_calls);
475 if !wc_calls.iter().any(|(n, _)| call_matches(n, &fd.name)) {
476 return None;
477 }
478 Some((literal_value, literal_arm_body, wildcard_arm_body))
479}
480
481pub(crate) fn single_int_countdown_param_index(fd: &FnDef) -> Option<usize> {
482 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
483 .into_iter()
484 .filter(|(name, _)| call_matches(name, &fd.name))
485 .map(|(_, args)| args)
486 .collect();
487 if recursive_calls.is_empty() {
488 return None;
489 }
490
491 fd.params
492 .iter()
493 .enumerate()
494 .find_map(|(idx, (param_name, param_ty))| {
495 if param_ty != "Int" {
496 return None;
497 }
498 let countdown_ok = recursive_calls.iter().all(|args| {
499 args.get(idx)
500 .cloned()
501 .is_some_and(|arg| is_int_minus_positive(arg, param_name))
502 });
503 if countdown_ok {
504 return Some(idx);
505 }
506
507 let ascent_ok = recursive_calls.iter().all(|args| {
510 args.get(idx)
511 .copied()
512 .is_some_and(|arg| is_int_plus_positive(arg, param_name))
513 });
514 (ascent_ok && has_negative_guarded_ascent(fd, param_name)).then_some(idx)
515 })
516}
517
518pub(crate) fn floor_div_shrink_of(
524 expr: &Spanned<Expr>,
525 param_name: &str,
526 inputs: &ProofLowerInputs,
527) -> Option<(i64, Option<String>)> {
528 if let Some(divisor) = inline_floor_div_shrink(expr, param_name) {
529 return Some((divisor, None));
530 }
531 let Expr::FnCall(callee, args) = &expr.node else {
535 return None;
536 };
537 let name = expr_to_dotted_name(callee)?;
538 if name.contains('.') || args.len() != 1 || !is_ident(&args[0], param_name) {
539 return None;
540 }
541 let fd = inputs.find_fn_def_by_call_name(&name)?;
542 if !fd.effects.is_empty() || fd.name != name {
543 return None;
544 }
545 let [(helper_param, helper_ty)] = fd.params.as_slice() else {
546 return None;
547 };
548 if helper_ty != "Int" || fd.return_type != "Int" {
549 return None;
550 }
551 let [crate::ast::Stmt::Expr(body)] = fd.body.stmts() else {
552 return None;
553 };
554 let divisor = inline_floor_div_shrink(body, helper_param)?;
555 Some((divisor, Some(fd.name.clone())))
556}
557
558fn inline_floor_div_shrink(expr: &Spanned<Expr>, param_name: &str) -> Option<i64> {
561 let Expr::FnCall(callee, args) = &expr.node else {
562 return None;
563 };
564 if expr_to_dotted_name(callee).as_deref() != Some("Result.withDefault") || args.len() != 2 {
565 return None;
566 }
567 if !matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(_))) {
568 return None;
569 }
570 let Expr::FnCall(div_callee, div_args) = &args[0].node else {
571 return None;
572 };
573 if expr_to_dotted_name(div_callee).as_deref() != Some("Int.div") || div_args.len() != 2 {
574 return None;
575 }
576 if !is_ident(&div_args[0], param_name) {
577 return None;
578 }
579 match &div_args[1].node {
580 Expr::Literal(crate::ast::Literal::Int(k)) if *k >= 2 => Some(*k),
581 _ => None,
582 }
583}
584
585pub(crate) fn collect_self_call_guard_chains(fd: &FnDef) -> Vec<Vec<Spanned<Expr>>> {
592 let mut out = Vec::new();
593 for stmt in fd.body.stmts() {
594 match stmt {
595 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
596 walk_self_call_guards(expr, &fd.name, &[], &mut out);
597 }
598 }
599 }
600 out
601}
602
603fn walk_self_call_guards(
604 expr: &Spanned<Expr>,
605 fn_name: &str,
606 enclosing: &[Spanned<Expr>],
607 out: &mut Vec<Vec<Spanned<Expr>>>,
608) {
609 match &expr.node {
610 Expr::FnCall(callee, args) => {
611 if let Some(name) = expr_to_dotted_name(callee)
612 && call_matches(&name, fn_name)
613 {
614 out.push(enclosing.to_vec());
615 }
616 walk_self_call_guards(callee, fn_name, enclosing, out);
617 for arg in args {
618 walk_self_call_guards(arg, fn_name, enclosing, out);
619 }
620 }
621 Expr::TailCall(boxed) => {
622 if boxed.target == fn_name {
623 out.push(enclosing.to_vec());
624 }
625 for arg in &boxed.args {
626 walk_self_call_guards(arg, fn_name, enclosing, out);
627 }
628 }
629 Expr::Match { subject, arms } => {
630 for arm in arms {
631 let mut new_guards: Vec<Spanned<Expr>> = enclosing.to_vec();
632 match &arm.pattern {
633 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
634 new_guards.push((**subject).clone());
635 }
636 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
637 if let Some(flipped) =
638 crate::codegen::recursion::flip_comparison_binop(subject)
639 {
640 new_guards.push(flipped);
641 }
642 }
643 _ => {}
644 }
645 walk_self_call_guards(&arm.body, fn_name, &new_guards, out);
646 }
647 walk_self_call_guards(subject, fn_name, enclosing, out);
648 }
649 Expr::BinOp(_, l, r) => {
650 walk_self_call_guards(l, fn_name, enclosing, out);
651 walk_self_call_guards(r, fn_name, enclosing, out);
652 }
653 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
654 walk_self_call_guards(inner, fn_name, enclosing, out);
655 }
656 Expr::Constructor(_, arg) => {
657 if let Some(inner) = arg {
658 walk_self_call_guards(inner, fn_name, enclosing, out);
659 }
660 }
661 Expr::InterpolatedStr(parts) => {
662 for p in parts {
663 if let crate::ast::StrPart::Parsed(inner) = p {
664 walk_self_call_guards(inner, fn_name, enclosing, out);
665 }
666 }
667 }
668 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
669 for item in items {
670 walk_self_call_guards(item, fn_name, enclosing, out);
671 }
672 }
673 Expr::MapLiteral(entries) => {
674 for (k, v) in entries {
675 walk_self_call_guards(k, fn_name, enclosing, out);
676 walk_self_call_guards(v, fn_name, enclosing, out);
677 }
678 }
679 Expr::RecordCreate { fields, .. } => {
680 for (_, v) in fields {
681 walk_self_call_guards(v, fn_name, enclosing, out);
682 }
683 }
684 Expr::RecordUpdate { base, updates, .. } => {
685 walk_self_call_guards(base, fn_name, enclosing, out);
686 for (_, v) in updates {
687 walk_self_call_guards(v, fn_name, enclosing, out);
688 }
689 }
690 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
691 }
692}
693
694pub(crate) fn guards_imply_param_ge_one(guards: &[Spanned<Expr>], param_name: &str) -> bool {
707 if guards
708 .iter()
709 .any(|g| guard_bounds_ident_ge_one(g, param_name))
710 {
711 return true;
712 }
713 guards.iter().any(|g| {
714 let Expr::BinOp(BinOp::Gte, left, right) = &g.node else {
715 return false;
716 };
717 if !is_ident(left, param_name) {
718 return false;
719 }
720 let scaled_var: Option<&str> = match &right.node {
721 Expr::Ident(q) => Some(q.as_str()),
722 Expr::Resolved { name: q, .. } => Some(q.as_str()),
723 Expr::BinOp(BinOp::Mul, l, r) => match (&l.node, &r.node) {
724 (Expr::Literal(crate::ast::Literal::Int(c)), _) if *c >= 1 => local_name_of(r),
725 (_, Expr::Literal(crate::ast::Literal::Int(c))) if *c >= 1 => local_name_of(l),
726 _ => None,
727 },
728 _ => None,
729 };
730 scaled_var.is_some_and(|q| guards.iter().any(|h| guard_bounds_ident_ge_one(h, q)))
731 })
732}
733
734fn guard_bounds_ident_ge_one(guard: &Spanned<Expr>, name: &str) -> bool {
737 let Expr::BinOp(op, left, right) = &guard.node else {
738 return false;
739 };
740 let lit = |e: &Spanned<Expr>| -> Option<i64> {
741 match &e.node {
742 Expr::Literal(crate::ast::Literal::Int(v)) => Some(*v),
743 _ => None,
744 }
745 };
746 match op {
747 BinOp::Gte => is_ident(left, name) && lit(right).is_some_and(|l| l >= 1),
748 BinOp::Gt => is_ident(left, name) && lit(right).is_some_and(|l| l >= 0),
749 BinOp::Lte => is_ident(right, name) && lit(left).is_some_and(|l| l >= 1),
750 BinOp::Lt => is_ident(right, name) && lit(left).is_some_and(|l| l >= 0),
751 _ => false,
752 }
753}
754
755pub(crate) fn single_int_floor_div_countdown(
763 fd: &FnDef,
764 inputs: &ProofLowerInputs,
765) -> Option<(usize, i64, Option<String>)> {
766 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
767 .into_iter()
768 .filter(|(name, _)| call_matches(name, &fd.name))
769 .map(|(_, args)| args)
770 .collect();
771 if recursive_calls.is_empty() {
772 return None;
773 }
774
775 let (param_index, divisor, helper_fn) =
776 fd.params
777 .iter()
778 .enumerate()
779 .find_map(|(idx, (param_name, param_ty))| {
780 if param_ty != "Int" {
781 return None;
782 }
783 let mut shrink: Option<(i64, Option<String>)> = None;
784 for args in &recursive_calls {
785 let arg = args.get(idx).copied()?;
786 let this = floor_div_shrink_of(arg, param_name, inputs)?;
787 match &shrink {
788 None => shrink = Some(this),
789 Some(prev) if *prev == this => {}
790 Some(_) => return None,
791 }
792 }
793 shrink.map(|(divisor, helper)| (idx, divisor, helper))
794 })?;
795
796 let (param_name, _) = fd.params.get(param_index)?;
801 let chains = collect_self_call_guard_chains(fd);
802 if chains.len() != recursive_calls.len() || chains.is_empty() {
803 return None;
804 }
805 if !chains
806 .iter()
807 .all(|chain| guards_imply_param_ge_one(chain, param_name))
808 {
809 return None;
810 }
811 Some((param_index, divisor, helper_fn))
812}
813
814pub(crate) fn has_negative_guarded_ascent(fd: &FnDef, param_name: &str) -> bool {
815 let Some(tail) = fd.body.tail_expr() else {
816 return false;
817 };
818 let Expr::Match { subject, arms, .. } = &tail.node else {
819 return false;
820 };
821 let Expr::BinOp(BinOp::Lt, left, right) = &subject.node else {
822 return false;
823 };
824 if !is_ident(left, param_name)
825 || !matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(0)))
826 {
827 return false;
828 }
829
830 let mut true_arm = None;
831 let mut false_arm = None;
832 for arm in arms {
833 match arm.pattern {
834 Pattern::Literal(crate::ast::Literal::Bool(true)) => true_arm = Some(arm.body.as_ref()),
835 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
836 false_arm = Some(arm.body.as_ref())
837 }
838 _ => return false,
839 }
840 }
841
842 let Some(true_arm) = true_arm else {
843 return false;
844 };
845 let Some(false_arm) = false_arm else {
846 return false;
847 };
848
849 let mut true_calls = Vec::new();
850 collect_calls_from_expr(true_arm, &mut true_calls);
851 let mut false_calls = Vec::new();
852 collect_calls_from_expr(false_arm, &mut false_calls);
853
854 true_calls
855 .iter()
856 .any(|(name, _)| call_matches(name, &fd.name))
857 && false_calls
858 .iter()
859 .all(|(name, _)| !call_matches(name, &fd.name))
860}
861
862pub(crate) fn single_int_ascending_param(fd: &FnDef) -> Option<(usize, Spanned<Expr>)> {
865 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
866 .into_iter()
867 .filter(|(name, _)| call_matches(name, &fd.name))
868 .map(|(_, args)| args)
869 .collect();
870 if recursive_calls.is_empty() {
871 return None;
872 }
873
874 for (idx, (param_name, param_ty)) in fd.params.iter().enumerate() {
875 if param_ty != "Int" {
876 continue;
877 }
878 let ascent_ok = recursive_calls.iter().all(|args| {
879 args.get(idx)
880 .cloned()
881 .is_some_and(|arg| is_int_plus_positive(arg, param_name))
882 });
883 if !ascent_ok {
884 continue;
885 }
886 if let Some(bound) = extract_equality_bound_expr(fd, param_name) {
887 return Some((idx, bound));
888 }
889 }
890 None
891}
892
893pub(crate) fn extract_equality_bound_expr(fd: &FnDef, param_name: &str) -> Option<Spanned<Expr>> {
897 let tail = fd.body.tail_expr()?;
898 let Expr::Match { subject, arms, .. } = &tail.node else {
899 return None;
900 };
901 let Expr::BinOp(BinOp::Eq, left, right) = &subject.node else {
902 return None;
903 };
904 if !is_ident(left, param_name) {
905 return None;
906 }
907 let mut true_has_self = false;
909 let mut false_has_self = false;
910 for arm in arms {
911 match arm.pattern {
912 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
913 let mut calls = Vec::new();
914 collect_calls_from_expr(&arm.body, &mut calls);
915 true_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
916 }
917 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
918 let mut calls = Vec::new();
919 collect_calls_from_expr(&arm.body, &mut calls);
920 false_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
921 }
922 _ => return None,
923 }
924 }
925 if true_has_self || !false_has_self {
926 return None;
927 }
928 Some((**right).clone())
929}
930
931pub(crate) fn supports_single_sizeof_structural(fd: &FnDef, inputs: &ProofLowerInputs) -> bool {
932 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
933 .into_iter()
934 .filter(|(name, _)| call_matches(name, &fd.name))
935 .map(|(_, args)| args)
936 .collect();
937 if recursive_calls.is_empty() {
938 return false;
939 }
940
941 let metric_indices = sizeof_measure_param_indices(fd);
942 if metric_indices.is_empty() {
943 return false;
944 }
945
946 let binder_sets: HashMap<usize, HashSet<String>> = metric_indices
947 .iter()
948 .filter_map(|idx| {
949 let (param_name, param_type) = fd.params.get(*idx)?;
950 inputs.recursive_type_names().contains(param_type).then(|| {
951 (
952 *idx,
953 collect_recursive_subterm_binders(fd, param_name, param_type, inputs),
954 )
955 })
956 })
957 .collect();
958
959 if binder_sets.values().all(HashSet::is_empty) {
960 return false;
961 }
962
963 recursive_calls.iter().all(|args| {
964 let mut strictly_smaller = false;
965 for idx in &metric_indices {
966 let Some((param_name, _)) = fd.params.get(*idx) else {
967 return false;
968 };
969 let Some(arg) = args.get(*idx).cloned() else {
970 return false;
971 };
972 if is_ident(arg, param_name) {
973 continue;
974 }
975 let Some(binders) = binder_sets.get(idx) else {
976 return false;
977 };
978 if local_name_of(arg).is_some_and(|id| binders.contains(id)) {
979 strictly_smaller = true;
980 continue;
981 }
982 return false;
983 }
984 strictly_smaller
985 })
986}
987
988pub(crate) fn single_list_structural_param_index(fd: &FnDef) -> Option<usize> {
989 fd.params
990 .iter()
991 .enumerate()
992 .find_map(|(param_index, (param_name, param_ty))| {
993 if !(param_ty.starts_with("List<") || param_ty == "List") {
994 return None;
995 }
996
997 let tails = collect_list_tail_binders(fd, param_name);
998 if tails.is_empty() {
999 return None;
1000 }
1001
1002 let recursive_calls: Vec<Option<&Spanned<Expr>>> =
1003 collect_calls_from_body(fd.body.as_ref())
1004 .into_iter()
1005 .filter(|(name, _)| call_matches(name, &fd.name))
1006 .map(|(_, args)| args.get(param_index).cloned())
1007 .collect();
1008 if recursive_calls.is_empty() {
1009 return None;
1010 }
1011
1012 recursive_calls
1013 .into_iter()
1014 .all(|arg| {
1015 arg.and_then(local_name_of)
1016 .is_some_and(|id| tails.contains(id))
1017 })
1018 .then_some(param_index)
1019 })
1020}
1021
1022pub(crate) fn param_decremented_in_recursion(fd: &FnDef, param_index: usize) -> bool {
1030 let calls: Vec<(String, Vec<&Spanned<Expr>>)> = collect_calls_from_body(fd.body.as_ref())
1031 .into_iter()
1032 .filter(|(name, _)| call_matches(name, &fd.name))
1033 .collect();
1034 if calls.is_empty() {
1035 return false;
1036 }
1037 let Some((pname, _)) = fd.params.get(param_index) else {
1038 return false;
1039 };
1040 calls.iter().all(|(_, args)| {
1041 args.get(param_index)
1042 .and_then(|a| local_name_of(a))
1043 .is_some_and(|id| id != pname)
1044 })
1045}
1046
1047pub(crate) fn param_threaded_in_recursion(fd: &FnDef, param_index: usize) -> bool {
1056 let calls: Vec<(String, Vec<&Spanned<Expr>>)> = collect_calls_from_body(fd.body.as_ref())
1057 .into_iter()
1058 .filter(|(name, _)| call_matches(name, &fd.name))
1059 .collect();
1060 if calls.is_empty() {
1061 return false;
1062 }
1063 calls.iter().all(|(_, args)| {
1064 args.get(param_index)
1065 .is_some_and(|a| local_name_of(a).is_none())
1066 })
1067}
1068
1069pub(crate) fn recurses_decrementing_both_args(fd: &FnDef) -> bool {
1084 if fd.params.len() != 2 {
1085 return false;
1086 }
1087 let (p0, t0) = &fd.params[0];
1088 let (p1, t1) = &fd.params[1];
1089 if t0 != t1 {
1090 return false;
1091 }
1092 let calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1094 .into_iter()
1095 .filter(|(name, _)| call_matches(name, &fd.name))
1096 .map(|(_, args)| args)
1097 .collect();
1098 if calls.len() != 1 {
1099 return false;
1100 }
1101 let args = &calls[0];
1102 let (Some(a0), Some(a1)) = (
1103 args.first().and_then(|a| local_name_of(a)),
1104 args.get(1).and_then(|a| local_name_of(a)),
1105 ) else {
1106 return false;
1107 };
1108 if a0 == p0 || a0 == p1 || a1 == p0 || a1 == p1 || a0 == a1 {
1109 return false;
1110 }
1111 let Some(tail) = fd.body.tail_expr() else {
1115 return false;
1116 };
1117 let Expr::Match { subject, arms, .. } = &tail.node else {
1118 return false;
1119 };
1120 if local_name_of(subject) != Some(p0.as_str()) {
1121 return false;
1122 }
1123 arms.iter().any(|arm| {
1124 let Pattern::Constructor(_, binders) = &arm.pattern else {
1125 return false;
1126 };
1127 if binders.len() != 1 || binders[0] != a0 {
1128 return false;
1129 }
1130 let Expr::Match {
1131 subject: inner_subj,
1132 arms: inner_arms,
1133 ..
1134 } = &arm.body.node
1135 else {
1136 return false;
1137 };
1138 if local_name_of(inner_subj) != Some(p1.as_str()) {
1139 return false;
1140 }
1141 inner_arms.iter().any(|inner| {
1142 matches!(&inner.pattern,
1143 Pattern::Constructor(_, b) if b.len() == 1 && b[0] == a1)
1144 })
1145 })
1146}
1147
1148pub(crate) fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
1149 local_name_of(expr).is_some_and(|id| id == name)
1150}
1151
1152pub(crate) fn is_int_plus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
1153 match &expr.node {
1154 Expr::BinOp(BinOp::Add, left, right) => {
1155 local_name_of(left).is_some_and(|id| id == param_name)
1156 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1157 }
1158 Expr::FnCall(callee, args) => {
1159 let Some(name) = expr_to_dotted_name(callee) else {
1160 return false;
1161 };
1162 (name == "Int.add" || name == "int.add")
1163 && args.len() == 2
1164 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
1165 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1166 }
1167 _ => false,
1168 }
1169}
1170
1171pub(crate) fn is_skip_ws_advance(
1172 expr: &Spanned<Expr>,
1173 string_param: &str,
1174 pos_param: &str,
1175) -> bool {
1176 let Expr::FnCall(callee, args) = &expr.node else {
1177 return false;
1178 };
1179 let Some(name) = expr_to_dotted_name(callee) else {
1180 return false;
1181 };
1182 if !call_matches(&name, "skipWs") || args.len() != 2 {
1183 return false;
1184 }
1185 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
1186}
1187
1188pub(crate) fn is_skip_ws_same(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
1189 let Expr::FnCall(callee, args) = &expr.node else {
1190 return false;
1191 };
1192 let Some(name) = expr_to_dotted_name(callee) else {
1193 return false;
1194 };
1195 if !call_matches(&name, "skipWs") || args.len() != 2 {
1196 return false;
1197 }
1198 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
1199}
1200
1201pub(crate) fn is_string_pos_advance(
1202 expr: &Spanned<Expr>,
1203 string_param: &str,
1204 pos_param: &str,
1205) -> bool {
1206 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
1207}
1208
1209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1210pub(crate) enum StringPosEdge {
1211 Same,
1212 Advance,
1213}
1214
1215pub(crate) fn classify_string_pos_edge(
1216 expr: &Spanned<Expr>,
1217 string_param: &str,
1218 pos_param: &str,
1219) -> Option<StringPosEdge> {
1220 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
1221 return Some(StringPosEdge::Same);
1222 }
1223 if is_string_pos_advance(expr, string_param, pos_param) {
1224 return Some(StringPosEdge::Advance);
1225 }
1226 if let Expr::FnCall(callee, args) = &expr.node {
1227 let name = expr_to_dotted_name(callee)?;
1228 if call_matches(&name, "skipWs")
1229 && args.len() == 2
1230 && is_ident(&args[0], string_param)
1231 && local_name_of(&args[1]).is_some_and(|id| id != pos_param)
1232 {
1233 return Some(StringPosEdge::Advance);
1234 }
1235 }
1236 if local_name_of(expr).is_some_and(|id| id != pos_param) {
1237 return Some(StringPosEdge::Advance);
1238 }
1239 None
1240}
1241
1242pub(crate) fn ranks_from_same_edges(
1243 names: &HashSet<String>,
1244 same_edges: &HashMap<String, HashSet<String>>,
1245) -> Option<HashMap<String, usize>> {
1246 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
1247 for outs in same_edges.values() {
1248 for to in outs {
1249 if let Some(entry) = indegree.get_mut(to) {
1250 *entry += 1;
1251 } else {
1252 return None;
1253 }
1254 }
1255 }
1256
1257 let mut queue: Vec<String> = indegree
1258 .iter()
1259 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
1260 .collect();
1261 queue.sort();
1262 let mut topo = Vec::new();
1263 while let Some(node) = queue.pop() {
1264 topo.push(node.clone());
1265 let outs = same_edges.get(&node).cloned().unwrap_or_default();
1266 let mut newly_zero = Vec::new();
1267 for to in outs {
1268 if let Some(entry) = indegree.get_mut(&to) {
1269 *entry -= 1;
1270 if *entry == 0 {
1271 newly_zero.push(to);
1272 }
1273 } else {
1274 return None;
1275 }
1276 }
1277 newly_zero.sort();
1278 queue.extend(newly_zero);
1279 }
1280
1281 if topo.len() != names.len() {
1282 return None;
1283 }
1284
1285 let n = topo.len();
1286 let mut ranks = HashMap::new();
1287 for (idx, name) in topo.into_iter().enumerate() {
1288 ranks.insert(name, n - idx);
1289 }
1290 Some(ranks)
1291}
1292
1293pub(crate) fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
1294 let Some((string_param, string_ty)) = fd.params.first() else {
1295 return false;
1296 };
1297 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
1298 return false;
1299 };
1300 if string_ty != "String" || pos_ty != "Int" {
1301 return false;
1302 }
1303
1304 type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
1305 let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
1306 .into_iter()
1307 .filter(|(name, _)| call_matches(name, &fd.name))
1308 .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
1309 .collect();
1310 if recursive_calls.is_empty() {
1311 return false;
1312 }
1313
1314 recursive_calls.into_iter().all(|(arg0, arg1)| {
1315 arg0.is_some_and(|e| is_ident(e, string_param))
1316 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
1317 })
1318}
1319
1320pub(crate) fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
1321 if component.len() < 2 {
1322 return false;
1323 }
1324 if component
1325 .iter()
1326 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
1327 {
1328 return false;
1329 }
1330 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1331 let mut any_intra = false;
1332 for fd in component {
1333 let param_name = &fd.params[0].0;
1334 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
1335 if !call_is_in_set(&callee, &names) {
1336 continue;
1337 }
1338 any_intra = true;
1339 let Some(arg0) = args.first().cloned() else {
1340 return false;
1341 };
1342 if !is_int_minus_positive(arg0, param_name) {
1343 return false;
1344 }
1345 }
1346 }
1347 any_intra
1348}
1349
1350pub(crate) fn supports_mutual_string_pos_advance(
1351 component: &[&FnDef],
1352) -> Option<HashMap<String, usize>> {
1353 if component.len() < 2 {
1354 return None;
1355 }
1356 if component.iter().any(|fd| {
1357 !matches!(fd.params.first(), Some((_, t)) if t == "String")
1358 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
1359 }) {
1360 return None;
1361 }
1362
1363 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1364 let mut same_edges: HashMap<String, HashSet<String>> =
1365 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1366 let mut any_intra = false;
1367
1368 for fd in component {
1369 let string_param = &fd.params[0].0;
1370 let pos_param = &fd.params[1].0;
1371 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1372 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1373 continue;
1374 };
1375 any_intra = true;
1376
1377 let arg0 = args.first().cloned()?;
1378 let arg1 = args.get(1).cloned()?;
1379
1380 if !is_ident(arg0, string_param) {
1381 return None;
1382 }
1383
1384 match classify_string_pos_edge(arg1, string_param, pos_param) {
1385 Some(StringPosEdge::Same) => {
1386 if let Some(edges) = same_edges.get_mut(&fd.name) {
1387 edges.insert(callee);
1388 } else {
1389 return None;
1390 }
1391 }
1392 Some(StringPosEdge::Advance) => {}
1393 None => return None,
1394 }
1395 }
1396 }
1397
1398 if !any_intra {
1399 return None;
1400 }
1401
1402 ranks_from_same_edges(&names, &same_edges)
1403}
1404
1405pub(crate) fn is_scalar_like_type(type_name: &str) -> bool {
1406 matches!(
1407 type_name,
1408 "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
1409 )
1410}
1411
1412pub fn sizeof_measure_param_indices(fd: &FnDef) -> Vec<usize> {
1418 fd.params
1419 .iter()
1420 .enumerate()
1421 .filter_map(|(idx, (_, type_name))| (!is_scalar_like_type(type_name)).then_some(idx))
1422 .collect()
1423}
1424
1425pub fn scc_has_growing_accumulator(fns: &[&FnDef]) -> bool {
1433 let names: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1434 let mut sizeof_indices: HashMap<String, Vec<usize>> = HashMap::new();
1435 for fd in fns {
1436 sizeof_indices.insert(fd.name.clone(), sizeof_measure_param_indices(fd));
1437 }
1438 for fd in fns {
1439 if body_has_intra_scc_call_in_interpolation(fd.body.as_ref(), &names) {
1447 return true;
1448 }
1449 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1450 let Some(callee_name) = canonical_callee_name(&callee_raw, &names) else {
1451 continue;
1452 };
1453 let Some(callee_indices) = sizeof_indices.get(&callee_name) else {
1454 continue;
1455 };
1456 for callee_idx in callee_indices {
1457 let Some(arg) = args.get(*callee_idx) else {
1458 continue;
1459 };
1460 if !arg_is_non_growing(arg) {
1461 return true;
1462 }
1463 }
1464 }
1465 }
1466 false
1467}
1468
1469fn body_has_intra_scc_call_in_interpolation(body: &FnBody, names: &HashSet<String>) -> bool {
1470 let FnBody::Block(stmts) = body;
1471 stmts.iter().any(|s| match s {
1472 Stmt::Binding(_, _, e) | Stmt::Expr(e) => {
1473 expr_has_intra_scc_call_in_interpolation(e, names)
1474 }
1475 })
1476}
1477
1478fn expr_has_intra_scc_call_in_interpolation(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1479 use crate::ast::StrPart;
1480 match &expr.node {
1481 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1482 StrPart::Parsed(inner) => expr_calls_into_set(inner, names),
1483 _ => false,
1484 }),
1485 Expr::Match { subject, arms, .. } => {
1486 expr_has_intra_scc_call_in_interpolation(subject, names)
1487 || arms
1488 .iter()
1489 .any(|a| expr_has_intra_scc_call_in_interpolation(&a.body, names))
1490 }
1491 Expr::FnCall(f, args) => {
1492 expr_has_intra_scc_call_in_interpolation(f, names)
1493 || args
1494 .iter()
1495 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names))
1496 }
1497 Expr::TailCall(boxed) => boxed
1498 .args
1499 .iter()
1500 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names)),
1501 Expr::BinOp(_, l, r) => {
1502 expr_has_intra_scc_call_in_interpolation(l, names)
1503 || expr_has_intra_scc_call_in_interpolation(r, names)
1504 }
1505 Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::Neg(inner) => {
1506 expr_has_intra_scc_call_in_interpolation(inner, names)
1507 }
1508 _ => false,
1509 }
1510}
1511
1512fn expr_calls_into_set(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1513 match &expr.node {
1514 Expr::FnCall(f, args) => {
1515 let head_hit = expr_to_dotted_name(f)
1516 .as_deref()
1517 .map(|n| canonical_callee_name(n, names).is_some())
1518 .unwrap_or(false);
1519 head_hit
1520 || args.iter().any(|a| expr_calls_into_set(a, names))
1521 || expr_calls_into_set(f, names)
1522 }
1523 Expr::TailCall(boxed) => {
1524 canonical_callee_name(&boxed.target, names).is_some()
1525 || boxed.args.iter().any(|a| expr_calls_into_set(a, names))
1526 }
1527 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::Constructor(_, Some(inner)) => {
1528 expr_calls_into_set(inner, names)
1529 }
1530 Expr::BinOp(_, l, r) => expr_calls_into_set(l, names) || expr_calls_into_set(r, names),
1531 Expr::Match { subject, arms, .. } => {
1532 expr_calls_into_set(subject, names)
1533 || arms.iter().any(|a| expr_calls_into_set(&a.body, names))
1534 }
1535 _ => false,
1536 }
1537}
1538
1539fn arg_is_non_growing(expr: &Spanned<Expr>) -> bool {
1540 matches!(
1541 &expr.node,
1542 Expr::Ident(_) | Expr::Resolved { .. } | Expr::Literal(_) | Expr::Attr(..)
1543 )
1544}
1545
1546pub(crate) fn supports_mutual_sizeof_ranked(
1547 component: &[&FnDef],
1548) -> Option<HashMap<String, usize>> {
1549 if component.len() < 2 {
1550 return None;
1551 }
1552 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1553 let metric_indices: HashMap<String, Vec<usize>> = component
1554 .iter()
1555 .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
1556 .collect();
1557 if component.iter().any(|fd| {
1558 metric_indices
1559 .get(&fd.name)
1560 .is_none_or(|indices| indices.is_empty())
1561 }) {
1562 return None;
1563 }
1564
1565 let mut same_edges: HashMap<String, HashSet<String>> =
1566 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1567 let mut any_intra = false;
1568 for fd in component {
1569 let caller_metric_indices = metric_indices.get(&fd.name)?;
1570 let caller_metric_params: Vec<&str> = caller_metric_indices
1571 .iter()
1572 .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
1573 .collect();
1574 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1575 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1576 continue;
1577 };
1578 any_intra = true;
1579 let callee_metric_indices = metric_indices.get(&callee)?;
1580 let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
1581 && callee_metric_indices
1582 .iter()
1583 .enumerate()
1584 .all(|(pos, callee_idx)| {
1585 let Some(arg) = args.get(*callee_idx).cloned() else {
1586 return false;
1587 };
1588 is_ident(arg, caller_metric_params[pos])
1589 });
1590 if is_same_edge {
1591 if let Some(edges) = same_edges.get_mut(&fd.name) {
1592 edges.insert(callee);
1593 } else {
1594 return None;
1595 }
1596 }
1597 }
1598 }
1599 if !any_intra {
1600 return None;
1601 }
1602
1603 let ranks = ranks_from_same_edges(&names, &same_edges)?;
1604 let mut out = HashMap::new();
1605 for fd in component {
1606 let rank = ranks.get(&fd.name).cloned()?;
1607 out.insert(fd.name.clone(), rank);
1608 }
1609 Some(out)
1610}
1611
1612pub(crate) fn is_closed_world_pure_fn(fn_name: &str, inputs: &ProofLowerInputs) -> bool {
1631 let entry_module = inputs.entry_items.iter().find_map(|item| match item {
1632 TopLevel::Module(m) => Some(m),
1633 _ => None,
1634 });
1635 let entry_fn_names: std::collections::HashSet<&str> = inputs
1636 .entry_items
1637 .iter()
1638 .filter_map(|item| match item {
1639 TopLevel::FnDef(fd) => Some(fd.name.as_str()),
1640 _ => None,
1641 })
1642 .collect();
1643 if entry_fn_names.contains(fn_name) {
1644 match entry_module {
1645 None => return true,
1646 Some(m) => {
1647 if m.exposes_line.is_none() {
1648 return true;
1649 }
1650 return !m.exposes.iter().any(|e| e == fn_name);
1651 }
1652 }
1653 }
1654 false
1655}
1656
1657pub(crate) fn find_single_external_caller_predicate(
1680 target_fn: &str,
1681 target_param_index: usize,
1682 callee_param_name: &str,
1683 inputs: &ProofLowerInputs,
1684) -> Option<Vec<Spanned<Expr>>> {
1685 let all_callers: Vec<&FnDef> = inputs
1686 .entry_items
1687 .iter()
1688 .filter_map(|item| match item {
1689 TopLevel::FnDef(fd) if fd.name != target_fn => Some(fd),
1690 _ => None,
1691 })
1692 .chain(
1693 inputs
1694 .dep_modules
1695 .iter()
1696 .flat_map(|m| m.fn_defs.iter().filter(|fd| fd.name != target_fn)),
1697 )
1698 .filter(|fd| {
1699 collect_calls_from_body(fd.body.as_ref())
1700 .iter()
1701 .any(|(n, _)| call_matches(n, target_fn))
1702 })
1703 .collect();
1704
1705 if all_callers.len() != 1 {
1706 return None;
1707 }
1708 let caller = all_callers[0];
1709
1710 let mut callsites: Vec<(Vec<Spanned<Expr>>, String)> = Vec::new();
1711 for stmt in caller.body.stmts() {
1712 match stmt {
1713 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
1714 walk_caller_collect_callsite_guards(
1715 expr,
1716 target_fn,
1717 target_param_index,
1718 &[],
1719 &mut callsites,
1720 );
1721 }
1722 }
1723 }
1724
1725 if callsites.is_empty() {
1726 return None;
1727 }
1728
1729 let (first_guards, first_arg_name) = &callsites[0];
1732 for (other_guards, other_arg_name) in &callsites[1..] {
1733 if other_arg_name != first_arg_name || other_guards != first_guards {
1734 return None;
1735 }
1736 }
1737
1738 let arg_name = first_arg_name.as_str();
1739 let predicate = first_guards
1740 .iter()
1741 .filter(|g| crate::codegen::recursion::expr_references_ident(g, arg_name))
1742 .map(|g| {
1743 crate::codegen::recursion::substitute_ident_in_expr(g, arg_name, callee_param_name)
1744 })
1745 .collect::<Vec<_>>();
1746
1747 Some(predicate)
1748}
1749
1750fn walk_caller_collect_callsite_guards(
1756 expr: &Spanned<Expr>,
1757 target_fn: &str,
1758 target_param_index: usize,
1759 enclosing_guards: &[Spanned<Expr>],
1760 out: &mut Vec<(Vec<Spanned<Expr>>, String)>,
1761) {
1762 match &expr.node {
1763 Expr::FnCall(callee, args) => {
1764 if let Some(name) = expr_to_dotted_name(callee)
1765 && call_matches(&name, target_fn)
1766 && let Some(arg_at_idx) = args.get(target_param_index)
1767 && let Some(arg_name) = local_name_of(arg_at_idx)
1768 {
1769 out.push((enclosing_guards.to_vec(), arg_name.to_string()));
1770 }
1771 walk_caller_collect_callsite_guards(
1772 callee,
1773 target_fn,
1774 target_param_index,
1775 enclosing_guards,
1776 out,
1777 );
1778 for arg in args {
1779 walk_caller_collect_callsite_guards(
1780 arg,
1781 target_fn,
1782 target_param_index,
1783 enclosing_guards,
1784 out,
1785 );
1786 }
1787 }
1788 Expr::TailCall(boxed) => {
1789 for arg in &boxed.args {
1790 walk_caller_collect_callsite_guards(
1791 arg,
1792 target_fn,
1793 target_param_index,
1794 enclosing_guards,
1795 out,
1796 );
1797 }
1798 }
1799 Expr::Match { subject, arms } => {
1800 for arm in arms {
1801 let mut new_guards: Vec<Spanned<Expr>> = enclosing_guards.to_vec();
1802 match &arm.pattern {
1803 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
1804 new_guards.push((**subject).clone());
1805 }
1806 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1807 if let Some(flipped) =
1808 crate::codegen::recursion::flip_comparison_binop(subject)
1809 {
1810 new_guards.push(flipped);
1811 } else {
1812 }
1817 }
1818 _ => {
1819 }
1824 }
1825 walk_caller_collect_callsite_guards(
1826 &arm.body,
1827 target_fn,
1828 target_param_index,
1829 &new_guards,
1830 out,
1831 );
1832 }
1833 walk_caller_collect_callsite_guards(
1834 subject,
1835 target_fn,
1836 target_param_index,
1837 enclosing_guards,
1838 out,
1839 );
1840 }
1841 Expr::BinOp(_, l, r) => {
1842 walk_caller_collect_callsite_guards(
1843 l,
1844 target_fn,
1845 target_param_index,
1846 enclosing_guards,
1847 out,
1848 );
1849 walk_caller_collect_callsite_guards(
1850 r,
1851 target_fn,
1852 target_param_index,
1853 enclosing_guards,
1854 out,
1855 );
1856 }
1857 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
1858 walk_caller_collect_callsite_guards(
1859 inner,
1860 target_fn,
1861 target_param_index,
1862 enclosing_guards,
1863 out,
1864 );
1865 }
1866 Expr::Constructor(_, arg) => {
1867 if let Some(inner) = arg {
1868 walk_caller_collect_callsite_guards(
1869 inner,
1870 target_fn,
1871 target_param_index,
1872 enclosing_guards,
1873 out,
1874 );
1875 }
1876 }
1877 Expr::InterpolatedStr(parts) => {
1878 for p in parts {
1879 if let crate::ast::StrPart::Parsed(inner) = p {
1880 walk_caller_collect_callsite_guards(
1881 inner,
1882 target_fn,
1883 target_param_index,
1884 enclosing_guards,
1885 out,
1886 );
1887 }
1888 }
1889 }
1890 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1891 for item in items {
1892 walk_caller_collect_callsite_guards(
1893 item,
1894 target_fn,
1895 target_param_index,
1896 enclosing_guards,
1897 out,
1898 );
1899 }
1900 }
1901 Expr::MapLiteral(entries) => {
1902 for (k, v) in entries {
1903 walk_caller_collect_callsite_guards(
1904 k,
1905 target_fn,
1906 target_param_index,
1907 enclosing_guards,
1908 out,
1909 );
1910 walk_caller_collect_callsite_guards(
1911 v,
1912 target_fn,
1913 target_param_index,
1914 enclosing_guards,
1915 out,
1916 );
1917 }
1918 }
1919 Expr::RecordCreate { fields, .. } => {
1920 for (_, v) in fields {
1921 walk_caller_collect_callsite_guards(
1922 v,
1923 target_fn,
1924 target_param_index,
1925 enclosing_guards,
1926 out,
1927 );
1928 }
1929 }
1930 Expr::RecordUpdate { base, updates, .. } => {
1931 walk_caller_collect_callsite_guards(
1932 base,
1933 target_fn,
1934 target_param_index,
1935 enclosing_guards,
1936 out,
1937 );
1938 for (_, v) in updates {
1939 walk_caller_collect_callsite_guards(
1940 v,
1941 target_fn,
1942 target_param_index,
1943 enclosing_guards,
1944 out,
1945 );
1946 }
1947 }
1948 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
1949 }
1950}
1951
1952pub fn analyze_plans_in_scope(
1975 inputs: &ProofLowerInputs,
1976 scope: Option<&str>,
1977 global_view: bool,
1978) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
1979 let mut plans = HashMap::new();
1980 let mut issues = Vec::new();
1981
1982 let all_pure = if global_view {
1983 inputs.pure_fns()
1984 } else {
1985 inputs.pure_fns_in_scope(scope)
1986 };
1987 let recursive_names = if global_view {
1988 inputs.recursive_pure_fn_names()
1989 } else {
1990 inputs.recursive_pure_fn_names_in_scope(scope)
1991 };
1992 let components = call_graph::ordered_fn_components(&all_pure, inputs.module_prefixes);
1993
1994 for component in components {
1995 if component.is_empty() {
1996 continue;
1997 }
1998 let is_recursive_component =
1999 component.len() > 1 || recursive_names.contains(&component[0].name);
2000 if !is_recursive_component {
2001 continue;
2002 }
2003
2004 if component.len() > 1 {
2005 if supports_mutual_int_countdown(&component) {
2006 for fd in &component {
2007 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
2008 }
2009 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
2010 for fd in &component {
2011 if let Some(rank) = ranks.get(&fd.name).cloned() {
2012 plans.insert(
2013 fd.name.clone(),
2014 RecursionPlan::MutualStringPosAdvance { rank },
2015 );
2016 }
2017 }
2018 } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
2019 for fd in &component {
2020 if let Some(rank) = rankings.get(&fd.name).cloned() {
2021 plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
2022 }
2023 }
2024 } else {
2025 let names = component
2026 .iter()
2027 .map(|fd| fd.name.clone())
2028 .collect::<Vec<_>>()
2029 .join(", ");
2030 let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
2031 issues.push(ProofModeIssue {
2032 line,
2033 message: format!(
2034 "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
2035 names
2036 ),
2037 });
2038 }
2039 continue;
2040 }
2041
2042 let fd = component[0];
2043 if crate::codegen::lean::recurrence::detect_second_order_int_linear_recurrence(fd).is_some()
2044 {
2045 plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
2046 } else if let Some((param_index, bound)) = single_int_ascending_param(fd) {
2047 plans.insert(
2048 fd.name.clone(),
2049 RecursionPlan::IntAscending { param_index, bound },
2050 );
2051 } else if let Some(param_index) = single_int_countdown_param_index(fd) {
2052 if is_closed_world_pure_fn(&fd.name, inputs)
2060 && let Some((base_arm_literal, base_arm_body, wildcard_arm_body)) =
2061 int_countdown_native_arms(fd, param_index)
2062 && let Some((callee_param_name, _)) = fd.params.get(param_index)
2063 {
2064 let precondition = find_single_external_caller_predicate(
2069 &fd.name,
2070 param_index,
2071 callee_param_name,
2072 inputs,
2073 )
2074 .unwrap_or_default();
2075 plans.insert(
2076 fd.name.clone(),
2077 RecursionPlan::IntCountdownGuarded {
2078 param_index,
2079 base_arm_literal,
2080 base_arm_body,
2081 wildcard_arm_body,
2082 precondition,
2083 },
2084 );
2085 } else {
2086 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
2087 }
2088 } else if let Some((param_index, divisor, helper_fn)) =
2089 single_int_floor_div_countdown(fd, inputs)
2090 {
2091 plans.insert(
2098 fd.name.clone(),
2099 RecursionPlan::IntFloorDivCountdown {
2100 param_index,
2101 divisor,
2102 helper_fn,
2103 },
2104 );
2105 } else if supports_single_sizeof_structural(fd, inputs) {
2106 plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
2107 } else if let Some(param_index) = single_list_structural_param_index(fd) {
2108 plans.insert(
2109 fd.name.clone(),
2110 RecursionPlan::ListStructural { param_index },
2111 );
2112 } else if supports_single_string_pos_advance(fd) {
2113 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
2114 } else {
2115 issues.push(ProofModeIssue {
2116 line: fd.line,
2117 message: format!(
2118 "recursive function '{}' is outside proof subset (currently supported: Int countdown, guard-validated Int floor-division countdown by a literal divisor, second-order affine Int recurrences with pair-state worker, structural recursion on List/recursive ADTs, String+position, mutual Int countdown, mutual String+position, and ranked sizeOf recursion)",
2119 fd.name
2120 ),
2121 });
2122 }
2123 }
2124
2125 (plans, issues)
2126}