Skip to main content

logicaffeine_kernel/
positivity.rs

1//! Strict positivity checking for inductive types.
2//!
3//! An inductive type must appear only in "strictly positive" positions in its constructors.
4//! Without this check, we could define paradoxical types like:
5//!
6//! ```text
7//! Inductive Bad := Cons : (Bad -> False) -> Bad
8//! ```
9//!
10//! This would allow encoding Russell's paradox and proving False.
11//!
12//! Strict Positivity Rules (from CIC):
13//!
14//! I is strictly positive in T iff:
15//! 1. I does not occur in T, OR
16//! 2. T = Π(x:A). B where:
17//!    - If A = I exactly, it's a "recursive argument" (allowed)
18//!    - Otherwise, I must NOT occur in A at all
19//!    - AND I must be strictly positive in B
20//!
21//! Examples:
22//! - `I -> I` is valid: first I is a recursive argument, second is result
23//! - `(I -> X) -> I` is INVALID: I occurs inside the param type of another arrow
24//! - `X -> I -> I` is valid: X has no I, second param is recursive arg
25
26use crate::error::{KernelError, KernelResult};
27use crate::term::Term;
28
29/// Check strict positivity of an inductive type in a constructor type.
30///
31/// This is the main entry point for positivity checking.
32pub fn check_positivity(inductive: &str, constructor: &str, ty: &Term) -> KernelResult<()> {
33    check_strictly_positive(&[inductive], constructor, ty)
34}
35
36/// Check strict positivity of a MUTUAL BLOCK of inductives in a constructor type.
37///
38/// A constructor of one block member may recursively reference ANY member — `Even`'s
39/// `even_succ : Π(n). Odd n → Even (Succ n)` places the sibling `Odd` in a
40/// strictly-positive recursive position. The block is treated as a single "inductive"
41/// for positivity: an occurrence of any member to the RIGHT of every arrow is a
42/// (recursive) occurrence and allowed; any member in a DOMAIN is a negative
43/// occurrence and rejected — so a cross-block paradox `(Even n → False) → Odd n` is
44/// caught exactly as a self-negative one is.
45pub fn check_positivity_mutual(block: &[&str], constructor: &str, ty: &Term) -> KernelResult<()> {
46    check_strictly_positive(block, constructor, ty)
47}
48
49/// Check that the inductive appears only strictly positively.
50///
51/// At the top level of constructor type, we allow:
52/// - I as a direct parameter type (recursive argument)
53/// - I in the final result type
54/// - But NOT I nested inside function types within parameters
55fn check_strictly_positive(block: &[&str], constructor: &str, ty: &Term) -> KernelResult<()> {
56    match ty {
57        // A universe-polymorphic reference cannot mention this (newly-declared) inductive.
58        Term::Const { .. } => Ok(()),
59
60        // Direct occurrence of a block member is always fine
61        // (either as recursive argument or result type)
62        Term::Global(name) if block.contains(&name.as_str()) => Ok(()),
63
64        // Pi type: Π(x:A). B
65        Term::Pi {
66            param_type,
67            body_type,
68            ..
69        } => {
70            // Check the parameter type A.
71            // If A is a recursive argument (a block member `I` applied to its
72            // parameters, with no block member occurring in the arguments — `I`,
73            // `I a`, `List A`, `Odd n`, …), it is a strictly-positive recursive
74            // occurrence (allowed). Otherwise no block member may occur in A at all.
75            if !is_recursive_arg(block, param_type) && occurs_in(block, param_type) {
76                return Err(KernelError::PositivityViolation {
77                    inductive: block.join("/"),
78                    constructor: constructor.to_string(),
79                    reason: format!(
80                        "'{}' occurs in negative position (inside parameter type)",
81                        block.join("/")
82                    ),
83                });
84            }
85
86            // Recursively check the body type B
87            check_strictly_positive(block, constructor, body_type)
88        }
89
90        // Application: check both parts
91        Term::App(func, arg) => {
92            check_strictly_positive(block, constructor, func)?;
93            check_strictly_positive(block, constructor, arg)
94        }
95
96        // Lambda (unusual in types, but handle it)
97        Term::Lambda {
98            param_type, body, ..
99        } => {
100            // Same rule as Pi for param_type
101            if !is_recursive_arg(block, param_type) && occurs_in(block, param_type) {
102                return Err(KernelError::PositivityViolation {
103                    inductive: block.join("/"),
104                    constructor: constructor.to_string(),
105                    reason: format!(
106                        "'{}' occurs in negative position (inside lambda parameter)",
107                        block.join("/")
108                    ),
109                });
110            }
111            check_strictly_positive(block, constructor, body)
112        }
113
114        // Other terms: no occurrences of the inductive to worry about
115        Term::Sort(_) => Ok(()),
116        Term::Var(_) => Ok(()),
117        Term::Global(_) => Ok(()), // Other globals, not a block member
118        Term::Lit(_) => Ok(()),    // Literals cannot contain inductives
119
120        // Match in types (unusual but possible)
121        Term::Match {
122            discriminant,
123            motive,
124            cases,
125        } => {
126            check_strictly_positive(block, constructor, discriminant)?;
127            check_strictly_positive(block, constructor, motive)?;
128            for case in cases {
129                check_strictly_positive(block, constructor, case)?;
130            }
131            Ok(())
132        }
133
134        // Fix in types (very unusual)
135        Term::Fix { body, .. } => check_strictly_positive(block, constructor, body),
136
137        // Mutual fix in types (very unusual): check every definition's body.
138        Term::MutualFix { defs, .. } => {
139            for (_, body) in defs {
140                check_strictly_positive(block, constructor, body)?;
141            }
142            Ok(())
143        }
144
145        // Let in types: the value and type are checked; the body carries the
146        // positivity obligation of the constructor's remaining shape.
147        Term::Let { ty, value, body, .. } => {
148            if occurs_in(block, ty) || occurs_in(block, value) {
149                return Err(KernelError::PositivityViolation {
150                    inductive: block.join("/"),
151                    constructor: constructor.to_string(),
152                    reason: format!("'{}' occurs in a let-binding's type or value", block.join("/")),
153                });
154            }
155            check_strictly_positive(block, constructor, body)
156        }
157
158        // Hole: type placeholder, no occurrences to check
159        Term::Hole => Ok(()),
160    }
161}
162
163/// True if `term` is a strictly-positive recursive occurrence of `inductive` —
164/// possibly a FUNCTIONAL one. Two shapes:
165/// - a TELESCOPE `Π(z:B). rest` where `inductive` does not occur in the domain
166///   `B` (so it is not in a negative position) and `rest` is itself a recursive
167///   occurrence — e.g. `Acc_intro`'s field `Π(y:A). R y x → Acc A R y`, or a
168///   rose tree's `Nat → Tree`; and
169/// - the BASE `I e₁ … eₙ`: the inductive applied to arguments that do not mention
170///   it (`I`, `I a`, `List A`, an indexed `Acc A R y`).
171///
172/// This is exactly CIC strict positivity: the inductive may appear only to the
173/// RIGHT of every arrow. A negative occurrence (`Bad → …`, `(Bad → X) → …`) puts
174/// it in a domain, so `is_recursive_arg` returns `false` and the caller's
175/// `occurs_in` check then rejects the constructor.
176fn is_recursive_arg(block: &[&str], term: &Term) -> bool {
177    match term {
178        Term::Pi { param_type, body_type, .. } => {
179            !occurs_in(block, param_type) && is_recursive_arg(block, body_type)
180        }
181        _ => {
182            let mut head = term;
183            let mut args: Vec<&Term> = Vec::new();
184            while let Term::App(func, arg) = head {
185                args.push(arg.as_ref());
186                head = func.as_ref();
187            }
188            matches!(head, Term::Global(name) if block.contains(&name.as_str()))
189                && args.iter().all(|a| !occurs_in(block, a))
190        }
191    }
192}
193
194/// Check if any block member's name occurs anywhere in the term.
195fn occurs_in(block: &[&str], term: &Term) -> bool {
196    match term {
197        Term::Global(name) => block.contains(&name.as_str()),
198        Term::Sort(_) | Term::Var(_) | Term::Lit(_) | Term::Const { .. } => false,
199        Term::Pi {
200            param_type,
201            body_type,
202            ..
203        } => occurs_in(block, param_type) || occurs_in(block, body_type),
204        Term::Lambda {
205            param_type, body, ..
206        } => occurs_in(block, param_type) || occurs_in(block, body),
207        Term::App(func, arg) => occurs_in(block, func) || occurs_in(block, arg),
208        Term::Match {
209            discriminant,
210            motive,
211            cases,
212        } => {
213            occurs_in(block, discriminant)
214                || occurs_in(block, motive)
215                || cases.iter().any(|c| occurs_in(block, c))
216        }
217        Term::Fix { body, .. } => occurs_in(block, body),
218        Term::MutualFix { defs, .. } => defs.iter().any(|(_, b)| occurs_in(block, b)),
219        Term::Let { ty, value, body, .. } => {
220            occurs_in(block, ty) || occurs_in(block, value) || occurs_in(block, body)
221        }
222        Term::Hole => false, // Holes don't contain inductives
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_simple_recursive_arg() {
232        // Nat -> Nat is valid (first Nat is direct recursive arg, second is result)
233        let ty = Term::Pi {
234            param: "_".to_string(),
235            param_type: Box::new(Term::Global("Nat".to_string())),
236            body_type: Box::new(Term::Global("Nat".to_string())),
237        };
238        assert!(check_positivity("Nat", "Succ", &ty).is_ok());
239    }
240
241    #[test]
242    fn test_negative_inside_arrow() {
243        // (Bad -> False) -> Bad has Bad inside an arrow within a param
244        // Bad occurs in param_type `Bad -> False`, which is not directly Bad
245        let bad_to_false = Term::Pi {
246            param: "_".to_string(),
247            param_type: Box::new(Term::Global("Bad".to_string())),
248            body_type: Box::new(Term::Global("False".to_string())),
249        };
250        let ty = Term::Pi {
251            param: "_".to_string(),
252            param_type: Box::new(bad_to_false),
253            body_type: Box::new(Term::Global("Bad".to_string())),
254        };
255        assert!(check_positivity("Bad", "Cons", &ty).is_err());
256    }
257
258    #[test]
259    fn test_nested_negative() {
260        // ((Tricky -> Nat) -> Nat) -> Tricky
261        // Tricky appears inside the param type of the outer Pi
262        // The outer param is ((Tricky -> Nat) -> Nat), which contains Tricky
263        let tricky_to_nat = Term::Pi {
264            param: "_".to_string(),
265            param_type: Box::new(Term::Global("Tricky".to_string())),
266            body_type: Box::new(Term::Global("Nat".to_string())),
267        };
268        let inner = Term::Pi {
269            param: "_".to_string(),
270            param_type: Box::new(tricky_to_nat),
271            body_type: Box::new(Term::Global("Nat".to_string())),
272        };
273        let make_type = Term::Pi {
274            param: "_".to_string(),
275            param_type: Box::new(inner),
276            body_type: Box::new(Term::Global("Tricky".to_string())),
277        };
278
279        let result = check_positivity("Tricky", "Make", &make_type);
280        assert!(result.is_err(), "Should reject nested negative: {:?}", result);
281    }
282
283    #[test]
284    fn test_list_cons_valid() {
285        // Cons : Nat -> List -> List
286        // Both params are fine: Nat doesn't contain List, second param IS List directly
287        let ty = Term::Pi {
288            param: "_".to_string(),
289            param_type: Box::new(Term::Global("Nat".to_string())),
290            body_type: Box::new(Term::Pi {
291                param: "_".to_string(),
292                param_type: Box::new(Term::Global("List".to_string())),
293                body_type: Box::new(Term::Global("List".to_string())),
294            }),
295        };
296        assert!(check_positivity("List", "Cons", &ty).is_ok());
297    }
298}