1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq)]
14pub enum ValidationError {
15 EmptyHead,
17 UnboundVariable { var_name: String },
19 CircularDependency { rule_id: u64 },
21 ExcessiveBodyLength { length: usize, max: usize },
23 DuplicateHead { existing_id: u64 },
25 InvalidWeight { weight: f64 },
27}
28
29#[derive(Debug, Clone)]
35pub struct ValidationResult {
36 pub rule_id: u64,
38 pub errors: Vec<ValidationError>,
40 pub warnings: Vec<String>,
42}
43
44impl ValidationResult {
45 #[inline]
47 pub fn is_valid(&self) -> bool {
48 self.errors.is_empty()
49 }
50
51 #[inline]
53 pub fn error_count(&self) -> usize {
54 self.errors.len()
55 }
56}
57
58#[derive(Debug, Clone)]
64pub struct ValidatorConfig {
65 pub max_body_length: usize,
67 pub allow_duplicate_heads: bool,
70 pub require_positive_weight: bool,
72}
73
74impl Default for ValidatorConfig {
75 fn default() -> Self {
76 Self {
77 max_body_length: 64,
78 allow_duplicate_heads: false,
79 require_positive_weight: true,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
90pub struct RuleSpec {
91 pub rule_id: u64,
93 pub head_terms: Vec<String>,
95 pub body_terms: Vec<String>,
97 pub variables: Vec<String>,
100 pub weight: f64,
102 pub depends_on: Vec<u64>,
105}
106
107pub struct TensorRuleValidator {
114 pub config: ValidatorConfig,
116 pub registered_heads: HashMap<String, u64>,
119}
120
121impl TensorRuleValidator {
122 pub fn new(config: ValidatorConfig) -> Self {
124 Self {
125 config,
126 registered_heads: HashMap::new(),
127 }
128 }
129
130 pub fn validate(&mut self, spec: &RuleSpec) -> ValidationResult {
135 let mut errors: Vec<ValidationError> = Vec::new();
136 let mut warnings: Vec<String> = Vec::new();
137
138 if spec.head_terms.is_empty() {
140 errors.push(ValidationError::EmptyHead);
141 }
142
143 for var in &spec.variables {
145 let bound = spec
146 .body_terms
147 .iter()
148 .any(|body_term| body_term.contains(var.as_str()));
149 if !bound {
150 errors.push(ValidationError::UnboundVariable {
151 var_name: var.clone(),
152 });
153 }
154 }
155
156 if spec.depends_on.contains(&spec.rule_id) {
158 errors.push(ValidationError::CircularDependency {
159 rule_id: spec.rule_id,
160 });
161 }
162
163 if spec.body_terms.len() > self.config.max_body_length {
165 errors.push(ValidationError::ExcessiveBodyLength {
166 length: spec.body_terms.len(),
167 max: self.config.max_body_length,
168 });
169 }
170
171 if !self.config.allow_duplicate_heads {
173 let signature = spec.head_terms.join("|");
174 if let Some(&existing_id) = self.registered_heads.get(&signature) {
175 errors.push(ValidationError::DuplicateHead { existing_id });
176 }
177 }
178
179 if self.config.require_positive_weight && (spec.weight <= 0.0 || spec.weight > 1.0) {
181 errors.push(ValidationError::InvalidWeight {
182 weight: spec.weight,
183 });
184 }
185
186 if spec.body_terms.is_empty() && !spec.head_terms.is_empty() {
188 warnings.push("Rule has no body terms (fact assertion)".to_string());
189 }
190
191 if errors.is_empty() {
193 let signature = spec.head_terms.join("|");
194 self.registered_heads.insert(signature, spec.rule_id);
195 }
196
197 ValidationResult {
198 rule_id: spec.rule_id,
199 errors,
200 warnings,
201 }
202 }
203
204 pub fn registered_count(&self) -> usize {
206 self.registered_heads.len()
207 }
208
209 pub fn clear_registry(&mut self) {
211 self.registered_heads.clear();
212 }
213}
214
215#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn default_validator() -> TensorRuleValidator {
226 TensorRuleValidator::new(ValidatorConfig::default())
227 }
228
229 fn valid_spec() -> RuleSpec {
231 RuleSpec {
232 rule_id: 1,
233 head_terms: vec!["parent(X, Y)".to_string()],
234 body_terms: vec!["father(X, Y)".to_string()],
235 variables: vec!["X".to_string(), "Y".to_string()],
236 weight: 1.0,
237 depends_on: vec![],
238 }
239 }
240
241 #[test]
243 fn test_valid_rule_passes() {
244 let mut v = default_validator();
245 let result = v.validate(&valid_spec());
246 assert!(result.is_valid());
247 assert_eq!(result.error_count(), 0);
248 }
249
250 #[test]
252 fn test_empty_head_detected() {
253 let mut v = default_validator();
254 let spec = RuleSpec {
255 head_terms: vec![],
256 ..valid_spec()
257 };
258 let result = v.validate(&spec);
259 assert!(!result.is_valid());
260 assert!(result.errors.contains(&ValidationError::EmptyHead));
261 }
262
263 #[test]
265 fn test_unbound_variable_detected() {
266 let mut v = default_validator();
267 let spec = RuleSpec {
268 variables: vec!["Z".to_string()],
269 body_terms: vec!["father(X, Y)".to_string()],
270 ..valid_spec()
271 };
272 let result = v.validate(&spec);
273 assert!(!result.is_valid());
274 assert!(result.errors.contains(&ValidationError::UnboundVariable {
275 var_name: "Z".to_string()
276 }));
277 }
278
279 #[test]
281 fn test_bound_variable_passes() {
282 let mut v = default_validator();
283 let spec = RuleSpec {
284 variables: vec!["X".to_string()],
285 body_terms: vec!["node(X)".to_string()],
286 ..valid_spec()
287 };
288 let result = v.validate(&spec);
289 assert!(result.is_valid());
290 }
291
292 #[test]
294 fn test_circular_dependency_detected() {
295 let mut v = default_validator();
296 let spec = RuleSpec {
297 depends_on: vec![1],
298 ..valid_spec()
299 };
300 let result = v.validate(&spec);
301 assert!(!result.is_valid());
302 assert!(result
303 .errors
304 .contains(&ValidationError::CircularDependency { rule_id: 1 }));
305 }
306
307 #[test]
309 fn test_excessive_body_length_detected() {
310 let mut v = default_validator();
311 let body: Vec<String> = (0..65).map(|i| format!("term_{i}(X)")).collect();
312 let spec = RuleSpec {
313 body_terms: body,
314 variables: vec!["X".to_string()],
315 ..valid_spec()
316 };
317 let result = v.validate(&spec);
318 assert!(!result.is_valid());
319 assert!(result.errors.iter().any(|e| matches!(
320 e,
321 ValidationError::ExcessiveBodyLength {
322 length: 65,
323 max: 64
324 }
325 )));
326 }
327
328 #[test]
330 fn test_body_length_at_limit_passes() {
331 let mut v = default_validator();
332 let body: Vec<String> = (0..64).map(|i| format!("term_{i}(X)")).collect();
333 let spec = RuleSpec {
334 body_terms: body,
335 variables: vec!["X".to_string()],
336 ..valid_spec()
337 };
338 let result = v.validate(&spec);
339 assert!(result.is_valid());
340 }
341
342 #[test]
344 fn test_duplicate_head_detected() {
345 let mut v = default_validator();
346 let spec = valid_spec();
347 let first = v.validate(&spec);
348 assert!(first.is_valid());
349
350 let spec2 = RuleSpec {
351 rule_id: 2,
352 ..valid_spec()
353 };
354 let second = v.validate(&spec2);
355 assert!(!second.is_valid());
356 assert!(second
357 .errors
358 .contains(&ValidationError::DuplicateHead { existing_id: 1 }));
359 }
360
361 #[test]
363 fn test_duplicate_head_allowed() {
364 let config = ValidatorConfig {
365 allow_duplicate_heads: true,
366 ..Default::default()
367 };
368 let mut v = TensorRuleValidator::new(config);
369 let spec = valid_spec();
370 let first = v.validate(&spec);
371 assert!(first.is_valid());
372
373 let spec2 = RuleSpec {
374 rule_id: 2,
375 ..valid_spec()
376 };
377 let second = v.validate(&spec2);
378 assert!(second.is_valid());
379 }
380
381 #[test]
383 fn test_invalid_weight_zero() {
384 let mut v = default_validator();
385 let spec = RuleSpec {
386 weight: 0.0,
387 ..valid_spec()
388 };
389 let result = v.validate(&spec);
390 assert!(!result.is_valid());
391 assert!(result
392 .errors
393 .contains(&ValidationError::InvalidWeight { weight: 0.0 }));
394 }
395
396 #[test]
398 fn test_invalid_weight_negative() {
399 let mut v = default_validator();
400 let spec = RuleSpec {
401 weight: -0.5,
402 ..valid_spec()
403 };
404 let result = v.validate(&spec);
405 assert!(!result.is_valid());
406 assert!(result
407 .errors
408 .contains(&ValidationError::InvalidWeight { weight: -0.5 }));
409 }
410
411 #[test]
413 fn test_invalid_weight_above_one() {
414 let mut v = default_validator();
415 let spec = RuleSpec {
416 weight: 1.01,
417 ..valid_spec()
418 };
419 let result = v.validate(&spec);
420 assert!(!result.is_valid());
421 assert!(result
422 .errors
423 .iter()
424 .any(|e| matches!(e, ValidationError::InvalidWeight { .. })));
425 }
426
427 #[test]
429 fn test_weight_exactly_one_passes() {
430 let mut v = default_validator();
431 let spec = RuleSpec {
432 weight: 1.0,
433 ..valid_spec()
434 };
435 let result = v.validate(&spec);
436 assert!(result.is_valid());
437 }
438
439 #[test]
441 fn test_weight_point_five_passes() {
442 let mut v = default_validator();
443 let spec = RuleSpec {
444 weight: 0.5,
445 ..valid_spec()
446 };
447 let result = v.validate(&spec);
448 assert!(result.is_valid());
449 }
450
451 #[test]
453 fn test_is_valid_true_no_errors() {
454 let mut v = default_validator();
455 let result = v.validate(&valid_spec());
456 assert!(result.is_valid());
457 assert_eq!(result.error_count(), 0);
458 }
459
460 #[test]
462 fn test_is_valid_false_with_errors() {
463 let mut v = default_validator();
464 let spec = RuleSpec {
465 head_terms: vec![],
466 ..valid_spec()
467 };
468 let result = v.validate(&spec);
469 assert!(!result.is_valid());
470 assert!(result.error_count() > 0);
471 }
472
473 #[test]
475 fn test_warning_for_fact_assertion() {
476 let mut v = default_validator();
477 let spec = RuleSpec {
478 body_terms: vec![],
479 variables: vec![],
480 ..valid_spec()
481 };
482 let result = v.validate(&spec);
483 assert!(result.is_valid());
484 assert!(result.warnings.iter().any(|w| w.contains("fact assertion")));
485 }
486
487 #[test]
489 fn test_registered_count_increments() {
490 let mut v = default_validator();
491 assert_eq!(v.registered_count(), 0);
492
493 let spec1 = valid_spec();
494 v.validate(&spec1);
495 assert_eq!(v.registered_count(), 1);
496
497 let spec2 = RuleSpec {
498 rule_id: 2,
499 head_terms: vec!["sibling(X, Y)".to_string()],
500 body_terms: vec!["parent(Z, X)".to_string(), "parent(Z, Y)".to_string()],
501 variables: vec!["X".to_string(), "Y".to_string(), "Z".to_string()],
502 weight: 0.9,
503 depends_on: vec![],
504 };
505 v.validate(&spec2);
506 assert_eq!(v.registered_count(), 2);
507 }
508
509 #[test]
511 fn test_clear_registry_resets_count() {
512 let mut v = default_validator();
513 v.validate(&valid_spec());
514 assert_eq!(v.registered_count(), 1);
515 v.clear_registry();
516 assert_eq!(v.registered_count(), 0);
517 }
518
519 #[test]
521 fn test_multiple_errors_accumulated() {
522 let mut v = default_validator();
523 let spec = RuleSpec {
525 rule_id: 42,
526 head_terms: vec![], body_terms: vec![],
528 variables: vec![],
529 weight: 0.0, depends_on: vec![42], };
532 let result = v.validate(&spec);
533 assert!(!result.is_valid());
534 assert!(result.errors.contains(&ValidationError::EmptyHead));
535 assert!(result
536 .errors
537 .contains(&ValidationError::CircularDependency { rule_id: 42 }));
538 assert!(result
539 .errors
540 .contains(&ValidationError::InvalidWeight { weight: 0.0 }));
541 assert!(result.error_count() >= 3);
542 }
543
544 #[test]
546 fn test_invalid_rule_not_registered() {
547 let mut v = default_validator();
548 let spec = RuleSpec {
549 weight: 0.0, ..valid_spec()
551 };
552 v.validate(&spec);
553 assert_eq!(v.registered_count(), 0);
554 }
555
556 #[test]
558 fn test_non_self_depends_on_ok() {
559 let mut v = default_validator();
560 let spec = RuleSpec {
561 depends_on: vec![99, 100],
562 ..valid_spec()
563 };
564 let result = v.validate(&spec);
565 assert!(result.is_valid());
566 }
567
568 #[test]
570 fn test_no_weight_requirement_allows_zero() {
571 let config = ValidatorConfig {
572 require_positive_weight: false,
573 ..Default::default()
574 };
575 let mut v = TensorRuleValidator::new(config);
576 let spec = RuleSpec {
577 weight: 0.0,
578 ..valid_spec()
579 };
580 let result = v.validate(&spec);
581 assert!(result.is_valid());
582 }
583
584 #[test]
586 fn test_result_rule_id_matches() {
587 let mut v = default_validator();
588 let spec = valid_spec();
589 let result = v.validate(&spec);
590 assert_eq!(result.rule_id, spec.rule_id);
591 }
592}