Skip to main content

aver/codegen/recursion/
detect.rs

1//! Proof-mode recursion classifier.
2//!
3//! Backend-neutral pattern matching over the Aver AST that decides,
4//! for each recursive pure fn (or mutual-recursion SCC), which
5//! [`RecursionPlan`] variant applies — or emits a [`ProofModeIssue`]
6//! when the shape falls outside the supported set.
7//!
8//! Lean and Dafny consume the same plans through
9//! [`crate::codegen::recursion::analyze_plans`]; a couple of helpers
10//! that depend on AST queries tied to Lean's `toplevel` (pure-fn
11//! predicate, recursive-type-def predicate, type-def name) still live
12//! in `crate::codegen::lean` and are re-used here through
13//! `pub(crate)` exports. That could move to a neutral AST helper
14//! module in a later pass.
15use 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
37/// Local-variable name regardless of whether the expression has been
38/// resolved. Resolver rewrites `Expr::Ident(name)` for locals into
39/// `Expr::Resolved { name, .. }` (slot-numbered for the VM); pre-resolution
40/// passes still see `Expr::Ident`. Recursion-shape detectors run AFTER
41/// resolution at codegen time, so they must accept both forms — otherwise
42/// every list/string-recursive function silently falls outside the proof
43/// subset.
44pub(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            // Negative-guarded ascent (match n < 0) is handled as countdown
431            // because the fuel is natAbs(n) which works for both directions.
432            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
489/// Detect ascending-index recursion and extract the bound expression
490/// as an Aver AST (`Spanned<Expr>`). Returns `(param_index, bound)`.
491pub(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
520/// Extract the bound expression from `match param == BOUND` as an
521/// Aver AST node. Each backend renders this into its own idiom (Lean
522/// via `bound_expr_to_lean`, Dafny via its own `emit_expr` path).
523pub(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    // Verify: true arm = base (no self-call), false arm = recursive (has self-call)
535    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)| (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
979/// Classify every recursive pure fn in `ctx`. The returned map assigns
980/// each supported function a [`RecursionPlan`]; anything that falls
981/// outside the recognised shapes becomes a [`ProofModeIssue`].
982pub 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}