1use std::collections::{HashMap, HashSet};
16
17use crate::ast::{
18 BinOp, Expr, FnBody, FnDef, MatchArm, Pattern, Spanned, Stmt, TailCallData, TypeDef,
19};
20use crate::call_graph;
21use crate::codegen::CodegenContext;
22use crate::codegen::lean::{
23 find_type_def, pure_fns, recursive_pure_fn_names, recursive_type_names,
24 sizeof_measure_param_indices,
25};
26
27use super::{ProofModeIssue, RecursionPlan};
28
29pub(crate) fn expr_to_dotted_name(expr: &Spanned<Expr>) -> Option<String> {
30 match &expr.node {
31 Expr::Ident(name) => Some(name.clone()),
32 Expr::Attr(obj, field) => expr_to_dotted_name(obj).map(|p| format!("{}.{}", p, field)),
33 _ => None,
34 }
35}
36
37pub(crate) fn local_name_of(expr: &Spanned<Expr>) -> Option<&str> {
45 match &expr.node {
46 Expr::Ident(name) => Some(name.as_str()),
47 Expr::Resolved { name, .. } => Some(name.as_str()),
48 _ => None,
49 }
50}
51
52pub(crate) fn call_matches(name: &str, target: &str) -> bool {
53 name == target || name.rsplit('.').next() == Some(target)
54}
55
56pub(crate) fn call_is_in_set(name: &str, targets: &HashSet<String>) -> bool {
57 call_matches_any(name, targets)
58}
59
60pub(crate) fn canonical_callee_name(name: &str, targets: &HashSet<String>) -> Option<String> {
61 if targets.contains(name) {
62 return Some(name.to_string());
63 }
64 name.rsplit('.')
65 .next()
66 .filter(|last| targets.contains(*last))
67 .map(ToString::to_string)
68}
69
70pub(crate) fn call_matches_any(name: &str, targets: &HashSet<String>) -> bool {
71 if targets.contains(name) {
72 return true;
73 }
74 match name.rsplit('.').next() {
75 Some(last) => targets.contains(last),
76 None => false,
77 }
78}
79
80pub(crate) fn is_int_minus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
81 match &expr.node {
82 Expr::BinOp(BinOp::Sub, left, right) => {
83 local_name_of(left).is_some_and(|id| id == param_name)
84 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
85 }
86 Expr::FnCall(callee, args) => {
87 let Some(name) = expr_to_dotted_name(callee) else {
88 return false;
89 };
90 (name == "Int.sub" || name == "int.sub")
91 && args.len() == 2
92 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
93 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
94 }
95 _ => false,
96 }
97}
98
99pub(crate) fn collect_calls_from_expr<'a>(
100 expr: &'a Spanned<Expr>,
101 out: &mut Vec<(String, Vec<&'a Spanned<Expr>>)>,
102) {
103 match &expr.node {
104 Expr::FnCall(callee, args) => {
105 if let Some(name) = expr_to_dotted_name(callee) {
106 out.push((name, args.iter().collect()));
107 }
108 collect_calls_from_expr(callee, out);
109 for arg in args {
110 collect_calls_from_expr(arg, out);
111 }
112 }
113 Expr::TailCall(boxed) => {
114 let TailCallData {
115 target: name, args, ..
116 } = boxed.as_ref();
117 out.push((name.clone(), args.iter().collect()));
118 for arg in args {
119 collect_calls_from_expr(arg, out);
120 }
121 }
122 Expr::Attr(obj, _) => collect_calls_from_expr(obj, out),
123 Expr::BinOp(_, left, right) => {
124 collect_calls_from_expr(left, out);
125 collect_calls_from_expr(right, out);
126 }
127 Expr::Neg(inner) => collect_calls_from_expr(inner, out),
128 Expr::Match { subject, arms, .. } => {
129 collect_calls_from_expr(subject, out);
130 for arm in arms {
131 collect_calls_from_expr(&arm.body, out);
132 }
133 }
134 Expr::Constructor(_, inner) => {
135 if let Some(inner) = inner {
136 collect_calls_from_expr(inner, out);
137 }
138 }
139 Expr::ErrorProp(inner) => collect_calls_from_expr(inner, out),
140 Expr::InterpolatedStr(parts) => {
141 for p in parts {
142 if let crate::ast::StrPart::Parsed(e) = p {
143 collect_calls_from_expr(e, out);
144 }
145 }
146 }
147 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
148 for item in items {
149 collect_calls_from_expr(item, out);
150 }
151 }
152 Expr::MapLiteral(entries) => {
153 for (k, v) in entries {
154 collect_calls_from_expr(k, out);
155 collect_calls_from_expr(v, out);
156 }
157 }
158 Expr::RecordCreate { fields, .. } => {
159 for (_, v) in fields {
160 collect_calls_from_expr(v, out);
161 }
162 }
163 Expr::RecordUpdate { base, updates, .. } => {
164 collect_calls_from_expr(base, out);
165 for (_, v) in updates {
166 collect_calls_from_expr(v, out);
167 }
168 }
169 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
170 }
171}
172
173pub(crate) fn collect_calls_from_body(body: &FnBody) -> Vec<(String, Vec<&Spanned<Expr>>)> {
174 let mut out = Vec::new();
175 for stmt in body.stmts() {
176 match stmt {
177 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => collect_calls_from_expr(expr, &mut out),
178 }
179 }
180 out
181}
182
183pub(crate) fn collect_list_tail_binders_from_expr(
184 expr: &Spanned<Expr>,
185 list_param_name: &str,
186 tails: &mut HashSet<String>,
187) {
188 match &expr.node {
189 Expr::Match { subject, arms, .. } => {
190 if local_name_of(subject).is_some_and(|id| id == list_param_name) {
191 for MatchArm { pattern, .. } in arms {
192 if let Pattern::Cons(_, tail) = pattern {
193 tails.insert(tail.clone());
194 }
195 }
196 }
197 for arm in arms {
198 collect_list_tail_binders_from_expr(&arm.body, list_param_name, tails);
199 }
200 collect_list_tail_binders_from_expr(subject, list_param_name, tails);
201 }
202 Expr::FnCall(callee, args) => {
203 collect_list_tail_binders_from_expr(callee, list_param_name, tails);
204 for arg in args {
205 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
206 }
207 }
208 Expr::TailCall(boxed) => {
209 let TailCallData {
210 target: _, args, ..
211 } = boxed.as_ref();
212 for arg in args {
213 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
214 }
215 }
216 Expr::Attr(obj, _) => collect_list_tail_binders_from_expr(obj, list_param_name, tails),
217 Expr::BinOp(_, left, right) => {
218 collect_list_tail_binders_from_expr(left, list_param_name, tails);
219 collect_list_tail_binders_from_expr(right, list_param_name, tails);
220 }
221 Expr::Neg(inner) => collect_list_tail_binders_from_expr(inner, list_param_name, tails),
222 Expr::Constructor(_, inner) => {
223 if let Some(inner) = inner {
224 collect_list_tail_binders_from_expr(inner, list_param_name, tails);
225 }
226 }
227 Expr::ErrorProp(inner) => {
228 collect_list_tail_binders_from_expr(inner, list_param_name, tails)
229 }
230 Expr::InterpolatedStr(parts) => {
231 for p in parts {
232 if let crate::ast::StrPart::Parsed(e) = p {
233 collect_list_tail_binders_from_expr(e, list_param_name, tails);
234 }
235 }
236 }
237 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
238 for item in items {
239 collect_list_tail_binders_from_expr(item, list_param_name, tails);
240 }
241 }
242 Expr::MapLiteral(entries) => {
243 for (k, v) in entries {
244 collect_list_tail_binders_from_expr(k, list_param_name, tails);
245 collect_list_tail_binders_from_expr(v, list_param_name, tails);
246 }
247 }
248 Expr::RecordCreate { fields, .. } => {
249 for (_, v) in fields {
250 collect_list_tail_binders_from_expr(v, list_param_name, tails);
251 }
252 }
253 Expr::RecordUpdate { base, updates, .. } => {
254 collect_list_tail_binders_from_expr(base, list_param_name, tails);
255 for (_, v) in updates {
256 collect_list_tail_binders_from_expr(v, list_param_name, tails);
257 }
258 }
259 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
260 }
261}
262
263pub(crate) fn collect_list_tail_binders(fd: &FnDef, list_param_name: &str) -> HashSet<String> {
264 let mut tails = HashSet::new();
265 for stmt in fd.body.stmts() {
266 match stmt {
267 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
268 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
269 }
270 }
271 }
272 tails
273}
274
275pub(crate) fn recursive_constructor_binders(
276 td: &TypeDef,
277 variant_name: &str,
278 binders: &[String],
279) -> Vec<String> {
280 let variant_short = variant_name.rsplit('.').next().unwrap_or(variant_name);
281 match td {
282 TypeDef::Sum { name, variants, .. } => variants
283 .iter()
284 .find(|variant| variant.name == variant_short)
285 .map(|variant| {
286 variant
287 .fields
288 .iter()
289 .zip(binders.iter())
290 .filter_map(|(field_ty, binder)| {
291 (field_ty.trim() == name).then_some(binder.clone())
292 })
293 .collect()
294 })
295 .unwrap_or_default(),
296 TypeDef::Product { .. } => Vec::new(),
297 }
298}
299
300pub(crate) fn grow_recursive_subterm_binders_from_expr(
301 expr: &Spanned<Expr>,
302 tracked: &HashSet<String>,
303 td: &TypeDef,
304 out: &mut HashSet<String>,
305) {
306 match &expr.node {
307 Expr::Match { subject, arms, .. } => {
308 if let Expr::Ident(subject_name) = &subject.node
309 && tracked.contains(subject_name)
310 {
311 for arm in arms {
312 if let Pattern::Constructor(variant_name, binders) = &arm.pattern {
313 out.extend(recursive_constructor_binders(td, variant_name, binders));
314 }
315 }
316 }
317 grow_recursive_subterm_binders_from_expr(subject, tracked, td, out);
318 for arm in arms {
319 grow_recursive_subterm_binders_from_expr(&arm.body, tracked, td, out);
320 }
321 }
322 Expr::FnCall(callee, args) => {
323 grow_recursive_subterm_binders_from_expr(callee, tracked, td, out);
324 for arg in args {
325 grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
326 }
327 }
328 Expr::Attr(obj, _) => grow_recursive_subterm_binders_from_expr(obj, tracked, td, out),
329 Expr::BinOp(_, left, right) => {
330 grow_recursive_subterm_binders_from_expr(left, tracked, td, out);
331 grow_recursive_subterm_binders_from_expr(right, tracked, td, out);
332 }
333 Expr::Neg(inner) => grow_recursive_subterm_binders_from_expr(inner, tracked, td, out),
334 Expr::Constructor(_, Some(inner)) | Expr::ErrorProp(inner) => {
335 grow_recursive_subterm_binders_from_expr(inner, tracked, td, out)
336 }
337 Expr::InterpolatedStr(parts) => {
338 for part in parts {
339 if let crate::ast::StrPart::Parsed(inner) = part {
340 grow_recursive_subterm_binders_from_expr(inner, tracked, td, out);
341 }
342 }
343 }
344 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
345 for item in items {
346 grow_recursive_subterm_binders_from_expr(item, tracked, td, out);
347 }
348 }
349 Expr::MapLiteral(entries) => {
350 for (k, v) in entries {
351 grow_recursive_subterm_binders_from_expr(k, tracked, td, out);
352 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
353 }
354 }
355 Expr::RecordCreate { fields, .. } => {
356 for (_, v) in fields {
357 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
358 }
359 }
360 Expr::RecordUpdate { base, updates, .. } => {
361 grow_recursive_subterm_binders_from_expr(base, tracked, td, out);
362 for (_, v) in updates {
363 grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
364 }
365 }
366 Expr::TailCall(boxed) => {
367 for arg in &boxed.args {
368 grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
369 }
370 }
371 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } | Expr::Constructor(_, None) => {}
372 }
373}
374
375pub(crate) fn collect_recursive_subterm_binders(
376 fd: &FnDef,
377 param_name: &str,
378 param_type: &str,
379 ctx: &CodegenContext,
380) -> HashSet<String> {
381 let Some(td) = find_type_def(ctx, param_type) else {
382 return HashSet::new();
383 };
384 let mut tracked: HashSet<String> = HashSet::from([param_name.to_string()]);
385 loop {
386 let mut discovered = HashSet::new();
387 for stmt in fd.body.stmts() {
388 match stmt {
389 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
390 grow_recursive_subterm_binders_from_expr(expr, &tracked, td, &mut discovered);
391 }
392 }
393 }
394 let before = tracked.len();
395 tracked.extend(discovered);
396 if tracked.len() == before {
397 break;
398 }
399 }
400 tracked.remove(param_name);
401 tracked
402}
403
404pub(crate) fn single_int_countdown_param_index(fd: &FnDef) -> Option<usize> {
405 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
406 .into_iter()
407 .filter(|(name, _)| call_matches(name, &fd.name))
408 .map(|(_, args)| args)
409 .collect();
410 if recursive_calls.is_empty() {
411 return None;
412 }
413
414 fd.params
415 .iter()
416 .enumerate()
417 .find_map(|(idx, (param_name, param_ty))| {
418 if param_ty != "Int" {
419 return None;
420 }
421 let countdown_ok = recursive_calls.iter().all(|args| {
422 args.get(idx)
423 .cloned()
424 .is_some_and(|arg| is_int_minus_positive(arg, param_name))
425 });
426 if countdown_ok {
427 return Some(idx);
428 }
429
430 let ascent_ok = recursive_calls.iter().all(|args| {
433 args.get(idx)
434 .copied()
435 .is_some_and(|arg| is_int_plus_positive(arg, param_name))
436 });
437 (ascent_ok && has_negative_guarded_ascent(fd, param_name)).then_some(idx)
438 })
439}
440
441pub(crate) fn has_negative_guarded_ascent(fd: &FnDef, param_name: &str) -> bool {
442 let Some(tail) = fd.body.tail_expr() else {
443 return false;
444 };
445 let Expr::Match { subject, arms, .. } = &tail.node else {
446 return false;
447 };
448 let Expr::BinOp(BinOp::Lt, left, right) = &subject.node else {
449 return false;
450 };
451 if !is_ident(left, param_name)
452 || !matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(0)))
453 {
454 return false;
455 }
456
457 let mut true_arm = None;
458 let mut false_arm = None;
459 for arm in arms {
460 match arm.pattern {
461 Pattern::Literal(crate::ast::Literal::Bool(true)) => true_arm = Some(arm.body.as_ref()),
462 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
463 false_arm = Some(arm.body.as_ref())
464 }
465 _ => return false,
466 }
467 }
468
469 let Some(true_arm) = true_arm else {
470 return false;
471 };
472 let Some(false_arm) = false_arm else {
473 return false;
474 };
475
476 let mut true_calls = Vec::new();
477 collect_calls_from_expr(true_arm, &mut true_calls);
478 let mut false_calls = Vec::new();
479 collect_calls_from_expr(false_arm, &mut false_calls);
480
481 true_calls
482 .iter()
483 .any(|(name, _)| call_matches(name, &fd.name))
484 && false_calls
485 .iter()
486 .all(|(name, _)| !call_matches(name, &fd.name))
487}
488
489pub(crate) fn single_int_ascending_param(fd: &FnDef) -> Option<(usize, Spanned<Expr>)> {
492 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
493 .into_iter()
494 .filter(|(name, _)| call_matches(name, &fd.name))
495 .map(|(_, args)| args)
496 .collect();
497 if recursive_calls.is_empty() {
498 return None;
499 }
500
501 for (idx, (param_name, param_ty)) in fd.params.iter().enumerate() {
502 if param_ty != "Int" {
503 continue;
504 }
505 let ascent_ok = recursive_calls.iter().all(|args| {
506 args.get(idx)
507 .cloned()
508 .is_some_and(|arg| is_int_plus_positive(arg, param_name))
509 });
510 if !ascent_ok {
511 continue;
512 }
513 if let Some(bound) = extract_equality_bound_expr(fd, param_name) {
514 return Some((idx, bound));
515 }
516 }
517 None
518}
519
520pub(crate) fn extract_equality_bound_expr(fd: &FnDef, param_name: &str) -> Option<Spanned<Expr>> {
524 let tail = fd.body.tail_expr()?;
525 let Expr::Match { subject, arms, .. } = &tail.node else {
526 return None;
527 };
528 let Expr::BinOp(BinOp::Eq, left, right) = &subject.node else {
529 return None;
530 };
531 if !is_ident(left, param_name) {
532 return None;
533 }
534 let mut true_has_self = false;
536 let mut false_has_self = false;
537 for arm in arms {
538 match arm.pattern {
539 Pattern::Literal(crate::ast::Literal::Bool(true)) => {
540 let mut calls = Vec::new();
541 collect_calls_from_expr(&arm.body, &mut calls);
542 true_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
543 }
544 Pattern::Literal(crate::ast::Literal::Bool(false)) => {
545 let mut calls = Vec::new();
546 collect_calls_from_expr(&arm.body, &mut calls);
547 false_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
548 }
549 _ => return None,
550 }
551 }
552 if true_has_self || !false_has_self {
553 return None;
554 }
555 Some((**right).clone())
556}
557
558pub(crate) fn supports_single_sizeof_structural(fd: &FnDef, ctx: &CodegenContext) -> bool {
559 let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
560 .into_iter()
561 .filter(|(name, _)| call_matches(name, &fd.name))
562 .map(|(_, args)| args)
563 .collect();
564 if recursive_calls.is_empty() {
565 return false;
566 }
567
568 let metric_indices = sizeof_measure_param_indices(fd);
569 if metric_indices.is_empty() {
570 return false;
571 }
572
573 let binder_sets: HashMap<usize, HashSet<String>> = metric_indices
574 .iter()
575 .filter_map(|idx| {
576 let (param_name, param_type) = fd.params.get(*idx)?;
577 recursive_type_names(ctx).contains(param_type).then(|| {
578 (
579 *idx,
580 collect_recursive_subterm_binders(fd, param_name, param_type, ctx),
581 )
582 })
583 })
584 .collect();
585
586 if binder_sets.values().all(HashSet::is_empty) {
587 return false;
588 }
589
590 recursive_calls.iter().all(|args| {
591 let mut strictly_smaller = false;
592 for idx in &metric_indices {
593 let Some((param_name, _)) = fd.params.get(*idx) else {
594 return false;
595 };
596 let Some(arg) = args.get(*idx).cloned() else {
597 return false;
598 };
599 if is_ident(arg, param_name) {
600 continue;
601 }
602 let Some(binders) = binder_sets.get(idx) else {
603 return false;
604 };
605 if local_name_of(arg).is_some_and(|id| binders.contains(id)) {
606 strictly_smaller = true;
607 continue;
608 }
609 return false;
610 }
611 strictly_smaller
612 })
613}
614
615pub(crate) fn single_list_structural_param_index(fd: &FnDef) -> Option<usize> {
616 fd.params
617 .iter()
618 .enumerate()
619 .find_map(|(param_index, (param_name, param_ty))| {
620 if !(param_ty.starts_with("List<") || param_ty == "List") {
621 return None;
622 }
623
624 let tails = collect_list_tail_binders(fd, param_name);
625 if tails.is_empty() {
626 return None;
627 }
628
629 let recursive_calls: Vec<Option<&Spanned<Expr>>> =
630 collect_calls_from_body(fd.body.as_ref())
631 .into_iter()
632 .filter(|(name, _)| call_matches(name, &fd.name))
633 .map(|(_, args)| args.get(param_index).cloned())
634 .collect();
635 if recursive_calls.is_empty() {
636 return None;
637 }
638
639 recursive_calls
640 .into_iter()
641 .all(|arg| {
642 arg.and_then(local_name_of)
643 .is_some_and(|id| tails.contains(id))
644 })
645 .then_some(param_index)
646 })
647}
648
649pub(crate) fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
650 local_name_of(expr).is_some_and(|id| id == name)
651}
652
653pub(crate) fn is_int_plus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
654 match &expr.node {
655 Expr::BinOp(BinOp::Add, left, right) => {
656 local_name_of(left).is_some_and(|id| id == param_name)
657 && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
658 }
659 Expr::FnCall(callee, args) => {
660 let Some(name) = expr_to_dotted_name(callee) else {
661 return false;
662 };
663 (name == "Int.add" || name == "int.add")
664 && args.len() == 2
665 && local_name_of(&args[0]).is_some_and(|id| id == param_name)
666 && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
667 }
668 _ => false,
669 }
670}
671
672pub(crate) fn is_skip_ws_advance(
673 expr: &Spanned<Expr>,
674 string_param: &str,
675 pos_param: &str,
676) -> bool {
677 let Expr::FnCall(callee, args) = &expr.node else {
678 return false;
679 };
680 let Some(name) = expr_to_dotted_name(callee) else {
681 return false;
682 };
683 if !call_matches(&name, "skipWs") || args.len() != 2 {
684 return false;
685 }
686 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
687}
688
689pub(crate) fn is_skip_ws_same(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
690 let Expr::FnCall(callee, args) = &expr.node else {
691 return false;
692 };
693 let Some(name) = expr_to_dotted_name(callee) else {
694 return false;
695 };
696 if !call_matches(&name, "skipWs") || args.len() != 2 {
697 return false;
698 }
699 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
700}
701
702pub(crate) fn is_string_pos_advance(
703 expr: &Spanned<Expr>,
704 string_param: &str,
705 pos_param: &str,
706) -> bool {
707 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
708}
709
710#[derive(Clone, Copy, Debug, Eq, PartialEq)]
711pub(crate) enum StringPosEdge {
712 Same,
713 Advance,
714}
715
716pub(crate) fn classify_string_pos_edge(
717 expr: &Spanned<Expr>,
718 string_param: &str,
719 pos_param: &str,
720) -> Option<StringPosEdge> {
721 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
722 return Some(StringPosEdge::Same);
723 }
724 if is_string_pos_advance(expr, string_param, pos_param) {
725 return Some(StringPosEdge::Advance);
726 }
727 if let Expr::FnCall(callee, args) = &expr.node {
728 let name = expr_to_dotted_name(callee)?;
729 if call_matches(&name, "skipWs")
730 && args.len() == 2
731 && is_ident(&args[0], string_param)
732 && local_name_of(&args[1]).is_some_and(|id| id != pos_param)
733 {
734 return Some(StringPosEdge::Advance);
735 }
736 }
737 if local_name_of(expr).is_some_and(|id| id != pos_param) {
738 return Some(StringPosEdge::Advance);
739 }
740 None
741}
742
743pub(crate) fn ranks_from_same_edges(
744 names: &HashSet<String>,
745 same_edges: &HashMap<String, HashSet<String>>,
746) -> Option<HashMap<String, usize>> {
747 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
748 for outs in same_edges.values() {
749 for to in outs {
750 if let Some(entry) = indegree.get_mut(to) {
751 *entry += 1;
752 } else {
753 return None;
754 }
755 }
756 }
757
758 let mut queue: Vec<String> = indegree
759 .iter()
760 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
761 .collect();
762 queue.sort();
763 let mut topo = Vec::new();
764 while let Some(node) = queue.pop() {
765 topo.push(node.clone());
766 let outs = same_edges.get(&node).cloned().unwrap_or_default();
767 let mut newly_zero = Vec::new();
768 for to in outs {
769 if let Some(entry) = indegree.get_mut(&to) {
770 *entry -= 1;
771 if *entry == 0 {
772 newly_zero.push(to);
773 }
774 } else {
775 return None;
776 }
777 }
778 newly_zero.sort();
779 queue.extend(newly_zero);
780 }
781
782 if topo.len() != names.len() {
783 return None;
784 }
785
786 let n = topo.len();
787 let mut ranks = HashMap::new();
788 for (idx, name) in topo.into_iter().enumerate() {
789 ranks.insert(name, n - idx);
790 }
791 Some(ranks)
792}
793
794pub(crate) fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
795 let Some((string_param, string_ty)) = fd.params.first() else {
796 return false;
797 };
798 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
799 return false;
800 };
801 if string_ty != "String" || pos_ty != "Int" {
802 return false;
803 }
804
805 type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
806 let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
807 .into_iter()
808 .filter(|(name, _)| call_matches(name, &fd.name))
809 .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
810 .collect();
811 if recursive_calls.is_empty() {
812 return false;
813 }
814
815 recursive_calls.into_iter().all(|(arg0, arg1)| {
816 arg0.is_some_and(|e| is_ident(e, string_param))
817 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
818 })
819}
820
821pub(crate) fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
822 if component.len() < 2 {
823 return false;
824 }
825 if component
826 .iter()
827 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
828 {
829 return false;
830 }
831 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
832 let mut any_intra = false;
833 for fd in component {
834 let param_name = &fd.params[0].0;
835 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
836 if !call_is_in_set(&callee, &names) {
837 continue;
838 }
839 any_intra = true;
840 let Some(arg0) = args.first().cloned() else {
841 return false;
842 };
843 if !is_int_minus_positive(arg0, param_name) {
844 return false;
845 }
846 }
847 }
848 any_intra
849}
850
851pub(crate) fn supports_mutual_string_pos_advance(
852 component: &[&FnDef],
853) -> Option<HashMap<String, usize>> {
854 if component.len() < 2 {
855 return None;
856 }
857 if component.iter().any(|fd| {
858 !matches!(fd.params.first(), Some((_, t)) if t == "String")
859 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
860 }) {
861 return None;
862 }
863
864 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
865 let mut same_edges: HashMap<String, HashSet<String>> =
866 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
867 let mut any_intra = false;
868
869 for fd in component {
870 let string_param = &fd.params[0].0;
871 let pos_param = &fd.params[1].0;
872 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
873 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
874 continue;
875 };
876 any_intra = true;
877
878 let arg0 = args.first().cloned()?;
879 let arg1 = args.get(1).cloned()?;
880
881 if !is_ident(arg0, string_param) {
882 return None;
883 }
884
885 match classify_string_pos_edge(arg1, string_param, pos_param) {
886 Some(StringPosEdge::Same) => {
887 if let Some(edges) = same_edges.get_mut(&fd.name) {
888 edges.insert(callee);
889 } else {
890 return None;
891 }
892 }
893 Some(StringPosEdge::Advance) => {}
894 None => return None,
895 }
896 }
897 }
898
899 if !any_intra {
900 return None;
901 }
902
903 ranks_from_same_edges(&names, &same_edges)
904}
905
906pub(crate) fn is_scalar_like_type(type_name: &str) -> bool {
907 matches!(
908 type_name,
909 "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
910 )
911}
912
913pub(crate) fn supports_mutual_sizeof_ranked(
914 component: &[&FnDef],
915) -> Option<HashMap<String, usize>> {
916 if component.len() < 2 {
917 return None;
918 }
919 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
920 let metric_indices: HashMap<String, Vec<usize>> = component
921 .iter()
922 .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
923 .collect();
924 if component.iter().any(|fd| {
925 metric_indices
926 .get(&fd.name)
927 .is_none_or(|indices| indices.is_empty())
928 }) {
929 return None;
930 }
931
932 let mut same_edges: HashMap<String, HashSet<String>> =
933 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
934 let mut any_intra = false;
935 for fd in component {
936 let caller_metric_indices = metric_indices.get(&fd.name)?;
937 let caller_metric_params: Vec<&str> = caller_metric_indices
938 .iter()
939 .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
940 .collect();
941 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
942 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
943 continue;
944 };
945 any_intra = true;
946 let callee_metric_indices = metric_indices.get(&callee)?;
947 let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
948 && callee_metric_indices
949 .iter()
950 .enumerate()
951 .all(|(pos, callee_idx)| {
952 let Some(arg) = args.get(*callee_idx).cloned() else {
953 return false;
954 };
955 is_ident(arg, caller_metric_params[pos])
956 });
957 if is_same_edge {
958 if let Some(edges) = same_edges.get_mut(&fd.name) {
959 edges.insert(callee);
960 } else {
961 return None;
962 }
963 }
964 }
965 }
966 if !any_intra {
967 return None;
968 }
969
970 let ranks = ranks_from_same_edges(&names, &same_edges)?;
971 let mut out = HashMap::new();
972 for fd in component {
973 let rank = ranks.get(&fd.name).cloned()?;
974 out.insert(fd.name.clone(), rank);
975 }
976 Some(out)
977}
978
979pub fn analyze_plans(
983 ctx: &CodegenContext,
984) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
985 let mut plans = HashMap::new();
986 let mut issues = Vec::new();
987
988 let all_pure = pure_fns(ctx);
989 let recursive_names = recursive_pure_fn_names(ctx);
990 let components = call_graph::ordered_fn_components(&all_pure, &ctx.module_prefixes);
991
992 for component in components {
993 if component.is_empty() {
994 continue;
995 }
996 let is_recursive_component =
997 component.len() > 1 || recursive_names.contains(&component[0].name);
998 if !is_recursive_component {
999 continue;
1000 }
1001
1002 if component.len() > 1 {
1003 if supports_mutual_int_countdown(&component) {
1004 for fd in &component {
1005 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
1006 }
1007 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
1008 for fd in &component {
1009 if let Some(rank) = ranks.get(&fd.name).cloned() {
1010 plans.insert(
1011 fd.name.clone(),
1012 RecursionPlan::MutualStringPosAdvance { rank },
1013 );
1014 }
1015 }
1016 } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
1017 for fd in &component {
1018 if let Some(rank) = rankings.get(&fd.name).cloned() {
1019 plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
1020 }
1021 }
1022 } else {
1023 let names = component
1024 .iter()
1025 .map(|fd| fd.name.clone())
1026 .collect::<Vec<_>>()
1027 .join(", ");
1028 let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
1029 issues.push(ProofModeIssue {
1030 line,
1031 message: format!(
1032 "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
1033 names
1034 ),
1035 });
1036 }
1037 continue;
1038 }
1039
1040 let fd = component[0];
1041 if crate::codegen::lean::recurrence::detect_second_order_int_linear_recurrence(fd).is_some()
1042 {
1043 plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
1044 } else if let Some((param_index, bound)) = single_int_ascending_param(fd) {
1045 plans.insert(
1046 fd.name.clone(),
1047 RecursionPlan::IntAscending { param_index, bound },
1048 );
1049 } else if let Some(param_index) = single_int_countdown_param_index(fd) {
1050 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
1051 } else if supports_single_sizeof_structural(fd, ctx) {
1052 plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
1053 } else if let Some(param_index) = single_list_structural_param_index(fd) {
1054 plans.insert(
1055 fd.name.clone(),
1056 RecursionPlan::ListStructural { param_index },
1057 );
1058 } else if supports_single_string_pos_advance(fd) {
1059 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
1060 } else {
1061 issues.push(ProofModeIssue {
1062 line: fd.line,
1063 message: format!(
1064 "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)",
1065 fd.name
1066 ),
1067 });
1068 }
1069 }
1070
1071 (plans, issues)
1072}