Skip to main content

meta_language/translation/check/
recursion.rs

1//! Recursion analysis and structural natural-number decomposition.
2
3use super::{
4    BinaryOp, Case, Decimal, Decl, Expr, FnDecl, HashMap, HashSet, LitValue, NAT, Node, Pattern,
5    Semantics, Type, binary_node, text_lit,
6};
7
8/// Inside `match v with | succ k => …`, the value `k + 1` is `v` itself.
9/// Rewriting it to `v` keeps recursion such as `fib (n + 1) + fib n`
10/// structural for Lean and Rocq without changing any value.
11pub(super) fn reuse_successors(expr: &Expr, predecessors: &HashMap<String, String>) -> Expr {
12    match &expr.node {
13        Node::Binary {
14            op: BinaryOp::Add,
15            left,
16            right,
17            ..
18        } if expr.ty == Type::Nat
19            && left
20                .var_name()
21                .is_some_and(|name| predecessors.contains_key(name))
22            && matches!(&right.node, Node::Lit { value: LitValue::Text(one) } if one == "1") =>
23        {
24            let name = &predecessors[left.var_name().unwrap_or_default()];
25            Expr::var(name.clone(), expr.ty.clone()).with_span(expr.span)
26        }
27        Node::Let { name, value, body } => {
28            let inner = without(predecessors, &[name.as_str()]);
29            Expr {
30                node: Node::Let {
31                    name: name.clone(),
32                    value: Box::new(reuse_successors(value, predecessors)),
33                    body: Box::new(reuse_successors(body, &inner)),
34                },
35                ..expr.clone()
36            }
37        }
38        Node::Match { scrutinee, cases } => {
39            let cases = cases
40                .iter()
41                .map(|kase| {
42                    let bound: Vec<&str> = match &kase.pattern {
43                        Pattern::NatSucc { name } => vec![name.as_str()],
44                        Pattern::Ctor { binds, .. } => {
45                            binds.iter().flatten().map(String::as_str).collect()
46                        }
47                        _ => Vec::new(),
48                    };
49                    let mut inner = without(predecessors, &bound);
50                    if let (Pattern::NatSucc { name }, Some(variable)) =
51                        (&kase.pattern, scrutinee.var_name())
52                        && !bound.contains(&variable)
53                    {
54                        inner.insert(name.clone(), variable.to_owned());
55                    }
56                    Case {
57                        pattern: kase.pattern.clone(),
58                        body: reuse_successors(&kase.body, &inner),
59                    }
60                })
61                .collect();
62            Expr {
63                node: Node::Match {
64                    scrutinee: Box::new(reuse_successors(scrutinee, predecessors)),
65                    cases,
66                },
67                ..expr.clone()
68            }
69        }
70        _ => expr.map_children(&mut |child| reuse_successors(child, predecessors)),
71    }
72}
73
74pub(super) fn without(map: &HashMap<String, String>, names: &[&str]) -> HashMap<String, String> {
75    map.iter()
76        .filter(|(key, value)| !names.contains(&key.as_str()) && !names.contains(&value.as_str()))
77        .map(|(key, value)| (key.clone(), value.clone()))
78        .collect()
79}
80
81/// Marks each function recursive or not and, for recursive functions, finds a
82/// structurally decreasing parameter. Without one, Lean writes a partial def and
83/// Rocq a Definition over `ml_fix`; the other targets accept general recursion.
84pub(super) fn analyse_recursion(declarations: &mut [Decl]) {
85    for decl in declarations.iter_mut() {
86        let Decl::Fn(entry) = decl else { continue };
87        let mut calls = Vec::new();
88        collect_calls(&entry.body, &mut calls);
89        entry.recursive = calls.contains(&entry.full_name);
90        entry.decreasing = if entry.recursive {
91            structural_parameter(entry)
92        } else {
93            None
94        };
95        if entry.recursive && entry.decreasing.is_none() {
96            expose_natural_cases(entry);
97        }
98    }
99    let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
100    for decl in declarations.iter() {
101        let Decl::Fn(entry) = decl else { continue };
102        let mut calls = Vec::new();
103        collect_calls(&entry.body, &mut calls);
104        graph.insert(
105            entry.full_name.clone(),
106            calls
107                .into_iter()
108                .filter(|call| *call != entry.full_name)
109                .collect(),
110        );
111    }
112    for decl in declarations.iter_mut() {
113        let Decl::Fn(entry) = decl else { continue };
114        let targets = &graph[&entry.full_name];
115        if targets
116            .iter()
117            .any(|target| reaches(&graph, target, &entry.full_name, &mut HashSet::new()))
118        {
119            entry.mutual = true;
120        }
121    }
122}
123
124pub(super) fn reaches(
125    graph: &HashMap<String, HashSet<String>>,
126    from: &str,
127    to: &str,
128    seen: &mut HashSet<String>,
129) -> bool {
130    if from == to {
131        return true;
132    }
133    if !seen.insert(from.to_owned()) {
134        return false;
135    }
136    graph
137        .get(from)
138        .is_some_and(|targets| targets.iter().any(|next| reaches(graph, next, to, seen)))
139}
140
141/// The functions an expression calls, in evaluation order.
142pub fn collect_calls(expr: &Expr, calls: &mut Vec<String>) {
143    if let Node::Call { func, .. } = &expr.node {
144        calls.push(func.clone());
145    }
146    for child in expr.children() {
147        collect_calls(child, calls);
148    }
149}
150
151pub(super) fn structural_parameter(entry: &FnDecl) -> Option<usize> {
152    entry.params.iter().enumerate().position(|(index, param)| {
153        decreases_on(
154            &entry.body,
155            &entry.full_name,
156            index,
157            &param.name,
158            &HashSet::new(),
159        )
160    })
161}
162
163/// Every recursive call's argument at `index` must be a strict subterm of `name`.
164pub(super) fn decreases_on(
165    expr: &Expr,
166    func: &str,
167    index: usize,
168    name: &str,
169    subterms: &HashSet<String>,
170) -> bool {
171    match &expr.node {
172        Node::Call { func: callee, args } => {
173            if callee == func {
174                let decreasing = args
175                    .get(index)
176                    .and_then(Expr::var_name)
177                    .is_some_and(|arg| subterms.contains(arg));
178                if !decreasing {
179                    return false;
180                }
181            }
182            args.iter()
183                .all(|arg| decreases_on(arg, func, index, name, subterms))
184        }
185        Node::Match { scrutinee, cases } => {
186            if !decreases_on(scrutinee, func, index, name, subterms) {
187                return false;
188            }
189            let is_param = scrutinee
190                .var_name()
191                .is_some_and(|variable| variable == name || subterms.contains(variable));
192            cases.iter().all(|kase| {
193                let mut inner = subterms.clone();
194                if is_param {
195                    match &kase.pattern {
196                        Pattern::NatSucc { name } => {
197                            inner.insert(name.clone());
198                        }
199                        Pattern::Ctor { binds, .. } => {
200                            inner.extend(binds.iter().flatten().cloned());
201                        }
202                        _ => {}
203                    }
204                }
205                decreases_on(&kase.body, func, index, name, &inner)
206            })
207        }
208        Node::Let {
209            name: local,
210            value,
211            body,
212        } => {
213            if !decreases_on(value, func, index, name, subterms) {
214                return false;
215            }
216            let mut inner = subterms.clone();
217            inner.remove(local);
218            // `let k := subterm` (introduced by pattern compilation) is itself a subterm.
219            if value
220                .var_name()
221                .is_some_and(|variable| subterms.contains(variable))
222            {
223                inner.insert(local.clone());
224            }
225            decreases_on(body, func, index, name, &inner)
226        }
227        _ => expr
228            .children()
229            .into_iter()
230            .all(|child| decreases_on(child, func, index, name, subterms)),
231    }
232}
233
234/// `if n == 0 then a else b` on a natural or unsigned machine-integer
235/// parameter is the case split `match n with 0 => a | succ p => b'`, where
236/// `b'` reads `n - k` as `p - (k - 1)` and `n == k` as `p == k - 1`: in `b`,
237/// `n` is at least 1, so both agree with truncated subtraction, with checked
238/// machine subtraction, and with a checked conversion of `n - k` back to a
239/// natural. A `let m := n` in `b` is an alias read the same way. Writing the
240/// split explicitly makes recursion such as `n * fact (n - 1)` structural,
241/// which Lean and Rocq require.
242pub(super) fn expose_natural_cases(entry: &mut FnDecl) {
243    for index in 0..entry.params.len() {
244        let param = &entry.params[index];
245        if !param.ty.is_natural() {
246            continue;
247        }
248        let mut fresh = 0;
249        let Some(body) = split_natural(&entry.body, &param.name, &mut fresh) else {
250            continue;
251        };
252        if decreases_on(&body, &entry.full_name, index, &param.name, &HashSet::new()) {
253            entry.body = body;
254            entry.decreasing = Some(index);
255            return;
256        }
257    }
258}
259
260/// A natural literal's value.
261pub(super) fn nat_literal(expr: &Expr) -> Option<Decimal> {
262    match &expr.node {
263        Node::Lit {
264            value: LitValue::Text(text),
265        } if !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()) => {
266            Decimal::parse(text)
267        }
268        _ => None,
269    }
270}
271
272/// `k - 1` for a positive canonical decimal `k`.
273pub(super) fn decrement(value: &Decimal) -> String {
274    let mut digits: Vec<u8> = value.digits.bytes().collect();
275    for digit in digits.iter_mut().rev() {
276        if *digit == b'0' {
277            *digit = b'9';
278        } else {
279            *digit -= 1;
280            break;
281        }
282    }
283    let text = String::from_utf8(digits).unwrap_or_default();
284    let trimmed = text.trim_start_matches('0');
285    if trimmed.is_empty() {
286        "0".to_owned()
287    } else {
288        trimmed.to_owned()
289    }
290}
291
292/// `n == 0`, `0 == n`: whether the zero test selects the `then` branch.
293pub(super) fn zero_test(cond: &Expr, name: &str) -> Option<bool> {
294    let Node::Binary {
295        op: op @ (BinaryOp::Eq | BinaryOp::Ne),
296        left,
297        right,
298        domain: Some(domain),
299        ..
300    } = &cond.node
301    else {
302        return None;
303    };
304    if !domain.is_natural() {
305        return None;
306    }
307    let zero_on = |a: &Expr, b: &Expr| {
308        a.var_name() == Some(name) && nat_literal(b).is_some_and(|value| value.digits == "0")
309    };
310    (zero_on(left, right) || zero_on(right, left)).then_some(*op == BinaryOp::Eq)
311}
312
313/// The locals a match binds in any case.
314pub(super) fn binds(cases: &[Case], name: &str) -> bool {
315    cases
316        .iter()
317        .any(|kase| kase.pattern.bound().contains(&name))
318}
319
320/// Splits zero tests on `name` into natural case splits; `None` when nothing changed.
321pub(super) fn split_natural(expr: &Expr, name: &str, fresh: &mut usize) -> Option<Expr> {
322    if let Node::If {
323        cond,
324        then,
325        otherwise,
326    } = &expr.node
327        && let Some(zero_then) = zero_test(cond, name)
328    {
329        let (zero, positive) = if zero_then {
330            (then, otherwise)
331        } else {
332            (otherwise, then)
333        };
334        *fresh += 1;
335        let pred = format!("ml_p{fresh}");
336        let ty = match &cond.node {
337            Node::Binary {
338                domain: Some(domain),
339                ..
340            } => domain.clone(),
341            _ => NAT,
342        };
343        let succ_body = predecessor_of(positive, name, &pred, &ty);
344        let succ_body = split_or_same(&succ_body, &pred, fresh);
345        let succ_body = split_or_same(&succ_body, name, fresh);
346        let zero = split_or_same(zero, name, fresh);
347        return Some(Expr {
348            node: Node::Match {
349                scrutinee: Box::new(Expr::var(name, ty).with_span(cond.span)),
350                cases: vec![
351                    Case {
352                        pattern: Pattern::NatZero,
353                        body: zero,
354                    },
355                    Case {
356                        pattern: Pattern::NatSucc { name: pred },
357                        body: succ_body,
358                    },
359                ],
360            },
361            ty: expr.ty.clone(),
362            span: expr.span,
363        });
364    }
365    match &expr.node {
366        Node::Let { name: local, .. } if local == name => return None,
367        Node::Match { cases, .. } if binds(cases, name) => return None,
368        _ => {}
369    }
370    let mut changed = false;
371    let copy = expr.map_children(&mut |child| {
372        split_natural(child, name, fresh).map_or_else(
373            || child.clone(),
374            |next| {
375                changed = true;
376                next
377            },
378        )
379    });
380    changed.then_some(copy)
381}
382
383pub(super) fn split_or_same(expr: &Expr, name: &str, fresh: &mut usize) -> Expr {
384    split_natural(expr, name, fresh).unwrap_or_else(|| expr.clone())
385}
386
387/// Rewrites uses of `name - k` and `name == k` (k ≥ 1) in terms of
388/// `pred = name - 1`, including through `let` aliases of `name`.
389pub(super) fn predecessor_of(expr: &Expr, name: &str, pred: &str, ty: &Type) -> Expr {
390    let names = HashSet::from([name.to_owned()]);
391    Predecessor { pred, ty }.visit(expr, &names)
392}
393
394pub(super) struct Predecessor<'a> {
395    pred: &'a str,
396    ty: &'a Type,
397}
398
399impl Predecessor<'_> {
400    fn pred_var(&self) -> Expr {
401        Expr::var(self.pred, self.ty.clone())
402    }
403
404    fn minus(&self, k: &Decimal, like: &Expr) -> Expr {
405        if k.digits == "1" {
406            return self.pred_var();
407        }
408        let Node::Binary {
409            op,
410            semantics,
411            rounding,
412            by_zero,
413            ..
414        } = &like.node
415        else {
416            return like.clone();
417        };
418        Expr {
419            node: binary_node(
420                *op,
421                Box::new(self.pred_var()),
422                Box::new(text_lit(self.ty.clone(), decrement(k))),
423                Some(self.ty.clone()),
424                *semantics,
425                *rounding,
426                *by_zero,
427            ),
428            ty: self.ty.clone(),
429            span: like.span,
430        }
431    }
432
433    fn visit(&self, node: &Expr, names: &HashSet<String>) -> Expr {
434        let is_alias = |candidate: &Expr| {
435            candidate
436                .var_name()
437                .is_some_and(|name| names.contains(name))
438        };
439        if let Node::Binary {
440            op,
441            left,
442            right,
443            domain: Some(domain),
444            semantics,
445            rounding,
446            by_zero,
447        } = &node.node
448            && domain.is_natural()
449            && is_alias(left)
450            && let Some(k) = nat_literal(right).filter(|k| k.digits != "0")
451        {
452            if *op == BinaryOp::Sub {
453                return self.minus(&k, node);
454            }
455            if matches!(op, BinaryOp::Eq | BinaryOp::Ne) {
456                return Expr {
457                    node: binary_node(
458                        *op,
459                        Box::new(self.pred_var()),
460                        Box::new(text_lit(self.ty.clone(), decrement(&k))),
461                        Some(domain.clone()),
462                        *semantics,
463                        *rounding,
464                        *by_zero,
465                    ),
466                    ..node.clone()
467                };
468            }
469        }
470        // JavaScript's `n - 1n` on a natural `n` is the checked conversion of
471        // an integer difference; with n ≥ 1 it is the natural predecessor.
472        if let Node::Cast {
473            arg, to: Type::Nat, ..
474        } = &node.node
475            && let Node::Binary {
476                op: BinaryOp::Sub,
477                left,
478                right,
479                ..
480            } = &arg.node
481            && let Node::Cast {
482                arg: inner,
483                from: Type::Nat,
484                ..
485            } = &left.node
486            && is_alias(inner)
487            && let Some(k) = nat_literal(right).filter(|k| k.digits != "0")
488        {
489            let like = Expr {
490                node: binary_node(
491                    BinaryOp::Sub,
492                    Box::new(self.pred_var()),
493                    Box::new(self.pred_var()),
494                    None,
495                    Some(Semantics::Truncated),
496                    None,
497                    None,
498                ),
499                ty: self.ty.clone(),
500                span: node.span,
501            };
502            return self.minus(&k, &like);
503        }
504        match &node.node {
505            Node::Let {
506                name: local,
507                value,
508                body,
509            } => {
510                let mut inner = names.clone();
511                inner.remove(local);
512                let value_visited = self.visit(value, names);
513                if local == self.pred {
514                    return Expr {
515                        node: Node::Let {
516                            name: local.clone(),
517                            value: Box::new(value_visited),
518                            body: body.clone(),
519                        },
520                        ..node.clone()
521                    };
522                }
523                if is_alias(value) {
524                    inner.insert(local.clone());
525                }
526                Expr {
527                    node: Node::Let {
528                        name: local.clone(),
529                        value: Box::new(value_visited),
530                        body: Box::new(self.visit(body, &inner)),
531                    },
532                    ..node.clone()
533                }
534            }
535            Node::Match { scrutinee, cases } => {
536                let scrutinee = Box::new(self.visit(scrutinee, names));
537                if binds(cases, self.pred) {
538                    return Expr {
539                        node: Node::Match {
540                            scrutinee,
541                            cases: cases.clone(),
542                        },
543                        ..node.clone()
544                    };
545                }
546                let cases = cases
547                    .iter()
548                    .map(|kase| {
549                        let bound = kase.pattern.bound();
550                        let inner: HashSet<String> = names
551                            .iter()
552                            .filter(|candidate| !bound.contains(&candidate.as_str()))
553                            .cloned()
554                            .collect();
555                        Case {
556                            pattern: kase.pattern.clone(),
557                            body: self.visit(&kase.body, &inner),
558                        }
559                    })
560                    .collect();
561                Expr {
562                    node: Node::Match { scrutinee, cases },
563                    ..node.clone()
564                }
565            }
566            _ => node.map_children(&mut |child| self.visit(child, names)),
567        }
568    }
569}