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_adt_structural_param_index(
1006 fd: &FnDef,
1007 inputs: &ProofLowerInputs,
1008) -> Option<usize> {
1009 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1010 .into_iter()
1011 .filter(|(name, _)| call_matches(name, &fd.name))
1012 .map(|(_, args)| args)
1013 .collect();
1014 if recursive_calls.is_empty() {
1015 return None;
1016 }
1017 let recursive_types = inputs.recursive_type_names();
1018 fd.params
1019 .iter()
1020 .enumerate()
1021 .find_map(|(param_index, (param_name, param_type))| {
1022 if !recursive_types.contains(param_type) {
1023 return None;
1024 }
1025 let binders = collect_recursive_subterm_binders(fd, param_name, param_type, inputs);
1026 if binders.is_empty() {
1027 return None;
1028 }
1029 recursive_calls
1030 .iter()
1031 .all(|args| {
1032 args.get(param_index)
1033 .and_then(|a| local_name_of(a))
1034 .is_some_and(|id| binders.contains(id))
1035 })
1036 .then_some(param_index)
1037 })
1038}
1039
1040pub(crate) fn single_list_structural_param_index(fd: &FnDef) -> Option<usize> {
1041 fd.params
1042 .iter()
1043 .enumerate()
1044 .find_map(|(param_index, (param_name, param_ty))| {
1045 if !(param_ty.starts_with("List<") || param_ty == "List") {
1046 return None;
1047 }
1048
1049 let tails = collect_list_tail_binders(fd, param_name);
1050 if tails.is_empty() {
1051 return None;
1052 }
1053
1054 let recursive_calls: Vec<Option<&Spanned<Expr>>> =
1055 collect_calls_from_body(fd.body.as_ref())
1056 .into_iter()
1057 .filter(|(name, _)| call_matches(name, &fd.name))
1058 .map(|(_, args)| args.get(param_index).cloned())
1059 .collect();
1060 if recursive_calls.is_empty() {
1061 return None;
1062 }
1063
1064 recursive_calls
1065 .into_iter()
1066 .all(|arg| {
1067 arg.and_then(local_name_of)
1068 .is_some_and(|id| tails.contains(id))
1069 })
1070 .then_some(param_index)
1071 })
1072}
1073
1074pub(crate) fn param_decremented_in_recursion(fd: &FnDef, param_index: usize) -> bool {
1082 let calls: Vec<(String, Vec<&Spanned<Expr>>)> = collect_calls_from_body(fd.body.as_ref())
1083 .into_iter()
1084 .filter(|(name, _)| call_matches(name, &fd.name))
1085 .collect();
1086 if calls.is_empty() {
1087 return false;
1088 }
1089 let Some((pname, _)) = fd.params.get(param_index) else {
1090 return false;
1091 };
1092 calls.iter().all(|(_, args)| {
1093 args.get(param_index)
1094 .and_then(|a| local_name_of(a))
1095 .is_some_and(|id| id != pname)
1096 })
1097}
1098
1099pub(crate) fn param_threaded_in_recursion(fd: &FnDef, param_index: usize) -> bool {
1108 let calls: Vec<(String, Vec<&Spanned<Expr>>)> = collect_calls_from_body(fd.body.as_ref())
1109 .into_iter()
1110 .filter(|(name, _)| call_matches(name, &fd.name))
1111 .collect();
1112 if calls.is_empty() {
1113 return false;
1114 }
1115 calls.iter().all(|(_, args)| {
1116 args.get(param_index)
1117 .is_some_and(|a| local_name_of(a).is_none())
1118 })
1119}
1120
1121pub(crate) fn recurses_decrementing_both_args(fd: &FnDef) -> bool {
1136 if fd.params.len() != 2 {
1137 return false;
1138 }
1139 let (p0, t0) = &fd.params[0];
1140 let (p1, t1) = &fd.params[1];
1141 if t0 != t1 {
1142 return false;
1143 }
1144 let calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1146 .into_iter()
1147 .filter(|(name, _)| call_matches(name, &fd.name))
1148 .map(|(_, args)| args)
1149 .collect();
1150 if calls.len() != 1 {
1151 return false;
1152 }
1153 let args = &calls[0];
1154 let (Some(a0), Some(a1)) = (
1155 args.first().and_then(|a| local_name_of(a)),
1156 args.get(1).and_then(|a| local_name_of(a)),
1157 ) else {
1158 return false;
1159 };
1160 if a0 == p0 || a0 == p1 || a1 == p0 || a1 == p1 || a0 == a1 {
1161 return false;
1162 }
1163 let Some(tail) = fd.body.tail_expr() else {
1167 return false;
1168 };
1169 let Expr::Match { subject, arms, .. } = &tail.node else {
1170 return false;
1171 };
1172 if local_name_of(subject) != Some(p0.as_str()) {
1173 return false;
1174 }
1175 arms.iter().any(|arm| {
1176 let Pattern::Constructor(_, binders) = &arm.pattern else {
1177 return false;
1178 };
1179 if binders.len() != 1 || binders[0] != a0 {
1180 return false;
1181 }
1182 let Expr::Match {
1183 subject: inner_subj,
1184 arms: inner_arms,
1185 ..
1186 } = &arm.body.node
1187 else {
1188 return false;
1189 };
1190 if local_name_of(inner_subj) != Some(p1.as_str()) {
1191 return false;
1192 }
1193 inner_arms.iter().any(|inner| {
1194 matches!(&inner.pattern,
1195 Pattern::Constructor(_, b) if b.len() == 1 && b[0] == a1)
1196 })
1197 })
1198}
1199
1200pub(crate) fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
1201 local_name_of(expr).is_some_and(|id| id == name)
1202}
1203
1204pub(crate) fn is_int_plus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
1205 match &expr.node {
1206 Expr::BinOp(BinOp::Add, left, right) => {
1207 local_name_of(left).is_some_and(|id| id == param_name)
1208 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1209 }
1210 Expr::FnCall(callee, args) => {
1211 let Some(name) = expr_to_dotted_name(callee) else {
1212 return false;
1213 };
1214 (name == "Int.add" || name == "int.add")
1215 && args.len() == 2
1216 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
1217 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1218 }
1219 _ => false,
1220 }
1221}
1222
1223pub(crate) fn is_skip_ws_advance(
1224 expr: &Spanned<Expr>,
1225 string_param: &str,
1226 pos_param: &str,
1227) -> bool {
1228 let Expr::FnCall(callee, args) = &expr.node else {
1229 return false;
1230 };
1231 let Some(name) = expr_to_dotted_name(callee) else {
1232 return false;
1233 };
1234 if !call_matches(&name, "skipWs") || args.len() != 2 {
1235 return false;
1236 }
1237 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
1238}
1239
1240pub(crate) fn is_skip_ws_same(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
1241 let Expr::FnCall(callee, args) = &expr.node else {
1242 return false;
1243 };
1244 let Some(name) = expr_to_dotted_name(callee) else {
1245 return false;
1246 };
1247 if !call_matches(&name, "skipWs") || args.len() != 2 {
1248 return false;
1249 }
1250 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
1251}
1252
1253pub(crate) fn is_string_pos_advance(
1254 expr: &Spanned<Expr>,
1255 string_param: &str,
1256 pos_param: &str,
1257) -> bool {
1258 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
1259}
1260
1261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1262pub(crate) enum StringPosEdge {
1263 Same,
1264 Advance,
1265}
1266
1267pub(crate) fn classify_string_pos_edge(
1268 expr: &Spanned<Expr>,
1269 string_param: &str,
1270 pos_param: &str,
1271) -> Option<StringPosEdge> {
1272 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
1273 return Some(StringPosEdge::Same);
1274 }
1275 if is_string_pos_advance(expr, string_param, pos_param) {
1276 return Some(StringPosEdge::Advance);
1277 }
1278 if let Expr::FnCall(callee, args) = &expr.node {
1279 let name = expr_to_dotted_name(callee)?;
1280 if call_matches(&name, "skipWs")
1281 && args.len() == 2
1282 && is_ident(&args[0], string_param)
1283 && local_name_of(&args[1]).is_some_and(|id| id != pos_param)
1284 {
1285 return Some(StringPosEdge::Advance);
1286 }
1287 }
1288 if local_name_of(expr).is_some_and(|id| id != pos_param) {
1289 return Some(StringPosEdge::Advance);
1290 }
1291 None
1292}
1293
1294pub(crate) fn ranks_from_same_edges(
1295 names: &HashSet<String>,
1296 same_edges: &HashMap<String, HashSet<String>>,
1297) -> Option<HashMap<String, usize>> {
1298 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
1299 for outs in same_edges.values() {
1300 for to in outs {
1301 if let Some(entry) = indegree.get_mut(to) {
1302 *entry += 1;
1303 } else {
1304 return None;
1305 }
1306 }
1307 }
1308
1309 let mut queue: Vec<String> = indegree
1310 .iter()
1311 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
1312 .collect();
1313 queue.sort();
1314 let mut topo = Vec::new();
1315 while let Some(node) = queue.pop() {
1316 topo.push(node.clone());
1317 let outs = same_edges.get(&node).cloned().unwrap_or_default();
1318 let mut newly_zero = Vec::new();
1319 for to in outs {
1320 if let Some(entry) = indegree.get_mut(&to) {
1321 *entry -= 1;
1322 if *entry == 0 {
1323 newly_zero.push(to);
1324 }
1325 } else {
1326 return None;
1327 }
1328 }
1329 newly_zero.sort();
1330 queue.extend(newly_zero);
1331 }
1332
1333 if topo.len() != names.len() {
1334 return None;
1335 }
1336
1337 let n = topo.len();
1338 let mut ranks = HashMap::new();
1339 for (idx, name) in topo.into_iter().enumerate() {
1340 ranks.insert(name, n - idx);
1341 }
1342 Some(ranks)
1343}
1344
1345pub(crate) fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
1346 let Some((string_param, string_ty)) = fd.params.first() else {
1347 return false;
1348 };
1349 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
1350 return false;
1351 };
1352 if string_ty != "String" || pos_ty != "Int" {
1353 return false;
1354 }
1355
1356 type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
1357 let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
1358 .into_iter()
1359 .filter(|(name, _)| call_matches(name, &fd.name))
1360 .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
1361 .collect();
1362 if recursive_calls.is_empty() {
1363 return false;
1364 }
1365
1366 recursive_calls.into_iter().all(|(arg0, arg1)| {
1367 arg0.is_some_and(|e| is_ident(e, string_param))
1368 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
1369 })
1370}
1371
1372pub(crate) fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
1373 if component.len() < 2 {
1374 return false;
1375 }
1376 if component
1377 .iter()
1378 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
1379 {
1380 return false;
1381 }
1382 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1383 let mut any_intra = false;
1384 for fd in component {
1385 let param_name = &fd.params[0].0;
1386 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
1387 if !call_is_in_set(&callee, &names) {
1388 continue;
1389 }
1390 any_intra = true;
1391 let Some(arg0) = args.first().cloned() else {
1392 return false;
1393 };
1394 if !is_int_minus_positive(arg0, param_name) {
1395 return false;
1396 }
1397 }
1398 }
1399 any_intra
1400}
1401
1402pub(crate) fn supports_mutual_string_pos_advance(
1403 component: &[&FnDef],
1404) -> Option<HashMap<String, usize>> {
1405 if component.len() < 2 {
1406 return None;
1407 }
1408 if component.iter().any(|fd| {
1409 !matches!(fd.params.first(), Some((_, t)) if t == "String")
1410 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
1411 }) {
1412 return None;
1413 }
1414
1415 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1416 let mut same_edges: HashMap<String, HashSet<String>> =
1417 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1418 let mut any_intra = false;
1419
1420 for fd in component {
1421 let string_param = &fd.params[0].0;
1422 let pos_param = &fd.params[1].0;
1423 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1424 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1425 continue;
1426 };
1427 any_intra = true;
1428
1429 let arg0 = args.first().cloned()?;
1430 let arg1 = args.get(1).cloned()?;
1431
1432 if !is_ident(arg0, string_param) {
1433 return None;
1434 }
1435
1436 match classify_string_pos_edge(arg1, string_param, pos_param) {
1437 Some(StringPosEdge::Same) => {
1438 if let Some(edges) = same_edges.get_mut(&fd.name) {
1439 edges.insert(callee);
1440 } else {
1441 return None;
1442 }
1443 }
1444 Some(StringPosEdge::Advance) => {}
1445 None => return None,
1446 }
1447 }
1448 }
1449
1450 if !any_intra {
1451 return None;
1452 }
1453
1454 ranks_from_same_edges(&names, &same_edges)
1455}
1456
1457pub(crate) fn is_scalar_like_type(type_name: &str) -> bool {
1458 matches!(
1459 type_name,
1460 "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
1461 )
1462}
1463
1464pub fn sizeof_measure_param_indices(fd: &FnDef) -> Vec<usize> {
1470 fd.params
1471 .iter()
1472 .enumerate()
1473 .filter_map(|(idx, (_, type_name))| (!is_scalar_like_type(type_name)).then_some(idx))
1474 .collect()
1475}
1476
1477pub fn scc_has_growing_accumulator(fns: &[&FnDef]) -> bool {
1485 let names: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1486 let mut sizeof_indices: HashMap<String, Vec<usize>> = HashMap::new();
1487 for fd in fns {
1488 sizeof_indices.insert(fd.name.clone(), sizeof_measure_param_indices(fd));
1489 }
1490 for fd in fns {
1491 if body_has_intra_scc_call_in_interpolation(fd.body.as_ref(), &names) {
1499 return true;
1500 }
1501 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1502 let Some(callee_name) = canonical_callee_name(&callee_raw, &names) else {
1503 continue;
1504 };
1505 let Some(callee_indices) = sizeof_indices.get(&callee_name) else {
1506 continue;
1507 };
1508 for callee_idx in callee_indices {
1509 let Some(arg) = args.get(*callee_idx) else {
1510 continue;
1511 };
1512 if !arg_is_non_growing(arg) {
1513 return true;
1514 }
1515 }
1516 }
1517 }
1518 false
1519}
1520
1521fn body_has_intra_scc_call_in_interpolation(body: &FnBody, names: &HashSet<String>) -> bool {
1522 let FnBody::Block(stmts) = body;
1523 stmts.iter().any(|s| match s {
1524 Stmt::Binding(_, _, e) | Stmt::Expr(e) => {
1525 expr_has_intra_scc_call_in_interpolation(e, names)
1526 }
1527 })
1528}
1529
1530fn expr_has_intra_scc_call_in_interpolation(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1531 use crate::ast::StrPart;
1532 match &expr.node {
1533 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1534 StrPart::Parsed(inner) => expr_calls_into_set(inner, names),
1535 _ => false,
1536 }),
1537 Expr::Match { subject, arms, .. } => {
1538 expr_has_intra_scc_call_in_interpolation(subject, names)
1539 || arms
1540 .iter()
1541 .any(|a| expr_has_intra_scc_call_in_interpolation(&a.body, names))
1542 }
1543 Expr::FnCall(f, args) => {
1544 expr_has_intra_scc_call_in_interpolation(f, names)
1545 || args
1546 .iter()
1547 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names))
1548 }
1549 Expr::TailCall(boxed) => boxed
1550 .args
1551 .iter()
1552 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names)),
1553 Expr::BinOp(_, l, r) => {
1554 expr_has_intra_scc_call_in_interpolation(l, names)
1555 || expr_has_intra_scc_call_in_interpolation(r, names)
1556 }
1557 Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::Neg(inner) => {
1558 expr_has_intra_scc_call_in_interpolation(inner, names)
1559 }
1560 _ => false,
1561 }
1562}
1563
1564fn expr_calls_into_set(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1565 match &expr.node {
1566 Expr::FnCall(f, args) => {
1567 let head_hit = expr_to_dotted_name(f)
1568 .as_deref()
1569 .map(|n| canonical_callee_name(n, names).is_some())
1570 .unwrap_or(false);
1571 head_hit
1572 || args.iter().any(|a| expr_calls_into_set(a, names))
1573 || expr_calls_into_set(f, names)
1574 }
1575 Expr::TailCall(boxed) => {
1576 canonical_callee_name(&boxed.target, names).is_some()
1577 || boxed.args.iter().any(|a| expr_calls_into_set(a, names))
1578 }
1579 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::Constructor(_, Some(inner)) => {
1580 expr_calls_into_set(inner, names)
1581 }
1582 Expr::BinOp(_, l, r) => expr_calls_into_set(l, names) || expr_calls_into_set(r, names),
1583 Expr::Match { subject, arms, .. } => {
1584 expr_calls_into_set(subject, names)
1585 || arms.iter().any(|a| expr_calls_into_set(&a.body, names))
1586 }
1587 _ => false,
1588 }
1589}
1590
1591fn arg_is_non_growing(expr: &Spanned<Expr>) -> bool {
1592 match &expr.node {
1593 Expr::Ident(_) | Expr::Resolved { .. } | Expr::Literal(_) | Expr::Attr(..) => true,
1594 Expr::FnCall(callee, args) => {
1602 expr_to_dotted_name(callee).as_deref() == Some("Map.entries")
1603 && args.len() == 1
1604 && arg_is_non_growing(&args[0])
1605 }
1606 _ => false,
1607 }
1608}
1609
1610pub(crate) fn supports_mutual_sizeof_ranked(
1611 component: &[&FnDef],
1612) -> Option<HashMap<String, usize>> {
1613 if component.len() < 2 {
1614 return None;
1615 }
1616 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1617 let metric_indices: HashMap<String, Vec<usize>> = component
1618 .iter()
1619 .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
1620 .collect();
1621 if component.iter().any(|fd| {
1622 metric_indices
1623 .get(&fd.name)
1624 .is_none_or(|indices| indices.is_empty())
1625 }) {
1626 return None;
1627 }
1628
1629 let mut same_edges: HashMap<String, HashSet<String>> =
1630 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1631 let mut any_intra = false;
1632 for fd in component {
1633 let caller_metric_indices = metric_indices.get(&fd.name)?;
1634 let caller_metric_params: Vec<&str> = caller_metric_indices
1635 .iter()
1636 .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
1637 .collect();
1638 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1639 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1640 continue;
1641 };
1642 any_intra = true;
1643 let callee_metric_indices = metric_indices.get(&callee)?;
1644 let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
1645 && callee_metric_indices
1646 .iter()
1647 .enumerate()
1648 .all(|(pos, callee_idx)| {
1649 let Some(arg) = args.get(*callee_idx).cloned() else {
1650 return false;
1651 };
1652 is_ident(arg, caller_metric_params[pos])
1653 });
1654 if is_same_edge {
1655 if let Some(edges) = same_edges.get_mut(&fd.name) {
1656 edges.insert(callee);
1657 } else {
1658 return None;
1659 }
1660 }
1661 }
1662 }
1663 if !any_intra {
1664 return None;
1665 }
1666
1667 let ranks = ranks_from_same_edges(&names, &same_edges)?;
1668 let mut out = HashMap::new();
1669 for fd in component {
1670 let rank = ranks.get(&fd.name).cloned()?;
1671 out.insert(fd.name.clone(), rank);
1672 }
1673 Some(out)
1674}
1675
1676pub(crate) fn is_closed_world_pure_fn(fn_name: &str, inputs: &ProofLowerInputs) -> bool {
1695 let entry_module = inputs.entry_items.iter().find_map(|item| match item {
1696 TopLevel::Module(m) => Some(m),
1697 _ => None,
1698 });
1699 let entry_fn_names: std::collections::HashSet<&str> = inputs
1700 .entry_items
1701 .iter()
1702 .filter_map(|item| match item {
1703 TopLevel::FnDef(fd) => Some(fd.name.as_str()),
1704 _ => None,
1705 })
1706 .collect();
1707 if entry_fn_names.contains(fn_name) {
1708 match entry_module {
1709 None => return true,
1710 Some(m) => {
1711 if m.exposes_line.is_none() {
1712 return true;
1713 }
1714 return !m.exposes.iter().any(|e| e == fn_name);
1715 }
1716 }
1717 }
1718 false
1719}
1720
1721pub(crate) fn find_single_external_caller_predicate(
1744 target_fn: &str,
1745 target_param_index: usize,
1746 callee_param_name: &str,
1747 inputs: &ProofLowerInputs,
1748) -> Option<Vec<Spanned<Expr>>> {
1749 let all_callers: Vec<&FnDef> = inputs
1750 .entry_items
1751 .iter()
1752 .filter_map(|item| match item {
1753 TopLevel::FnDef(fd) if fd.name != target_fn => Some(fd),
1754 _ => None,
1755 })
1756 .chain(
1757 inputs
1758 .dep_modules
1759 .iter()
1760 .flat_map(|m| m.fn_defs.iter().filter(|fd| fd.name != target_fn)),
1761 )
1762 .filter(|fd| {
1763 collect_calls_from_body(fd.body.as_ref())
1764 .iter()
1765 .any(|(n, _)| call_matches(n, target_fn))
1766 })
1767 .collect();
1768
1769 if all_callers.len() != 1 {
1770 return None;
1771 }
1772 let caller = all_callers[0];
1773
1774 let mut callsites: Vec<(Vec<Spanned<Expr>>, String)> = Vec::new();
1775 for stmt in caller.body.stmts() {
1776 match stmt {
1777 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
1778 walk_caller_collect_callsite_guards(
1779 expr,
1780 target_fn,
1781 target_param_index,
1782 &[],
1783 &mut callsites,
1784 );
1785 }
1786 }
1787 }
1788
1789 if callsites.is_empty() {
1790 return None;
1791 }
1792
1793 let (first_guards, first_arg_name) = &callsites[0];
1796 for (other_guards, other_arg_name) in &callsites[1..] {
1797 if other_arg_name != first_arg_name || other_guards != first_guards {
1798 return None;
1799 }
1800 }
1801
1802 let arg_name = first_arg_name.as_str();
1803 let predicate = first_guards
1804 .iter()
1805 .filter(|g| crate::codegen::recursion::expr_references_ident(g, arg_name))
1806 .map(|g| {
1807 crate::codegen::recursion::substitute_ident_in_expr(g, arg_name, callee_param_name)
1808 })
1809 .collect::<Vec<_>>();
1810
1811 Some(predicate)
1812}
1813
1814fn walk_caller_collect_callsite_guards(
1820 expr: &Spanned<Expr>,
1821 target_fn: &str,
1822 target_param_index: usize,
1823 enclosing_guards: &[Spanned<Expr>],
1824 out: &mut Vec<(Vec<Spanned<Expr>>, String)>,
1825) {
1826 match &expr.node {
1827 Expr::FnCall(callee, args) => {
1828 if let Some(name) = expr_to_dotted_name(callee)
1829 && call_matches(&name, target_fn)
1830 && let Some(arg_at_idx) = args.get(target_param_index)
1831 && let Some(arg_name) = local_name_of(arg_at_idx)
1832 {
1833 out.push((enclosing_guards.to_vec(), arg_name.to_string()));
1834 }
1835 walk_caller_collect_callsite_guards(
1836 callee,
1837 target_fn,
1838 target_param_index,
1839 enclosing_guards,
1840 out,
1841 );
1842 for arg in args {
1843 walk_caller_collect_callsite_guards(
1844 arg,
1845 target_fn,
1846 target_param_index,
1847 enclosing_guards,
1848 out,
1849 );
1850 }
1851 }
1852 Expr::TailCall(boxed) => {
1853 for arg in &boxed.args {
1854 walk_caller_collect_callsite_guards(
1855 arg,
1856 target_fn,
1857 target_param_index,
1858 enclosing_guards,
1859 out,
1860 );
1861 }
1862 }
1863 Expr::Match { subject, arms } => {
1864 for arm in arms {
1865 let mut new_guards: Vec<Spanned<Expr>> = enclosing_guards.to_vec();
1866 match &arm.pattern {
1867 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
1868 new_guards.push((**subject).clone());
1869 }
1870 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1871 if let Some(flipped) =
1872 crate::codegen::recursion::flip_comparison_binop(subject)
1873 {
1874 new_guards.push(flipped);
1875 } else {
1876 }
1881 }
1882 _ => {
1883 }
1888 }
1889 walk_caller_collect_callsite_guards(
1890 &arm.body,
1891 target_fn,
1892 target_param_index,
1893 &new_guards,
1894 out,
1895 );
1896 }
1897 walk_caller_collect_callsite_guards(
1898 subject,
1899 target_fn,
1900 target_param_index,
1901 enclosing_guards,
1902 out,
1903 );
1904 }
1905 Expr::BinOp(_, l, r) => {
1906 walk_caller_collect_callsite_guards(
1907 l,
1908 target_fn,
1909 target_param_index,
1910 enclosing_guards,
1911 out,
1912 );
1913 walk_caller_collect_callsite_guards(
1914 r,
1915 target_fn,
1916 target_param_index,
1917 enclosing_guards,
1918 out,
1919 );
1920 }
1921 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
1922 walk_caller_collect_callsite_guards(
1923 inner,
1924 target_fn,
1925 target_param_index,
1926 enclosing_guards,
1927 out,
1928 );
1929 }
1930 Expr::Constructor(_, arg) => {
1931 if let Some(inner) = arg {
1932 walk_caller_collect_callsite_guards(
1933 inner,
1934 target_fn,
1935 target_param_index,
1936 enclosing_guards,
1937 out,
1938 );
1939 }
1940 }
1941 Expr::InterpolatedStr(parts) => {
1942 for p in parts {
1943 if let crate::ast::StrPart::Parsed(inner) = p {
1944 walk_caller_collect_callsite_guards(
1945 inner,
1946 target_fn,
1947 target_param_index,
1948 enclosing_guards,
1949 out,
1950 );
1951 }
1952 }
1953 }
1954 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1955 for item in items {
1956 walk_caller_collect_callsite_guards(
1957 item,
1958 target_fn,
1959 target_param_index,
1960 enclosing_guards,
1961 out,
1962 );
1963 }
1964 }
1965 Expr::MapLiteral(entries) => {
1966 for (k, v) in entries {
1967 walk_caller_collect_callsite_guards(
1968 k,
1969 target_fn,
1970 target_param_index,
1971 enclosing_guards,
1972 out,
1973 );
1974 walk_caller_collect_callsite_guards(
1975 v,
1976 target_fn,
1977 target_param_index,
1978 enclosing_guards,
1979 out,
1980 );
1981 }
1982 }
1983 Expr::RecordCreate { fields, .. } => {
1984 for (_, v) in fields {
1985 walk_caller_collect_callsite_guards(
1986 v,
1987 target_fn,
1988 target_param_index,
1989 enclosing_guards,
1990 out,
1991 );
1992 }
1993 }
1994 Expr::RecordUpdate { base, updates, .. } => {
1995 walk_caller_collect_callsite_guards(
1996 base,
1997 target_fn,
1998 target_param_index,
1999 enclosing_guards,
2000 out,
2001 );
2002 for (_, v) in updates {
2003 walk_caller_collect_callsite_guards(
2004 v,
2005 target_fn,
2006 target_param_index,
2007 enclosing_guards,
2008 out,
2009 );
2010 }
2011 }
2012 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
2013 }
2014}
2015
2016pub fn analyze_plans_in_scope(
2039 inputs: &ProofLowerInputs,
2040 scope: Option<&str>,
2041 global_view: bool,
2042) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
2043 let mut plans = HashMap::new();
2044 let mut issues = Vec::new();
2045
2046 let all_pure = if global_view {
2047 inputs.pure_fns()
2048 } else {
2049 inputs.pure_fns_in_scope(scope)
2050 };
2051 let recursive_names = if global_view {
2052 inputs.recursive_pure_fn_names()
2053 } else {
2054 inputs.recursive_pure_fn_names_in_scope(scope)
2055 };
2056 let components = call_graph::ordered_fn_components(&all_pure, inputs.module_prefixes);
2057
2058 for component in components {
2059 if component.is_empty() {
2060 continue;
2061 }
2062 let is_recursive_component =
2063 component.len() > 1 || recursive_names.contains(&component[0].name);
2064 if !is_recursive_component {
2065 continue;
2066 }
2067
2068 if component.len() > 1 {
2069 if supports_mutual_int_countdown(&component) {
2070 for fd in &component {
2071 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
2072 }
2073 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
2074 for fd in &component {
2075 if let Some(rank) = ranks.get(&fd.name).cloned() {
2076 plans.insert(
2077 fd.name.clone(),
2078 RecursionPlan::MutualStringPosAdvance { rank },
2079 );
2080 }
2081 }
2082 } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
2083 for fd in &component {
2084 if let Some(rank) = rankings.get(&fd.name).cloned() {
2085 plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
2086 }
2087 }
2088 } else {
2089 let names = component
2090 .iter()
2091 .map(|fd| fd.name.clone())
2092 .collect::<Vec<_>>()
2093 .join(", ");
2094 let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
2095 issues.push(ProofModeIssue {
2096 line,
2097 message: format!(
2098 "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
2099 names
2100 ),
2101 });
2102 }
2103 continue;
2104 }
2105
2106 let fd = component[0];
2107 if crate::codegen::lean::recurrence::detect_second_order_int_linear_recurrence(fd).is_some()
2108 {
2109 plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
2110 } else if let Some((param_index, bound)) = single_int_ascending_param(fd) {
2111 plans.insert(
2112 fd.name.clone(),
2113 RecursionPlan::IntAscending { param_index, bound },
2114 );
2115 } else if let Some(param_index) = single_int_countdown_param_index(fd) {
2116 if is_closed_world_pure_fn(&fd.name, inputs)
2124 && let Some((base_arm_literal, base_arm_body, wildcard_arm_body)) =
2125 int_countdown_native_arms(fd, param_index)
2126 && let Some((callee_param_name, _)) = fd.params.get(param_index)
2127 {
2128 let precondition = find_single_external_caller_predicate(
2133 &fd.name,
2134 param_index,
2135 callee_param_name,
2136 inputs,
2137 )
2138 .unwrap_or_default();
2139 plans.insert(
2140 fd.name.clone(),
2141 RecursionPlan::IntCountdownGuarded {
2142 param_index,
2143 base_arm_literal,
2144 base_arm_body,
2145 wildcard_arm_body,
2146 precondition,
2147 },
2148 );
2149 } else {
2150 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
2151 }
2152 } else if let Some((param_index, divisor, helper_fn)) =
2153 single_int_floor_div_countdown(fd, inputs)
2154 {
2155 plans.insert(
2162 fd.name.clone(),
2163 RecursionPlan::IntFloorDivCountdown {
2164 param_index,
2165 divisor,
2166 helper_fn,
2167 },
2168 );
2169 } else if supports_single_sizeof_structural(fd, inputs)
2170 || (single_list_structural_param_index(fd).is_none()
2171 && single_adt_structural_param_index(fd, inputs).is_some())
2172 {
2173 plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
2183 } else if let Some(param_index) = single_list_structural_param_index(fd) {
2184 plans.insert(
2185 fd.name.clone(),
2186 RecursionPlan::ListStructural { param_index },
2187 );
2188 } else if supports_single_string_pos_advance(fd) {
2189 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
2190 } else {
2191 issues.push(ProofModeIssue {
2192 line: fd.line,
2193 message: format!(
2194 "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)",
2195 fd.name
2196 ),
2197 });
2198 }
2199 }
2200
2201 (plans, issues)
2202}