use crate::alethe::{AletheProof, AletheRule, AletheStep};
use crate::theory::{TheoryProof, TheoryRule, TheoryStepId};
use std::collections::HashSet;
use std::fmt;
#[derive(Debug, Clone)]
pub enum CheckResult {
Valid,
Invalid {
step: u32,
error: CheckError,
},
MultipleErrors(Vec<(u32, CheckError)>),
}
impl CheckResult {
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
#[must_use]
pub fn error(&self) -> Option<&CheckError> {
match self {
Self::Invalid { error, .. } => Some(error),
_ => None,
}
}
}
impl fmt::Display for CheckResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Valid => write!(f, "✓ Proof is valid"),
Self::Invalid { step, error } => {
writeln!(f, "✗ Proof is invalid")?;
writeln!(f, " Step: {}", step)?;
writeln!(f, " [{:?}] {}", error.severity(), error)?;
if let Some(suggestion) = error.suggestion() {
writeln!(f, " Suggestion: {}", suggestion)?;
}
Ok(())
}
Self::MultipleErrors(errors) => {
writeln!(f, "✗ Proof has {} error(s):", errors.len())?;
for (step, error) in errors {
writeln!(f, "\n Step {}:", step)?;
writeln!(f, " [{:?}] {}", error.severity(), error)?;
if let Some(suggestion) = error.suggestion() {
writeln!(f, " Suggestion: {}", suggestion)?;
}
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckError {
MissingPremise(u32),
WrongPremiseCount { expected: usize, got: usize },
WrongArgumentCount { expected: usize, got: usize },
RuleNotApplicable(String),
InvalidConclusion(String),
CyclicDependency,
EmptyProof,
MalformedTerm(String),
UnknownRule(String),
Custom(String),
}
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingPremise(id) => {
write!(
f,
"Missing premise: step {} does not exist or has not been defined yet. \
Premises must be defined before they are referenced.",
id
)
}
Self::WrongPremiseCount { expected, got } => {
write!(
f,
"Wrong premise count: rule requires {} premise(s), but {} {} provided. \
Check the rule definition for the correct number of premises.",
expected,
got,
if *got == 1 { "was" } else { "were" }
)
}
Self::WrongArgumentCount { expected, got } => {
write!(
f,
"Wrong argument count: rule expects {} argument(s), but {} {} provided. \
Ensure all required arguments are supplied.",
expected,
got,
if *got == 1 { "was" } else { "were" }
)
}
Self::RuleNotApplicable(msg) => {
write!(
f,
"Rule not applicable: {}. \
Verify that the rule's preconditions are met.",
msg
)
}
Self::InvalidConclusion(msg) => {
write!(
f,
"Invalid conclusion: {}. \
The conclusion does not follow from the premises using the specified rule.",
msg
)
}
Self::CyclicDependency => {
write!(
f,
"Cyclic dependency detected in proof structure. \
A proof step cannot depend on itself (directly or indirectly). \
Check for circular references in premise chains."
)
}
Self::EmptyProof => {
write!(
f,
"Empty proof: no proof steps provided. \
A valid proof must contain at least one step."
)
}
Self::MalformedTerm(msg) => {
write!(
f,
"Malformed term: {}. \
Check for syntax errors or invalid term structure.",
msg
)
}
Self::UnknownRule(name) => {
write!(
f,
"Unknown rule: '{}'. \
This rule is not recognized by the proof checker. \
Verify the rule name is spelled correctly.",
name
)
}
Self::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for CheckError {}
impl CheckError {
#[must_use]
pub fn suggestion(&self) -> Option<&str> {
match self {
Self::MissingPremise(_) => {
Some("Ensure all premise steps are added to the proof before referencing them.")
}
Self::WrongPremiseCount { .. } => {
Some("Consult the rule documentation for the correct number of premises.")
}
Self::WrongArgumentCount { .. } => {
Some("Review the rule definition to determine which arguments are required.")
}
Self::RuleNotApplicable(_) => {
Some("Check that the premise types match what the rule expects.")
}
Self::InvalidConclusion(_) => {
Some("Verify that the rule is being applied correctly to the given premises.")
}
Self::CyclicDependency => {
Some("Reorganize proof steps to eliminate circular dependencies.")
}
Self::EmptyProof => Some("Add at least one axiom or assumption to the proof."),
Self::MalformedTerm(_) => Some("Check the term syntax against the expected format."),
Self::UnknownRule(_) => {
Some("Use a standard proof rule or define a custom rule handler.")
}
Self::Custom(_) => None,
}
}
#[must_use]
pub fn severity(&self) -> ErrorSeverity {
match self {
Self::CyclicDependency | Self::EmptyProof => ErrorSeverity::Critical,
Self::MissingPremise(_) | Self::InvalidConclusion(_) | Self::UnknownRule(_) => {
ErrorSeverity::Error
}
Self::WrongPremiseCount { .. }
| Self::WrongArgumentCount { .. }
| Self::RuleNotApplicable(_)
| Self::MalformedTerm(_) => ErrorSeverity::Warning,
Self::Custom(_) => ErrorSeverity::Error,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Warning,
Error,
Critical,
}
impl fmt::Display for ErrorSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Warning => write!(f, "WARNING"),
Self::Error => write!(f, "ERROR"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CheckerConfig {
pub continue_on_error: bool,
pub verify_conclusions: bool,
pub allow_cycles: bool,
}
#[derive(Debug, Default)]
pub struct ProofChecker {
config: CheckerConfig,
errors: Vec<(u32, CheckError)>,
validated: HashSet<u32>,
in_progress: HashSet<u32>,
}
impl ProofChecker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_config(config: CheckerConfig) -> Self {
Self {
config,
errors: Vec::new(),
validated: HashSet::new(),
in_progress: HashSet::new(),
}
}
pub fn reset(&mut self) {
self.errors.clear();
self.validated.clear();
self.in_progress.clear();
}
pub fn check_theory_proof(&mut self, proof: &TheoryProof) -> CheckResult {
self.reset();
if proof.is_empty() {
return CheckResult::Invalid {
step: 0,
error: CheckError::EmptyProof,
};
}
for step in proof.steps() {
if let Err(error) = self.check_theory_step(proof, step.id) {
if self.config.continue_on_error {
self.errors.push((step.id.0, error));
} else {
return CheckResult::Invalid {
step: step.id.0,
error,
};
}
}
}
if self.errors.is_empty() {
CheckResult::Valid
} else {
CheckResult::MultipleErrors(std::mem::take(&mut self.errors))
}
}
fn check_theory_step(
&mut self,
proof: &TheoryProof,
step_id: TheoryStepId,
) -> Result<(), CheckError> {
if !self.config.allow_cycles {
if self.in_progress.contains(&step_id.0) {
return Err(CheckError::CyclicDependency);
}
if self.validated.contains(&step_id.0) {
return Ok(());
}
self.in_progress.insert(step_id.0);
}
let step = proof
.get_step(step_id)
.ok_or(CheckError::MissingPremise(step_id.0))?;
for premise_id in &step.premises {
if proof.get_step(*premise_id).is_none() {
return Err(CheckError::MissingPremise(premise_id.0));
}
if !self.config.allow_cycles {
self.check_theory_step(proof, *premise_id)?;
}
}
self.check_theory_rule(&step.rule, step.premises.len(), step.args.len())?;
if !self.config.allow_cycles {
self.in_progress.remove(&step_id.0);
self.validated.insert(step_id.0);
}
Ok(())
}
fn check_theory_rule(
&self,
rule: &TheoryRule,
premise_count: usize,
arg_count: usize,
) -> Result<(), CheckError> {
match rule {
TheoryRule::Refl if premise_count != 0 => {
return Err(CheckError::WrongPremiseCount {
expected: 0,
got: premise_count,
});
}
TheoryRule::Symm if premise_count != 1 => {
return Err(CheckError::WrongPremiseCount {
expected: 1,
got: premise_count,
});
}
TheoryRule::Trans if premise_count != 2 => {
return Err(CheckError::WrongPremiseCount {
expected: 2,
got: premise_count,
});
}
TheoryRule::Cong => {
}
TheoryRule::LaGeneric if premise_count < 2 => {
return Err(CheckError::WrongPremiseCount {
expected: 2,
got: premise_count,
});
}
TheoryRule::ArrReadWrite1 if premise_count != 0 => {
return Err(CheckError::WrongPremiseCount {
expected: 0,
got: premise_count,
});
}
TheoryRule::ArrReadWrite2 if premise_count != 1 => {
return Err(CheckError::WrongPremiseCount {
expected: 1,
got: premise_count,
});
}
TheoryRule::LaMult if arg_count < 1 => {
return Err(CheckError::WrongArgumentCount {
expected: 1,
got: arg_count,
});
}
_ => {}
}
Ok(())
}
pub fn check_alethe_proof(&mut self, proof: &AletheProof) -> CheckResult {
self.reset();
if proof.is_empty() {
return CheckResult::Invalid {
step: 0,
error: CheckError::EmptyProof,
};
}
let steps = proof.steps();
let mut step_indices: HashSet<u32> = HashSet::new();
for step in steps {
match step {
AletheStep::Assume { index, .. } => {
step_indices.insert(*index);
}
AletheStep::Step { index, .. } => {
step_indices.insert(*index);
}
AletheStep::Anchor { step: index, .. } => {
step_indices.insert(*index);
}
AletheStep::DefineFun { .. } => {}
}
}
for (idx, step) in steps.iter().enumerate() {
if let Err(error) = self.check_alethe_step(step, &step_indices) {
if self.config.continue_on_error {
self.errors.push((idx as u32, error));
} else {
return CheckResult::Invalid {
step: idx as u32,
error,
};
}
}
}
if self.errors.is_empty() {
CheckResult::Valid
} else {
CheckResult::MultipleErrors(std::mem::take(&mut self.errors))
}
}
fn check_alethe_step(
&self,
step: &AletheStep,
step_indices: &HashSet<u32>,
) -> Result<(), CheckError> {
match step {
AletheStep::Assume { .. } => {
Ok(())
}
AletheStep::Step { rule, premises, .. } => {
for premise in premises {
if !step_indices.contains(premise) {
return Err(CheckError::MissingPremise(*premise));
}
}
self.check_alethe_rule(rule, premises.len())
}
AletheStep::Anchor { .. } => {
Ok(())
}
AletheStep::DefineFun { .. } => {
Ok(())
}
}
}
fn check_alethe_rule(&self, rule: &AletheRule, premise_count: usize) -> Result<(), CheckError> {
match rule {
AletheRule::Resolution if premise_count < 2 => {
return Err(CheckError::WrongPremiseCount {
expected: 2,
got: premise_count,
});
}
AletheRule::Refl if premise_count != 0 => {
return Err(CheckError::WrongPremiseCount {
expected: 0,
got: premise_count,
});
}
AletheRule::Trans if premise_count < 2 => {
return Err(CheckError::WrongPremiseCount {
expected: 2,
got: premise_count,
});
}
_ => {}
}
Ok(())
}
}
pub trait Checkable {
fn check(&self) -> CheckResult;
fn check_with_config(&self, config: CheckerConfig) -> CheckResult;
}
impl Checkable for TheoryProof {
fn check(&self) -> CheckResult {
ProofChecker::new().check_theory_proof(self)
}
fn check_with_config(&self, config: CheckerConfig) -> CheckResult {
ProofChecker::with_config(config).check_theory_proof(self)
}
}
impl Checkable for AletheProof {
fn check(&self) -> CheckResult {
ProofChecker::new().check_alethe_proof(self)
}
fn check_with_config(&self, config: CheckerConfig) -> CheckResult {
ProofChecker::with_config(config).check_alethe_proof(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use crate::theory::ProofTerm;
#[test]
fn test_check_result_is_valid() {
assert!(CheckResult::Valid.is_valid());
assert!(
!CheckResult::Invalid {
step: 0,
error: CheckError::EmptyProof
}
.is_valid()
);
}
#[test]
fn test_check_error_display() {
let err = CheckError::MissingPremise(5);
let msg = format!("{}", err);
assert!(msg.contains("5"));
assert!(msg.contains("does not exist"));
let err = CheckError::WrongPremiseCount {
expected: 2,
got: 1,
};
let msg = format!("{}", err);
assert!(msg.contains("requires 2"));
assert!(msg.contains("1 was provided"));
}
#[test]
fn test_theory_proof_empty() {
let proof = TheoryProof::new();
let result = proof.check();
assert!(!result.is_valid());
assert!(matches!(result.error(), Some(CheckError::EmptyProof)));
}
#[test]
fn test_theory_proof_valid_refl() {
let mut proof = TheoryProof::new();
proof.refl("x");
let result = proof.check();
assert!(result.is_valid());
}
#[test]
fn test_theory_proof_valid_transitivity() {
let mut proof = TheoryProof::new();
let s1 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= a b)");
let s2 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= b c)");
proof.trans(s1, s2, "a", "c");
let result = proof.check();
assert!(result.is_valid());
}
#[test]
fn test_theory_proof_invalid_trans_premises() {
let mut proof = TheoryProof::new();
let s1 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= a b)");
proof.add_step(TheoryRule::Trans, vec![s1], "(= a c)");
let result = proof.check();
assert!(!result.is_valid());
}
#[test]
fn test_theory_proof_missing_premise() {
let mut proof = TheoryProof::new();
proof.add_step(
TheoryRule::Trans,
vec![TheoryStepId(99), TheoryStepId(100)],
"(= a c)",
);
let result = proof.check();
assert!(!result.is_valid());
assert!(matches!(
result.error(),
Some(CheckError::MissingPremise(_))
));
}
#[test]
fn test_alethe_proof_empty() {
let proof = AletheProof::new();
let result = proof.check();
assert!(!result.is_valid());
}
#[test]
fn test_alethe_proof_valid() {
let mut proof = AletheProof::new();
proof.assume("p");
proof.step_simple(vec![], AletheRule::Refl);
let result = proof.check();
assert!(result.is_valid());
}
#[test]
fn test_checker_continue_on_error() {
let mut proof = TheoryProof::new();
proof.add_step(TheoryRule::Trans, vec![TheoryStepId(99)], "(= a b)");
proof.add_step(TheoryRule::Trans, vec![TheoryStepId(100)], "(= c d)");
let config = CheckerConfig {
continue_on_error: true,
..Default::default()
};
let result = proof.check_with_config(config);
assert!(matches!(result, CheckResult::MultipleErrors(_)));
}
#[test]
fn test_checker_refl_with_premises_fails() {
let mut proof = TheoryProof::new();
let s1 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= a b)");
proof.add_step(TheoryRule::Refl, vec![s1], "(= x x)");
let result = proof.check();
assert!(!result.is_valid());
}
#[test]
fn test_checker_farkas_needs_premises() {
let mut proof = TheoryProof::new();
let s1 = proof.add_axiom(TheoryRule::Custom("bound".into()), "(>= x 0)");
proof.add_step(TheoryRule::LaGeneric, vec![s1], "false");
let result = proof.check();
assert!(!result.is_valid());
}
#[test]
fn test_checker_arr_read_write_1_axiom() {
let mut proof = TheoryProof::new();
proof.add_axiom(TheoryRule::ArrReadWrite1, "(= (select (store a i v) i) v)");
let result = proof.check();
assert!(result.is_valid());
}
#[test]
fn test_checker_arr_read_write_2_needs_premise() {
let mut proof = TheoryProof::new();
proof.add_axiom(
TheoryRule::ArrReadWrite2,
"(= (select (store a i v) j) (select a j))",
);
let result = proof.check();
assert!(!result.is_valid());
}
}