1use crate::proof::Proof;
7use rustc_hash::FxHashSet;
8use std::fmt;
9
10pub type ValidationResult<T> = Result<T, ValidationError>;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ValidationError {
16 MissingField { field: String, location: String },
18 InvalidReference { step_id: String, reference: String },
20 MalformedStructure { reason: String },
22 UnsupportedFeature { feature: String, format: String },
24 InvalidConclusion { conclusion: String, reason: String },
26 EmptyProof,
28 CircularDependency { steps: Vec<String> },
30 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
65pub struct FormatValidator {
67 allow_empty: bool,
69 check_cycles: bool,
71 validate_syntax: bool,
73}
74
75impl Default for FormatValidator {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl FormatValidator {
82 pub fn new() -> Self {
84 Self {
85 allow_empty: false,
86 check_cycles: true,
87 validate_syntax: true,
88 }
89 }
90
91 pub fn allow_empty(mut self, allow: bool) -> Self {
93 self.allow_empty = allow;
94 self
95 }
96
97 pub fn check_cycles(mut self, check: bool) -> Self {
99 self.check_cycles = check;
100 self
101 }
102
103 pub fn validate_syntax(mut self, validate: bool) -> Self {
105 self.validate_syntax = validate;
106 self
107 }
108
109 pub fn validate_proof(&self, proof: &Proof) -> ValidationResult<()> {
111 if proof.is_empty() {
113 if self.allow_empty {
114 return Ok(());
115 } else {
116 return Err(ValidationError::EmptyProof);
117 }
118 }
119
120 if self.check_cycles {
122 self.check_proof_cycles(proof)?;
123 }
124
125 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 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 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 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 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 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 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}