logicaffeine_kernel/
termination.rs

1//! Termination checking for fixpoints.
2//!
3//! This module implements the syntactic guard condition that ensures
4//! all recursive functions terminate. Without this check, the type system
5//! would be unsound - we could "prove" False by writing `fix f. f`.
6//!
7//! The algorithm (following Coq):
8//! 1. Identify the "structural parameter" - the first inductive-typed argument
9//! 2. Track variables that are "structurally smaller" than the structural parameter
10//! 3. Verify all recursive calls use a smaller argument
11//!
12//! A variable `k` is smaller than `n` if it was bound by matching on `n`:
13//! `match n with Succ k => ...` means k < n structurally.
14
15use std::collections::HashSet;
16
17use crate::context::Context;
18use crate::error::{KernelError, KernelResult};
19use crate::term::Term;
20
21/// Context for termination checking.
22struct GuardContext {
23    /// The name of the fixpoint being checked
24    fix_name: String,
25    /// The structural parameter (decreasing argument)
26    struct_param: String,
27    /// The type of the structural parameter (inductive name)
28    struct_type: String,
29    /// Variables known to be structurally smaller than struct_param
30    smaller_than: HashSet<String>,
31}
32
33/// Check that a Fix term satisfies the syntactic guard condition.
34///
35/// This is the main entry point for termination checking.
36pub fn check_termination(ctx: &Context, fix_name: &str, body: &Term) -> KernelResult<()> {
37    // Extract the structural parameter (first inductive-typed lambda param)
38    let (struct_param, struct_type, inner) = extract_structural_param(ctx, body)?;
39
40    // Create the guard context with empty smaller set
41    let guard_ctx = GuardContext {
42        fix_name: fix_name.to_string(),
43        struct_param,
44        struct_type,
45        smaller_than: HashSet::new(),
46    };
47
48    // Check all recursive calls are guarded
49    check_guarded(ctx, &guard_ctx, inner)
50}
51
52/// Extract the first inductive-typed parameter as the structural argument.
53///
54/// Returns (param_name, inductive_name, remaining_body).
55fn extract_structural_param<'a>(
56    ctx: &Context,
57    body: &'a Term,
58) -> KernelResult<(String, String, &'a Term)> {
59    match body {
60        Term::Lambda {
61            param,
62            param_type,
63            body,
64        } => {
65            // Check if param_type is an inductive (handles both Nat and List A)
66            if let Some(type_name) = extract_inductive_name(ctx, param_type) {
67                return Ok((param.clone(), type_name, body));
68            }
69            // Not an inductive type, try the next parameter
70            extract_structural_param(ctx, body)
71        }
72        _ => Err(KernelError::TerminationViolation {
73            fix_name: String::new(),
74            reason: "No inductive parameter found for structural recursion".to_string(),
75        }),
76    }
77}
78
79/// Extract the inductive type name from a type.
80///
81/// Handles both simple inductives like `Nat` and polymorphic ones like `List A`.
82fn extract_inductive_name(ctx: &Context, ty: &Term) -> Option<String> {
83    match ty {
84        Term::Global(name) if ctx.is_inductive(name) => Some(name.clone()),
85        Term::App(func, _) => extract_inductive_name(ctx, func),
86        _ => None,
87    }
88}
89
90/// Check that all recursive calls in `term` are guarded (use smaller arguments).
91fn check_guarded(ctx: &Context, guard_ctx: &GuardContext, term: &Term) -> KernelResult<()> {
92    match term {
93        // Application: check for recursive calls
94        Term::App(func, arg) => {
95            // Check if this is a recursive call to the fixpoint
96            check_recursive_call(ctx, guard_ctx, func, arg)?;
97
98            // Recursively check subterms
99            check_guarded(ctx, guard_ctx, func)?;
100            check_guarded(ctx, guard_ctx, arg)
101        }
102
103        // Match on structural parameter introduces smaller variables
104        Term::Match {
105            discriminant,
106            cases,
107            ..
108        } => {
109            // Check if we're matching on the structural parameter
110            if let Term::Var(disc_name) = discriminant.as_ref() {
111                if disc_name == &guard_ctx.struct_param {
112                    // This match guards recursive calls - check cases with
113                    // constructor-bound variables marked as smaller
114                    return check_match_cases_guarded(ctx, guard_ctx, cases);
115                }
116            }
117
118            // Not matching on structural param - just recurse normally
119            check_guarded(ctx, guard_ctx, discriminant)?;
120            for case in cases {
121                check_guarded(ctx, guard_ctx, case)?;
122            }
123            Ok(())
124        }
125
126        // Lambda: recurse into body (param shadows nothing relevant)
127        Term::Lambda { body, .. } => check_guarded(ctx, guard_ctx, body),
128
129        // Pi: recurse into body type
130        Term::Pi { body_type, .. } => check_guarded(ctx, guard_ctx, body_type),
131
132        // Nested fixpoint: check its body (with its own fix_name)
133        Term::Fix { body, .. } => {
134            // Note: nested fixpoints should have their own termination check
135            // when they are type-checked. Here we just recurse.
136            check_guarded(ctx, guard_ctx, body)
137        }
138
139        // Leaves: no recursive calls possible
140        Term::Sort(_) | Term::Var(_) | Term::Global(_) | Term::Lit(_) | Term::Hole => Ok(()),
141    }
142}
143
144/// Check if an application is a recursive call, and if so, verify it uses a smaller argument.
145fn check_recursive_call(
146    _ctx: &Context,
147    guard_ctx: &GuardContext,
148    func: &Term,
149    arg: &Term,
150) -> KernelResult<()> {
151    // Walk through nested applications to find the actual function
152    // For `plus k m`, func is `App(Var("plus"), Var("k"))`, arg is `Var("m")`
153    // So we need to find if the head of func is our fixpoint
154    let (head, first_arg) = extract_head_and_first_arg(func, arg);
155
156    // Check if the head is a reference to our fixpoint
157    if let Term::Var(name) = head {
158        if name == &guard_ctx.fix_name {
159            // This is a recursive call! Check the first argument (structural argument)
160            match first_arg {
161                Term::Var(arg_name) => {
162                    if !guard_ctx.smaller_than.contains(arg_name) {
163                        return Err(KernelError::TerminationViolation {
164                            fix_name: guard_ctx.fix_name.clone(),
165                            reason: format!(
166                                "Recursive call with '{}' which is not structurally smaller than '{}'",
167                                arg_name, guard_ctx.struct_param
168                            ),
169                        });
170                    }
171                    // Valid: argument is in the smaller set
172                    Ok(())
173                }
174                _ => {
175                    // Argument is not a simple variable - could be a complex expression
176                    // For safety, we reject this unless it's clearly smaller
177                    Err(KernelError::TerminationViolation {
178                        fix_name: guard_ctx.fix_name.clone(),
179                        reason: format!(
180                            "Recursive call with complex argument - cannot verify termination"
181                        ),
182                    })
183                }
184            }
185        } else {
186            Ok(()) // Not a recursive call
187        }
188    } else {
189        Ok(()) // Head is not a variable, so not a recursive call
190    }
191}
192
193/// Extract the head function and first argument from a (possibly nested) application.
194///
195/// For `f a b c`, returns `(f, a)`.
196/// For `f a`, returns `(f, a)`.
197fn extract_head_and_first_arg<'a>(func: &'a Term, arg: &'a Term) -> (&'a Term, &'a Term) {
198    // Walk left through applications to find the head
199    let mut current = func;
200    let mut first_arg = arg;
201
202    while let Term::App(inner_func, inner_arg) = current {
203        first_arg = inner_arg;
204        current = inner_func;
205    }
206
207    (current, first_arg)
208}
209
210/// Check match cases with structural variables marked as smaller.
211fn check_match_cases_guarded(
212    ctx: &Context,
213    guard_ctx: &GuardContext,
214    cases: &[Term],
215) -> KernelResult<()> {
216    // Get constructors for the inductive type
217    let constructors = ctx.get_constructors(&guard_ctx.struct_type);
218
219    for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
220        // Count constructor parameters
221        let param_count = count_pi_params(ctor_type);
222
223        // Extract the smaller variables from this case
224        // The case is typically: λx1. λx2. ... λxn. body
225        // where x1..xn are the constructor parameters
226        let (smaller_vars, case_body) = extract_lambda_params(case, param_count);
227
228        // Create extended guard context with these variables as smaller
229        let mut extended_ctx = GuardContext {
230            fix_name: guard_ctx.fix_name.clone(),
231            struct_param: guard_ctx.struct_param.clone(),
232            struct_type: guard_ctx.struct_type.clone(),
233            smaller_than: guard_ctx.smaller_than.clone(),
234        };
235
236        // Only add variables that are of the same inductive type (recursive arguments)
237        // For Succ : Nat -> Nat, the param `k` is smaller
238        // For Zero : Nat, no smaller vars
239        let recursive_params = get_recursive_params(ctx, &guard_ctx.struct_type, ctor_type);
240        for (idx, _) in recursive_params {
241            if idx < smaller_vars.len() {
242                extended_ctx.smaller_than.insert(smaller_vars[idx].clone());
243            }
244        }
245
246        // Also mark ALL constructor params as smaller (conservative approach)
247        // This handles cases like `Succ k` where k is the direct subterm
248        for var in &smaller_vars {
249            extended_ctx.smaller_than.insert(var.clone());
250        }
251
252        // Check the case body with the extended context
253        check_guarded(ctx, &extended_ctx, case_body)?;
254    }
255
256    Ok(())
257}
258
259/// Count the number of Pi parameters in a type.
260fn count_pi_params(ty: &Term) -> usize {
261    match ty {
262        Term::Pi { body_type, .. } => 1 + count_pi_params(body_type),
263        _ => 0,
264    }
265}
266
267/// Extract lambda parameters and return (param_names, body).
268fn extract_lambda_params(term: &Term, count: usize) -> (Vec<String>, &Term) {
269    if count == 0 {
270        return (Vec::new(), term);
271    }
272
273    match term {
274        Term::Lambda { param, body, .. } => {
275            let (mut params, final_body) = extract_lambda_params(body, count - 1);
276            params.insert(0, param.clone());
277            (params, final_body)
278        }
279        _ => (Vec::new(), term),
280    }
281}
282
283/// Get indices of parameters that are of the inductive type (recursive positions).
284fn get_recursive_params(ctx: &Context, inductive: &str, ctor_type: &Term) -> Vec<(usize, String)> {
285    let mut result = Vec::new();
286    let mut current = ctor_type;
287    let mut idx = 0;
288
289    while let Term::Pi {
290        param,
291        param_type,
292        body_type,
293    } = current
294    {
295        // Check if param_type is the inductive type (handles polymorphic types like List A)
296        if let Some(type_name) = extract_inductive_name(ctx, param_type) {
297            if type_name == inductive {
298                result.push((idx, param.clone()));
299            }
300        }
301        idx += 1;
302        current = body_type;
303    }
304
305    result
306}