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 let entry = indegree.get_mut(to)?;
1302 *entry += 1;
1303 }
1304 }
1305
1306 let mut queue: Vec<String> = indegree
1307 .iter()
1308 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
1309 .collect();
1310 queue.sort();
1311 let mut topo = Vec::new();
1312 while let Some(node) = queue.pop() {
1313 topo.push(node.clone());
1314 let outs = same_edges.get(&node).cloned().unwrap_or_default();
1315 let mut newly_zero = Vec::new();
1316 for to in outs {
1317 let entry = indegree.get_mut(&to)?;
1318 *entry -= 1;
1319 if *entry == 0 {
1320 newly_zero.push(to);
1321 }
1322 }
1323 newly_zero.sort();
1324 queue.extend(newly_zero);
1325 }
1326
1327 if topo.len() != names.len() {
1328 return None;
1329 }
1330
1331 let n = topo.len();
1332 let mut ranks = HashMap::new();
1333 for (idx, name) in topo.into_iter().enumerate() {
1334 ranks.insert(name, n - idx);
1335 }
1336 Some(ranks)
1337}
1338
1339pub(crate) fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
1340 let Some((string_param, string_ty)) = fd.params.first() else {
1341 return false;
1342 };
1343 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
1344 return false;
1345 };
1346 if string_ty != "String" || pos_ty != "Int" {
1347 return false;
1348 }
1349
1350 type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
1351 let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
1352 .into_iter()
1353 .filter(|(name, _)| call_matches(name, &fd.name))
1354 .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
1355 .collect();
1356 if recursive_calls.is_empty() {
1357 return false;
1358 }
1359
1360 recursive_calls.into_iter().all(|(arg0, arg1)| {
1361 arg0.is_some_and(|e| is_ident(e, string_param))
1362 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
1363 })
1364}
1365
1366pub(crate) fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
1367 if component.len() < 2 {
1368 return false;
1369 }
1370 if component
1371 .iter()
1372 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
1373 {
1374 return false;
1375 }
1376 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1377 let mut any_intra = false;
1378 for fd in component {
1379 let param_name = &fd.params[0].0;
1380 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
1381 if !call_is_in_set(&callee, &names) {
1382 continue;
1383 }
1384 any_intra = true;
1385 let Some(arg0) = args.first().cloned() else {
1386 return false;
1387 };
1388 if !is_int_minus_positive(arg0, param_name) {
1389 return false;
1390 }
1391 }
1392 }
1393 any_intra
1394}
1395
1396pub(crate) fn supports_mutual_string_pos_advance(
1397 component: &[&FnDef],
1398) -> Option<HashMap<String, usize>> {
1399 if component.len() < 2 {
1400 return None;
1401 }
1402 if component.iter().any(|fd| {
1403 !matches!(fd.params.first(), Some((_, t)) if t == "String")
1404 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
1405 }) {
1406 return None;
1407 }
1408
1409 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1410 let mut same_edges: HashMap<String, HashSet<String>> =
1411 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1412 let mut any_intra = false;
1413
1414 for fd in component {
1415 let string_param = &fd.params[0].0;
1416 let pos_param = &fd.params[1].0;
1417 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1418 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1419 continue;
1420 };
1421 any_intra = true;
1422
1423 let arg0 = args.first().cloned()?;
1424 let arg1 = args.get(1).cloned()?;
1425
1426 if !is_ident(arg0, string_param) {
1427 return None;
1428 }
1429
1430 match classify_string_pos_edge(arg1, string_param, pos_param) {
1431 Some(StringPosEdge::Same) => {
1432 let edges = same_edges.get_mut(&fd.name)?;
1433 edges.insert(callee);
1434 }
1435 Some(StringPosEdge::Advance) => {}
1436 None => return None,
1437 }
1438 }
1439 }
1440
1441 if !any_intra {
1442 return None;
1443 }
1444
1445 ranks_from_same_edges(&names, &same_edges)
1446}
1447
1448pub(crate) fn is_scalar_like_type(type_name: &str) -> bool {
1449 matches!(
1450 type_name,
1451 "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
1452 )
1453}
1454
1455pub fn sizeof_measure_param_indices(fd: &FnDef) -> Vec<usize> {
1461 fd.params
1462 .iter()
1463 .enumerate()
1464 .filter_map(|(idx, (_, type_name))| (!is_scalar_like_type(type_name)).then_some(idx))
1465 .collect()
1466}
1467
1468pub fn scc_has_growing_accumulator(fns: &[&FnDef]) -> bool {
1476 let names: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1477 let mut sizeof_indices: HashMap<String, Vec<usize>> = HashMap::new();
1478 for fd in fns {
1479 sizeof_indices.insert(fd.name.clone(), sizeof_measure_param_indices(fd));
1480 }
1481 for fd in fns {
1482 if body_has_intra_scc_call_in_interpolation(fd.body.as_ref(), &names) {
1490 return true;
1491 }
1492 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1493 let Some(callee_name) = canonical_callee_name(&callee_raw, &names) else {
1494 continue;
1495 };
1496 let Some(callee_indices) = sizeof_indices.get(&callee_name) else {
1497 continue;
1498 };
1499 for callee_idx in callee_indices {
1500 let Some(arg) = args.get(*callee_idx) else {
1501 continue;
1502 };
1503 if !arg_is_non_growing(arg) {
1504 return true;
1505 }
1506 }
1507 }
1508 }
1509 false
1510}
1511
1512fn body_has_intra_scc_call_in_interpolation(body: &FnBody, names: &HashSet<String>) -> bool {
1513 let FnBody::Block(stmts) = body;
1514 stmts.iter().any(|s| match s {
1515 Stmt::Binding(_, _, e) | Stmt::Expr(e) => {
1516 expr_has_intra_scc_call_in_interpolation(e, names)
1517 }
1518 })
1519}
1520
1521fn expr_has_intra_scc_call_in_interpolation(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1522 use crate::ast::StrPart;
1523 match &expr.node {
1524 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1525 StrPart::Parsed(inner) => expr_calls_into_set(inner, names),
1526 _ => false,
1527 }),
1528 Expr::Match { subject, arms, .. } => {
1529 expr_has_intra_scc_call_in_interpolation(subject, names)
1530 || arms
1531 .iter()
1532 .any(|a| expr_has_intra_scc_call_in_interpolation(&a.body, names))
1533 }
1534 Expr::FnCall(f, args) => {
1535 expr_has_intra_scc_call_in_interpolation(f, names)
1536 || args
1537 .iter()
1538 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names))
1539 }
1540 Expr::TailCall(boxed) => boxed
1541 .args
1542 .iter()
1543 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names)),
1544 Expr::BinOp(_, l, r) => {
1545 expr_has_intra_scc_call_in_interpolation(l, names)
1546 || expr_has_intra_scc_call_in_interpolation(r, names)
1547 }
1548 Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::Neg(inner) => {
1549 expr_has_intra_scc_call_in_interpolation(inner, names)
1550 }
1551 _ => false,
1552 }
1553}
1554
1555fn expr_calls_into_set(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1556 match &expr.node {
1557 Expr::FnCall(f, args) => {
1558 let head_hit = expr_to_dotted_name(f)
1559 .as_deref()
1560 .map(|n| canonical_callee_name(n, names).is_some())
1561 .unwrap_or(false);
1562 head_hit
1563 || args.iter().any(|a| expr_calls_into_set(a, names))
1564 || expr_calls_into_set(f, names)
1565 }
1566 Expr::TailCall(boxed) => {
1567 canonical_callee_name(&boxed.target, names).is_some()
1568 || boxed.args.iter().any(|a| expr_calls_into_set(a, names))
1569 }
1570 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::Constructor(_, Some(inner)) => {
1571 expr_calls_into_set(inner, names)
1572 }
1573 Expr::BinOp(_, l, r) => expr_calls_into_set(l, names) || expr_calls_into_set(r, names),
1574 Expr::Match { subject, arms, .. } => {
1575 expr_calls_into_set(subject, names)
1576 || arms.iter().any(|a| expr_calls_into_set(&a.body, names))
1577 }
1578 _ => false,
1579 }
1580}
1581
1582fn arg_is_non_growing(expr: &Spanned<Expr>) -> bool {
1583 match &expr.node {
1584 Expr::Ident(_) | Expr::Resolved { .. } | Expr::Literal(_) | Expr::Attr(..) => true,
1585 Expr::FnCall(callee, args) => {
1593 expr_to_dotted_name(callee).as_deref() == Some("Map.entries")
1594 && args.len() == 1
1595 && arg_is_non_growing(&args[0])
1596 }
1597 _ => false,
1598 }
1599}
1600
1601pub(crate) fn supports_mutual_sizeof_ranked(
1602 component: &[&FnDef],
1603) -> Option<HashMap<String, usize>> {
1604 if component.len() < 2 {
1605 return None;
1606 }
1607 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1608 let metric_indices: HashMap<String, Vec<usize>> = component
1609 .iter()
1610 .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
1611 .collect();
1612 if component.iter().any(|fd| {
1613 metric_indices
1614 .get(&fd.name)
1615 .is_none_or(|indices| indices.is_empty())
1616 }) {
1617 return None;
1618 }
1619
1620 let mut same_edges: HashMap<String, HashSet<String>> =
1621 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1622 let mut any_intra = false;
1623 for fd in component {
1624 let caller_metric_indices = metric_indices.get(&fd.name)?;
1625 let caller_metric_params: Vec<&str> = caller_metric_indices
1626 .iter()
1627 .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
1628 .collect();
1629 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1630 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1631 continue;
1632 };
1633 any_intra = true;
1634 let callee_metric_indices = metric_indices.get(&callee)?;
1635 let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
1636 && callee_metric_indices
1637 .iter()
1638 .enumerate()
1639 .all(|(pos, callee_idx)| {
1640 let Some(arg) = args.get(*callee_idx).cloned() else {
1641 return false;
1642 };
1643 is_ident(arg, caller_metric_params[pos])
1644 });
1645 if is_same_edge {
1646 let edges = same_edges.get_mut(&fd.name)?;
1647 edges.insert(callee);
1648 }
1649 }
1650 }
1651 if !any_intra {
1652 return None;
1653 }
1654
1655 let ranks = ranks_from_same_edges(&names, &same_edges)?;
1656 let mut out = HashMap::new();
1657 for fd in component {
1658 let rank = ranks.get(&fd.name).cloned()?;
1659 out.insert(fd.name.clone(), rank);
1660 }
1661 Some(out)
1662}
1663
1664pub(crate) fn is_closed_world_pure_fn(fn_name: &str, inputs: &ProofLowerInputs) -> bool {
1683 let entry_module = inputs.entry_items.iter().find_map(|item| match item {
1684 TopLevel::Module(m) => Some(m),
1685 _ => None,
1686 });
1687 let entry_fn_names: std::collections::HashSet<&str> = inputs
1688 .entry_items
1689 .iter()
1690 .filter_map(|item| match item {
1691 TopLevel::FnDef(fd) => Some(fd.name.as_str()),
1692 _ => None,
1693 })
1694 .collect();
1695 if entry_fn_names.contains(fn_name) {
1696 match entry_module {
1697 None => return true,
1698 Some(m) => {
1699 if m.exposes_line.is_none() {
1700 return true;
1701 }
1702 return !m.exposes.iter().any(|e| e == fn_name);
1703 }
1704 }
1705 }
1706 false
1707}
1708
1709pub(crate) fn find_single_external_caller_predicate(
1732 target_fn: &str,
1733 target_param_index: usize,
1734 callee_param_name: &str,
1735 inputs: &ProofLowerInputs,
1736) -> Option<Vec<Spanned<Expr>>> {
1737 let all_callers: Vec<&FnDef> = inputs
1738 .entry_items
1739 .iter()
1740 .filter_map(|item| match item {
1741 TopLevel::FnDef(fd) if fd.name != target_fn => Some(fd),
1742 _ => None,
1743 })
1744 .chain(
1745 inputs
1746 .dep_modules
1747 .iter()
1748 .flat_map(|m| m.fn_defs.iter().filter(|fd| fd.name != target_fn)),
1749 )
1750 .filter(|fd| {
1751 collect_calls_from_body(fd.body.as_ref())
1752 .iter()
1753 .any(|(n, _)| call_matches(n, target_fn))
1754 })
1755 .collect();
1756
1757 if all_callers.len() != 1 {
1758 return None;
1759 }
1760 let caller = all_callers[0];
1761
1762 let mut callsites: Vec<(Vec<Spanned<Expr>>, String)> = Vec::new();
1763 for stmt in caller.body.stmts() {
1764 match stmt {
1765 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
1766 walk_caller_collect_callsite_guards(
1767 expr,
1768 target_fn,
1769 target_param_index,
1770 &[],
1771 &mut callsites,
1772 );
1773 }
1774 }
1775 }
1776
1777 if callsites.is_empty() {
1778 return None;
1779 }
1780
1781 let (first_guards, first_arg_name) = &callsites[0];
1784 for (other_guards, other_arg_name) in &callsites[1..] {
1785 if other_arg_name != first_arg_name || other_guards != first_guards {
1786 return None;
1787 }
1788 }
1789
1790 let arg_name = first_arg_name.as_str();
1791 let predicate = first_guards
1792 .iter()
1793 .filter(|g| crate::codegen::recursion::expr_references_ident(g, arg_name))
1794 .map(|g| {
1795 crate::codegen::recursion::substitute_ident_in_expr(g, arg_name, callee_param_name)
1796 })
1797 .collect::<Vec<_>>();
1798
1799 Some(predicate)
1800}
1801
1802fn walk_caller_collect_callsite_guards(
1808 expr: &Spanned<Expr>,
1809 target_fn: &str,
1810 target_param_index: usize,
1811 enclosing_guards: &[Spanned<Expr>],
1812 out: &mut Vec<(Vec<Spanned<Expr>>, String)>,
1813) {
1814 match &expr.node {
1815 Expr::FnCall(callee, args) => {
1816 if let Some(name) = expr_to_dotted_name(callee)
1817 && call_matches(&name, target_fn)
1818 && let Some(arg_at_idx) = args.get(target_param_index)
1819 && let Some(arg_name) = local_name_of(arg_at_idx)
1820 {
1821 out.push((enclosing_guards.to_vec(), arg_name.to_string()));
1822 }
1823 walk_caller_collect_callsite_guards(
1824 callee,
1825 target_fn,
1826 target_param_index,
1827 enclosing_guards,
1828 out,
1829 );
1830 for arg in args {
1831 walk_caller_collect_callsite_guards(
1832 arg,
1833 target_fn,
1834 target_param_index,
1835 enclosing_guards,
1836 out,
1837 );
1838 }
1839 }
1840 Expr::TailCall(boxed) => {
1841 for arg in &boxed.args {
1842 walk_caller_collect_callsite_guards(
1843 arg,
1844 target_fn,
1845 target_param_index,
1846 enclosing_guards,
1847 out,
1848 );
1849 }
1850 }
1851 Expr::Match { subject, arms } => {
1852 for arm in arms {
1853 let mut new_guards: Vec<Spanned<Expr>> = enclosing_guards.to_vec();
1854 match &arm.pattern {
1855 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
1856 new_guards.push((**subject).clone());
1857 }
1858 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1859 if let Some(flipped) =
1860 crate::codegen::recursion::flip_comparison_binop(subject)
1861 {
1862 new_guards.push(flipped);
1863 } else {
1864 }
1869 }
1870 _ => {
1871 }
1876 }
1877 walk_caller_collect_callsite_guards(
1878 &arm.body,
1879 target_fn,
1880 target_param_index,
1881 &new_guards,
1882 out,
1883 );
1884 }
1885 walk_caller_collect_callsite_guards(
1886 subject,
1887 target_fn,
1888 target_param_index,
1889 enclosing_guards,
1890 out,
1891 );
1892 }
1893 Expr::BinOp(_, l, r) => {
1894 walk_caller_collect_callsite_guards(
1895 l,
1896 target_fn,
1897 target_param_index,
1898 enclosing_guards,
1899 out,
1900 );
1901 walk_caller_collect_callsite_guards(
1902 r,
1903 target_fn,
1904 target_param_index,
1905 enclosing_guards,
1906 out,
1907 );
1908 }
1909 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
1910 walk_caller_collect_callsite_guards(
1911 inner,
1912 target_fn,
1913 target_param_index,
1914 enclosing_guards,
1915 out,
1916 );
1917 }
1918 Expr::Constructor(_, arg) => {
1919 if let Some(inner) = arg {
1920 walk_caller_collect_callsite_guards(
1921 inner,
1922 target_fn,
1923 target_param_index,
1924 enclosing_guards,
1925 out,
1926 );
1927 }
1928 }
1929 Expr::InterpolatedStr(parts) => {
1930 for p in parts {
1931 if let crate::ast::StrPart::Parsed(inner) = p {
1932 walk_caller_collect_callsite_guards(
1933 inner,
1934 target_fn,
1935 target_param_index,
1936 enclosing_guards,
1937 out,
1938 );
1939 }
1940 }
1941 }
1942 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1943 for item in items {
1944 walk_caller_collect_callsite_guards(
1945 item,
1946 target_fn,
1947 target_param_index,
1948 enclosing_guards,
1949 out,
1950 );
1951 }
1952 }
1953 Expr::MapLiteral(entries) => {
1954 for (k, v) in entries {
1955 walk_caller_collect_callsite_guards(
1956 k,
1957 target_fn,
1958 target_param_index,
1959 enclosing_guards,
1960 out,
1961 );
1962 walk_caller_collect_callsite_guards(
1963 v,
1964 target_fn,
1965 target_param_index,
1966 enclosing_guards,
1967 out,
1968 );
1969 }
1970 }
1971 Expr::RecordCreate { fields, .. } => {
1972 for (_, v) in fields {
1973 walk_caller_collect_callsite_guards(
1974 v,
1975 target_fn,
1976 target_param_index,
1977 enclosing_guards,
1978 out,
1979 );
1980 }
1981 }
1982 Expr::RecordUpdate { base, updates, .. } => {
1983 walk_caller_collect_callsite_guards(
1984 base,
1985 target_fn,
1986 target_param_index,
1987 enclosing_guards,
1988 out,
1989 );
1990 for (_, v) in updates {
1991 walk_caller_collect_callsite_guards(
1992 v,
1993 target_fn,
1994 target_param_index,
1995 enclosing_guards,
1996 out,
1997 );
1998 }
1999 }
2000 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
2001 }
2002}
2003
2004pub fn analyze_plans_in_scope(
2027 inputs: &ProofLowerInputs,
2028 scope: Option<&str>,
2029 global_view: bool,
2030) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
2031 let mut plans = HashMap::new();
2032 let mut issues = Vec::new();
2033
2034 let all_pure = if global_view {
2035 inputs.pure_fns()
2036 } else {
2037 inputs.pure_fns_in_scope(scope)
2038 };
2039 let recursive_names = if global_view {
2040 inputs.recursive_pure_fn_names()
2041 } else {
2042 inputs.recursive_pure_fn_names_in_scope(scope)
2043 };
2044 let components = call_graph::ordered_fn_components(&all_pure, inputs.module_prefixes);
2045
2046 for component in components {
2047 if component.is_empty() {
2048 continue;
2049 }
2050 let is_recursive_component =
2051 component.len() > 1 || recursive_names.contains(&component[0].name);
2052 if !is_recursive_component {
2053 continue;
2054 }
2055
2056 if component.len() > 1 {
2057 if supports_mutual_int_countdown(&component) {
2058 for fd in &component {
2059 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
2060 }
2061 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
2062 for fd in &component {
2063 if let Some(rank) = ranks.get(&fd.name).cloned() {
2064 plans.insert(
2065 fd.name.clone(),
2066 RecursionPlan::MutualStringPosAdvance { rank },
2067 );
2068 }
2069 }
2070 } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
2071 for fd in &component {
2072 if let Some(rank) = rankings.get(&fd.name).cloned() {
2073 plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
2074 }
2075 }
2076 } else {
2077 let names = component
2078 .iter()
2079 .map(|fd| fd.name.clone())
2080 .collect::<Vec<_>>()
2081 .join(", ");
2082 let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
2083 issues.push(ProofModeIssue {
2084 line,
2085 message: format!(
2086 "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
2087 names
2088 ),
2089 });
2090 }
2091 continue;
2092 }
2093
2094 let fd = component[0];
2095 if crate::codegen::lean::recurrence::detect_second_order_int_linear_recurrence(fd).is_some()
2096 {
2097 plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
2098 } else if let Some((param_index, bound)) = single_int_ascending_param(fd) {
2099 plans.insert(
2100 fd.name.clone(),
2101 RecursionPlan::IntAscending { param_index, bound },
2102 );
2103 } else if let Some(param_index) = single_int_countdown_param_index(fd) {
2104 if is_closed_world_pure_fn(&fd.name, inputs)
2112 && let Some((base_arm_literal, base_arm_body, wildcard_arm_body)) =
2113 int_countdown_native_arms(fd, param_index)
2114 && let Some((callee_param_name, _)) = fd.params.get(param_index)
2115 {
2116 let precondition = find_single_external_caller_predicate(
2121 &fd.name,
2122 param_index,
2123 callee_param_name,
2124 inputs,
2125 )
2126 .unwrap_or_default();
2127 plans.insert(
2128 fd.name.clone(),
2129 RecursionPlan::IntCountdownGuarded {
2130 param_index,
2131 base_arm_literal,
2132 base_arm_body,
2133 wildcard_arm_body,
2134 precondition,
2135 },
2136 );
2137 } else {
2138 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
2139 }
2140 } else if let Some((param_index, divisor, helper_fn)) =
2141 single_int_floor_div_countdown(fd, inputs)
2142 {
2143 plans.insert(
2150 fd.name.clone(),
2151 RecursionPlan::IntFloorDivCountdown {
2152 param_index,
2153 divisor,
2154 helper_fn,
2155 },
2156 );
2157 } else if supports_single_sizeof_structural(fd, inputs)
2158 || (single_list_structural_param_index(fd).is_none()
2159 && single_adt_structural_param_index(fd, inputs).is_some())
2160 {
2161 plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
2171 } else if let Some(param_index) = single_list_structural_param_index(fd) {
2172 plans.insert(
2173 fd.name.clone(),
2174 RecursionPlan::ListStructural { param_index },
2175 );
2176 } else if supports_single_string_pos_advance(fd) {
2177 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
2178 } else {
2179 issues.push(ProofModeIssue {
2180 line: fd.line,
2181 message: format!(
2182 "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)",
2183 fd.name
2184 ),
2185 });
2186 }
2187 }
2188
2189 (plans, issues)
2190}