1use crate::frozen::FrozenIndexedDataset;
13use crate::path::{PathBackend, node_of, succ};
14use crate::sparql::SparqlExecutor;
15use crate::validate::{NonStratifiable, ShapeEvaluator, focus_nodes_with, uses_shapes_graph};
16use oxrdf::{Graph, NamedNode, Term, Triple};
17use serde::{Deserialize, Serialize};
18use shifty_algebra::{Path, Schema, Shape, ShapeArena, ShapeId};
19use shifty_opt::analyze;
20use std::collections::{BTreeSet, HashSet, VecDeque};
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct FocusWitness {
26 pub focus: Term,
27 pub statement: usize,
29 pub failure: Witness,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum RelKind {
35 Eq,
36 Disj,
37 Lt,
38 Le,
39 UniqueLang,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub enum PathSupport {
46 Empty,
48 Edge(Triple),
50 Chain(Vec<PathSupport>),
52 Alt(Vec<PathSupport>),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub enum Witness {
59 Atom {
63 shape: ShapeId,
64 node: Term,
65 reached_by: Path,
66 produced_by: Option<PathSupport>,
67 },
68 Relational {
72 shape: ShapeId,
73 node: Term,
74 kind: RelKind,
75 lhs: Vec<(Term, PathSupport)>,
76 rhs: Vec<(Term, PathSupport)>,
77 offending: Vec<(Term, Term)>,
78 },
79 Closed {
81 shape: ShapeId,
82 node: Term,
83 offenders: Vec<(NamedNode, Term)>,
84 },
85 Not {
87 shape: ShapeId,
88 node: Term,
89 inner: Box<SatTrace>,
90 },
91 All {
93 shape: ShapeId,
94 node: Term,
95 failed: Vec<Witness>,
96 },
97 Any {
99 shape: ShapeId,
100 node: Term,
101 branches: Vec<Witness>,
102 },
103 CountLow {
112 shape: ShapeId,
113 node: Term,
114 path: Path,
115 qualifier: ShapeId,
116 have: u64,
117 min: u64,
118 sibling_qualifiers: Vec<ShapeId>,
119 },
120 CountHigh {
124 shape: ShapeId,
125 node: Term,
126 path: Path,
127 qualifier: ShapeId,
128 matched: Vec<(Term, PathSupport)>,
129 max: u64,
130 per_value: Vec<(Term, Witness)>,
131 },
132 Opaque { shape: ShapeId, node: Term },
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub enum SatTrace {
140 Irrefutable { shape: ShapeId },
142 Atom {
144 shape: ShapeId,
145 node: Term,
146 reached_by: Path,
147 produced_by: PathSupport,
148 },
149 AllHeld {
151 shape: ShapeId,
152 node: Term,
153 children: Vec<SatTrace>,
154 },
155 AnyHeld {
157 shape: ShapeId,
158 node: Term,
159 satisfied: Vec<SatTrace>,
160 },
161 CountHeld {
164 shape: ShapeId,
165 node: Term,
166 path: Path,
167 qualifier: ShapeId,
168 matches: Vec<(Term, SatTrace)>,
169 min: Option<u64>,
170 max: Option<u64>,
171 },
172 NotHeld {
174 shape: ShapeId,
175 node: Term,
176 inner_fails: Box<Witness>,
177 },
178 Blocked {
181 shape: ShapeId,
182 node: Term,
183 reason: BlockReason,
184 },
185 Coinductive { shape: ShapeId, node: Term },
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub enum BlockReason {
193 OpaqueSparql,
194 ClosedNeedsAdd,
196 Unsupported,
198}
199
200type Stack = HashSet<(ShapeId, Term)>;
201
202fn prepare(context: &Graph, schema: &Schema) -> Result<SparqlExecutor, NonStratifiable> {
209 let strat = analyze(&schema.arena);
210 if !strat.stratifiable {
211 let components = strat
212 .strata
213 .iter()
214 .filter(|s| !s.stratifiable)
215 .map(|s| s.shapes.clone())
216 .collect();
217 return Err(NonStratifiable { components });
218 }
219
220 let uses_shapes = uses_shapes_graph(&schema.arena);
221 let frozen = if uses_shapes {
222 FrozenIndexedDataset::from_graphs(context, context)
223 } else {
224 FrozenIndexedDataset::from_graph(context)
225 };
226 Ok(SparqlExecutor::from_frozen(frozen, uses_shapes))
227}
228
229pub fn witness_violations(
235 data: &Graph,
236 context: &Graph,
237 schema: &Schema,
238) -> Result<Vec<FocusWitness>, NonStratifiable> {
239 let sparql = prepare(context, schema)?;
240 let backend = sparql
241 .frozen()
242 .expect("witness executor always has a frozen dataset");
243 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &sparql);
244
245 let mut out = Vec::new();
246 for (i, st) in schema.statements.iter().enumerate() {
247 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
248 let mut stack = Stack::new();
249 if let Some(failure) = witness(
250 &mut evaluator,
251 &v,
252 st.shape,
253 &Path::Id,
254 None,
255 &[],
256 &mut stack,
257 ) {
258 out.push(FocusWitness {
259 focus: v,
260 statement: i,
261 failure,
262 });
263 }
264 }
265 }
266 Ok(out)
267}
268
269pub fn shape_id_for_iri(schema: &Schema, iri: &str) -> Option<ShapeId> {
272 schema
273 .names
274 .iter()
275 .find_map(|(id, name)| (name == iri).then_some(*id))
276}
277
278pub fn witness_shape(
284 data: &Graph,
285 context: &Graph,
286 schema: &Schema,
287 shape: ShapeId,
288) -> Result<Vec<FocusWitness>, NonStratifiable> {
289 let sparql = prepare(context, schema)?;
290 let backend = sparql
291 .frozen()
292 .expect("witness executor always has a frozen dataset");
293 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &sparql);
294
295 let mut out = Vec::new();
296 for (i, st) in schema.statements.iter().enumerate() {
297 if st.shape != shape {
298 continue;
299 }
300 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
301 let mut stack = Stack::new();
302 if let Some(failure) = witness(
303 &mut evaluator,
304 &v,
305 st.shape,
306 &Path::Id,
307 None,
308 &[],
309 &mut stack,
310 ) {
311 out.push(FocusWitness {
312 focus: v,
313 statement: i,
314 failure,
315 });
316 }
317 }
318 }
319 Ok(out)
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct FocusSat {
327 pub focus: Term,
328 pub statement: usize,
330 pub trace: SatTrace,
331}
332
333pub fn satisfy_shape(
338 data: &Graph,
339 context: &Graph,
340 schema: &Schema,
341 shape: ShapeId,
342) -> Result<Vec<FocusSat>, NonStratifiable> {
343 let sparql = prepare(context, schema)?;
344 let backend = sparql
345 .frozen()
346 .expect("witness executor always has a frozen dataset");
347 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &sparql);
348
349 let mut out = Vec::new();
350 for (i, st) in schema.statements.iter().enumerate() {
351 if st.shape != shape {
352 continue;
353 }
354 for v in focus_nodes_with(data, backend, &st.selector, &schema.arena, &sparql) {
355 let mut stack = Stack::new();
356 if let Some(trace) =
357 sat_trace(&mut evaluator, &v, st.shape, &Path::Id, None, &mut stack)
358 {
359 out.push(FocusSat {
360 focus: v,
361 statement: i,
362 trace,
363 });
364 }
365 }
366 }
367 Ok(out)
368}
369
370pub fn witness_node(
381 context: &Graph,
382 schema: &Schema,
383 node: &Term,
384 shape: ShapeId,
385) -> Result<Option<FocusWitness>, NonStratifiable> {
386 let sparql = prepare(context, schema)?;
387 let backend = sparql
388 .frozen()
389 .expect("witness executor always has a frozen dataset");
390 let mut evaluator = ShapeEvaluator::new(backend, &schema.arena, &sparql);
391
392 let mut stack = Stack::new();
393 Ok(witness(
394 &mut evaluator,
395 node,
396 shape,
397 &Path::Id,
398 None,
399 &[],
400 &mut stack,
401 )
402 .map(|failure| FocusWitness {
403 focus: node.clone(),
404 statement: usize::MAX,
405 failure,
406 }))
407}
408
409fn witness(
420 eval: &mut ShapeEvaluator<'_>,
421 node: &Term,
422 id: ShapeId,
423 reached_by: &Path,
424 produced_by: Option<&PathSupport>,
425 scope: &[(Path, ShapeId)],
426 stack: &mut Stack,
427) -> Option<Witness> {
428 let key = (id, node.clone());
429 if !stack.insert(key.clone()) {
430 return None; }
432 if eval.holds(node, id) {
433 stack.remove(&key);
434 return None; }
436 let shape = eval.arena().get(id).clone();
437 let result = match shape {
438 Shape::Top | Shape::Pending => None,
439 Shape::Annotated { shape, .. } => {
441 let inner = witness(eval, node, shape, reached_by, produced_by, scope, stack);
442 stack.remove(&key);
443 return inner;
444 }
445 Shape::TestConst(_) | Shape::TestType(_) | Shape::TestKind(_) => Some(Witness::Atom {
446 shape: id,
447 node: node.clone(),
448 reached_by: reached_by.clone(),
449 produced_by: produced_by.cloned(),
450 }),
451 Shape::Eq(..) | Shape::Disj(..) | Shape::Lt(..) | Shape::Le(..) | Shape::UniqueLang(_) => {
452 Some(relational_witness(eval, node, id, &shape))
453 }
454 Shape::Closed(ref q) => {
455 let offenders = closed_offenders(eval.backend(), node, q);
456 (!offenders.is_empty()).then(|| Witness::Closed {
457 shape: id,
458 node: node.clone(),
459 offenders,
460 })
461 }
462 Shape::Not(c) => {
463 sat_trace(eval, node, c, reached_by, produced_by, stack).map(|t| Witness::Not {
466 shape: id,
467 node: node.clone(),
468 inner: Box::new(t),
469 })
470 }
471 Shape::And(cs) => {
472 let mut child_scope: Vec<(Path, ShapeId)> = scope.to_vec();
475 child_scope.extend(cs.iter().filter_map(|&c| as_universal(eval.arena(), c)));
476 let failed: Vec<Witness> = cs
477 .iter()
478 .filter_map(|c| {
479 witness(eval, node, *c, reached_by, produced_by, &child_scope, stack)
480 })
481 .collect();
482 (!failed.is_empty()).then(|| Witness::All {
483 shape: id,
484 node: node.clone(),
485 failed,
486 })
487 }
488 Shape::Or(cs) => {
489 let branches: Vec<Witness> = cs
493 .iter()
494 .filter_map(|c| witness(eval, node, *c, reached_by, produced_by, scope, stack))
495 .collect();
496 (!branches.is_empty()).then(|| Witness::Any {
497 shape: id,
498 node: node.clone(),
499 branches,
500 })
501 }
502 Shape::Count {
503 path,
504 min,
505 max,
506 qualifier,
507 } => count_witness(
508 eval, node, id, &path, min, max, qualifier, reached_by, scope, stack,
509 ),
510 Shape::Sparql(_) | Shape::Expression(_) => Some(Witness::Opaque {
511 shape: id,
512 node: node.clone(),
513 }),
514 };
515 stack.remove(&key);
516 result
517}
518
519fn peel_annotated(arena: &ShapeArena, mut id: ShapeId) -> ShapeId {
521 while let Shape::Annotated { shape, .. } = arena.get(id) {
522 id = *shape;
523 }
524 id
525}
526
527fn as_universal(arena: &ShapeArena, id: ShapeId) -> Option<(Path, ShapeId)> {
532 let Shape::Count {
533 path,
534 max: Some(0),
535 qualifier,
536 ..
537 } = arena.get(peel_annotated(arena, id)).clone()
538 else {
539 return None;
540 };
541 match arena.get(peel_annotated(arena, qualifier)) {
542 Shape::Not(inner) => Some((path, *inner)),
543 _ => None,
544 }
545}
546
547#[allow(clippy::too_many_arguments)]
548fn count_witness(
549 eval: &mut ShapeEvaluator<'_>,
550 node: &Term,
551 id: ShapeId,
552 path: &Path,
553 min: Option<u64>,
554 max: Option<u64>,
555 qualifier: ShapeId,
556 reached_by: &Path,
557 scope: &[(Path, ShapeId)],
558 stack: &mut Stack,
559) -> Option<Witness> {
560 let values: Vec<Term> = succ(eval.backend(), node, path).into_iter().collect();
561 let matched: Vec<Term> = values
562 .into_iter()
563 .filter(|u| eval.holds(u, qualifier))
564 .collect();
565 let n = matched.len() as u64;
566
567 if let Some(m) = min
568 && n < m
569 {
570 let sibling_qualifiers = scope
572 .iter()
573 .filter(|(p, _)| p == path)
574 .map(|(_, inner)| *inner)
575 .collect();
576 return Some(Witness::CountLow {
577 shape: id,
578 node: node.clone(),
579 path: path.clone(),
580 qualifier,
581 have: n,
582 min: m,
583 sibling_qualifiers,
584 });
585 }
586 if let Some(m) = max
587 && n > m
588 {
589 let matched_sup: Vec<(Term, PathSupport)> = matched
590 .iter()
591 .map(|u| {
592 (
593 u.clone(),
594 path_support(eval.backend(), node, path, u).unwrap_or(PathSupport::Empty),
595 )
596 })
597 .collect();
598 let per_value = if m == 0 {
600 if let Shape::Not(inner) = eval.arena().get(qualifier).clone() {
601 let reached = Path::seq(vec![reached_by.clone(), path.clone()]);
602 let mut pv = Vec::new();
603 for u in &matched {
604 let ps = path_support(eval.backend(), node, path, u);
605 if let Some(w) = witness(eval, u, inner, &reached, ps.as_ref(), &[], stack) {
608 pv.push((u.clone(), w));
609 }
610 }
611 pv
612 } else {
613 Vec::new()
614 }
615 } else {
616 Vec::new()
617 };
618 return Some(Witness::CountHigh {
619 shape: id,
620 node: node.clone(),
621 path: path.clone(),
622 qualifier,
623 matched: matched_sup,
624 max: m,
625 per_value,
626 });
627 }
628 None
629}
630
631fn relational_witness(
632 eval: &ShapeEvaluator<'_>,
633 node: &Term,
634 id: ShapeId,
635 shape: &Shape,
636) -> Witness {
637 let g = eval.backend();
638 let (kind, lpath, rpred) = match shape {
639 Shape::Eq(p, q) => (RelKind::Eq, p.clone(), Some(q.clone())),
640 Shape::Disj(p, q) => (RelKind::Disj, p.clone(), Some(q.clone())),
641 Shape::Lt(p, q) => (RelKind::Lt, p.clone(), Some(q.clone())),
642 Shape::Le(p, q) => (RelKind::Le, p.clone(), Some(q.clone())),
643 Shape::UniqueLang(p) => (RelKind::UniqueLang, p.clone(), None),
644 _ => unreachable!("relational_witness on non-relational shape"),
645 };
646 let with_support = |g: &dyn PathBackend, p: &Path| -> Vec<(Term, PathSupport)> {
647 succ(g, node, p)
648 .into_iter()
649 .map(|v| {
650 let s = path_support(g, node, p, &v).unwrap_or(PathSupport::Empty);
651 (v, s)
652 })
653 .collect()
654 };
655 let lhs = with_support(g, &lpath);
656 let rhs = match &rpred {
657 Some(q) => with_support(g, &Path::Pred(q.clone())),
658 None => Vec::new(),
659 };
660 let offending = offending_pairs(kind, &lhs, &rhs);
661 Witness::Relational {
662 shape: id,
663 node: node.clone(),
664 kind,
665 lhs,
666 rhs,
667 offending,
668 }
669}
670
671fn offending_pairs(
673 kind: RelKind,
674 lhs: &[(Term, PathSupport)],
675 rhs: &[(Term, PathSupport)],
676) -> Vec<(Term, Term)> {
677 let lvals: Vec<&Term> = lhs.iter().map(|(v, _)| v).collect();
678 let rvals: Vec<&Term> = rhs.iter().map(|(v, _)| v).collect();
679 match kind {
680 RelKind::Eq => lvals
681 .iter()
682 .filter(|v| !rvals.contains(v))
683 .chain(rvals.iter().filter(|v| !lvals.contains(v)))
684 .map(|v| ((*v).clone(), (*v).clone()))
685 .collect(),
686 RelKind::Disj => lvals
687 .iter()
688 .filter(|v| rvals.contains(v))
689 .map(|v| ((*v).clone(), (*v).clone()))
690 .collect(),
691 RelKind::Lt | RelKind::Le => {
692 let mut bad = Vec::new();
694 for a in &lvals {
695 for b in &rvals {
696 let ok = match crate::value::compare_terms(a, b) {
697 Some(std::cmp::Ordering::Less) => true,
698 Some(std::cmp::Ordering::Equal) => kind == RelKind::Le,
699 _ => false,
700 };
701 if !ok {
702 bad.push(((*a).clone(), (*b).clone()));
703 }
704 }
705 }
706 bad
707 }
708 RelKind::UniqueLang => {
709 let mut bad = Vec::new();
711 for i in 0..lvals.len() {
712 for j in (i + 1)..lvals.len() {
713 if let (Term::Literal(a), Term::Literal(b)) = (lvals[i], lvals[j])
714 && let (Some(la), Some(lb)) = (a.language(), b.language())
715 && la.eq_ignore_ascii_case(lb)
716 {
717 bad.push((lvals[i].clone(), lvals[j].clone()));
718 }
719 }
720 }
721 bad
722 }
723 }
724}
725
726fn sat_trace(
728 eval: &mut ShapeEvaluator<'_>,
729 node: &Term,
730 id: ShapeId,
731 reached_by: &Path,
732 produced_by: Option<&PathSupport>,
733 stack: &mut Stack,
734) -> Option<SatTrace> {
735 let key = (id, node.clone());
736 if !stack.insert(key.clone()) {
737 return Some(SatTrace::Coinductive {
739 shape: id,
740 node: node.clone(),
741 });
742 }
743 if !eval.holds(node, id) {
744 stack.remove(&key);
745 return None; }
747 let shape = eval.arena().get(id).clone();
748 let result = match shape {
749 Shape::Top | Shape::Pending => Some(SatTrace::Irrefutable { shape: id }),
750 Shape::Annotated { shape, .. } => {
752 let inner = sat_trace(eval, node, shape, reached_by, produced_by, stack);
753 stack.remove(&key);
754 return inner;
755 }
756 Shape::TestConst(_) | Shape::TestType(_) | Shape::TestKind(_) => Some(SatTrace::Atom {
757 shape: id,
758 node: node.clone(),
759 reached_by: reached_by.clone(),
760 produced_by: produced_by.cloned().unwrap_or(PathSupport::Empty),
761 }),
762 Shape::Eq(..) | Shape::Disj(..) | Shape::Lt(..) | Shape::Le(..) | Shape::UniqueLang(_) => {
763 Some(SatTrace::Blocked {
764 shape: id,
765 node: node.clone(),
766 reason: BlockReason::Unsupported,
767 })
768 }
769 Shape::Closed(_) => Some(SatTrace::Blocked {
770 shape: id,
771 node: node.clone(),
772 reason: BlockReason::ClosedNeedsAdd,
773 }),
774 Shape::Sparql(_) => Some(SatTrace::Blocked {
775 shape: id,
776 node: node.clone(),
777 reason: BlockReason::OpaqueSparql,
778 }),
779 Shape::Expression(_) => Some(SatTrace::Blocked {
782 shape: id,
783 node: node.clone(),
784 reason: BlockReason::Unsupported,
785 }),
786 Shape::Not(c) => {
787 witness(eval, node, c, reached_by, produced_by, &[], stack).map(|w| SatTrace::NotHeld {
788 shape: id,
789 node: node.clone(),
790 inner_fails: Box::new(w),
791 })
792 }
793 Shape::And(cs) => {
794 let children: Vec<SatTrace> = cs
796 .iter()
797 .filter_map(|c| sat_trace(eval, node, *c, reached_by, produced_by, stack))
798 .collect();
799 Some(SatTrace::AllHeld {
800 shape: id,
801 node: node.clone(),
802 children,
803 })
804 }
805 Shape::Or(cs) => {
806 let satisfied: Vec<SatTrace> = cs
807 .iter()
808 .filter_map(|c| sat_trace(eval, node, *c, reached_by, produced_by, stack))
809 .collect();
810 Some(SatTrace::AnyHeld {
811 shape: id,
812 node: node.clone(),
813 satisfied,
814 })
815 }
816 Shape::Count {
817 path,
818 min,
819 max,
820 qualifier,
821 } => {
822 let values: Vec<Term> = succ(eval.backend(), node, &path).into_iter().collect();
823 let reached = Path::seq(vec![reached_by.clone(), path.clone()]);
824 let mut matches = Vec::new();
825 for u in values {
826 if eval.holds(&u, qualifier) {
827 let ps = path_support(eval.backend(), node, &path, &u);
828 if let Some(t) = sat_trace(eval, &u, qualifier, &reached, ps.as_ref(), stack) {
829 matches.push((u, t));
830 }
831 }
832 }
833 Some(SatTrace::CountHeld {
834 shape: id,
835 node: node.clone(),
836 path: path.clone(),
837 qualifier,
838 matches,
839 min,
840 max,
841 })
842 }
843 };
844 stack.remove(&key);
845 result
846}
847
848fn closed_offenders(
850 g: &dyn PathBackend,
851 node: &Term,
852 q: &BTreeSet<NamedNode>,
853) -> Vec<(NamedNode, Term)> {
854 let allowed: HashSet<&NamedNode> = q.iter().collect();
855 let mut out = Vec::new();
856 for p in g.out_predicates(node) {
857 if !allowed.contains(&p) {
858 for o in g.objects(node, &p) {
859 out.push((p.clone(), o));
860 }
861 }
862 }
863 out
864}
865
866fn path_support(g: &dyn PathBackend, from: &Term, path: &Path, to: &Term) -> Option<PathSupport> {
869 match path {
870 Path::Id => (from == to).then_some(PathSupport::Empty),
871 Path::Pred(q) => {
872 if g.objects(from, q).contains(to) {
873 let s = node_of(from)?;
874 Some(PathSupport::Edge(Triple::new(s, q.clone(), to.clone())))
875 } else {
876 None
877 }
878 }
879 Path::Inverse(p) => path_support(g, to, p, from),
880 Path::Alt(ps) => ps.iter().find_map(|p| path_support(g, from, p, to)),
881 Path::Seq(ps) => seq_support(g, from, ps, to),
882 Path::Star(p) => star_support(g, from, p, to),
883 }
884}
885
886fn seq_support(g: &dyn PathBackend, from: &Term, ps: &[Path], to: &Term) -> Option<PathSupport> {
887 let Some((first, rest)) = ps.split_first() else {
888 return (from == to).then_some(PathSupport::Empty);
889 };
890 for mid in succ(g, from, first) {
891 let Some(head) = path_support(g, from, first, &mid) else {
892 continue;
893 };
894 if let Some(tail) = seq_support(g, &mid, rest, to) {
895 let mut chain = vec![head];
896 match tail {
897 PathSupport::Empty => {}
898 PathSupport::Chain(v) => chain.extend(v),
899 other => chain.push(other),
900 }
901 return Some(PathSupport::Chain(chain));
902 }
903 }
904 None
905}
906
907fn star_support(g: &dyn PathBackend, from: &Term, p: &Path, to: &Term) -> Option<PathSupport> {
908 if from == to {
909 return Some(PathSupport::Empty);
910 }
911 let mut visited: HashSet<Term> = HashSet::from([from.clone()]);
912 let mut queue: VecDeque<(Term, Vec<PathSupport>)> =
913 VecDeque::from([(from.clone(), Vec::new())]);
914 while let Some((cur, chain)) = queue.pop_front() {
915 for next in succ(g, &cur, p) {
916 let Some(edge) = path_support(g, &cur, p, &next) else {
917 continue;
918 };
919 let mut chain2 = chain.clone();
920 chain2.push(edge);
921 if next == *to {
922 return Some(PathSupport::Chain(chain2));
923 }
924 if visited.insert(next.clone()) {
925 queue.push_back((next, chain2));
926 }
927 }
928 }
929 None
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935 use shifty_parse::{load_turtle, parse_turtle};
936
937 const PREFIXES: &str = r#"
938 @prefix sh: <http://www.w3.org/ns/shacl#> .
939 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
940 @prefix ex: <http://ex/> .
941 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
942 "#;
943
944 fn run(ttl: &str) -> Vec<FocusWitness> {
945 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
946 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
947 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable")
948 }
949
950 fn any(w: &Witness, pred: &impl Fn(&Witness) -> bool) -> bool {
952 if pred(w) {
953 return true;
954 }
955 match w {
956 Witness::All { failed, .. } => failed.iter().any(|c| any(c, pred)),
957 Witness::Any { branches, .. } => branches.iter().any(|c| any(c, pred)),
958 Witness::CountHigh { per_value, .. } => per_value.iter().any(|(_, c)| any(c, pred)),
959 _ => false,
960 }
961 }
962
963 #[test]
964 fn conforming_graph_yields_no_witnesses() {
965 let ttl = format!(
966 "{PREFIXES}
967 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
968 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
969 ex:x ex:p ex:y .
970 "
971 );
972 assert!(run(&ttl).is_empty());
973 }
974
975 #[test]
976 fn min_count_violation_is_count_low() {
977 let ttl = format!(
978 "{PREFIXES}
979 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
980 sh:property [ sh:path ex:p ; sh:minCount 2 ] .
981 ex:x ex:p ex:y .
982 "
983 );
984 let ws = run(&ttl);
985 assert_eq!(ws.len(), 1);
986 assert_eq!(ws[0].focus.to_string(), "<http://ex/x>");
987 assert!(any(&ws[0].failure, &|w| matches!(
988 w,
989 Witness::CountLow {
990 have: 1,
991 min: 2,
992 ..
993 }
994 )));
995 }
996
997 fn count_low_siblings(w: &Witness) -> Vec<Vec<ShapeId>> {
999 fn go(w: &Witness, out: &mut Vec<Vec<ShapeId>>) {
1000 match w {
1001 Witness::CountLow {
1002 sibling_qualifiers, ..
1003 } => out.push(sibling_qualifiers.clone()),
1004 Witness::All { failed, .. } => failed.iter().for_each(|c| go(c, out)),
1005 Witness::Any { branches, .. } => branches.iter().for_each(|c| go(c, out)),
1006 Witness::CountHigh { per_value, .. } => {
1007 per_value.iter().for_each(|(_, c)| go(c, out))
1008 }
1009 _ => {}
1010 }
1011 }
1012 let mut out = Vec::new();
1013 go(w, &mut out);
1014 out
1015 }
1016
1017 #[test]
1018 fn cross_property_universals_attach_to_count_low() {
1019 let ttl = format!(
1024 "{PREFIXES}
1025 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1026 sh:property [ sh:path ex:p ; sh:minCount 1 ] ;
1027 sh:property [ sh:path ex:p ; sh:class ex:C ] .
1028 ex:x a ex:Thing .
1029 "
1030 );
1031 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1032 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1033 let ws =
1034 witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).expect("stratifiable");
1035 assert_eq!(ws.len(), 1);
1036 let sibs = count_low_siblings(&ws[0].failure);
1037 assert_eq!(sibs.len(), 1, "one CountLow");
1038 assert_eq!(sibs[0].len(), 1, "the class universal is attached");
1039 assert_eq!(
1040 class_of(sibs[0][0], &parsed.schema.arena),
1041 Some("http://ex/C".to_string()),
1042 );
1043 }
1044
1045 #[test]
1046 fn disjoint_or_branch_universal_does_not_attach() {
1047 let ttl = format!(
1052 "{PREFIXES}
1053 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1054 sh:or (
1055 [ sh:path ex:p ; sh:minCount 2 ]
1056 [ sh:path ex:p ; sh:class ex:C ]
1057 ) .
1058 ex:x a ex:Thing ; ex:p ex:y .
1059 "
1060 );
1061 let ws = run(&ttl);
1062 assert_eq!(ws.len(), 1);
1063 let sibs = count_low_siblings(&ws[0].failure);
1064 assert!(!sibs.is_empty(), "the count branch yields a CountLow");
1065 assert!(
1066 sibs.iter().all(|s| s.is_empty()),
1067 "no universal leaks across the disjunction: {sibs:?}",
1068 );
1069 }
1070
1071 fn class_of(id: ShapeId, arena: &ShapeArena) -> Option<String> {
1073 match shifty_algebra::render::class_target_shape(id, arena)? {
1074 Term::NamedNode(n) => Some(n.as_str().to_string()),
1075 _ => None,
1076 }
1077 }
1078
1079 #[test]
1080 fn datatype_violation_is_an_atom_with_support() {
1081 let ttl = format!(
1082 "{PREFIXES}
1083 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1084 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
1085 ex:x ex:p \"hello\" .
1086 "
1087 );
1088 let ws = run(&ttl);
1089 assert_eq!(ws.len(), 1);
1090 assert!(any(&ws[0].failure, &|w| matches!(
1092 w,
1093 Witness::Atom {
1094 produced_by: Some(PathSupport::Edge(_)),
1095 ..
1096 }
1097 )));
1098 }
1099
1100 #[test]
1101 fn focus_level_nodekind_atom_has_no_support() {
1102 let ttl = format!(
1103 "{PREFIXES}
1104 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1105 sh:nodeKind sh:IRI .
1106 ex:x ex:p \"v\" .
1107 ex:y ex:q ex:x .
1108 "
1109 );
1110 let _ = ttl;
1112 let ttl2 = format!(
1113 "{PREFIXES}
1114 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1115 sh:nodeKind sh:Literal .
1116 ex:x ex:p ex:y .
1117 "
1118 );
1119 let ws = run(&ttl2);
1120 assert_eq!(ws.len(), 1);
1121 assert!(matches!(
1122 ws[0].failure,
1123 Witness::Atom {
1124 produced_by: None,
1125 ..
1126 }
1127 ));
1128 }
1129
1130 #[test]
1131 fn non_stratifiable_schema_is_diagnosed() {
1132 let ttl = format!(
1133 "{PREFIXES}
1134 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1135 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
1136 ex:x ex:p ex:y .
1137 "
1138 );
1139 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1140 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1141 assert!(witness_violations(&loaded.graph, &loaded.graph, &parsed.schema).is_err());
1142 }
1143
1144 #[test]
1145 fn witness_shape_and_satisfy_shape_scope_to_one_shape() {
1146 let ttl = format!(
1149 "{PREFIXES}
1150 ex:S a sh:NodeShape ; sh:targetClass ex:C ;
1151 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1152 ex:T a sh:NodeShape ; sh:targetClass ex:D ;
1153 sh:property [ sh:path ex:q ; sh:minCount 1 ] .
1154 ex:good a ex:C ; ex:p ex:y .
1155 ex:bad a ex:C .
1156 ex:other a ex:D .
1157 "
1158 );
1159 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1160 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1161 let schema = &parsed.schema;
1162
1163 let s = shape_id_for_iri(schema, "http://ex/S").expect("ex:S is named");
1164 assert!(shape_id_for_iri(schema, "http://ex/missing").is_none());
1165
1166 let fails = witness_shape(&loaded.graph, &loaded.graph, schema, s).expect("stratifiable");
1168 assert_eq!(fails.len(), 1);
1169 assert_eq!(fails[0].focus.to_string(), "<http://ex/bad>");
1170
1171 let sats = satisfy_shape(&loaded.graph, &loaded.graph, schema, s).expect("stratifiable");
1173 assert_eq!(sats.len(), 1);
1174 assert_eq!(sats[0].focus.to_string(), "<http://ex/good>");
1175 assert!(any_sat(&sats[0].trace, &|t| matches!(
1177 t,
1178 SatTrace::CountHeld { matches, .. } if matches.iter().any(|(v, _)| v.to_string() == "<http://ex/y>")
1179 )));
1180 }
1181
1182 fn any_sat(t: &SatTrace, pred: &impl Fn(&SatTrace) -> bool) -> bool {
1184 if pred(t) {
1185 return true;
1186 }
1187 match t {
1188 SatTrace::AllHeld { children, .. } => children.iter().any(|c| any_sat(c, pred)),
1189 SatTrace::AnyHeld { satisfied, .. } => satisfied.iter().any(|c| any_sat(c, pred)),
1190 SatTrace::CountHeld { matches, .. } => matches.iter().any(|(_, c)| any_sat(c, pred)),
1191 _ => false,
1192 }
1193 }
1194}