1use std::collections::hash_map::DefaultHasher;
27use std::fmt;
28use std::hash::{Hash, Hasher};
29use std::time::Instant;
30
31use tensorlogic_compiler::compile_to_einsum;
32use tensorlogic_infer::{ExecutorError, TlAutodiff};
33use tensorlogic_ir::TLExpr;
34use tensorlogic_scirs_backend::Scirs2Exec;
35
36use crate::shacl::validation::{ValidationReport, ValidationResult, ValidationSeverity};
37
38#[derive(Debug, Clone)]
47pub struct ValidationExecutorConfig {
48 pub max_tensor_size: usize,
53
54 pub float_precision: usize,
57
58 pub base_iri: String,
60}
61
62impl Default for ValidationExecutorConfig {
63 fn default() -> Self {
64 Self {
65 max_tensor_size: 65536,
66 float_precision: 6,
67 base_iri: "https://tensorlogic.local/".into(),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
78pub struct ExecutionStats {
79 pub compile_time_us: u64,
81 pub execute_time_us: u64,
83 pub graph_node_count: usize,
85 pub output_tensor_count: usize,
87 pub total_elements: usize,
89}
90
91#[derive(Debug, Clone)]
97pub struct ExecutionTensor {
98 pub name: String,
100 pub shape: Vec<usize>,
102 pub values: Vec<f64>,
104}
105
106impl ExecutionTensor {
107 pub fn has_nan(&self) -> bool {
109 self.values.iter().any(|v| v.is_nan())
110 }
111
112 pub fn has_inf(&self) -> bool {
114 self.values.iter().any(|v| v.is_infinite())
115 }
116
117 pub fn all_finite(&self) -> bool {
119 self.values.iter().all(|v| v.is_finite())
120 }
121
122 pub fn min_value(&self) -> Option<f64> {
124 self.values.iter().copied().reduce(f64::min)
125 }
126
127 pub fn max_value(&self) -> Option<f64> {
129 self.values.iter().copied().reduce(f64::max)
130 }
131
132 pub fn non_finite_count(&self) -> usize {
134 self.values.iter().filter(|v| !v.is_finite()).count()
135 }
136}
137
138#[derive(Debug, Clone)]
144pub struct ExecutionResult {
145 pub expression_repr: String,
147 pub graph_node_count: usize,
149 pub output_tensors: Vec<ExecutionTensor>,
151 pub stats: ExecutionStats,
153}
154
155#[derive(Debug)]
161pub enum ValidationExecutorError {
162 Compile(anyhow::Error),
167 Execute(ExecutorError),
169 TensorTooLarge {
171 name: String,
173 size: usize,
175 max: usize,
177 },
178 EmptyGraph,
180}
181
182impl fmt::Display for ValidationExecutorError {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 match self {
185 Self::Compile(e) => write!(f, "compilation error: {e}"),
186 Self::Execute(e) => write!(f, "executor error: {e}"),
187 Self::TensorTooLarge { name, size, max } => write!(
188 f,
189 "output tensor '{name}' has {size} elements which exceeds the limit of {max}"
190 ),
191 Self::EmptyGraph => write!(f, "compiled graph is empty (no tensors or nodes)"),
192 }
193 }
194}
195
196impl std::error::Error for ValidationExecutorError {
197 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
198 match self {
199 Self::Compile(_) => None,
203 Self::Execute(e) => Some(e),
204 Self::TensorTooLarge { .. } | Self::EmptyGraph => None,
205 }
206 }
207}
208
209pub struct ValidationExecutor {
230 config: ValidationExecutorConfig,
231}
232
233impl ValidationExecutor {
234 pub fn new(config: ValidationExecutorConfig) -> Self {
236 Self { config }
237 }
238
239 pub fn config(&self) -> &ValidationExecutorConfig {
241 &self.config
242 }
243
244 pub fn execute_rule(&self, expr: &TLExpr) -> Result<ExecutionResult, ValidationExecutorError> {
256 let t_compile_start = Instant::now();
258 let graph = compile_to_einsum(expr).map_err(ValidationExecutorError::Compile)?;
259 let compile_time_us = t_compile_start.elapsed().as_micros() as u64;
260
261 if graph.is_empty() {
262 return Err(ValidationExecutorError::EmptyGraph);
263 }
264
265 let t_exec_start = Instant::now();
273 let mut exec = Scirs2Exec::new();
274
275 for (i, tensor_name) in graph.tensors.iter().enumerate() {
276 let base_name = tensor_name
278 .split('[')
279 .next()
280 .unwrap_or(tensor_name.as_str());
281
282 if base_name.starts_with("const_") || tensor_name.starts_with("const_") {
283 continue;
285 }
286
287 let val = 0.1 + 0.1 * (i % 9) as f64;
292 let placeholder = scirs2_core::ndarray::Array1::from_vec(vec![val]).into_dyn();
293 exec.add_tensor(tensor_name.clone(), placeholder);
294 }
295
296 let result_tensor = exec
298 .forward(&graph)
299 .map_err(ValidationExecutorError::Execute)?;
300 let execute_time_us = t_exec_start.elapsed().as_micros() as u64;
301
302 let shape: Vec<usize> = result_tensor.shape().to_vec();
304 let values: Vec<f64> = result_tensor.iter().copied().collect();
305 let total_elements = values.len();
306
307 if total_elements > self.config.max_tensor_size {
308 return Err(ValidationExecutorError::TensorTooLarge {
309 name: "output".to_string(),
310 size: total_elements,
311 max: self.config.max_tensor_size,
312 });
313 }
314
315 let output_tensor = ExecutionTensor {
316 name: "output".to_string(),
317 shape,
318 values,
319 };
320
321 let graph_node_count = graph.nodes.len();
322
323 Ok(ExecutionResult {
324 expression_repr: format!("{expr:?}"),
325 graph_node_count,
326 output_tensors: vec![output_tensor],
327 stats: ExecutionStats {
328 compile_time_us,
329 execute_time_us,
330 graph_node_count,
331 output_tensor_count: 1,
332 total_elements,
333 },
334 })
335 }
336
337 pub fn generate_validation_report(&self, result: &ExecutionResult) -> ValidationReport {
346 let mut report = ValidationReport::new();
347
348 for tensor in &result.output_tensors {
349 if !tensor.all_finite() {
350 let non_finite = tensor.non_finite_count();
351 let has_nan = tensor.has_nan();
352 let has_inf = tensor.has_inf();
353
354 let kind_desc = match (has_nan, has_inf) {
355 (true, true) => "NaN and Inf values",
356 (true, false) => "NaN values",
357 (false, true) => "Inf values",
358 (false, false) => "non-finite values",
359 };
360
361 let message = format!(
362 "Output tensor '{}' contains {} {} (shape: {:?})",
363 tensor.name, non_finite, kind_desc, tensor.shape,
364 );
365
366 let focus_node = format!("{}tensor/{}", self.config.base_iri, tensor.name);
367 let source_shape = format!("{}shape/FiniteValueConstraint", self.config.base_iri);
368 let constraint_component = format!(
369 "{}constraint/FiniteValueConstraintComponent",
370 self.config.base_iri
371 );
372
373 let vr =
374 ValidationResult::new(focus_node, source_shape, constraint_component, message)
375 .with_severity(ValidationSeverity::Violation)
376 .with_value(format!("{non_finite} non-finite elements"));
377
378 report.add_result(vr);
379 }
380 }
381
382 report
383 }
384
385 pub fn export_as_rdf(&self, result: &ExecutionResult) -> String {
397 let base_iri = &self.config.base_iri;
398 let prec = self.config.float_precision;
399
400 let exec_hash = {
402 let mut h = DefaultHasher::new();
403 result.expression_repr.hash(&mut h);
404 h.finish()
405 };
406
407 let escaped_repr = escape_turtle_literal(&result.expression_repr);
409
410 let all_conforms = result.output_tensors.iter().all(|t| t.all_finite());
412
413 let mut out = String::with_capacity(512);
414
415 out.push_str(&format!("@prefix tl: <{base_iri}> .\n"));
417 out.push_str("@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n");
418 out.push('\n');
419
420 out.push_str(&format!(
422 "tl:exec_{exec_hash:016x} a tl:ExecutionResult ;\n"
423 ));
424 out.push_str(&format!(" tl:expressionRepr \"{escaped_repr}\" ;\n"));
425 out.push_str(&format!(
426 " tl:graphNodeCount {graph_node_count}^^xsd:integer ;\n",
427 graph_node_count = result.graph_node_count
428 ));
429 out.push_str(&format!(
430 " tl:compileTimeUs {compile_us}^^xsd:integer ;\n",
431 compile_us = result.stats.compile_time_us
432 ));
433 out.push_str(&format!(
434 " tl:executeTimeUs {execute_us}^^xsd:integer ;\n",
435 execute_us = result.stats.execute_time_us
436 ));
437 out.push_str(&format!(
438 " tl:totalElements {total}^^xsd:integer ;\n",
439 total = result.stats.total_elements
440 ));
441 out.push_str(&format!(
442 " tl:conforms {conforms}^^xsd:boolean",
443 conforms = all_conforms
444 ));
445
446 if result.output_tensors.is_empty() {
447 out.push_str(" .\n");
448 } else {
449 out.push_str(" ;\n");
451 let tensor_count = result.output_tensors.len();
452 for (idx, tensor) in result.output_tensors.iter().enumerate() {
453 let is_last = idx == tensor_count - 1;
454 let tensor_node = format_tensor_blank_node(tensor, prec);
455 if is_last {
456 out.push_str(&format!(" tl:outputTensor {tensor_node} .\n"));
457 } else {
458 out.push_str(&format!(" tl:outputTensor {tensor_node} ;\n"));
459 }
460 }
461 }
462
463 out
464 }
465}
466
467fn escape_turtle_literal(s: &str) -> String {
473 let mut out = String::with_capacity(s.len());
474 for ch in s.chars() {
475 match ch {
476 '\\' => out.push_str("\\\\"),
477 '"' => out.push_str("\\\""),
478 '\n' => out.push_str("\\n"),
479 '\r' => out.push_str("\\r"),
480 '\t' => out.push_str("\\t"),
481 other => out.push(other),
482 }
483 }
484 out
485}
486
487fn format_tensor_blank_node(tensor: &ExecutionTensor, prec: usize) -> String {
489 let shape_str: Vec<String> = tensor.shape.iter().map(|d| d.to_string()).collect();
490 let shape_literal = shape_str.join(",");
491 let all_finite = tensor.all_finite();
492
493 let min_str = tensor
494 .min_value()
495 .map(|v| format!("{v:.prec$}"))
496 .unwrap_or_else(|| "null".to_string());
497 let max_str = tensor
498 .max_value()
499 .map(|v| format!("{v:.prec$}"))
500 .unwrap_or_else(|| "null".to_string());
501
502 let mut node = String::new();
503 node.push_str("[\n");
504 node.push_str(" a tl:OutputTensor ;\n");
505 node.push_str(&format!(
506 " tl:tensorName \"{name}\" ;\n",
507 name = escape_turtle_literal(&tensor.name)
508 ));
509 node.push_str(&format!(" tl:shape \"{shape_literal}\" ;\n",));
510 node.push_str(&format!(
511 " tl:elementCount {count}^^xsd:integer ;\n",
512 count = tensor.values.len()
513 ));
514 node.push_str(&format!(
515 " tl:allFinite {all_finite}^^xsd:boolean ;\n",
516 ));
517 node.push_str(&format!(
518 " tl:minValue \"{min_str}\"^^xsd:decimal ;\n",
519 ));
520 node.push_str(&format!(" tl:maxValue \"{max_str}\"^^xsd:decimal\n",));
521 node.push_str(" ]");
522 node
523}
524
525#[cfg(test)]
530mod tests {
531 use tensorlogic_ir::{TLExpr, Term};
532
533 use super::*;
534
535 fn default_executor() -> ValidationExecutor {
536 ValidationExecutor::new(ValidationExecutorConfig::default())
537 }
538
539 #[test]
540 fn test_simple_predicate_compiles_and_runs() {
541 let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
542 let executor = default_executor();
543 let result = executor
544 .execute_rule(&expr)
545 .expect("execute simple predicate");
546 assert!(
549 !result.output_tensors.is_empty(),
550 "expected at least one output tensor"
551 );
552 }
553
554 #[test]
555 fn test_finite_output_conforms() {
556 let expr = TLExpr::pred("p", vec![Term::var("x")]);
557 let executor = default_executor();
558 let result = executor.execute_rule(&expr).expect("execute predicate");
559 let report = executor.generate_validation_report(&result);
560 assert!(report.conforms, "finite outputs should conform");
562 }
563
564 #[test]
565 fn test_export_rdf_contains_required_prefixes() {
566 let expr = TLExpr::pred("q", vec![Term::var("a")]);
567 let executor = default_executor();
568 let result = executor.execute_rule(&expr).expect("execute");
569 let rdf = executor.export_as_rdf(&result);
570 assert!(rdf.contains("@prefix tl:"), "missing tl: prefix");
571 assert!(rdf.contains("@prefix xsd:"), "missing xsd: prefix");
572 assert!(
573 rdf.contains("tl:ExecutionResult"),
574 "missing ExecutionResult type"
575 );
576 }
577
578 #[test]
579 fn test_export_rdf_conforms_field() {
580 let expr = TLExpr::pred("r", vec![Term::var("x")]);
581 let executor = default_executor();
582 let result = executor.execute_rule(&expr).expect("execute");
583 let rdf = executor.export_as_rdf(&result);
584 assert!(
585 rdf.contains("tl:conforms true") || rdf.contains("tl:conforms false"),
586 "missing conforms field in RDF: {rdf}"
587 );
588 }
589
590 #[test]
591 fn test_execution_stats_recorded() {
592 let p = TLExpr::pred("s", vec![Term::var("x")]);
594 let q = TLExpr::pred("t", vec![Term::var("x")]);
595 let expr = TLExpr::and(p, q);
596 let executor = default_executor();
597 let result = executor
598 .execute_rule(&expr)
599 .expect("execute AND expression");
600 assert!(
602 result.stats.graph_node_count > 0,
603 "expected at least one graph node"
604 );
605 }
606
607 #[test]
608 fn test_max_tensor_size_zero_returns_error_or_empty() {
609 let config = ValidationExecutorConfig {
610 max_tensor_size: 0,
611 ..Default::default()
612 };
613 let expr = TLExpr::pred("t", vec![Term::var("x")]);
614 let executor = ValidationExecutor::new(config);
615 let _ = executor.execute_rule(&expr);
618 }
619
620 #[test]
621 fn test_execution_tensor_helpers_all_finite() {
622 let t = ExecutionTensor {
623 name: "test".to_string(),
624 shape: vec![3],
625 values: vec![1.0, 2.0, 3.0],
626 };
627 assert!(t.all_finite());
628 assert!(!t.has_nan());
629 assert!(!t.has_inf());
630 assert_eq!(t.min_value(), Some(1.0));
631 assert_eq!(t.max_value(), Some(3.0));
632 assert_eq!(t.non_finite_count(), 0);
633 }
634
635 #[test]
636 fn test_execution_tensor_helpers_with_nan() {
637 let t = ExecutionTensor {
638 name: "bad".to_string(),
639 shape: vec![2],
640 values: vec![f64::NAN, 1.0],
641 };
642 assert!(!t.all_finite());
643 assert!(t.has_nan());
644 assert_eq!(t.non_finite_count(), 1);
645 }
646
647 #[test]
648 fn test_error_display_empty_graph() {
649 let e = ValidationExecutorError::EmptyGraph;
650 assert!(e.to_string().contains("empty"), "unexpected: {e}");
651 }
652
653 #[test]
654 fn test_error_display_tensor_too_large() {
655 let e = ValidationExecutorError::TensorTooLarge {
656 name: "out".into(),
657 size: 100,
658 max: 50,
659 };
660 let s = e.to_string();
661 assert!(s.contains("out"), "unexpected: {s}");
662 assert!(s.contains("100"), "unexpected: {s}");
663 assert!(s.contains("50"), "unexpected: {s}");
664 }
665
666 #[test]
667 fn test_escape_turtle_literal_special_chars() {
668 let raw = "Hello\nworld\\foo\"bar";
669 let escaped = escape_turtle_literal(raw);
670 assert!(escaped.contains("\\n"), "newline not escaped");
671 assert!(escaped.contains("\\\\"), "backslash not escaped");
672 assert!(escaped.contains("\\\""), "quote not escaped");
673 }
674
675 #[test]
676 fn test_validation_report_for_infinite_tensor() {
677 let executor = default_executor();
678 let result = ExecutionResult {
679 expression_repr: "test".to_string(),
680 graph_node_count: 1,
681 output_tensors: vec![ExecutionTensor {
682 name: "output".to_string(),
683 shape: vec![1],
684 values: vec![f64::INFINITY],
685 }],
686 stats: ExecutionStats {
687 compile_time_us: 0,
688 execute_time_us: 0,
689 graph_node_count: 1,
690 output_tensor_count: 1,
691 total_elements: 1,
692 },
693 };
694 let report = executor.generate_validation_report(&result);
695 assert!(!report.conforms, "Inf tensor should not conform");
696 assert!(
697 !report.results.is_empty(),
698 "expected at least one violation"
699 );
700 }
701}