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 Expr::Ident(subject_name) = &subject.node
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 has_negative_guarded_ascent(fd: &FnDef, param_name: &str) -> bool {
519 let Some(tail) = fd.body.tail_expr() else {
520 return false;
521 };
522 let Expr::Match { subject, arms, .. } = &tail.node else {
523 return false;
524 };
525 let Expr::BinOp(BinOp::Lt, left, right) = &subject.node else {
526 return false;
527 };
528 if !is_ident(left, param_name)
529 || !matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(0)))
530 {
531 return false;
532 }
533
534 let mut true_arm = None;
535 let mut false_arm = None;
536 for arm in arms {
537 match arm.pattern {
538 Pattern::Literal(crate::ast::Literal::Bool(true)) => true_arm = Some(arm.body.as_ref()),
539 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
540 false_arm = Some(arm.body.as_ref())
541 }
542 _ => return false,
543 }
544 }
545
546 let Some(true_arm) = true_arm else {
547 return false;
548 };
549 let Some(false_arm) = false_arm else {
550 return false;
551 };
552
553 let mut true_calls = Vec::new();
554 collect_calls_from_expr(true_arm, &mut true_calls);
555 let mut false_calls = Vec::new();
556 collect_calls_from_expr(false_arm, &mut false_calls);
557
558 true_calls
559 .iter()
560 .any(|(name, _)| call_matches(name, &fd.name))
561 && false_calls
562 .iter()
563 .all(|(name, _)| !call_matches(name, &fd.name))
564}
565
566pub(crate) fn single_int_ascending_param(fd: &FnDef) -> Option<(usize, Spanned<Expr>)> {
569 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
570 .into_iter()
571 .filter(|(name, _)| call_matches(name, &fd.name))
572 .map(|(_, args)| args)
573 .collect();
574 if recursive_calls.is_empty() {
575 return None;
576 }
577
578 for (idx, (param_name, param_ty)) in fd.params.iter().enumerate() {
579 if param_ty != "Int" {
580 continue;
581 }
582 let ascent_ok = recursive_calls.iter().all(|args| {
583 args.get(idx)
584 .cloned()
585 .is_some_and(|arg| is_int_plus_positive(arg, param_name))
586 });
587 if !ascent_ok {
588 continue;
589 }
590 if let Some(bound) = extract_equality_bound_expr(fd, param_name) {
591 return Some((idx, bound));
592 }
593 }
594 None
595}
596
597pub(crate) fn extract_equality_bound_expr(fd: &FnDef, param_name: &str) -> Option<Spanned<Expr>> {
601 let tail = fd.body.tail_expr()?;
602 let Expr::Match { subject, arms, .. } = &tail.node else {
603 return None;
604 };
605 let Expr::BinOp(BinOp::Eq, left, right) = &subject.node else {
606 return None;
607 };
608 if !is_ident(left, param_name) {
609 return None;
610 }
611 let mut true_has_self = false;
613 let mut false_has_self = false;
614 for arm in arms {
615 match arm.pattern {
616 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
617 let mut calls = Vec::new();
618 collect_calls_from_expr(&arm.body, &mut calls);
619 true_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
620 }
621 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
622 let mut calls = Vec::new();
623 collect_calls_from_expr(&arm.body, &mut calls);
624 false_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
625 }
626 _ => return None,
627 }
628 }
629 if true_has_self || !false_has_self {
630 return None;
631 }
632 Some((**right).clone())
633}
634
635pub(crate) fn supports_single_sizeof_structural(fd: &FnDef, inputs: &ProofLowerInputs) -> bool {
636 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
637 .into_iter()
638 .filter(|(name, _)| call_matches(name, &fd.name))
639 .map(|(_, args)| args)
640 .collect();
641 if recursive_calls.is_empty() {
642 return false;
643 }
644
645 let metric_indices = sizeof_measure_param_indices(fd);
646 if metric_indices.is_empty() {
647 return false;
648 }
649
650 let binder_sets: HashMap<usize, HashSet<String>> = metric_indices
651 .iter()
652 .filter_map(|idx| {
653 let (param_name, param_type) = fd.params.get(*idx)?;
654 inputs.recursive_type_names().contains(param_type).then(|| {
655 (
656 *idx,
657 collect_recursive_subterm_binders(fd, param_name, param_type, inputs),
658 )
659 })
660 })
661 .collect();
662
663 if binder_sets.values().all(HashSet::is_empty) {
664 return false;
665 }
666
667 recursive_calls.iter().all(|args| {
668 let mut strictly_smaller = false;
669 for idx in &metric_indices {
670 let Some((param_name, _)) = fd.params.get(*idx) else {
671 return false;
672 };
673 let Some(arg) = args.get(*idx).cloned() else {
674 return false;
675 };
676 if is_ident(arg, param_name) {
677 continue;
678 }
679 let Some(binders) = binder_sets.get(idx) else {
680 return false;
681 };
682 if local_name_of(arg).is_some_and(|id| binders.contains(id)) {
683 strictly_smaller = true;
684 continue;
685 }
686 return false;
687 }
688 strictly_smaller
689 })
690}
691
692pub(crate) fn single_list_structural_param_index(fd: &FnDef) -> Option<usize> {
693 fd.params
694 .iter()
695 .enumerate()
696 .find_map(|(param_index, (param_name, param_ty))| {
697 if !(param_ty.starts_with("List<") || param_ty == "List") {
698 return None;
699 }
700
701 let tails = collect_list_tail_binders(fd, param_name);
702 if tails.is_empty() {
703 return None;
704 }
705
706 let recursive_calls: Vec<Option<&Spanned<Expr>>> =
707 collect_calls_from_body(fd.body.as_ref())
708 .into_iter()
709 .filter(|(name, _)| call_matches(name, &fd.name))
710 .map(|(_, args)| args.get(param_index).cloned())
711 .collect();
712 if recursive_calls.is_empty() {
713 return None;
714 }
715
716 recursive_calls
717 .into_iter()
718 .all(|arg| {
719 arg.and_then(local_name_of)
720 .is_some_and(|id| tails.contains(id))
721 })
722 .then_some(param_index)
723 })
724}
725
726pub(crate) fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
727 local_name_of(expr).is_some_and(|id| id == name)
728}
729
730pub(crate) fn is_int_plus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
731 match &expr.node {
732 Expr::BinOp(BinOp::Add, left, right) => {
733 local_name_of(left).is_some_and(|id| id == param_name)
734 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
735 }
736 Expr::FnCall(callee, args) => {
737 let Some(name) = expr_to_dotted_name(callee) else {
738 return false;
739 };
740 (name == "Int.add" || name == "int.add")
741 && args.len() == 2
742 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
743 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
744 }
745 _ => false,
746 }
747}
748
749pub(crate) fn is_skip_ws_advance(
750 expr: &Spanned<Expr>,
751 string_param: &str,
752 pos_param: &str,
753) -> bool {
754 let Expr::FnCall(callee, args) = &expr.node else {
755 return false;
756 };
757 let Some(name) = expr_to_dotted_name(callee) else {
758 return false;
759 };
760 if !call_matches(&name, "skipWs") || args.len() != 2 {
761 return false;
762 }
763 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
764}
765
766pub(crate) fn is_skip_ws_same(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
767 let Expr::FnCall(callee, args) = &expr.node else {
768 return false;
769 };
770 let Some(name) = expr_to_dotted_name(callee) else {
771 return false;
772 };
773 if !call_matches(&name, "skipWs") || args.len() != 2 {
774 return false;
775 }
776 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
777}
778
779pub(crate) fn is_string_pos_advance(
780 expr: &Spanned<Expr>,
781 string_param: &str,
782 pos_param: &str,
783) -> bool {
784 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
785}
786
787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
788pub(crate) enum StringPosEdge {
789 Same,
790 Advance,
791}
792
793pub(crate) fn classify_string_pos_edge(
794 expr: &Spanned<Expr>,
795 string_param: &str,
796 pos_param: &str,
797) -> Option<StringPosEdge> {
798 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
799 return Some(StringPosEdge::Same);
800 }
801 if is_string_pos_advance(expr, string_param, pos_param) {
802 return Some(StringPosEdge::Advance);
803 }
804 if let Expr::FnCall(callee, args) = &expr.node {
805 let name = expr_to_dotted_name(callee)?;
806 if call_matches(&name, "skipWs")
807 && args.len() == 2
808 && is_ident(&args[0], string_param)
809 && local_name_of(&args[1]).is_some_and(|id| id != pos_param)
810 {
811 return Some(StringPosEdge::Advance);
812 }
813 }
814 if local_name_of(expr).is_some_and(|id| id != pos_param) {
815 return Some(StringPosEdge::Advance);
816 }
817 None
818}
819
820pub(crate) fn ranks_from_same_edges(
821 names: &HashSet<String>,
822 same_edges: &HashMap<String, HashSet<String>>,
823) -> Option<HashMap<String, usize>> {
824 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
825 for outs in same_edges.values() {
826 for to in outs {
827 if let Some(entry) = indegree.get_mut(to) {
828 *entry += 1;
829 } else {
830 return None;
831 }
832 }
833 }
834
835 let mut queue: Vec<String> = indegree
836 .iter()
837 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
838 .collect();
839 queue.sort();
840 let mut topo = Vec::new();
841 while let Some(node) = queue.pop() {
842 topo.push(node.clone());
843 let outs = same_edges.get(&node).cloned().unwrap_or_default();
844 let mut newly_zero = Vec::new();
845 for to in outs {
846 if let Some(entry) = indegree.get_mut(&to) {
847 *entry -= 1;
848 if *entry == 0 {
849 newly_zero.push(to);
850 }
851 } else {
852 return None;
853 }
854 }
855 newly_zero.sort();
856 queue.extend(newly_zero);
857 }
858
859 if topo.len() != names.len() {
860 return None;
861 }
862
863 let n = topo.len();
864 let mut ranks = HashMap::new();
865 for (idx, name) in topo.into_iter().enumerate() {
866 ranks.insert(name, n - idx);
867 }
868 Some(ranks)
869}
870
871pub(crate) fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
872 let Some((string_param, string_ty)) = fd.params.first() else {
873 return false;
874 };
875 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
876 return false;
877 };
878 if string_ty != "String" || pos_ty != "Int" {
879 return false;
880 }
881
882 type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
883 let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
884 .into_iter()
885 .filter(|(name, _)| call_matches(name, &fd.name))
886 .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
887 .collect();
888 if recursive_calls.is_empty() {
889 return false;
890 }
891
892 recursive_calls.into_iter().all(|(arg0, arg1)| {
893 arg0.is_some_and(|e| is_ident(e, string_param))
894 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
895 })
896}
897
898pub(crate) fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
899 if component.len() < 2 {
900 return false;
901 }
902 if component
903 .iter()
904 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
905 {
906 return false;
907 }
908 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
909 let mut any_intra = false;
910 for fd in component {
911 let param_name = &fd.params[0].0;
912 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
913 if !call_is_in_set(&callee, &names) {
914 continue;
915 }
916 any_intra = true;
917 let Some(arg0) = args.first().cloned() else {
918 return false;
919 };
920 if !is_int_minus_positive(arg0, param_name) {
921 return false;
922 }
923 }
924 }
925 any_intra
926}
927
928pub(crate) fn supports_mutual_string_pos_advance(
929 component: &[&FnDef],
930) -> Option<HashMap<String, usize>> {
931 if component.len() < 2 {
932 return None;
933 }
934 if component.iter().any(|fd| {
935 !matches!(fd.params.first(), Some((_, t)) if t == "String")
936 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
937 }) {
938 return None;
939 }
940
941 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
942 let mut same_edges: HashMap<String, HashSet<String>> =
943 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
944 let mut any_intra = false;
945
946 for fd in component {
947 let string_param = &fd.params[0].0;
948 let pos_param = &fd.params[1].0;
949 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
950 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
951 continue;
952 };
953 any_intra = true;
954
955 let arg0 = args.first().cloned()?;
956 let arg1 = args.get(1).cloned()?;
957
958 if !is_ident(arg0, string_param) {
959 return None;
960 }
961
962 match classify_string_pos_edge(arg1, string_param, pos_param) {
963 Some(StringPosEdge::Same) => {
964 if let Some(edges) = same_edges.get_mut(&fd.name) {
965 edges.insert(callee);
966 } else {
967 return None;
968 }
969 }
970 Some(StringPosEdge::Advance) => {}
971 None => return None,
972 }
973 }
974 }
975
976 if !any_intra {
977 return None;
978 }
979
980 ranks_from_same_edges(&names, &same_edges)
981}
982
983pub(crate) fn is_scalar_like_type(type_name: &str) -> bool {
984 matches!(
985 type_name,
986 "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
987 )
988}
989
990pub fn sizeof_measure_param_indices(fd: &FnDef) -> Vec<usize> {
996 fd.params
997 .iter()
998 .enumerate()
999 .filter_map(|(idx, (_, type_name))| (!is_scalar_like_type(type_name)).then_some(idx))
1000 .collect()
1001}
1002
1003pub fn scc_has_growing_accumulator(fns: &[&FnDef]) -> bool {
1011 let names: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1012 let mut sizeof_indices: HashMap<String, Vec<usize>> = HashMap::new();
1013 for fd in fns {
1014 sizeof_indices.insert(fd.name.clone(), sizeof_measure_param_indices(fd));
1015 }
1016 for fd in fns {
1017 if body_has_intra_scc_call_in_interpolation(fd.body.as_ref(), &names) {
1025 return true;
1026 }
1027 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1028 let Some(callee_name) = canonical_callee_name(&callee_raw, &names) else {
1029 continue;
1030 };
1031 let Some(callee_indices) = sizeof_indices.get(&callee_name) else {
1032 continue;
1033 };
1034 for callee_idx in callee_indices {
1035 let Some(arg) = args.get(*callee_idx) else {
1036 continue;
1037 };
1038 if !arg_is_non_growing(arg) {
1039 return true;
1040 }
1041 }
1042 }
1043 }
1044 false
1045}
1046
1047fn body_has_intra_scc_call_in_interpolation(body: &FnBody, names: &HashSet<String>) -> bool {
1048 let FnBody::Block(stmts) = body;
1049 stmts.iter().any(|s| match s {
1050 Stmt::Binding(_, _, e) | Stmt::Expr(e) => {
1051 expr_has_intra_scc_call_in_interpolation(e, names)
1052 }
1053 })
1054}
1055
1056fn expr_has_intra_scc_call_in_interpolation(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1057 use crate::ast::StrPart;
1058 match &expr.node {
1059 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1060 StrPart::Parsed(inner) => expr_calls_into_set(inner, names),
1061 _ => false,
1062 }),
1063 Expr::Match { subject, arms, .. } => {
1064 expr_has_intra_scc_call_in_interpolation(subject, names)
1065 || arms
1066 .iter()
1067 .any(|a| expr_has_intra_scc_call_in_interpolation(&a.body, names))
1068 }
1069 Expr::FnCall(f, args) => {
1070 expr_has_intra_scc_call_in_interpolation(f, names)
1071 || args
1072 .iter()
1073 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names))
1074 }
1075 Expr::TailCall(boxed) => boxed
1076 .args
1077 .iter()
1078 .any(|a| expr_has_intra_scc_call_in_interpolation(a, names)),
1079 Expr::BinOp(_, l, r) => {
1080 expr_has_intra_scc_call_in_interpolation(l, names)
1081 || expr_has_intra_scc_call_in_interpolation(r, names)
1082 }
1083 Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::Neg(inner) => {
1084 expr_has_intra_scc_call_in_interpolation(inner, names)
1085 }
1086 _ => false,
1087 }
1088}
1089
1090fn expr_calls_into_set(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1091 match &expr.node {
1092 Expr::FnCall(f, args) => {
1093 let head_hit = expr_to_dotted_name(f)
1094 .as_deref()
1095 .map(|n| canonical_callee_name(n, names).is_some())
1096 .unwrap_or(false);
1097 head_hit
1098 || args.iter().any(|a| expr_calls_into_set(a, names))
1099 || expr_calls_into_set(f, names)
1100 }
1101 Expr::TailCall(boxed) => {
1102 canonical_callee_name(&boxed.target, names).is_some()
1103 || boxed.args.iter().any(|a| expr_calls_into_set(a, names))
1104 }
1105 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::Constructor(_, Some(inner)) => {
1106 expr_calls_into_set(inner, names)
1107 }
1108 Expr::BinOp(_, l, r) => expr_calls_into_set(l, names) || expr_calls_into_set(r, names),
1109 Expr::Match { subject, arms, .. } => {
1110 expr_calls_into_set(subject, names)
1111 || arms.iter().any(|a| expr_calls_into_set(&a.body, names))
1112 }
1113 _ => false,
1114 }
1115}
1116
1117fn arg_is_non_growing(expr: &Spanned<Expr>) -> bool {
1118 matches!(
1119 &expr.node,
1120 Expr::Ident(_) | Expr::Resolved { .. } | Expr::Literal(_) | Expr::Attr(..)
1121 )
1122}
1123
1124pub(crate) fn supports_mutual_sizeof_ranked(
1125 component: &[&FnDef],
1126) -> Option<HashMap<String, usize>> {
1127 if component.len() < 2 {
1128 return None;
1129 }
1130 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1131 let metric_indices: HashMap<String, Vec<usize>> = component
1132 .iter()
1133 .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
1134 .collect();
1135 if component.iter().any(|fd| {
1136 metric_indices
1137 .get(&fd.name)
1138 .is_none_or(|indices| indices.is_empty())
1139 }) {
1140 return None;
1141 }
1142
1143 let mut same_edges: HashMap<String, HashSet<String>> =
1144 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1145 let mut any_intra = false;
1146 for fd in component {
1147 let caller_metric_indices = metric_indices.get(&fd.name)?;
1148 let caller_metric_params: Vec<&str> = caller_metric_indices
1149 .iter()
1150 .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
1151 .collect();
1152 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1153 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1154 continue;
1155 };
1156 any_intra = true;
1157 let callee_metric_indices = metric_indices.get(&callee)?;
1158 let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
1159 && callee_metric_indices
1160 .iter()
1161 .enumerate()
1162 .all(|(pos, callee_idx)| {
1163 let Some(arg) = args.get(*callee_idx).cloned() else {
1164 return false;
1165 };
1166 is_ident(arg, caller_metric_params[pos])
1167 });
1168 if is_same_edge {
1169 if let Some(edges) = same_edges.get_mut(&fd.name) {
1170 edges.insert(callee);
1171 } else {
1172 return None;
1173 }
1174 }
1175 }
1176 }
1177 if !any_intra {
1178 return None;
1179 }
1180
1181 let ranks = ranks_from_same_edges(&names, &same_edges)?;
1182 let mut out = HashMap::new();
1183 for fd in component {
1184 let rank = ranks.get(&fd.name).cloned()?;
1185 out.insert(fd.name.clone(), rank);
1186 }
1187 Some(out)
1188}
1189
1190pub(crate) fn is_closed_world_pure_fn(fn_name: &str, inputs: &ProofLowerInputs) -> bool {
1209 let entry_module = inputs.entry_items.iter().find_map(|item| match item {
1210 TopLevel::Module(m) => Some(m),
1211 _ => None,
1212 });
1213 let entry_fn_names: std::collections::HashSet<&str> = inputs
1214 .entry_items
1215 .iter()
1216 .filter_map(|item| match item {
1217 TopLevel::FnDef(fd) => Some(fd.name.as_str()),
1218 _ => None,
1219 })
1220 .collect();
1221 if entry_fn_names.contains(fn_name) {
1222 match entry_module {
1223 None => return true,
1224 Some(m) => {
1225 if m.exposes_line.is_none() {
1226 return true;
1227 }
1228 return !m.exposes.iter().any(|e| e == fn_name);
1229 }
1230 }
1231 }
1232 false
1233}
1234
1235pub(crate) fn find_single_external_caller_predicate(
1258 target_fn: &str,
1259 target_param_index: usize,
1260 callee_param_name: &str,
1261 inputs: &ProofLowerInputs,
1262) -> Option<Vec<Spanned<Expr>>> {
1263 let all_callers: Vec<&FnDef> = inputs
1264 .entry_items
1265 .iter()
1266 .filter_map(|item| match item {
1267 TopLevel::FnDef(fd) if fd.name != target_fn => Some(fd),
1268 _ => None,
1269 })
1270 .chain(
1271 inputs
1272 .dep_modules
1273 .iter()
1274 .flat_map(|m| m.fn_defs.iter().filter(|fd| fd.name != target_fn)),
1275 )
1276 .filter(|fd| {
1277 collect_calls_from_body(fd.body.as_ref())
1278 .iter()
1279 .any(|(n, _)| call_matches(n, target_fn))
1280 })
1281 .collect();
1282
1283 if all_callers.len() != 1 {
1284 return None;
1285 }
1286 let caller = all_callers[0];
1287
1288 let mut callsites: Vec<(Vec<Spanned<Expr>>, String)> = Vec::new();
1289 for stmt in caller.body.stmts() {
1290 match stmt {
1291 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
1292 walk_caller_collect_callsite_guards(
1293 expr,
1294 target_fn,
1295 target_param_index,
1296 &[],
1297 &mut callsites,
1298 );
1299 }
1300 }
1301 }
1302
1303 if callsites.is_empty() {
1304 return None;
1305 }
1306
1307 let (first_guards, first_arg_name) = &callsites[0];
1310 for (other_guards, other_arg_name) in &callsites[1..] {
1311 if other_arg_name != first_arg_name || other_guards != first_guards {
1312 return None;
1313 }
1314 }
1315
1316 let arg_name = first_arg_name.as_str();
1317 let predicate = first_guards
1318 .iter()
1319 .filter(|g| crate::codegen::recursion::expr_references_ident(g, arg_name))
1320 .map(|g| {
1321 crate::codegen::recursion::substitute_ident_in_expr(g, arg_name, callee_param_name)
1322 })
1323 .collect::<Vec<_>>();
1324
1325 Some(predicate)
1326}
1327
1328fn walk_caller_collect_callsite_guards(
1334 expr: &Spanned<Expr>,
1335 target_fn: &str,
1336 target_param_index: usize,
1337 enclosing_guards: &[Spanned<Expr>],
1338 out: &mut Vec<(Vec<Spanned<Expr>>, String)>,
1339) {
1340 match &expr.node {
1341 Expr::FnCall(callee, args) => {
1342 if let Some(name) = expr_to_dotted_name(callee)
1343 && call_matches(&name, target_fn)
1344 && let Some(arg_at_idx) = args.get(target_param_index)
1345 && let Some(arg_name) = local_name_of(arg_at_idx)
1346 {
1347 out.push((enclosing_guards.to_vec(), arg_name.to_string()));
1348 }
1349 walk_caller_collect_callsite_guards(
1350 callee,
1351 target_fn,
1352 target_param_index,
1353 enclosing_guards,
1354 out,
1355 );
1356 for arg in args {
1357 walk_caller_collect_callsite_guards(
1358 arg,
1359 target_fn,
1360 target_param_index,
1361 enclosing_guards,
1362 out,
1363 );
1364 }
1365 }
1366 Expr::TailCall(boxed) => {
1367 for arg in &boxed.args {
1368 walk_caller_collect_callsite_guards(
1369 arg,
1370 target_fn,
1371 target_param_index,
1372 enclosing_guards,
1373 out,
1374 );
1375 }
1376 }
1377 Expr::Match { subject, arms } => {
1378 for arm in arms {
1379 let mut new_guards: Vec<Spanned<Expr>> = enclosing_guards.to_vec();
1380 match &arm.pattern {
1381 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
1382 new_guards.push((**subject).clone());
1383 }
1384 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1385 if let Some(flipped) =
1386 crate::codegen::recursion::flip_comparison_binop(subject)
1387 {
1388 new_guards.push(flipped);
1389 } else {
1390 }
1395 }
1396 _ => {
1397 }
1402 }
1403 walk_caller_collect_callsite_guards(
1404 &arm.body,
1405 target_fn,
1406 target_param_index,
1407 &new_guards,
1408 out,
1409 );
1410 }
1411 walk_caller_collect_callsite_guards(
1412 subject,
1413 target_fn,
1414 target_param_index,
1415 enclosing_guards,
1416 out,
1417 );
1418 }
1419 Expr::BinOp(_, l, r) => {
1420 walk_caller_collect_callsite_guards(
1421 l,
1422 target_fn,
1423 target_param_index,
1424 enclosing_guards,
1425 out,
1426 );
1427 walk_caller_collect_callsite_guards(
1428 r,
1429 target_fn,
1430 target_param_index,
1431 enclosing_guards,
1432 out,
1433 );
1434 }
1435 Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
1436 walk_caller_collect_callsite_guards(
1437 inner,
1438 target_fn,
1439 target_param_index,
1440 enclosing_guards,
1441 out,
1442 );
1443 }
1444 Expr::Constructor(_, arg) => {
1445 if let Some(inner) = arg {
1446 walk_caller_collect_callsite_guards(
1447 inner,
1448 target_fn,
1449 target_param_index,
1450 enclosing_guards,
1451 out,
1452 );
1453 }
1454 }
1455 Expr::InterpolatedStr(parts) => {
1456 for p in parts {
1457 if let crate::ast::StrPart::Parsed(inner) = p {
1458 walk_caller_collect_callsite_guards(
1459 inner,
1460 target_fn,
1461 target_param_index,
1462 enclosing_guards,
1463 out,
1464 );
1465 }
1466 }
1467 }
1468 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1469 for item in items {
1470 walk_caller_collect_callsite_guards(
1471 item,
1472 target_fn,
1473 target_param_index,
1474 enclosing_guards,
1475 out,
1476 );
1477 }
1478 }
1479 Expr::MapLiteral(entries) => {
1480 for (k, v) in entries {
1481 walk_caller_collect_callsite_guards(
1482 k,
1483 target_fn,
1484 target_param_index,
1485 enclosing_guards,
1486 out,
1487 );
1488 walk_caller_collect_callsite_guards(
1489 v,
1490 target_fn,
1491 target_param_index,
1492 enclosing_guards,
1493 out,
1494 );
1495 }
1496 }
1497 Expr::RecordCreate { fields, .. } => {
1498 for (_, v) in fields {
1499 walk_caller_collect_callsite_guards(
1500 v,
1501 target_fn,
1502 target_param_index,
1503 enclosing_guards,
1504 out,
1505 );
1506 }
1507 }
1508 Expr::RecordUpdate { base, updates, .. } => {
1509 walk_caller_collect_callsite_guards(
1510 base,
1511 target_fn,
1512 target_param_index,
1513 enclosing_guards,
1514 out,
1515 );
1516 for (_, v) in updates {
1517 walk_caller_collect_callsite_guards(
1518 v,
1519 target_fn,
1520 target_param_index,
1521 enclosing_guards,
1522 out,
1523 );
1524 }
1525 }
1526 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
1527 }
1528}
1529
1530pub fn analyze_plans_in_scope(
1553 inputs: &ProofLowerInputs,
1554 scope: Option<&str>,
1555 global_view: bool,
1556) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
1557 let mut plans = HashMap::new();
1558 let mut issues = Vec::new();
1559
1560 let all_pure = if global_view {
1561 inputs.pure_fns()
1562 } else {
1563 inputs.pure_fns_in_scope(scope)
1564 };
1565 let recursive_names = if global_view {
1566 inputs.recursive_pure_fn_names()
1567 } else {
1568 inputs.recursive_pure_fn_names_in_scope(scope)
1569 };
1570 let components = call_graph::ordered_fn_components(&all_pure, inputs.module_prefixes);
1571
1572 for component in components {
1573 if component.is_empty() {
1574 continue;
1575 }
1576 let is_recursive_component =
1577 component.len() > 1 || recursive_names.contains(&component[0].name);
1578 if !is_recursive_component {
1579 continue;
1580 }
1581
1582 if component.len() > 1 {
1583 if supports_mutual_int_countdown(&component) {
1584 for fd in &component {
1585 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
1586 }
1587 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
1588 for fd in &component {
1589 if let Some(rank) = ranks.get(&fd.name).cloned() {
1590 plans.insert(
1591 fd.name.clone(),
1592 RecursionPlan::MutualStringPosAdvance { rank },
1593 );
1594 }
1595 }
1596 } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
1597 for fd in &component {
1598 if let Some(rank) = rankings.get(&fd.name).cloned() {
1599 plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
1600 }
1601 }
1602 } else {
1603 let names = component
1604 .iter()
1605 .map(|fd| fd.name.clone())
1606 .collect::<Vec<_>>()
1607 .join(", ");
1608 let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
1609 issues.push(ProofModeIssue {
1610 line,
1611 message: format!(
1612 "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
1613 names
1614 ),
1615 });
1616 }
1617 continue;
1618 }
1619
1620 let fd = component[0];
1621 if crate::codegen::lean::recurrence::detect_second_order_int_linear_recurrence(fd).is_some()
1622 {
1623 plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
1624 } else if let Some((param_index, bound)) = single_int_ascending_param(fd) {
1625 plans.insert(
1626 fd.name.clone(),
1627 RecursionPlan::IntAscending { param_index, bound },
1628 );
1629 } else if let Some(param_index) = single_int_countdown_param_index(fd) {
1630 if is_closed_world_pure_fn(&fd.name, inputs)
1638 && let Some((base_arm_literal, base_arm_body, wildcard_arm_body)) =
1639 int_countdown_native_arms(fd, param_index)
1640 && let Some((callee_param_name, _)) = fd.params.get(param_index)
1641 {
1642 let precondition = find_single_external_caller_predicate(
1647 &fd.name,
1648 param_index,
1649 callee_param_name,
1650 inputs,
1651 )
1652 .unwrap_or_default();
1653 plans.insert(
1654 fd.name.clone(),
1655 RecursionPlan::IntCountdownGuarded {
1656 param_index,
1657 base_arm_literal,
1658 base_arm_body,
1659 wildcard_arm_body,
1660 precondition,
1661 },
1662 );
1663 } else {
1664 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
1665 }
1666 } else if supports_single_sizeof_structural(fd, inputs) {
1667 plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
1668 } else if let Some(param_index) = single_list_structural_param_index(fd) {
1669 plans.insert(
1670 fd.name.clone(),
1671 RecursionPlan::ListStructural { param_index },
1672 );
1673 } else if supports_single_string_pos_advance(fd) {
1674 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
1675 } else {
1676 issues.push(ProofModeIssue {
1677 line: fd.line,
1678 message: format!(
1679 "recursive function '{}' is outside proof subset (currently supported: Int countdown, 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)",
1680 fd.name
1681 ),
1682 });
1683 }
1684 }
1685
1686 (plans, issues)
1687}