Skip to main content

tensorlogic_oxirs_bridge/
execute_validate.rs

1//! Compile-execute-validate pipeline tying together TensorLogic rules and RDF.
2//!
3//! [`ValidationExecutor`] compiles a [`TLExpr`] to an [`tensorlogic_ir::EinsumGraph`] using
4//! [`tensorlogic_compiler`], executes it with [`Scirs2Exec`] via the
5//! [`TlAutodiff`] trait, generates a SHACL-style [`ValidationReport`], and can
6//! export the execution result as RDF Turtle.
7//!
8//! # Pipeline overview
9//!
10//! ```text
11//! TLExpr
12//!   │  compile_to_einsum()
13//!   ▼
14//! EinsumGraph
15//!   │  Scirs2Exec::forward()
16//!   ▼
17//! Scirs2Tensor  ──►  ExecutionResult
18//!                        │  generate_validation_report()
19//!                        ▼
20//!                   ValidationReport
21//!                        │  export_as_rdf()
22//!                        ▼
23//!                   Turtle string
24//! ```
25
26use 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// ─────────────────────────────────────────────────────────────────────────────
39// Configuration
40// ─────────────────────────────────────────────────────────────────────────────
41
42/// Configuration for [`ValidationExecutor`].
43///
44/// All fields have sensible defaults available via `Default::default()` /
45/// `ValidationExecutorConfig::default()`.
46#[derive(Debug, Clone)]
47pub struct ValidationExecutorConfig {
48    /// Maximum number of elements allowed per output tensor.
49    ///
50    /// If the forward pass produces a tensor with more elements than this limit,
51    /// [`ValidationExecutorError::TensorTooLarge`] is returned.  Default: 65536.
52    pub max_tensor_size: usize,
53
54    /// Decimal places used when formatting float values in the RDF Turtle export.
55    /// Default: 6.
56    pub float_precision: usize,
57
58    /// Base IRI for generated RDF triples.  Default: `"https://tensorlogic.local/"`.
59    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// ─────────────────────────────────────────────────────────────────────────────
73// Timing / statistics
74// ─────────────────────────────────────────────────────────────────────────────
75
76/// Timing and size statistics captured during [`ValidationExecutor::execute_rule`].
77#[derive(Debug, Clone)]
78pub struct ExecutionStats {
79    /// Wall-clock microseconds spent in the compilation phase.
80    pub compile_time_us: u64,
81    /// Wall-clock microseconds spent in the executor forward pass.
82    pub execute_time_us: u64,
83    /// Number of operation nodes in the compiled [`tensorlogic_ir::EinsumGraph`].
84    pub graph_node_count: usize,
85    /// Number of output tensors collected from the forward pass.
86    pub output_tensor_count: usize,
87    /// Total number of scalar elements across all output tensors.
88    pub total_elements: usize,
89}
90
91// ─────────────────────────────────────────────────────────────────────────────
92// Output tensor wrapper
93// ─────────────────────────────────────────────────────────────────────────────
94
95/// A named, flattened snapshot of one output tensor.
96#[derive(Debug, Clone)]
97pub struct ExecutionTensor {
98    /// Logical name for this tensor (e.g. `"output"`).
99    pub name: String,
100    /// Shape of the tensor (product equals `values.len()`).
101    pub shape: Vec<usize>,
102    /// Flattened element values in row-major order.
103    pub values: Vec<f64>,
104}
105
106impl ExecutionTensor {
107    /// Returns `true` if any element is `NaN`.
108    pub fn has_nan(&self) -> bool {
109        self.values.iter().any(|v| v.is_nan())
110    }
111
112    /// Returns `true` if any element is positive or negative infinity.
113    pub fn has_inf(&self) -> bool {
114        self.values.iter().any(|v| v.is_infinite())
115    }
116
117    /// Returns `true` when every element is a finite number (not NaN, not Inf).
118    pub fn all_finite(&self) -> bool {
119        self.values.iter().all(|v| v.is_finite())
120    }
121
122    /// Minimum element value, or `None` if the tensor is empty.
123    pub fn min_value(&self) -> Option<f64> {
124        self.values.iter().copied().reduce(f64::min)
125    }
126
127    /// Maximum element value, or `None` if the tensor is empty.
128    pub fn max_value(&self) -> Option<f64> {
129        self.values.iter().copied().reduce(f64::max)
130    }
131
132    /// Count of elements that are not finite (NaN or Inf).
133    pub fn non_finite_count(&self) -> usize {
134        self.values.iter().filter(|v| !v.is_finite()).count()
135    }
136}
137
138// ─────────────────────────────────────────────────────────────────────────────
139// Execution result
140// ─────────────────────────────────────────────────────────────────────────────
141
142/// The result of a successful [`ValidationExecutor::execute_rule`] call.
143#[derive(Debug, Clone)]
144pub struct ExecutionResult {
145    /// `Debug` representation of the input [`TLExpr`].
146    pub expression_repr: String,
147    /// Number of operation nodes in the compiled graph.
148    pub graph_node_count: usize,
149    /// Output tensors produced by the forward pass.
150    pub output_tensors: Vec<ExecutionTensor>,
151    /// Timing and size statistics for this execution.
152    pub stats: ExecutionStats,
153}
154
155// ─────────────────────────────────────────────────────────────────────────────
156// Error type
157// ─────────────────────────────────────────────────────────────────────────────
158
159/// Errors that can occur during the compile-execute-validate pipeline.
160#[derive(Debug)]
161pub enum ValidationExecutorError {
162    /// The [`tensorlogic_compiler`] rejected the expression.
163    ///
164    /// The inner [`anyhow::Error`] carries the full diagnostic chain produced
165    /// by the compiler (type errors, unsupported constructs, etc.).
166    Compile(anyhow::Error),
167    /// The [`Scirs2Exec`] forward pass failed.
168    Execute(ExecutorError),
169    /// An output tensor exceeded the configured `max_tensor_size` limit.
170    TensorTooLarge {
171        /// Name of the offending tensor.
172        name: String,
173        /// Actual number of elements.
174        size: usize,
175        /// Configured maximum.
176        max: usize,
177    },
178    /// The compiled [`tensorlogic_ir::EinsumGraph`] contains no tensors or nodes.
179    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            // anyhow::Error does not implement std::error::Error directly
200            // so we cannot return it as a source; the Display chain already
201            // includes the full context.
202            Self::Compile(_) => None,
203            Self::Execute(e) => Some(e),
204            Self::TensorTooLarge { .. } | Self::EmptyGraph => None,
205        }
206    }
207}
208
209// ─────────────────────────────────────────────────────────────────────────────
210// Executor
211// ─────────────────────────────────────────────────────────────────────────────
212
213/// Compile-execute-validate pipeline for TensorLogic expressions.
214///
215/// # Example
216///
217/// ```rust
218/// use tensorlogic_ir::{TLExpr, Term};
219/// use tensorlogic_oxirs_bridge::{ValidationExecutor, ValidationExecutorConfig};
220///
221/// let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
222/// let executor = ValidationExecutor::new(ValidationExecutorConfig::default());
223/// let result = executor.execute_rule(&expr).unwrap();
224/// let report = executor.generate_validation_report(&result);
225/// assert!(report.conforms);
226/// let rdf = executor.export_as_rdf(&result);
227/// assert!(rdf.contains("@prefix tl:"));
228/// ```
229pub struct ValidationExecutor {
230    config: ValidationExecutorConfig,
231}
232
233impl ValidationExecutor {
234    /// Create a new executor with the given configuration.
235    pub fn new(config: ValidationExecutorConfig) -> Self {
236        Self { config }
237    }
238
239    /// Return a reference to the active configuration.
240    pub fn config(&self) -> &ValidationExecutorConfig {
241        &self.config
242    }
243
244    // ── Core pipeline ──────────────────────────────────────────────────────
245
246    /// Compile `expr` to an [`tensorlogic_ir::EinsumGraph`], execute it with placeholder
247    /// input tensors via [`Scirs2Exec`], and return an [`ExecutionResult`].
248    ///
249    /// # Errors
250    ///
251    /// - [`ValidationExecutorError::Compile`] — compiler rejected the expression.
252    /// - [`ValidationExecutorError::EmptyGraph`] — compiled graph is trivially empty.
253    /// - [`ValidationExecutorError::Execute`] — forward pass failed.
254    /// - [`ValidationExecutorError::TensorTooLarge`] — output exceeds configured limit.
255    pub fn execute_rule(&self, expr: &TLExpr) -> Result<ExecutionResult, ValidationExecutorError> {
256        // ── Phase 1: compile ──────────────────────────────────────────────
257        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        // ── Phase 2: pre-populate placeholder tensors ─────────────────────
266        //
267        // The executor looks up named tensors in `self.tensors`.  Tensors
268        // whose names start with `const_` are handled automatically by the
269        // forward pass (it parses the numeric suffix).  All others get a
270        // deterministic scalar placeholder so that the graph can execute
271        // without real data.
272        let t_exec_start = Instant::now();
273        let mut exec = Scirs2Exec::new();
274
275        for (i, tensor_name) in graph.tensors.iter().enumerate() {
276            // Strip any axis-annotation suffix (e.g. "age[a]" → "age")
277            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                // Auto-handled by the forward pass — nothing to pre-load.
284                continue;
285            }
286
287            // Deterministic non-zero placeholder: 0.1 + 0.1*(i % 9).
288            // Use a 1-element 1-D tensor (shape [1]) rather than a 0-D scalar
289            // so that einsum specs with at least one index can address axis 0
290            // without a shape-mismatch error.
291            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        // ── Phase 3: forward pass ─────────────────────────────────────────
297        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        // ── Phase 4: collect output ───────────────────────────────────────
303        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    // ── Validation report ──────────────────────────────────────────────────
338
339    /// Generate a SHACL-style [`ValidationReport`] from an [`ExecutionResult`].
340    ///
341    /// The report conforms (`report.conforms == true`) when every output tensor
342    /// contains only finite values.  A `sh:Violation`-severity
343    /// [`ValidationResult`] is added for each tensor that contains NaN or Inf
344    /// elements, carrying the tensor name and non-finite count in the message.
345    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    // ── RDF Turtle export ──────────────────────────────────────────────────
386
387    /// Serialise an [`ExecutionResult`] as an RDF Turtle string.
388    ///
389    /// The generated document uses a `tl:` prefix for the configured
390    /// `base_iri` and declares standard XSD types.  Each execution is
391    /// identified by a stable IRI derived from the `expression_repr` hash so
392    /// that repeated calls for the same expression produce consistent IRIs.
393    ///
394    /// Each output tensor is appended as a blank-node `tl:OutputTensor` with
395    /// shape, element count, allFinite, min, and max.
396    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        // Stable hash of the expression representation.
401        let exec_hash = {
402            let mut h = DefaultHasher::new();
403            result.expression_repr.hash(&mut h);
404            h.finish()
405        };
406
407        // Escape the expression repr for use in a Turtle string literal.
408        let escaped_repr = escape_turtle_literal(&result.expression_repr);
409
410        // Whether all outputs are fully finite.
411        let all_conforms = result.output_tensors.iter().all(|t| t.all_finite());
412
413        let mut out = String::with_capacity(512);
414
415        // Prefix declarations
416        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        // Main execution result node
421        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            // Link to blank-node tensor descriptions
450            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
467// ─────────────────────────────────────────────────────────────────────────────
468// Internal helpers
469// ─────────────────────────────────────────────────────────────────────────────
470
471/// Escape a string for safe embedding in a Turtle string literal.
472fn 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
487/// Format an [`ExecutionTensor`] as an inline Turtle blank node.
488fn 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// ─────────────────────────────────────────────────────────────────────────────
526// Tests
527// ─────────────────────────────────────────────────────────────────────────────
528
529#[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        // A simple predicate may compile to a graph with zero computational nodes
547        // (pass-through with no connectives).  We only verify it ran successfully.
548        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        // Placeholder inputs are finite, so the result should conform.
561        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        // Use a conjunctive expression to guarantee at least one computational node.
593        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        // An AND expression always compiles to at least one node.
601        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        // Either succeeds with an empty tensor or fails with TensorTooLarge.
616        // Neither path should panic.
617        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}