Skip to main content

oxiz_proof/
validation.rs

1//! Format validation utilities for proof formats.
2//!
3//! This module provides validation for various proof formats to ensure
4//! correctness before export or conversion.
5
6use crate::proof::Proof;
7use rustc_hash::FxHashSet;
8use std::fmt;
9
10/// Result of format validation.
11pub type ValidationResult<T> = Result<T, ValidationError>;
12
13/// Errors that can occur during format validation.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ValidationError {
16    /// Missing required field
17    MissingField { field: String, location: String },
18    /// Invalid step reference
19    InvalidReference { step_id: String, reference: String },
20    /// Malformed proof structure
21    MalformedStructure { reason: String },
22    /// Unsupported rule or operation
23    UnsupportedFeature { feature: String, format: String },
24    /// Invalid conclusion format
25    InvalidConclusion { conclusion: String, reason: String },
26    /// Empty proof
27    EmptyProof,
28    /// Circular dependency detected
29    CircularDependency { steps: Vec<String> },
30    /// Type mismatch in proof
31    TypeMismatch { expected: String, found: String },
32}
33
34impl fmt::Display for ValidationError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            ValidationError::MissingField { field, location } => {
38                write!(f, "Missing required field '{}' in {}", field, location)
39            }
40            ValidationError::InvalidReference { step_id, reference } => {
41                write!(f, "Invalid reference '{}' in step '{}'", reference, step_id)
42            }
43            ValidationError::MalformedStructure { reason } => {
44                write!(f, "Malformed proof structure: {}", reason)
45            }
46            ValidationError::UnsupportedFeature { feature, format } => {
47                write!(f, "Unsupported feature '{}' in {} format", feature, format)
48            }
49            ValidationError::InvalidConclusion { conclusion, reason } => {
50                write!(f, "Invalid conclusion '{}': {}", conclusion, reason)
51            }
52            ValidationError::EmptyProof => write!(f, "Proof is empty"),
53            ValidationError::CircularDependency { steps } => {
54                write!(f, "Circular dependency detected: {}", steps.join(" -> "))
55            }
56            ValidationError::TypeMismatch { expected, found } => {
57                write!(f, "Type mismatch: expected {}, found {}", expected, found)
58            }
59        }
60    }
61}
62
63impl std::error::Error for ValidationError {}
64
65/// Validator for proof formats.
66pub struct FormatValidator {
67    /// Allow empty proofs
68    allow_empty: bool,
69    /// Check for circular dependencies
70    check_cycles: bool,
71    /// Validate conclusion syntax
72    validate_syntax: bool,
73}
74
75impl Default for FormatValidator {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl FormatValidator {
82    /// Create a new format validator with default settings.
83    pub fn new() -> Self {
84        Self {
85            allow_empty: false,
86            check_cycles: true,
87            validate_syntax: true,
88        }
89    }
90
91    /// Allow empty proofs.
92    pub fn allow_empty(mut self, allow: bool) -> Self {
93        self.allow_empty = allow;
94        self
95    }
96
97    /// Enable/disable cycle checking.
98    pub fn check_cycles(mut self, check: bool) -> Self {
99        self.check_cycles = check;
100        self
101    }
102
103    /// Enable/disable syntax validation.
104    pub fn validate_syntax(mut self, validate: bool) -> Self {
105        self.validate_syntax = validate;
106        self
107    }
108
109    /// Validate a generic proof.
110    pub fn validate_proof(&self, proof: &Proof) -> ValidationResult<()> {
111        // Check if proof is empty
112        if proof.is_empty() {
113            if self.allow_empty {
114                return Ok(());
115            } else {
116                return Err(ValidationError::EmptyProof);
117            }
118        }
119
120        // Check for circular dependencies
121        if self.check_cycles {
122            self.check_proof_cycles(proof)?;
123        }
124
125        // Validate each node
126        for node in proof.nodes() {
127            if self.validate_syntax {
128                self.validate_conclusion_syntax(node.conclusion())?;
129            }
130        }
131
132        Ok(())
133    }
134
135    // Helper: Check for circular dependencies in proof
136    fn check_proof_cycles(&self, proof: &Proof) -> ValidationResult<()> {
137        let mut visiting = FxHashSet::default();
138        let mut visited = FxHashSet::default();
139        let mut path = Vec::new();
140
141        for node in proof.nodes() {
142            if !visited.contains(&node.id) {
143                Self::visit_node(proof, node.id, &mut visiting, &mut visited, &mut path)?;
144            }
145        }
146
147        Ok(())
148    }
149
150    // Helper: Visit node in DFS for cycle detection
151    fn visit_node(
152        proof: &Proof,
153        node_id: crate::proof::ProofNodeId,
154        visiting: &mut FxHashSet<crate::proof::ProofNodeId>,
155        visited: &mut FxHashSet<crate::proof::ProofNodeId>,
156        path: &mut Vec<String>,
157    ) -> ValidationResult<()> {
158        if visiting.contains(&node_id) {
159            // Cycle detected
160            path.push(node_id.to_string());
161            return Err(ValidationError::CircularDependency {
162                steps: path.clone(),
163            });
164        }
165
166        if visited.contains(&node_id) {
167            return Ok(());
168        }
169
170        visiting.insert(node_id);
171        path.push(node_id.to_string());
172
173        // Visit premises
174        if let Some(node) = proof.get_node(node_id)
175            && let crate::proof::ProofStep::Inference { premises, .. } = &node.step
176        {
177            for &premise_id in premises.iter() {
178                Self::visit_node(proof, premise_id, visiting, visited, path)?;
179            }
180        }
181
182        path.pop();
183        visiting.remove(&node_id);
184        visited.insert(node_id);
185
186        Ok(())
187    }
188
189    // Helper: Validate conclusion syntax
190    fn validate_conclusion_syntax(&self, conclusion: &str) -> ValidationResult<()> {
191        if conclusion.trim().is_empty() {
192            return Err(ValidationError::InvalidConclusion {
193                conclusion: conclusion.to_string(),
194                reason: "Empty conclusion".to_string(),
195            });
196        }
197
198        // Check for balanced parentheses
199        let mut depth = 0;
200        for ch in conclusion.chars() {
201            match ch {
202                '(' => depth += 1,
203                ')' => {
204                    depth -= 1;
205                    if depth < 0 {
206                        return Err(ValidationError::InvalidConclusion {
207                            conclusion: conclusion.to_string(),
208                            reason: "Unbalanced parentheses".to_string(),
209                        });
210                    }
211                }
212                _ => {}
213            }
214        }
215
216        if depth != 0 {
217            return Err(ValidationError::InvalidConclusion {
218                conclusion: conclusion.to_string(),
219                reason: "Unbalanced parentheses".to_string(),
220            });
221        }
222
223        Ok(())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_validator_new() {
233        let validator = FormatValidator::new();
234        assert!(!validator.allow_empty);
235        assert!(validator.check_cycles);
236        assert!(validator.validate_syntax);
237    }
238
239    #[test]
240    fn test_validator_with_settings() {
241        let validator = FormatValidator::new()
242            .allow_empty(true)
243            .check_cycles(false)
244            .validate_syntax(false);
245        assert!(validator.allow_empty);
246        assert!(!validator.check_cycles);
247        assert!(!validator.validate_syntax);
248    }
249
250    #[test]
251    fn test_validate_empty_proof() {
252        let validator = FormatValidator::new();
253        let proof = Proof::new();
254        assert!(validator.validate_proof(&proof).is_err());
255
256        let validator = FormatValidator::new().allow_empty(true);
257        assert!(validator.validate_proof(&proof).is_ok());
258    }
259
260    #[test]
261    fn test_validate_syntax_balanced_parens() {
262        let validator = FormatValidator::new();
263        assert!(validator.validate_conclusion_syntax("(x = y)").is_ok());
264        assert!(validator.validate_conclusion_syntax("f(x, g(y))").is_ok());
265    }
266
267    #[test]
268    fn test_validate_syntax_unbalanced_parens() {
269        let validator = FormatValidator::new();
270        assert!(validator.validate_conclusion_syntax("(x = y").is_err());
271        assert!(validator.validate_conclusion_syntax("x = y)").is_err());
272    }
273
274    #[test]
275    fn test_validate_syntax_empty() {
276        let validator = FormatValidator::new();
277        assert!(validator.validate_conclusion_syntax("").is_err());
278        assert!(validator.validate_conclusion_syntax("   ").is_err());
279    }
280
281    #[test]
282    fn test_validation_error_display() {
283        let err = ValidationError::EmptyProof;
284        assert_eq!(err.to_string(), "Proof is empty");
285
286        let err = ValidationError::MissingField {
287            field: "conclusion".to_string(),
288            location: "step 5".to_string(),
289        };
290        assert!(err.to_string().contains("Missing required field"));
291    }
292
293    #[test]
294    fn test_validate_nonempty_proof() {
295        let validator = FormatValidator::new();
296        let mut proof = Proof::new();
297        proof.add_axiom("x = x");
298        assert!(validator.validate_proof(&proof).is_ok());
299    }
300
301    #[test]
302    fn test_validate_with_invalid_syntax() {
303        let validator = FormatValidator::new();
304        assert!(validator.validate_conclusion_syntax("(x = y").is_err());
305    }
306}