use crate::error::{KernelError, KernelResult};
use crate::term::Term;
pub fn check_positivity(inductive: &str, constructor: &str, ty: &Term) -> KernelResult<()> {
check_strictly_positive(&[inductive], constructor, ty)
}
pub fn check_positivity_mutual(block: &[&str], constructor: &str, ty: &Term) -> KernelResult<()> {
check_strictly_positive(block, constructor, ty)
}
fn check_strictly_positive(block: &[&str], constructor: &str, ty: &Term) -> KernelResult<()> {
match ty {
Term::Const { .. } => Ok(()),
Term::Global(name) if block.contains(&name.as_str()) => Ok(()),
Term::Pi {
param_type,
body_type,
..
} => {
if !is_recursive_arg(block, param_type) && occurs_in(block, param_type) {
return Err(KernelError::PositivityViolation {
inductive: block.join("/"),
constructor: constructor.to_string(),
reason: format!(
"'{}' occurs in negative position (inside parameter type)",
block.join("/")
),
});
}
check_strictly_positive(block, constructor, body_type)
}
Term::App(func, arg) => {
check_strictly_positive(block, constructor, func)?;
check_strictly_positive(block, constructor, arg)
}
Term::Lambda {
param_type, body, ..
} => {
if !is_recursive_arg(block, param_type) && occurs_in(block, param_type) {
return Err(KernelError::PositivityViolation {
inductive: block.join("/"),
constructor: constructor.to_string(),
reason: format!(
"'{}' occurs in negative position (inside lambda parameter)",
block.join("/")
),
});
}
check_strictly_positive(block, constructor, body)
}
Term::Sort(_) => Ok(()),
Term::Var(_) => Ok(()),
Term::Global(_) => Ok(()), Term::Lit(_) => Ok(()),
Term::Match {
discriminant,
motive,
cases,
} => {
check_strictly_positive(block, constructor, discriminant)?;
check_strictly_positive(block, constructor, motive)?;
for case in cases {
check_strictly_positive(block, constructor, case)?;
}
Ok(())
}
Term::Fix { body, .. } => check_strictly_positive(block, constructor, body),
Term::MutualFix { defs, .. } => {
for (_, body) in defs {
check_strictly_positive(block, constructor, body)?;
}
Ok(())
}
Term::Let { ty, value, body, .. } => {
if occurs_in(block, ty) || occurs_in(block, value) {
return Err(KernelError::PositivityViolation {
inductive: block.join("/"),
constructor: constructor.to_string(),
reason: format!("'{}' occurs in a let-binding's type or value", block.join("/")),
});
}
check_strictly_positive(block, constructor, body)
}
Term::Hole => Ok(()),
}
}
fn is_recursive_arg(block: &[&str], term: &Term) -> bool {
match term {
Term::Pi { param_type, body_type, .. } => {
!occurs_in(block, param_type) && is_recursive_arg(block, body_type)
}
_ => {
let mut head = term;
let mut args: Vec<&Term> = Vec::new();
while let Term::App(func, arg) = head {
args.push(arg.as_ref());
head = func.as_ref();
}
matches!(head, Term::Global(name) if block.contains(&name.as_str()))
&& args.iter().all(|a| !occurs_in(block, a))
}
}
}
fn occurs_in(block: &[&str], term: &Term) -> bool {
match term {
Term::Global(name) => block.contains(&name.as_str()),
Term::Sort(_) | Term::Var(_) | Term::Lit(_) | Term::Const { .. } => false,
Term::Pi {
param_type,
body_type,
..
} => occurs_in(block, param_type) || occurs_in(block, body_type),
Term::Lambda {
param_type, body, ..
} => occurs_in(block, param_type) || occurs_in(block, body),
Term::App(func, arg) => occurs_in(block, func) || occurs_in(block, arg),
Term::Match {
discriminant,
motive,
cases,
} => {
occurs_in(block, discriminant)
|| occurs_in(block, motive)
|| cases.iter().any(|c| occurs_in(block, c))
}
Term::Fix { body, .. } => occurs_in(block, body),
Term::MutualFix { defs, .. } => defs.iter().any(|(_, b)| occurs_in(block, b)),
Term::Let { ty, value, body, .. } => {
occurs_in(block, ty) || occurs_in(block, value) || occurs_in(block, body)
}
Term::Hole => false, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_recursive_arg() {
let ty = Term::Pi {
param: "_".to_string(),
param_type: Box::new(Term::Global("Nat".to_string())),
body_type: Box::new(Term::Global("Nat".to_string())),
};
assert!(check_positivity("Nat", "Succ", &ty).is_ok());
}
#[test]
fn test_negative_inside_arrow() {
let bad_to_false = Term::Pi {
param: "_".to_string(),
param_type: Box::new(Term::Global("Bad".to_string())),
body_type: Box::new(Term::Global("False".to_string())),
};
let ty = Term::Pi {
param: "_".to_string(),
param_type: Box::new(bad_to_false),
body_type: Box::new(Term::Global("Bad".to_string())),
};
assert!(check_positivity("Bad", "Cons", &ty).is_err());
}
#[test]
fn test_nested_negative() {
let tricky_to_nat = Term::Pi {
param: "_".to_string(),
param_type: Box::new(Term::Global("Tricky".to_string())),
body_type: Box::new(Term::Global("Nat".to_string())),
};
let inner = Term::Pi {
param: "_".to_string(),
param_type: Box::new(tricky_to_nat),
body_type: Box::new(Term::Global("Nat".to_string())),
};
let make_type = Term::Pi {
param: "_".to_string(),
param_type: Box::new(inner),
body_type: Box::new(Term::Global("Tricky".to_string())),
};
let result = check_positivity("Tricky", "Make", &make_type);
assert!(result.is_err(), "Should reject nested negative: {:?}", result);
}
#[test]
fn test_list_cons_valid() {
let ty = Term::Pi {
param: "_".to_string(),
param_type: Box::new(Term::Global("Nat".to_string())),
body_type: Box::new(Term::Pi {
param: "_".to_string(),
param_type: Box::new(Term::Global("List".to_string())),
body_type: Box::new(Term::Global("List".to_string())),
}),
};
assert!(check_positivity("List", "Cons", &ty).is_ok());
}
}