1use crate::error::{KernelError, KernelResult};
27use crate::term::Term;
28
29pub fn check_positivity(inductive: &str, constructor: &str, ty: &Term) -> KernelResult<()> {
33 check_strictly_positive(inductive, constructor, ty)
34}
35
36fn check_strictly_positive(inductive: &str, constructor: &str, ty: &Term) -> KernelResult<()> {
43 match ty {
44 Term::Global(name) if name == inductive => Ok(()),
47
48 Term::Pi {
50 param_type,
51 body_type,
52 ..
53 } => {
54 match param_type.as_ref() {
58 Term::Global(name) if name == inductive => {
59 }
61 _ => {
62 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 check_strictly_positive(inductive, constructor, body_type)
78 }
79
80 Term::App(func, arg) => {
82 check_strictly_positive(inductive, constructor, func)?;
83 check_strictly_positive(inductive, constructor, arg)
84 }
85
86 Term::Lambda {
88 param_type, body, ..
89 } => {
90 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 Term::Sort(_) => Ok(()),
111 Term::Var(_) => Ok(()),
112 Term::Global(_) => Ok(()), Term::Lit(_) => Ok(()), 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 Term::Fix { body, .. } => check_strictly_positive(inductive, constructor, body),
131
132 Term::Hole => Ok(()),
134 }
135}
136
137fn 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, }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_simple_recursive_arg() {
171 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 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 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 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}