use crate::proof::Proof;
use rustc_hash::FxHashSet;
use std::fmt;
pub type ValidationResult<T> = Result<T, ValidationError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
MissingField { field: String, location: String },
InvalidReference { step_id: String, reference: String },
MalformedStructure { reason: String },
UnsupportedFeature { feature: String, format: String },
InvalidConclusion { conclusion: String, reason: String },
EmptyProof,
CircularDependency { steps: Vec<String> },
TypeMismatch { expected: String, found: String },
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::MissingField { field, location } => {
write!(f, "Missing required field '{}' in {}", field, location)
}
ValidationError::InvalidReference { step_id, reference } => {
write!(f, "Invalid reference '{}' in step '{}'", reference, step_id)
}
ValidationError::MalformedStructure { reason } => {
write!(f, "Malformed proof structure: {}", reason)
}
ValidationError::UnsupportedFeature { feature, format } => {
write!(f, "Unsupported feature '{}' in {} format", feature, format)
}
ValidationError::InvalidConclusion { conclusion, reason } => {
write!(f, "Invalid conclusion '{}': {}", conclusion, reason)
}
ValidationError::EmptyProof => write!(f, "Proof is empty"),
ValidationError::CircularDependency { steps } => {
write!(f, "Circular dependency detected: {}", steps.join(" -> "))
}
ValidationError::TypeMismatch { expected, found } => {
write!(f, "Type mismatch: expected {}, found {}", expected, found)
}
}
}
}
impl std::error::Error for ValidationError {}
pub struct FormatValidator {
allow_empty: bool,
check_cycles: bool,
validate_syntax: bool,
}
impl Default for FormatValidator {
fn default() -> Self {
Self::new()
}
}
impl FormatValidator {
pub fn new() -> Self {
Self {
allow_empty: false,
check_cycles: true,
validate_syntax: true,
}
}
pub fn allow_empty(mut self, allow: bool) -> Self {
self.allow_empty = allow;
self
}
pub fn check_cycles(mut self, check: bool) -> Self {
self.check_cycles = check;
self
}
pub fn validate_syntax(mut self, validate: bool) -> Self {
self.validate_syntax = validate;
self
}
pub fn validate_proof(&self, proof: &Proof) -> ValidationResult<()> {
if proof.is_empty() {
if self.allow_empty {
return Ok(());
} else {
return Err(ValidationError::EmptyProof);
}
}
if self.check_cycles {
self.check_proof_cycles(proof)?;
}
for node in proof.nodes() {
if self.validate_syntax {
self.validate_conclusion_syntax(node.conclusion())?;
}
}
Ok(())
}
fn check_proof_cycles(&self, proof: &Proof) -> ValidationResult<()> {
let mut visiting = FxHashSet::default();
let mut visited = FxHashSet::default();
let mut path = Vec::new();
for node in proof.nodes() {
if !visited.contains(&node.id) {
Self::visit_node(proof, node.id, &mut visiting, &mut visited, &mut path)?;
}
}
Ok(())
}
fn visit_node(
proof: &Proof,
node_id: crate::proof::ProofNodeId,
visiting: &mut FxHashSet<crate::proof::ProofNodeId>,
visited: &mut FxHashSet<crate::proof::ProofNodeId>,
path: &mut Vec<String>,
) -> ValidationResult<()> {
if visiting.contains(&node_id) {
path.push(node_id.to_string());
return Err(ValidationError::CircularDependency {
steps: path.clone(),
});
}
if visited.contains(&node_id) {
return Ok(());
}
visiting.insert(node_id);
path.push(node_id.to_string());
if let Some(node) = proof.get_node(node_id)
&& let crate::proof::ProofStep::Inference { premises, .. } = &node.step
{
for &premise_id in premises.iter() {
Self::visit_node(proof, premise_id, visiting, visited, path)?;
}
}
path.pop();
visiting.remove(&node_id);
visited.insert(node_id);
Ok(())
}
fn validate_conclusion_syntax(&self, conclusion: &str) -> ValidationResult<()> {
if conclusion.trim().is_empty() {
return Err(ValidationError::InvalidConclusion {
conclusion: conclusion.to_string(),
reason: "Empty conclusion".to_string(),
});
}
let mut depth = 0;
for ch in conclusion.chars() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth < 0 {
return Err(ValidationError::InvalidConclusion {
conclusion: conclusion.to_string(),
reason: "Unbalanced parentheses".to_string(),
});
}
}
_ => {}
}
}
if depth != 0 {
return Err(ValidationError::InvalidConclusion {
conclusion: conclusion.to_string(),
reason: "Unbalanced parentheses".to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validator_new() {
let validator = FormatValidator::new();
assert!(!validator.allow_empty);
assert!(validator.check_cycles);
assert!(validator.validate_syntax);
}
#[test]
fn test_validator_with_settings() {
let validator = FormatValidator::new()
.allow_empty(true)
.check_cycles(false)
.validate_syntax(false);
assert!(validator.allow_empty);
assert!(!validator.check_cycles);
assert!(!validator.validate_syntax);
}
#[test]
fn test_validate_empty_proof() {
let validator = FormatValidator::new();
let proof = Proof::new();
assert!(validator.validate_proof(&proof).is_err());
let validator = FormatValidator::new().allow_empty(true);
assert!(validator.validate_proof(&proof).is_ok());
}
#[test]
fn test_validate_syntax_balanced_parens() {
let validator = FormatValidator::new();
assert!(validator.validate_conclusion_syntax("(x = y)").is_ok());
assert!(validator.validate_conclusion_syntax("f(x, g(y))").is_ok());
}
#[test]
fn test_validate_syntax_unbalanced_parens() {
let validator = FormatValidator::new();
assert!(validator.validate_conclusion_syntax("(x = y").is_err());
assert!(validator.validate_conclusion_syntax("x = y)").is_err());
}
#[test]
fn test_validate_syntax_empty() {
let validator = FormatValidator::new();
assert!(validator.validate_conclusion_syntax("").is_err());
assert!(validator.validate_conclusion_syntax(" ").is_err());
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::EmptyProof;
assert_eq!(err.to_string(), "Proof is empty");
let err = ValidationError::MissingField {
field: "conclusion".to_string(),
location: "step 5".to_string(),
};
assert!(err.to_string().contains("Missing required field"));
}
#[test]
fn test_validate_nonempty_proof() {
let validator = FormatValidator::new();
let mut proof = Proof::new();
proof.add_axiom("x = x");
assert!(validator.validate_proof(&proof).is_ok());
}
#[test]
fn test_validate_with_invalid_syntax() {
let validator = FormatValidator::new();
assert!(validator.validate_conclusion_syntax("(x = y").is_err());
}
}