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 that the inductive appears only strictly positively.
37///
38/// At the top level of constructor type, we allow:
39/// - I as a direct parameter type (recursive argument)
40/// - I in the final result type
41/// - But NOT I nested inside function types within parameters
42fn check_strictly_positive(inductive: &str, constructor: &str, ty: &Term) -> KernelResult<()> {
43    match ty {
44        // Direct occurrence of the inductive is always fine
45        // (either as recursive argument or result type)
46        Term::Global(name) if name == inductive => Ok(()),
47
48        // Pi type: Π(x:A). B
49        Term::Pi {
50            param_type,
51            body_type,
52            ..
53        } => {
54            // Check the parameter type A
55            // If A = I directly, it's a recursive argument (allowed)
56            // Otherwise, I must not occur in A at all (checked via occurs_in)
57            match param_type.as_ref() {
58                Term::Global(name) if name == inductive => {
59                    // Direct recursive argument - allowed
60                }
61                _ => {
62                    // A is not directly I, so I must not occur anywhere in A
63                    if occurs_in(inductive, param_type) {
64                        return Err(KernelError::PositivityViolation {
65                            inductive: inductive.to_string(),
66                            constructor: constructor.to_string(),
67                            reason: format!(
68                                "'{}' occurs in negative position (inside parameter type)",
69                                inductive
70                            ),
71                        });
72                    }
73                }
74            }
75
76            // Recursively check the body type B
77            check_strictly_positive(inductive, constructor, body_type)
78        }
79
80        // Application: check both parts
81        Term::App(func, arg) => {
82            check_strictly_positive(inductive, constructor, func)?;
83            check_strictly_positive(inductive, constructor, arg)
84        }
85
86        // Lambda (unusual in types, but handle it)
87        Term::Lambda {
88            param_type, body, ..
89        } => {
90            // Same rule as Pi for param_type
91            match param_type.as_ref() {
92                Term::Global(name) if name == inductive => {}
93                _ => {
94                    if occurs_in(inductive, param_type) {
95                        return Err(KernelError::PositivityViolation {
96                            inductive: inductive.to_string(),
97                            constructor: constructor.to_string(),
98                            reason: format!(
99                                "'{}' occurs in negative position (inside lambda parameter)",
100                                inductive
101                            ),
102                        });
103                    }
104                }
105            }
106            check_strictly_positive(inductive, constructor, body)
107        }
108
109        // Other terms: no occurrences of the inductive to worry about
110        Term::Sort(_) => Ok(()),
111        Term::Var(_) => Ok(()),
112        Term::Global(_) => Ok(()), // Other globals, not the inductive
113        Term::Lit(_) => Ok(()),    // Literals cannot contain inductives
114
115        // Match in types (unusual but possible)
116        Term::Match {
117            discriminant,
118            motive,
119            cases,
120        } => {
121            check_strictly_positive(inductive, constructor, discriminant)?;
122            check_strictly_positive(inductive, constructor, motive)?;
123            for case in cases {
124                check_strictly_positive(inductive, constructor, case)?;
125            }
126            Ok(())
127        }
128
129        // Fix in types (very unusual)
130        Term::Fix { body, .. } => check_strictly_positive(inductive, constructor, body),
131
132        // Hole: type placeholder, no occurrences to check
133        Term::Hole => Ok(()),
134    }
135}
136
137/// Check if the inductive name occurs anywhere in the term.
138fn occurs_in(inductive: &str, term: &Term) -> bool {
139    match term {
140        Term::Global(name) => name == inductive,
141        Term::Sort(_) | Term::Var(_) | Term::Lit(_) => false,
142        Term::Pi {
143            param_type,
144            body_type,
145            ..
146        } => occurs_in(inductive, param_type) || occurs_in(inductive, body_type),
147        Term::Lambda {
148            param_type, body, ..
149        } => occurs_in(inductive, param_type) || occurs_in(inductive, body),
150        Term::App(func, arg) => occurs_in(inductive, func) || occurs_in(inductive, arg),
151        Term::Match {
152            discriminant,
153            motive,
154            cases,
155        } => {
156            occurs_in(inductive, discriminant)
157                || occurs_in(inductive, motive)
158                || cases.iter().any(|c| occurs_in(inductive, c))
159        }
160        Term::Fix { body, .. } => occurs_in(inductive, body),
161        Term::Hole => false, // Holes don't contain inductives
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_simple_recursive_arg() {
171        // Nat -> Nat is valid (first Nat is direct recursive arg, second is result)
172        let ty = Term::Pi {
173            param: "_".to_string(),
174            param_type: Box::new(Term::Global("Nat".to_string())),
175            body_type: Box::new(Term::Global("Nat".to_string())),
176        };
177        assert!(check_positivity("Nat", "Succ", &ty).is_ok());
178    }
179
180    #[test]
181    fn test_negative_inside_arrow() {
182        // (Bad -> False) -> Bad has Bad inside an arrow within a param
183        // Bad occurs in param_type `Bad -> False`, which is not directly Bad
184        let bad_to_false = Term::Pi {
185            param: "_".to_string(),
186            param_type: Box::new(Term::Global("Bad".to_string())),
187            body_type: Box::new(Term::Global("False".to_string())),
188        };
189        let ty = Term::Pi {
190            param: "_".to_string(),
191            param_type: Box::new(bad_to_false),
192            body_type: Box::new(Term::Global("Bad".to_string())),
193        };
194        assert!(check_positivity("Bad", "Cons", &ty).is_err());
195    }
196
197    #[test]
198    fn test_nested_negative() {
199        // ((Tricky -> Nat) -> Nat) -> Tricky
200        // Tricky appears inside the param type of the outer Pi
201        // The outer param is ((Tricky -> Nat) -> Nat), which contains Tricky
202        let tricky_to_nat = Term::Pi {
203            param: "_".to_string(),
204            param_type: Box::new(Term::Global("Tricky".to_string())),
205            body_type: Box::new(Term::Global("Nat".to_string())),
206        };
207        let inner = Term::Pi {
208            param: "_".to_string(),
209            param_type: Box::new(tricky_to_nat),
210            body_type: Box::new(Term::Global("Nat".to_string())),
211        };
212        let make_type = Term::Pi {
213            param: "_".to_string(),
214            param_type: Box::new(inner),
215            body_type: Box::new(Term::Global("Tricky".to_string())),
216        };
217
218        let result = check_positivity("Tricky", "Make", &make_type);
219        assert!(result.is_err(), "Should reject nested negative: {:?}", result);
220    }
221
222    #[test]
223    fn test_list_cons_valid() {
224        // Cons : Nat -> List -> List
225        // Both params are fine: Nat doesn't contain List, second param IS List directly
226        let ty = Term::Pi {
227            param: "_".to_string(),
228            param_type: Box::new(Term::Global("Nat".to_string())),
229            body_type: Box::new(Term::Pi {
230                param: "_".to_string(),
231                param_type: Box::new(Term::Global("List".to_string())),
232                body_type: Box::new(Term::Global("List".to_string())),
233            }),
234        };
235        assert!(check_positivity("List", "Cons", &ty).is_ok());
236    }
237}