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}