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}