1use crate::strata::analyze;
15use shifty_algebra::{
16 NodeExpr, NodeKindSet, Path, Rule, RuleHead, Schema, Selector, Shape, ShapeArena, ShapeId,
17 Statement, ValueType,
18};
19use std::collections::{HashMap, HashSet};
20
21pub fn normalize(schema: &Schema) -> Schema {
23 let mut z = Interner::new(&schema.arena);
24 let mut seen: HashSet<(Selector, ShapeId)> = HashSet::new();
26 let statements = schema
27 .statements
28 .iter()
29 .map(|st| Statement {
30 selector: z.selector(&st.selector),
31 shape: z.intern(st.shape),
32 })
33 .filter(|st| seen.insert((st.selector.clone(), st.shape)))
34 .collect();
35 let rules = schema.rules.iter().map(|r| z.rule(r)).collect();
36 let names = schema
38 .names
39 .iter()
40 .filter_map(|(old, name)| z.memo.get(old).map(|new| (*new, name.clone())))
41 .collect();
42 let normalized = Schema {
43 arena: z.dst,
44 statements,
45 rules,
46 names,
47 };
48 normalized.arena.debug_assert_finalized();
49 normalized
50}
51
52fn push_inverse(path: Path) -> Path {
55 match path {
56 Path::Id => Path::Id,
57 Path::Inverse(inner) => normalize_path(*inner), Path::Seq(steps) => {
59 Path::seq(steps.into_iter().rev().map(push_inverse).collect())
61 }
62 Path::Alt(alts) => Path::alt(alts.into_iter().map(push_inverse).collect()),
63 Path::Star(inner) => Path::star(push_inverse(*inner)), pred => Path::Inverse(Box::new(pred)), }
66}
67
68fn normalize_path(path: Path) -> Path {
71 match path {
72 Path::Inverse(inner) => push_inverse(*inner),
73 Path::Seq(steps) => {
74 let steps: Vec<Path> = steps.into_iter().map(normalize_path).collect();
75 let mut merged: Vec<Path> = Vec::with_capacity(steps.len());
77 for step in steps {
78 match merged.last() {
79 Some(last) if matches!(last, Path::Star(_)) && last == &step => {}
80 _ => merged.push(step),
81 }
82 }
83 Path::seq(merged)
84 }
85 Path::Alt(alts) => {
86 let mut seen = HashSet::new();
88 let deduped: Vec<Path> = alts
89 .into_iter()
90 .map(normalize_path)
91 .filter(|p| seen.insert(p.clone()))
92 .collect();
93 Path::alt(deduped)
94 }
95 Path::Star(inner) => {
96 let inner = normalize_path(*inner);
97 let inner = match inner {
99 Path::Alt(alts) => {
100 let without_id: Vec<Path> =
101 alts.into_iter().filter(|p| *p != Path::Id).collect();
102 Path::alt(without_id)
103 }
104 other => other,
105 };
106 inner.star()
107 }
108 other => other,
109 }
110}
111
112fn tighter_lower(a: Option<u64>, b: Option<u64>) -> Option<u64> {
114 match (a, b) {
115 (Some(x), Some(y)) => Some(x.max(y)),
116 (s, None) | (None, s) => s,
117 }
118}
119
120fn tighter_upper(a: Option<u64>, b: Option<u64>) -> Option<u64> {
122 match (a, b) {
123 (Some(x), Some(y)) => Some(x.min(y)),
124 (s, None) | (None, s) => s,
125 }
126}
127
128struct Interner<'a> {
129 src: &'a ShapeArena,
130 dst: ShapeArena,
131 memo: HashMap<ShapeId, ShapeId>,
133 cons: HashMap<Shape, ShapeId>,
135 cyclic: HashSet<ShapeId>,
137 cyclic_dst: HashSet<ShapeId>,
139}
140
141impl<'a> Interner<'a> {
142 fn new(src: &'a ShapeArena) -> Self {
143 let strat = analyze(src);
144 let cyclic = strat
145 .strata
146 .iter()
147 .filter(|s| s.recursive)
148 .flat_map(|s| s.shapes.iter().copied())
149 .collect();
150 Self {
151 src,
152 dst: ShapeArena::new(),
153 memo: HashMap::new(),
154 cons: HashMap::new(),
155 cyclic,
156 cyclic_dst: HashSet::new(),
157 }
158 }
159
160 fn cons(&mut self, shape: Shape) -> ShapeId {
161 if let Some(&d) = self.cons.get(&shape) {
162 return d;
163 }
164 let d = self.dst.insert(shape.clone());
165 self.cons.insert(shape, d);
166 d
167 }
168
169 fn top(&mut self) -> ShapeId {
170 self.cons(Shape::Top)
171 }
172
173 fn bottom(&mut self) -> ShapeId {
174 let t = self.top();
175 self.cons(Shape::Not(t))
176 }
177
178 fn is_top(&self, id: ShapeId) -> bool {
179 matches!(self.dst.get(id), Shape::Top)
180 }
181
182 fn is_bottom(&self, id: ShapeId) -> bool {
183 matches!(self.dst.get(id), Shape::Not(x) if matches!(self.dst.get(*x), Shape::Top))
184 }
185
186 fn intern(&mut self, id: ShapeId) -> ShapeId {
187 if let Some(&d) = self.memo.get(&id) {
188 return d;
189 }
190 if self.cyclic.contains(&id) {
191 let d = self.dst.reserve();
192 self.memo.insert(id, d);
193 self.cyclic_dst.insert(d);
194 let shape = self.rebuild_cyclic(id);
195 self.dst.set(d, shape);
196 d
197 } else {
198 let r = self.simplify(id);
199 self.memo.insert(id, r);
200 r
201 }
202 }
203
204 fn simplify(&mut self, id: ShapeId) -> ShapeId {
207 match self.src.get(id).clone() {
208 Shape::Annotated {
209 severity,
210 messages,
211 shape,
212 } => {
213 let shape = self.intern(shape);
214 self.cons(Shape::Annotated {
215 severity,
216 messages,
217 shape,
218 })
219 }
220 Shape::Top => self.top(),
221 Shape::Not(c) => {
222 let cn = self.intern(c);
223 self.mk_not(cn)
224 }
225 Shape::And(cs) => {
226 let ids = cs.iter().map(|c| self.intern(*c)).collect();
227 self.mk_and(ids)
228 }
229 Shape::Or(cs) => {
230 let ids = cs.iter().map(|c| self.intern(*c)).collect();
231 self.mk_or(ids)
232 }
233 Shape::Count {
234 path,
235 min,
236 max,
237 qualifier,
238 } => {
239 let q = self.intern(qualifier);
240 self.mk_count(normalize_path(path), min, max, q)
241 }
242 Shape::TestType(vt) => match vt.normalize() {
244 None => self.bottom(), Some(ValueType::Any) => self.top(), Some(v) => self.cons(Shape::TestType(v)),
247 },
248 Shape::Eq(path, nn) => self.cons(Shape::Eq(normalize_path(path), nn)),
250 Shape::Disj(path, nn) => self.cons(Shape::Disj(normalize_path(path), nn)),
251 Shape::Lt(path, nn) => self.cons(Shape::Lt(normalize_path(path), nn)),
252 Shape::Le(path, nn) => self.cons(Shape::Le(normalize_path(path), nn)),
253 Shape::UniqueLang(path) => self.cons(Shape::UniqueLang(normalize_path(path))),
254 Shape::Expression(e) => {
256 let e = self.node_expr(&e);
257 self.cons(Shape::Expression(e))
258 }
259 leaf => self.cons(leaf),
261 }
262 }
263
264 fn rebuild_cyclic(&mut self, id: ShapeId) -> Shape {
267 match self.src.get(id).clone() {
268 Shape::Annotated {
269 severity,
270 messages,
271 shape,
272 } => Shape::Annotated {
273 severity,
274 messages,
275 shape: self.intern(shape),
276 },
277 Shape::Not(c) => Shape::Not(self.intern(c)),
278 Shape::And(cs) => Shape::And(self.intern_set(&cs)),
279 Shape::Or(cs) => Shape::Or(self.intern_set(&cs)),
280 Shape::Count {
281 path,
282 min,
283 max,
284 qualifier,
285 } => Shape::Count {
286 path: normalize_path(path),
287 min,
288 max,
289 qualifier: self.intern(qualifier),
290 },
291 Shape::Expression(e) => Shape::Expression(self.node_expr(&e)),
292 leaf => leaf,
293 }
294 }
295
296 fn intern_set(&mut self, cs: &[ShapeId]) -> Vec<ShapeId> {
297 let mut v: Vec<ShapeId> = cs.iter().map(|c| self.intern(*c)).collect();
298 v.sort();
299 v.dedup();
300 v
301 }
302
303 fn mk_not(&mut self, c: ShapeId) -> ShapeId {
306 if let Shape::Not(x) = self.dst.get(c) {
307 return *x; }
309 if self.cyclic_dst.contains(&c) {
310 return self.cons(Shape::Not(c)); }
312 match self.dst.get(c).clone() {
313 Shape::And(cs) => {
315 let neg = cs.iter().map(|c| self.mk_not(*c)).collect();
316 self.mk_or(neg)
317 }
318 Shape::Or(cs) => {
319 let neg = cs.iter().map(|c| self.mk_not(*c)).collect();
320 self.mk_and(neg)
321 }
322 Shape::Count {
324 path,
325 min,
326 max,
327 qualifier,
328 } => {
329 let mut alts = Vec::new();
330 if let Some(a) = min
331 && a > 0
332 {
333 alts.push(self.mk_count(path.clone(), None, Some(a - 1), qualifier));
334 }
335 if let Some(b) = max {
336 alts.push(self.mk_count(path, Some(b + 1), None, qualifier));
337 }
338 self.mk_or(alts)
339 }
340 Shape::TestKind(k) => {
342 let comp: NodeKindSet = k.complement();
343 if comp.is_empty() {
344 self.bottom() } else {
346 self.cons(Shape::TestKind(comp))
347 }
348 }
349 _ => self.cons(Shape::Not(c)),
351 }
352 }
353
354 fn mk_and(&mut self, ids: Vec<ShapeId>) -> ShapeId {
355 let mut flat = Vec::new();
357 for id in ids {
358 match self.dst.get(id) {
359 Shape::And(inner) => flat.extend(inner.iter().copied()),
360 _ => flat.push(id),
361 }
362 }
363 let flat = self.merge_counts(flat);
365 let flat = self.merge_value_types(flat);
367 let flat = self.merge_node_kinds(flat);
369 let mut acc = Vec::new();
371 for id in flat {
372 if self.is_bottom(id) {
373 return id; }
375 if self.is_top(id) {
376 continue; }
378 acc.push(id);
379 }
380 acc.sort();
381 acc.dedup();
382 if self.has_complement(&acc) {
383 return self.bottom(); }
385 match acc.len() {
386 0 => self.top(),
387 1 => acc[0],
388 _ => self.cons(Shape::And(acc)),
389 }
390 }
391
392 fn merge_node_kinds(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
396 let mut acc: Option<NodeKindSet> = None;
397 let mut others = Vec::new();
398 for id in flat {
399 match self.dst.get(id) {
400 Shape::TestKind(k) => {
401 acc = Some(match acc {
402 None => *k,
403 Some(prev) => NodeKindSet {
404 iri: prev.iri && k.iri,
405 blank: prev.blank && k.blank,
406 literal: prev.literal && k.literal,
407 },
408 });
409 }
410 _ => others.push(id),
411 }
412 }
413 if let Some(k) = acc {
414 if k.is_empty() {
415 others.push(self.bottom()); } else if k.iri && k.blank && k.literal {
417 } else {
419 let id = self.cons(Shape::TestKind(k));
420 others.push(id);
421 }
422 }
423 others
424 }
425
426 fn merge_counts(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
430 let mut keys: Vec<(Path, ShapeId)> = Vec::new();
431 let mut bounds: Vec<(Option<u64>, Option<u64>)> = Vec::new();
432 let mut index: HashMap<(Path, ShapeId), usize> = HashMap::new();
433 let mut others = Vec::new();
434
435 for id in flat {
436 if let Shape::Count {
437 path,
438 min,
439 max,
440 qualifier,
441 } = self.dst.get(id).clone()
442 {
443 let key = (path, qualifier);
444 match index.get(&key) {
445 Some(&i) => {
446 bounds[i].0 = tighter_lower(bounds[i].0, min);
447 bounds[i].1 = tighter_upper(bounds[i].1, max);
448 }
449 None => {
450 index.insert(key.clone(), keys.len());
451 keys.push(key);
452 bounds.push((min, max));
453 }
454 }
455 } else {
456 others.push(id);
457 }
458 }
459
460 let mut result = others;
461 for ((path, q), (min, max)) in keys.into_iter().zip(bounds) {
462 let merged = self.mk_count(path, min, max, q);
463 result.push(merged);
464 }
465 result
466 }
467
468 fn merge_value_types(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
473 let mut facets: Vec<ValueType> = Vec::new();
474 let mut others = Vec::new();
475 for id in flat {
476 match self.dst.get(id) {
477 Shape::TestType(vt) => facets.push(vt.clone()),
478 _ => others.push(id),
479 }
480 }
481 if facets.is_empty() {
482 return others;
483 }
484 match ValueType::and(facets).normalize() {
485 None => others.push(self.bottom()), Some(ValueType::Any) => {} Some(v) => {
488 let id = self.cons(Shape::TestType(v));
489 others.push(id);
490 }
491 }
492 others
493 }
494
495 fn mk_or(&mut self, ids: Vec<ShapeId>) -> ShapeId {
496 let mut flat = Vec::new();
497 for id in ids {
498 if self.is_top(id) {
499 return id; }
501 if self.is_bottom(id) {
502 continue; }
504 match self.dst.get(id) {
505 Shape::Or(inner) => flat.extend(inner.iter().copied()),
506 _ => flat.push(id),
507 }
508 }
509 flat.sort();
510 flat.dedup();
511 if self.has_complement(&flat) {
512 return self.top(); }
514 match flat.len() {
515 0 => self.bottom(),
516 1 => flat[0],
517 _ => self.cons(Shape::Or(flat)),
518 }
519 }
520
521 fn has_complement(&self, ids: &[ShapeId]) -> bool {
523 let set: HashSet<ShapeId> = ids.iter().copied().collect();
524 ids.iter().any(|&id| match self.dst.get(id) {
525 Shape::Not(x) => set.contains(x),
526 _ => false,
527 })
528 }
529
530 fn mk_count(
531 &mut self,
532 path: shifty_algebra::Path,
533 min: Option<u64>,
534 max: Option<u64>,
535 q: ShapeId,
536 ) -> ShapeId {
537 if max.is_none() && matches!(min, None | Some(0)) {
538 return self.top(); }
540 if let (Some(a), Some(b)) = (min, max)
541 && a > b
542 {
543 return self.bottom(); }
545 if matches!(&path, Path::Alt(v) if v.is_empty()) {
547 return if min.unwrap_or(0) >= 1 {
548 self.bottom() } else {
550 self.top() };
552 }
553 if self.is_bottom(q) {
555 return if min.unwrap_or(0) >= 1 {
556 self.bottom() } else {
558 self.top() };
560 }
561 if path == Path::Id {
563 let lo = min.unwrap_or(0);
564 if lo >= 2 {
565 return self.bottom(); }
567 return match (lo, max) {
568 (0, Some(0)) => self.mk_not(q), (1, _) => q, _ => self.top(), };
572 }
573 self.cons(Shape::Count {
574 path,
575 min,
576 max,
577 qualifier: q,
578 })
579 }
580
581 fn selector(&mut self, sel: &Selector) -> Selector {
582 match sel {
583 Selector::HasPath(p, q) => {
584 let path = normalize_path(p.clone());
585 let shape = self.intern(*q);
586 if self.is_top(shape) {
589 match &path {
590 Path::Pred(nn) => return Selector::HasOut(nn.clone()),
591 Path::Inverse(inner) => {
592 if let Path::Pred(nn) = inner.as_ref() {
593 return Selector::HasIn(nn.clone());
594 }
595 }
596 _ => {}
597 }
598 }
599 Selector::HasPath(path, shape)
600 }
601 other => other.clone(),
602 }
603 }
604
605 fn rule(&mut self, r: &Rule) -> Rule {
606 Rule {
607 selector: self.selector(&r.selector),
608 conditions: r.conditions.iter().map(|c| self.intern(*c)).collect(),
609 head: self.head(&r.head),
610 order: r.order,
611 deactivated: r.deactivated,
612 }
613 }
614
615 fn head(&mut self, h: &RuleHead) -> RuleHead {
616 match h {
617 RuleHead::Triple {
618 subject,
619 predicate,
620 object,
621 } => RuleHead::Triple {
622 subject: self.node_expr(subject),
623 predicate: self.node_expr(predicate),
624 object: self.node_expr(object),
625 },
626 RuleHead::Sparql(s) => RuleHead::Sparql(s.clone()),
627 }
628 }
629
630 fn node_expr(&mut self, e: &NodeExpr) -> NodeExpr {
631 match e {
632 NodeExpr::Filter { input, shape } => NodeExpr::Filter {
633 input: Box::new(self.node_expr(input)),
634 shape: self.intern(*shape),
635 },
636 NodeExpr::Intersection(v) => {
637 NodeExpr::Intersection(v.iter().map(|x| self.node_expr(x)).collect())
638 }
639 NodeExpr::Union(v) => NodeExpr::Union(v.iter().map(|x| self.node_expr(x)).collect()),
640 NodeExpr::Function { iri, args } => NodeExpr::Function {
641 iri: iri.clone(),
642 args: args.iter().map(|x| self.node_expr(x)).collect(),
643 },
644 other => other.clone(),
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use shifty_algebra::{NodeKindSet, Path, Selector};
653
654 fn schema_with(arena: ShapeArena, root: ShapeId) -> Schema {
655 Schema {
656 arena,
657 statements: vec![Statement {
658 selector: Selector::IsConst(shifty_algebra::Term::NamedNode(
659 shifty_algebra::NamedNode::new("http://ex/x").unwrap(),
660 )),
661 shape: root,
662 }],
663 rules: Vec::new(),
664 names: Default::default(),
665 }
666 }
667
668 #[test]
669 fn cse_dedups_identical_subshapes() {
670 let mut a = ShapeArena::new();
671 let t1 = a.insert(Shape::TestKind(NodeKindSet::IRI));
672 let t2 = a.insert(Shape::TestKind(NodeKindSet::IRI)); let root = a.insert(Shape::And(vec![t1, t2]));
674 let n = normalize(&schema_with(a, root));
675 let rooted = n.statements[0].shape;
677 assert!(matches!(n.arena.get(rooted), Shape::TestKind(_)));
678 assert_eq!(n.arena.len(), 1);
680 }
681
682 #[test]
683 fn bottom_absorbs_conjunction() {
684 let mut a = ShapeArena::new();
685 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
686 let t = a.insert(Shape::Top);
687 let bot = a.insert(Shape::Not(t));
688 let root = a.insert(Shape::And(vec![k, bot]));
689 let n = normalize(&schema_with(a, root));
690 let rooted = n.statements[0].shape;
691 assert!(
692 matches!(n.arena.get(rooted), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
693 );
694 }
695
696 #[test]
697 fn top_absorbs_disjunction() {
698 let mut a = ShapeArena::new();
699 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
700 let t = a.insert(Shape::Top);
701 let root = a.insert(Shape::Or(vec![k, t]));
702 let n = normalize(&schema_with(a, root));
703 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
704 }
705
706 #[test]
707 fn complement_is_unsat() {
708 let mut a = ShapeArena::new();
709 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
710 let nk = a.insert(Shape::Not(k));
711 let root = a.insert(Shape::And(vec![k, nk]));
712 let n = normalize(&schema_with(a, root));
713 assert!(
714 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
715 );
716 }
717
718 #[test]
719 fn nnf_pushes_negation_through_and() {
720 let mut a = ShapeArena::new();
723 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
724 let top = a.insert(Shape::Top);
725 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
726 let cnt = a.insert(Shape::Count {
727 path: p,
728 min: Some(1),
729 max: None,
730 qualifier: top,
731 });
732 let and = a.insert(Shape::And(vec![k, cnt]));
733 let root = a.insert(Shape::Not(and));
734 let n = normalize(&schema_with(a, root));
735 match n.arena.get(n.statements[0].shape) {
736 Shape::Or(cs) => {
737 assert_eq!(cs.len(), 2);
738 let kinds: Vec<_> = cs
739 .iter()
740 .map(|c| n.arena.get(*c))
741 .map(|s| matches!(s, Shape::TestKind(_) | Shape::Count { .. }))
742 .collect();
743 assert!(
744 kinds.iter().all(|&b| b),
745 "expected Or of TestKind+Count, got something else"
746 );
747 }
748 other => panic!("expected Or of two shapes, got {other:?}"),
749 }
750 }
751
752 #[test]
753 fn disjoint_node_kinds_in_and_is_unsat() {
754 let mut a = ShapeArena::new();
756 let iri = a.insert(Shape::TestKind(NodeKindSet::IRI));
757 let lit = a.insert(Shape::TestKind(NodeKindSet::LITERAL));
758 let root = a.insert(Shape::And(vec![iri, lit]));
759 let n = normalize(&schema_with(a, root));
760 assert!(
761 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
762 );
763 }
764
765 #[test]
766 fn nnf_flips_count_bound() {
767 let mut a = ShapeArena::new();
769 let top = a.insert(Shape::Top);
770 let count = a.insert(Shape::Count {
771 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
772 min: Some(2),
773 max: None,
774 qualifier: top,
775 });
776 let root = a.insert(Shape::Not(count));
777 let n = normalize(&schema_with(a, root));
778 match n.arena.get(n.statements[0].shape) {
779 Shape::Count { min, max, .. } => {
780 assert_eq!((*min, *max), (None, Some(1)));
781 }
782 other => panic!("expected ∃≤1, got {other:?}"),
783 }
784 }
785
786 #[test]
787 fn merges_min_and_max_counts() {
788 let mut a = ShapeArena::new();
790 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
791 let t1 = a.insert(Shape::Top);
792 let t2 = a.insert(Shape::Top);
793 let lo = a.insert(Shape::Count {
794 path: p.clone(),
795 min: Some(1),
796 max: None,
797 qualifier: t1,
798 });
799 let hi = a.insert(Shape::Count {
800 path: p,
801 min: None,
802 max: Some(1),
803 qualifier: t2,
804 });
805 let root = a.insert(Shape::And(vec![lo, hi]));
806 let n = normalize(&schema_with(a, root));
807 match n.arena.get(n.statements[0].shape) {
808 Shape::Count { min, max, .. } => assert_eq!((*min, *max), (Some(1), Some(1))),
809 other => panic!("expected one fused ∃[1..1], got {other:?}"),
810 }
811 }
812
813 #[test]
814 fn merged_counts_can_be_unsat() {
815 let mut a = ShapeArena::new();
817 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
818 let t1 = a.insert(Shape::Top);
819 let t2 = a.insert(Shape::Top);
820 let lo = a.insert(Shape::Count {
821 path: p.clone(),
822 min: Some(2),
823 max: None,
824 qualifier: t1,
825 });
826 let hi = a.insert(Shape::Count {
827 path: p,
828 min: None,
829 max: Some(1),
830 qualifier: t2,
831 });
832 let root = a.insert(Shape::And(vec![lo, hi]));
833 let n = normalize(&schema_with(a, root));
834 assert!(
835 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
836 );
837 }
838
839 #[test]
840 fn unsat_value_type_absorbs_conjunction() {
841 use shifty_algebra::{Bound, Literal, NamedNode, ValueType};
843 let int = |n: i64| {
844 Literal::new_typed_literal(
845 n.to_string(),
846 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
847 )
848 };
849 let mut a = ShapeArena::new();
850 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
851 let bad = a.insert(Shape::TestType(ValueType::NumericRange {
852 lo: Some(Bound {
853 value: int(5),
854 inclusive: true,
855 }),
856 hi: Some(Bound {
857 value: int(3),
858 inclusive: true,
859 }),
860 }));
861 let root = a.insert(Shape::And(vec![k, bad]));
862 let n = normalize(&schema_with(a, root));
863 assert!(matches!(n.arena.get(n.statements[0].shape),
864 Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
865 }
866
867 #[test]
868 fn conjoined_range_facets_merge() {
869 use shifty_algebra::{Bound, Literal, NamedNode, ValueType};
871 let int = |n: i64| {
872 Literal::new_typed_literal(
873 n.to_string(),
874 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
875 )
876 };
877 let mut a = ShapeArena::new();
878 let lo = a.insert(Shape::TestType(ValueType::NumericRange {
879 lo: Some(Bound {
880 value: int(1),
881 inclusive: true,
882 }),
883 hi: None,
884 }));
885 let hi = a.insert(Shape::TestType(ValueType::NumericRange {
886 lo: None,
887 hi: Some(Bound {
888 value: int(10),
889 inclusive: true,
890 }),
891 }));
892 let root = a.insert(Shape::And(vec![lo, hi]));
893 let n = normalize(&schema_with(a, root));
894 match n.arena.get(n.statements[0].shape) {
895 Shape::TestType(ValueType::NumericRange { lo, hi }) => {
896 assert!(lo.is_some() && hi.is_some(), "expected fused [1,10]");
897 }
898 other => panic!("expected one fused range facet, got {other:?}"),
899 }
900 }
901
902 #[test]
903 fn negating_recursive_shape_terminates() {
904 let mut a = ShapeArena::new();
906 let s = a.reserve();
907 a.set(
908 s,
909 Shape::Count {
910 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
911 min: Some(1),
912 max: None,
913 qualifier: s,
914 },
915 );
916 let root = a.insert(Shape::Not(s));
917 let n = normalize(&schema_with(a, root));
918 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Not(_)));
919 }
920
921 #[test]
922 fn qualifier_bottom_min1_is_bottom() {
923 let mut a = ShapeArena::new();
925 let top = a.insert(Shape::Top);
926 let bot = a.insert(Shape::Not(top));
927 let count = a.insert(Shape::Count {
928 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
929 min: Some(1),
930 max: None,
931 qualifier: bot,
932 });
933 let n = normalize(&schema_with(a, count));
934 let r = n.statements[0].shape;
935 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
936 }
937
938 #[test]
939 fn qualifier_bottom_max_bound_is_top() {
940 let mut a = ShapeArena::new();
942 let top = a.insert(Shape::Top);
943 let bot = a.insert(Shape::Not(top));
944 let count = a.insert(Shape::Count {
945 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
946 min: None,
947 max: Some(2),
948 qualifier: bot,
949 });
950 let n = normalize(&schema_with(a, count));
951 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
952 }
953
954 #[test]
955 fn id_path_min1_is_qualifier() {
956 let mut a = ShapeArena::new();
958 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
959 let count = a.insert(Shape::Count {
960 path: Path::Id,
961 min: Some(1),
962 max: None,
963 qualifier: k,
964 });
965 let n = normalize(&schema_with(a, count));
966 assert!(matches!(
967 n.arena.get(n.statements[0].shape),
968 Shape::TestKind(NodeKindSet::IRI)
969 ));
970 }
971
972 #[test]
973 fn id_path_max0_is_negation() {
974 let mut a = ShapeArena::new();
976 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
977 let count = a.insert(Shape::Count {
978 path: Path::Id,
979 min: None,
980 max: Some(0),
981 qualifier: k,
982 });
983 let n = normalize(&schema_with(a, count));
984 match n.arena.get(n.statements[0].shape) {
985 Shape::TestKind(nk) => {
986 assert_eq!(*nk, NodeKindSet::BLANK_NODE_OR_LITERAL);
987 }
988 other => panic!("expected TestKind(Blank|Lit), got {other:?}"),
989 }
990 }
991
992 #[test]
993 fn id_path_min2_is_bottom() {
994 let mut a = ShapeArena::new();
996 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
997 let count = a.insert(Shape::Count {
998 path: Path::Id,
999 min: Some(2),
1000 max: None,
1001 qualifier: k,
1002 });
1003 let n = normalize(&schema_with(a, count));
1004 let r = n.statements[0].shape;
1005 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
1006 }
1007
1008 #[test]
1009 fn converse_pushdown_through_seq() {
1010 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1012 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1013 let inv_seq = Path::Inverse(Box::new(Path::Seq(vec![
1014 Path::Pred(p.clone()),
1015 Path::Pred(q.clone()),
1016 ])));
1017 let mut a = ShapeArena::new();
1018 let top = a.insert(Shape::Top);
1019 let count = a.insert(Shape::Count {
1020 path: inv_seq,
1021 min: Some(1),
1022 max: None,
1023 qualifier: top,
1024 });
1025 let n = normalize(&schema_with(a, count));
1026 match n.arena.get(n.statements[0].shape) {
1027 Shape::Count { path, .. } => {
1028 let expected = Path::Seq(vec![
1029 Path::Inverse(Box::new(Path::Pred(q))),
1030 Path::Inverse(Box::new(Path::Pred(p))),
1031 ]);
1032 assert_eq!(*path, expected, "expected (p·q)⁻ normalized to q⁻·p⁻");
1033 }
1034 other => panic!("expected Count with normalized path, got {other:?}"),
1035 }
1036 }
1037
1038 #[test]
1039 fn converse_pushdown_through_alt_and_star() {
1040 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1042 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1043 let alt = Path::Alt(vec![Path::Pred(p.clone()), Path::Pred(q.clone())]);
1044 assert_eq!(
1045 normalize_path(Path::Inverse(Box::new(alt))),
1046 Path::Alt(vec![
1047 Path::Inverse(Box::new(Path::Pred(p.clone()))),
1048 Path::Inverse(Box::new(Path::Pred(q))),
1049 ])
1050 );
1051 let star_inv = Path::Inverse(Box::new(Path::Star(Box::new(Path::Pred(p.clone())))));
1052 assert_eq!(
1053 normalize_path(star_inv),
1054 Path::Star(Box::new(Path::Inverse(Box::new(Path::Pred(p)))))
1055 );
1056 }
1057
1058 #[test]
1059 fn recursive_shape_survives_normalization() {
1060 let mut a = ShapeArena::new();
1062 let s = a.reserve();
1063 a.set(
1064 s,
1065 Shape::Count {
1066 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
1067 min: Some(1),
1068 max: None,
1069 qualifier: s,
1070 },
1071 );
1072 let n = normalize(&schema_with(a, s));
1073 let rooted = n.statements[0].shape;
1074 match n.arena.get(rooted) {
1075 Shape::Count { qualifier, .. } => assert_eq!(*qualifier, rooted),
1076 other => panic!("expected self-referential Count, got {other:?}"),
1077 }
1078 }
1079
1080 #[test]
1081 fn negkind_iri_becomes_blank_or_literal() {
1082 let mut a = ShapeArena::new();
1084 let iri = a.insert(Shape::TestKind(NodeKindSet::IRI));
1085 let root = a.insert(Shape::Not(iri));
1086 let n = normalize(&schema_with(a, root));
1087 assert!(matches!(
1088 n.arena.get(n.statements[0].shape),
1089 Shape::TestKind(NodeKindSet::BLANK_NODE_OR_LITERAL)
1090 ));
1091 }
1092
1093 #[test]
1094 fn empty_alt_path_min1_is_bottom() {
1095 let mut a = ShapeArena::new();
1097 let top = a.insert(Shape::Top);
1098 let count = a.insert(Shape::Count {
1099 path: Path::Alt(vec![]),
1100 min: Some(1),
1101 max: None,
1102 qualifier: top,
1103 });
1104 let n = normalize(&schema_with(a, count));
1105 let r = n.statements[0].shape;
1106 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
1107 }
1108
1109 #[test]
1110 fn empty_alt_path_max_bound_is_top() {
1111 let mut a = ShapeArena::new();
1113 let top = a.insert(Shape::Top);
1114 let count = a.insert(Shape::Count {
1115 path: Path::Alt(vec![]),
1116 min: None,
1117 max: Some(3),
1118 qualifier: top,
1119 });
1120 let n = normalize(&schema_with(a, count));
1121 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
1122 }
1123
1124 #[test]
1125 fn star_drops_id_from_alt() {
1126 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1128 let alt_id = Path::Alt(vec![Path::Pred(p.clone()), Path::Id]);
1129 let star = Path::Star(Box::new(alt_id));
1130 assert_eq!(normalize_path(star), Path::Star(Box::new(Path::Pred(p))));
1131 }
1132
1133 #[test]
1134 fn seq_merges_adjacent_equal_stars() {
1135 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1137 let star = Path::Star(Box::new(Path::Pred(p.clone())));
1138 let seq = Path::Seq(vec![star.clone(), star]);
1139 assert_eq!(normalize_path(seq), Path::Star(Box::new(Path::Pred(p))));
1140 }
1141
1142 #[test]
1143 fn alt_deduplicates_paths() {
1144 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1146 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1147 let dup = Path::Alt(vec![
1148 Path::Pred(p.clone()),
1149 Path::Pred(p.clone()),
1150 Path::Pred(q.clone()),
1151 ]);
1152 let result = normalize_path(dup);
1153 assert_eq!(result, Path::Alt(vec![Path::Pred(p), Path::Pred(q)]));
1154 }
1155
1156 #[test]
1157 fn selector_haspath_pred_top_becomes_hasout() {
1158 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1160 let mut a = ShapeArena::new();
1161 let top = a.insert(Shape::Top);
1162 let schema = Schema {
1163 arena: a,
1164 statements: vec![Statement {
1165 selector: Selector::HasPath(Path::Pred(q.clone()), top),
1166 shape: top,
1167 }],
1168 rules: vec![],
1169 names: Default::default(),
1170 };
1171 let n = normalize(&schema);
1172 assert_eq!(n.statements[0].selector, Selector::HasOut(q));
1173 }
1174
1175 #[test]
1176 fn selector_haspath_inv_pred_top_becomes_hasin() {
1177 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1179 let mut a = ShapeArena::new();
1180 let top = a.insert(Shape::Top);
1181 let schema = Schema {
1182 arena: a,
1183 statements: vec![Statement {
1184 selector: Selector::HasPath(Path::Inverse(Box::new(Path::Pred(q.clone()))), top),
1185 shape: top,
1186 }],
1187 rules: vec![],
1188 names: Default::default(),
1189 };
1190 let n = normalize(&schema);
1191 assert_eq!(n.statements[0].selector, Selector::HasIn(q));
1192 }
1193
1194 #[test]
1195 fn statement_dedup_removes_identical() {
1196 let mut a = ShapeArena::new();
1197 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
1198 let node =
1199 shifty_algebra::Term::NamedNode(shifty_algebra::NamedNode::new("http://ex/x").unwrap());
1200 let sel = Selector::IsConst(node);
1201 let schema = Schema {
1202 arena: a,
1203 statements: vec![
1204 Statement {
1205 selector: sel.clone(),
1206 shape: k,
1207 },
1208 Statement {
1209 selector: sel.clone(),
1210 shape: k,
1211 },
1212 ],
1213 rules: vec![],
1214 names: Default::default(),
1215 };
1216 let n = normalize(&schema);
1217 assert_eq!(n.statements.len(), 1);
1218 }
1219}